First git commit

This commit is contained in:
root
2026-09-08 18:21:10 +01:00
commit 66911dc450
65 changed files with 7012 additions and 0 deletions

305
public_html/lib/mud.php Normal file
View 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."];
}