Raids: alarm at the gates, battle replay and report popup for everyone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 13:12:12 +01:00
parent fcaabf5ae2
commit a35bcd6512
5 changed files with 146 additions and 7 deletions

39
game.py
View file

@ -743,7 +743,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
WORLD_VERSION = 14
WORLD_VERSION = 15
def new_world(now: float, speed: float = 1.0) -> dict:
@ -872,9 +872,29 @@ def migrate(world: dict, now: float | None = None) -> dict:
stop(player, "Shields are made in Construction now")
_return_overfunding(world)
world["version"] = 14
if world["version"] < 15:
# Raid reports pop up once for everyone; ones from before that don't.
latest = last_threat(world)
for player in world["players"].values():
player["seen_threat"] = latest["id"] if latest else 0
world["version"] = 15
return world
def last_threat(world: dict) -> dict | None:
"""The most recent raid or dragon attack that has played out."""
return next((e for e in reversed(world["events"]) if e["status"] == "done" and e["kind"] in THREATS
and e.get("report")), None)
def unseen_threat(world: dict, player: dict) -> dict | None:
"""The latest raid or dragon attack this player hasn't watched yet, if it happened while they were here."""
event = last_threat(world)
if event and event["id"] > player.get("seen_threat", 0) and event["resolved"] >= player["joined"]:
return event
return None
def _migrate_potions(world: dict) -> None:
"""Potions from before the tiers become their tiered equivalents, uses and all."""
def renamed(stock: dict) -> None:
@ -2221,16 +2241,22 @@ SIZE_LABELS = [(30, "small"), (60, "medium"), (100, "large"), (math.inf, "huge")
WINDOW_HOURS = [24, 8, 2, 0]
THREATS = ("raid", "dragon")
GATES_WARNING = 300 # seconds of warning before a threat strikes
def event_view(world: dict, event: dict, now: float) -> dict:
"""What the city knows about an upcoming event, filtered by intel."""
level = intel_level(world, event, now)
half = WINDOW_HOURS[level] * HOUR / speed(world)
costs = intel_costs(event) if "strength" in event else [0, 0, 0]
centre = event["fire_at"] + event["jitter"] * half * 0.6
# In the last few minutes the sentries see them coming, whatever the intel.
gates = event["kind"] in THREATS and event["fire_at"] - now <= GATES_WARNING / speed(world)
view = {
"id": event["id"], "kind": event["kind"], "intel": level,
"id": event["id"], "kind": event["kind"], "intel": level, "at_gates": gates,
"earliest": max(now, centre - half), "latest": max(now, centre + half),
"exact": event["fire_at"] if level >= 3 else None,
"exact": event["fire_at"] if level >= 3 or gates else None,
"recon": int(min(event["recon"], costs[-1])), "recon_full": costs[-1],
"recon_next": max(0, round(costs[level] - event["recon"])) if level < 3 else 0,
}
@ -2250,6 +2276,8 @@ def event_view(world: dict, event: dict, now: float) -> dict:
view["strength"] = round(strength)
view["trait"] = event["trait"]
view["requests"] = council_requests(world, event)
if gates:
view["headline"] = "The dragon is overhead!"
elif event["kind"] == "raid":
strength = event["strength"] * (1 - event["sabotage"])
view["headline"] = "Travellers speak of trouble on the roads."
@ -2269,6 +2297,8 @@ def event_view(world: dict, event: dict, now: float) -> dict:
view["camp"] = True
view["sabotage"] = round(event["sabotage"] * 100)
view["requests"] = council_requests(world, event)
if gates:
view["headline"] = "Raiders at the gates!"
elif event["kind"] == "merchant":
view["headline"] = "A merchant caravan is on its way. Sell prices +50% while it's here."
else:
@ -2378,7 +2408,8 @@ def _resolve_raid(world: dict, event: dict, now: float, rng: random.Random) -> N
lines.extend(_wear_lines(broken, arrows))
if gold or taken:
stolen = ", ".join(amount(item, qty) for item, qty in taken.items())
lines.append(f"The raiders carried off {gold:,} gold" + (f" and {stolen}." if stolen else "."))
loot = [f"{gold:,} gold"] if gold else []
lines.append("The raiders carried off " + " and ".join(loot + ([stolen] if stolen else [])) + ".")
built = [key for key, b in city["buildings"].items() if b["level"] and not b["damaged"]]
if severity > 0.35 and built:
key = rng.choice(built)