First git commit
This commit is contained in:
305
public_html/lib/mud.php
Normal file
305
public_html/lib/mud.php
Normal file
@ -0,0 +1,305 @@
|
||||
<?php
|
||||
// MUD engine shared by the WAP/XHTML front-end (mud.php).
|
||||
// The telnet BBS (bbs/mud.py) implements the same rules against the same
|
||||
// tables, so a character, room and mob exist once and can be played from
|
||||
// either front-end. All mutations go through these functions.
|
||||
|
||||
const MUD_START_HP = 30;
|
||||
const MUD_START_ATK = 4;
|
||||
const MUD_START_DEF = 2;
|
||||
const MUD_RESPAWN_SEC = 30;
|
||||
|
||||
// RPGBBS-style classes: small combat identity + a flavour bonus.
|
||||
$MUD_CLASSES = [
|
||||
'fighter' => ['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."];
|
||||
}
|
||||
Reference in New Issue
Block a user