['name' => 'Fighter', 'bonus_atk' => 2, 'bonus_def' => 0, 'bonus_hp' => 6], 'mage' => ['name' => 'Mage', 'bonus_atk' => 4, 'bonus_def' => 0, 'bonus_hp' => 0], 'thief' => ['name' => 'Thief', 'bonus_atk' => 1, 'bonus_def' => 2, 'bonus_hp' => 2], ]; function mud_class_bonus(string $class): array { global $MUD_CLASSES; return $MUD_CLASSES[$class] ?? $MUD_CLASSES['fighter']; } function mud_echo($msg, int $room_id, $pdo = null): void { $pdo = $pdo ?: db(); $pdo->prepare("INSERT INTO mud_events (room_id,ts,text) VALUES (?,?,?)") ->execute([$room_id, time(), $msg]); } function mud_item($key) { $st = db()->prepare("SELECT * FROM mud_items WHERE key_name=?"); $st->execute([$key]); return $st->fetch(PDO::FETCH_ASSOC); } function mud_char_for(int $uid) { $st = db()->prepare("SELECT c.* FROM mud_chars c WHERE user_id=?"); $st->execute([$uid]); return $st->fetch(PDO::FETCH_ASSOC); } function mud_room(int $id) { $st = db()->prepare("SELECT * FROM mud_rooms WHERE id=?"); $st->execute([$id]); $r = $st->fetch(PDO::FETCH_ASSOC); if ($r) $r['exits'] = json_decode($r['exits'], true); return $r; } // build a fresh character from a user row function mud_create_char(array $user, string $name, string $class = 'fighter'): array { $pdo = db(); $class = array_key_exists($class, $GLOBALS['MUD_CLASSES']) ? $class : 'fighter'; $b = mud_class_bonus($class); $maxhp = MUD_START_HP + $b['bonus_hp']; $pdo->prepare("INSERT INTO mud_chars (user_id,name,class,room_id,hp,max_hp,atk,def,xp,level,gold) VALUES (?,?,?,1,?,?,?,?,0,1,0)") ->execute([$user['id'], $name, $class, $maxhp, $maxhp, MUD_START_ATK + $b['bonus_atk'], MUD_START_DEF + $b['bonus_def']]); mud_echo("{$name} the {$b['name']} arrives in the world.", 1); return mud_char_for($user['id']); } function mud_alive_spawns(int $room_id) { $st = db()->prepare("SELECT s.*, m.key_name, m.name, m.descr FROM mud_spawn s JOIN mud_mobs m ON m.id=s.mob_id WHERE s.room_id=? AND s.alive=1 ORDER BY s.id"); $st->execute([$room_id]); return $st->fetchAll(PDO::FETCH_ASSOC); } function mud_ground_items(int $room_id) { $st = db()->prepare("SELECT g.id, i.key_name, i.name, i.descr FROM mud_ground g JOIN mud_items i ON i.id=g.item_id WHERE g.room_id=? ORDER BY g.id"); $st->execute([$room_id]); return $st->fetchAll(PDO::FETCH_ASSOC); } function mud_respawn(PDO $pdo = null): void { $pdo = $pdo ?: db(); $now = time(); $st = $pdo->prepare("SELECT s.id, s.mob_id, m.hp FROM mud_spawn s JOIN mud_mobs m ON m.id=s.mob_id WHERE s.alive=0 AND s.next_respawn<=?"); $st->execute([$now]); $dead = $st->fetchAll(PDO::FETCH_ASSOC); foreach ($dead as $d) { $pdo->prepare("UPDATE mud_spawn SET alive=1, hp=? WHERE id=?") ->execute([$d['hp'], $d['id']]); } } function mud_level_check(array &$c, PDO $pdo = null): ?string { $pdo = $pdo ?: db(); $need = $c['level'] * 50; if ($c['xp'] >= $need) { $c['xp'] -= $need; $c['level']++; $c['max_hp'] += 8; $c['atk'] += 2; $c['def'] += 1; $c['hp'] = $c['max_hp']; $pdo->prepare("UPDATE mud_chars SET level=?,max_hp=?,atk=?,def=?,xp=?,hp=? WHERE id=?") ->execute([$c['level'], $c['max_hp'], $c['atk'], $c['def'], $c['xp'], $c['hp'], $c['id']]); return "You reached level {$c['level']}! HP/ATK/DEF increased."; } return null; } // Resolve a single combat round. Returns array of messages. function mud_fight(int $spawn_id, array &$c, PDO $pdo = null): array { $pdo = $pdo ?: db(); // NOTE: select columns explicitly so s.hp (current spawn HP) is not // shadowed by m.hp (the mob's base HP) which would break HP tracking. $st = $pdo->prepare( "SELECT s.id AS id, s.hp AS hp, s.alive, s.next_respawn, m.name, m.descr, m.atk, m.def, m.xp, m.gold, m.loot, m.respawn FROM mud_spawn s JOIN mud_mobs m ON m.id=s.mob_id WHERE s.id=?"); $st->execute([$spawn_id]); $sp = $st->fetch(PDO::FETCH_ASSOC); if (!$sp || !$sp['alive']) return ["There is nothing to fight here."]; $msgs = []; // player hits $pDmg = max(1, ($c['atk'] + ($c['weapon'] ? (mud_item($c['weapon'])['atk'] ?? 0) : 0)) - $sp['def'] + rand(-1, 1)); $sp['hp'] -= $pDmg; $msgs[] = "You hit {$sp['name']} for $pDmg."; if ($sp['hp'] <= 0) { // victory $c['xp'] += $sp['xp']; $c['gold']+= $sp['gold']; $msgs[] = "{$sp['name']} dies! +{$sp['xp']} xp, +{$sp['gold']} gold."; // loot $loot = json_decode($sp['loot'], true); foreach ($loot as $lk) { $it = mud_item($lk); if ($it) { $c['inv'] = json_decode($c['inv'], true); $c['inv'][] = $lk; $c['inv'] = json_encode($c['inv']); $msgs[] = "You take {$it['name']}."; } } $pdo->prepare("UPDATE mud_spawn SET alive=0, next_respawn=? WHERE id=?") ->execute([time() + $sp['respawn'], $sp['id']]); mud_echo("{$c['name']} slew {$sp['name']}.", $c['room_id'], $pdo); $up = $pdo->prepare("UPDATE mud_chars SET xp=?,gold=?,inv=? WHERE id=?"); $up->execute([$c['xp'], $c['gold'], $c['inv'], $c['id']]); $lv = mud_level_check($c, $pdo); if ($lv) $msgs[] = $lv; return $msgs; } // mob hits back $mDmg = max(1, ($sp['atk'] - ($c['def'] + ($c['armor'] ? (mud_item($c['armor'])['def'] ?? 0) : 0))) + rand(-1, 1)); $c['hp'] -= $mDmg; $msgs[] = "{$sp['name']} hits you for $mDmg."; $pdo->prepare("UPDATE mud_spawn SET hp=? WHERE id=?")->execute([$sp['hp'], $sp['id']]); if ($c['hp'] <= 0) { $c['hp'] = 0; $msgs[] = "You have fallen! You wake in the Village Square."; mud_echo("{$c['name']} was slain by {$sp['name']} and fades away.", $c['room_id'], $pdo); // death: drop to start, lose a little gold, heal $lost = intval($c['gold'] * 0.2); $c['gold'] -= $lost; $c['room_id'] = 1; $c['hp'] = $c['max_hp']; $pdo->prepare("UPDATE mud_chars SET room_id=1,hp=?,gold=?,last_cmd_at=? WHERE id=?") ->execute([$c['hp'], $c['gold'], time(), $c['id']]); } else { $pdo->prepare("UPDATE mud_chars SET hp=? WHERE id=?")->execute([$c['hp'], $c['id']]); } return $msgs; } // ---- RPGBBS-style economy & PvP ---- function mud_players_in_room(int $room_id): array { return db()->query( "SELECT c.*, u.username AS username FROM mud_chars c JOIN users u ON u.id=c.user_id WHERE c.room_id=$room_id ORDER BY c.level DESC") ->fetchAll(PDO::FETCH_ASSOC); } // Find a player in the same room by a fragment of their character name or username. function mud_find_player_in_room(int $room_id, string $frag): ?array { $frag = strtolower($frag); foreach (mud_players_in_room($room_id) as $p) { if ($p['id'] == 0) continue; if (str_contains(strtolower($p['name']), $frag) || str_contains(strtolower($p['username']), $frag)) { return $p; } } return null; } function mud_shop_list(): array { return db()->query("SELECT * FROM mud_items WHERE price>0 ORDER BY price")->fetchAll(PDO::FETCH_ASSOC); } function mud_buy(array &$c, string $key): array { $pdo = db(); $it = mud_item($key); if (!$it || $it['price'] <= 0) return ["The trader doesn't sell that."]; if ($c['gold'] < $it['price']) return ["You can't afford the {$it['name']} ({$it['price']}g)."]; $c['gold'] -= $it['price']; $inv = json_decode($c['inv'], true); $inv[] = $it['key_name']; $c['inv'] = json_encode($inv); $pdo->prepare("UPDATE mud_chars SET gold=?,inv=? WHERE id=?")->execute([$c['gold'], $c['inv'], $c['id']]); return ["You buy {$it['name']} for {$it['price']}g."]; } function mud_deposit(array &$c, int $amt): array { $pdo = db(); $amt = max(0, min($amt, $c['gold'])); if ($amt <= 0) return ["You have no gold to bank."]; $c['gold'] -= $amt; $c['bank'] += $amt; $pdo->prepare("UPDATE mud_chars SET gold=?,bank=? WHERE id=?")->execute([$c['gold'], $c['bank'], $c['id']]); return ["You deposit {$amt}g. Bank balance: {$c['bank']}g."]; } function mud_withdraw(array &$c, int $amt): array { $pdo = db(); $amt = max(0, min($amt, $c['bank'])); if ($amt <= 0) return ["Nothing to withdraw."]; $c['bank'] -= $amt; $c['gold'] += $amt; $pdo->prepare("UPDATE mud_chars SET gold=?,bank=? WHERE id=?")->execute([$c['gold'], $c['bank'], $c['id']]); return ["You withdraw {$amt}g. You carry {$c['gold']}g."]; } // Sir Joe Mollicone, the Taxman: takes a 10% tithe (gold sink), safe room 11. function mud_tax(array &$c): array { $pdo = db(); $due = intval($c['gold'] * 0.10); if ($due <= 0) return ["Sir Joe squints. \"Come back when ye've coins to tithe.\""]; $c['gold'] -= $due; $pdo->prepare("UPDATE mud_chars SET gold=? WHERE id=?")->execute([$c['gold'], $c['id']]); return ["Sir Joe pockets {$due}g. \"That's the price of civilisation, adventurer.\""]; } function mud_leaderboard(int $limit = 10): array { return db()->query("SELECT name,class,level,gold,kills,deaths FROM mud_chars ORDER BY level DESC, gold DESC LIMIT $limit")->fetchAll(PDO::FETCH_ASSOC); } // PvP: attack another player in the same room (RPGBBS "live the game"). function mud_pvp(array &$att, array $def, PDO $pdo = null): array { $pdo = $pdo ?: db(); $msgs = []; if ($def['id'] === $att['id']) return ["You can't attack yourself."]; if ($def['room_id'] != $att['room_id']) return ["They aren't here."]; $aWep = $att['weapon'] ? (mud_item($att['weapon'])['atk'] ?? 0) : 0; $dWep = $def['weapon'] ? (mud_item($def['weapon'])['atk'] ?? 0) : 0; $aArm = $att['armor'] ? (mud_item($att['armor'])['def'] ?? 0) : 0; $dArm = $def['armor'] ? (mud_item($def['armor'])['def'] ?? 0) : 0; $aDmg = max(1, $att['atk'] + $aWep - $def['def'] - $dArm + rand(-1,1)); $dDmg = max(1, $def['atk'] + $dWep - $att['def'] - $aArm + rand(-1,1)); $att['hp'] -= $dDmg; $def['hp'] -= $aDmg; $msgs[] = "You strike {$def['name']} for $aDmg. {$def['name']} strikes you for $dDmg."; // attacker wins if ($def['hp'] <= 0 && $att['hp'] > 0) { $loot = intval($def['gold'] * 0.5); $att['gold'] += $loot; $att['kills']++; $def['gold'] -= $loot; $def['deaths']++; $def['room_id'] = 1; $def['hp'] = $def['max_hp']; $bounty = intval($def['bounty']); if ($bounty > 0) { $att['gold'] += $bounty; $def['bounty'] = 0; $msgs[] = "You collect the {$bounty}g bounty on {$def['name']}!"; } $msgs[] = "You slay {$def['name']}! +{$loot}g looted" . ($bounty? ", +{$bounty}g bounty." : "."); mud_echo("{$att['name']} cut down {$def['name']} in cold blood.", $att['room_id'], $pdo); $pdo->prepare("UPDATE mud_chars SET gold=?,kills=?,hp=?,room_id=? WHERE id=?") ->execute([$att['gold'], $att['kills'], $att['hp'], $att['room_id'], $att['id']]); $pdo->prepare("UPDATE mud_chars SET gold=?,deaths=?,bounty=?,hp=?,room_id=? WHERE id=?") ->execute([$def['gold'], $def['deaths'], $def['bounty'], $def['hp'], $def['id']]); $pdo->prepare("DELETE FROM mud_bounties WHERE target_id=?")->execute([$def['id']]); return $msgs; } // defender wins if ($att['hp'] <= 0 && $def['hp'] > 0) { $loot = intval($att['gold'] * 0.5); $def['gold'] += $loot; $def['kills']++; $att['gold'] -= $loot; $att['deaths']++; $att['room_id'] = 1; $att['hp'] = $att['max_hp']; $msgs[] = "{$def['name']} bests you! You lose {$loot}g and wake in the Village Square."; mud_echo("{$def['name']} cut down {$att['name']}.", $att['room_id'], $pdo); $pdo->prepare("UPDATE mud_chars SET gold=?,deaths=?,hp=?,room_id=? WHERE id=?") ->execute([$att['gold'], $att['deaths'], $att['hp'], $att['room_id'], $att['id']]); $pdo->prepare("UPDATE mud_chars SET gold=?,kills=? WHERE id=?") ->execute([$def['gold'], $def['kills'], $def['id']]); return $msgs; } // both survive a round $pdo->prepare("UPDATE mud_chars SET hp=? WHERE id=?")->execute([$att['hp'], $att['id']]); $pdo->prepare("UPDATE mud_chars SET hp=? WHERE id=?")->execute([$def['hp'], $def['id']]); return $msgs; } function mud_set_bounty(array &$c, array $target, int $amt): array { $pdo = db(); if ($target['id'] === $c['id']) return ["You can't bounty yourself."]; if ($amt <= 0) return ["A bounty needs to be worth something."]; if ($c['gold'] < $amt) return ["You can't post a {$amt}g bounty."]; $c['gold'] -= $amt; $pdo->prepare("UPDATE mud_chars SET bounty=bounty+?, gold=? WHERE id=?") ->execute([$amt, $c['gold'], $target['id']]); $pdo->prepare("INSERT INTO mud_bounties (target_id,by_id,amount,ts) VALUES (?,?,?,?)") ->execute([$target['id'], $c['id'], $amt, time()]); $pdo->prepare("UPDATE mud_chars SET gold=? WHERE id=?")->execute([$c['gold'], $c['id']]); return ["You post a {$amt}g bounty on {$target['name']}. The realm will remember."]; }