Delete your own board messages; drop chips name their item

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-19 09:56:11 +01:00
parent 70246f78ea
commit 2eed9b7ec4
4 changed files with 28 additions and 4 deletions

11
app.py
View file

@ -330,6 +330,14 @@ class Handler(BaseHTTPRequestHandler):
return "There's no raid to stand guard for" return "There's no raid to stand guard for"
player["garrison"] = bool(body.get("join")) player["garrison"] = bool(body.get("join"))
return None return None
if path == "/api/board/delete":
message = next((m for m in world["board"] if m.get("id") == body.get("id")), None)
if not message:
return "That message is gone"
if message["who"] != player["id"]:
return "You can only delete your own messages"
world["board"].remove(message)
return None
if path == "/api/aid": if path == "/api/aid":
return game.aid_problem(world, player, str(body.get("kind")), str(body.get("item")), str(body.get("source")), now) return game.aid_problem(world, player, str(body.get("kind")), str(body.get("item")), str(body.get("source")), now)
if path == "/api/hire": if path == "/api/hire":
@ -358,7 +366,8 @@ class Handler(BaseHTTPRequestHandler):
text = str(body.get("text", "")).strip()[:500] text = str(body.get("text", "")).strip()[:500]
if not text: if not text:
return "Say something" return "Say something"
world["board"].append({"at": now, "who": player["id"], "text": text}) world["next_message_id"] = world.get("next_message_id", 1) + 1
world["board"].append({"id": world["next_message_id"] - 1, "at": now, "who": player["id"], "text": text})
del world["board"][:-game.MAX_LOG] del world["board"][:-game.MAX_LOG]
return None return None
return "Not found" return "Not found"

View file

@ -496,7 +496,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world --------------------------------------------------------------- # --- world ---------------------------------------------------------------
WORLD_VERSION = 8 WORLD_VERSION = 9
def new_world(now: float, speed: float = 1.0) -> dict: def new_world(now: float, speed: float = 1.0) -> dict:
@ -528,6 +528,7 @@ def new_world(now: float, speed: float = 1.0) -> dict:
"next_event_id": 1, "next_event_id": 1,
"ledger": [], "ledger": [],
"board": [], "board": [],
"next_message_id": 1,
} }
@ -592,6 +593,12 @@ def migrate(world: dict, now: float | None = None) -> dict:
world.setdefault("next_hardship_at", now + 24 * HOUR / speed(world)) world.setdefault("next_hardship_at", now + 24 * HOUR / speed(world))
world.setdefault("next_fortune_at", now + 8 * HOUR / speed(world)) world.setdefault("next_fortune_at", now + 8 * HOUR / speed(world))
world["version"] = 8 world["version"] = 8
if world["version"] < 9:
# Board messages get ids so their authors can delete them.
for number, message in enumerate(world["board"], 1):
message["id"] = number
world["next_message_id"] = len(world["board"]) + 1
world["version"] = 9
return world return world

View file

@ -471,7 +471,7 @@ function actionCard(a) {
if (a.skill === "combat") { if (a.skill === "combat") {
const chance = Math.min(0.97, Math.max(0.15, 0.5 + (me.power - a.difficulty) * 0.04)); const chance = Math.min(0.97, Math.max(0.15, 0.5 + (me.power - a.difficulty) * 0.04));
extra = `<div class="small">Win chance <b>${Math.round(chance * 100)}%</b> · about <b>${me.fight_damage[a.id]}</b> damage a fight${a.gold ? ` · ${a.gold} gold` : ""}</div>`; extra = `<div class="small">Win chance <b>${Math.round(chance * 100)}%</b> · about <b>${me.fight_damage[a.id]}</b> damage a fight${a.gold ? ` · ${a.gold} gold` : ""}</div>`;
if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${Math.round(odds * 100)}% chance">${item(id).icon} ${q} <span class="have">${Math.round(odds * 100)}%</span></span>`).join(""); if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${esc(item(id).name)}: ${Math.round(odds * 100)}% chance">${item(id).icon} ${q} <span class="have">${Math.round(odds * 100)}%</span></span>`).join("");
if (a.special === "sabotage") extra += `<div class="small muted">Each win weakens the coming raid by 3%, up to 30%.</div>`; if (a.special === "sabotage") extra += `<div class="small muted">Each win weakens the coming raid by 3%, up to 30%.</div>`;
} }
if (a.special === "recon") extra = `<div class="small muted">+${a.recon} recon on the next event</div>`; if (a.special === "recon") extra = `<div class="small muted">+${a.recon} recon on the next event</div>`;
@ -818,7 +818,9 @@ function raidPrep(ev) {
// --- board & roster ------------------------------------------------------- // --- board & roster -------------------------------------------------------
function board() { function board() {
const msgs = S.board.map((m) => `<div class="msg"><span class="who">${esc(playerName(m.who))}</span><span class="when">${ago(m.at)}</span><div class="text">${esc(m.text)}</div></div>`).join(""); const msgs = S.board.map((m) => `<div class="msg"><span class="who">${esc(playerName(m.who))}</span><span class="when">${ago(m.at)}</span>
${m.who === S.uid && m.id ? `<button class="msg-delete" data-unpost="${m.id}" title="Delete this message">✕</button>` : ""}
<div class="text">${esc(m.text)}</div></div>`).join("");
const ledger = [...S.ledger].reverse().map((l) => `<li>${esc(l.text)}<span class="when">${ago(l.at)}</span></li>`).join(""); const ledger = [...S.ledger].reverse().map((l) => `<li>${esc(l.text)}<span class="when">${ago(l.at)}</span></li>`).join("");
return `<div class="cols"> return `<div class="cols">
<div class="panel"><h2>Town board</h2> <div class="panel"><h2>Town board</h2>
@ -1000,6 +1002,10 @@ document.addEventListener("click", (e) => {
if (d.favour) return act("/api/favour", { kind: d.favour }); if (d.favour) return act("/api/favour", { kind: d.favour });
if (d.food) return act("/api/food", { item: d.food }); if (d.food) return act("/api/food", { item: d.food });
if (d.potion) return act("/api/potion", { item: d.potion }); if (d.potion) return act("/api/potion", { item: d.potion });
if (d.unpost) {
if (!confirm("Delete this message?")) return;
return act("/api/board/delete", { id: Number(d.unpost) });
}
if (d.aid) return act("/api/aid", { kind: d.aid, item: d.item, source: d.source }); if (d.aid) return act("/api/aid", { kind: d.aid, item: d.item, source: d.source });
if ("hire" in d) return act("/api/hire", {}); if ("hire" in d) return act("/api/hire", {});
if (d.devHappen) return act("/api/dev/happen", { track: d.devHappen }); if (d.devHappen) return act("/api/dev/happen", { track: d.devHappen });

View file

@ -395,3 +395,5 @@ button.sheet-skill:hover { background: var(--panel-2); }
.happening .grow { display: flex; flex-direction: column; gap: 4px; } .happening .grow { display: flex; flex-direction: column; gap: 4px; }
.happening > .btn, .happening > .small { align-self: center; } .happening > .btn, .happening > .small { align-self: center; }
.skill-happenings { flex-direction: column; align-items: flex-start; gap: 4px; } .skill-happenings { flex-direction: column; align-items: flex-start; gap: 4px; }
.msg-delete { background: none; border: 0; color: var(--muted); padding: 0 4px; margin-left: 4px; font-size: 12px; }
.msg-delete:hover { color: var(--bad); }