Raids wear out the armoury: issued swords and shields break, worst first
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f2e4ea39de
commit
366a195c6d
4 changed files with 91 additions and 10 deletions
|
|
@ -53,6 +53,8 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
|
|||
where their camp is, so fighters can sabotage it. Defeat kills or drives
|
||||
off people, loots the stockpile and treasury, and may damage a building.
|
||||
Damaged buildings stop working until repaired but keep their level.
|
||||
Every fight uses up arrows and breaks some of the swords and shields
|
||||
handed out (worst first), more in a close fight or a defeat.
|
||||
Now and then a dragon comes instead of bandits.
|
||||
- **Happenings:** three more tracks run on their own cooldowns. Minor threats
|
||||
(wolves, a mine collapse, goblin thieves…) slow or block a skill until
|
||||
|
|
|
|||
65
game.py
65
game.py
|
|
@ -1448,6 +1448,45 @@ def kit_count(stock: dict, slot: str) -> int:
|
|||
return sum(qty for item, qty in stock.items() if ITEMS[item].get("slot") == slot)
|
||||
|
||||
|
||||
def wear_share(ratio: float) -> float:
|
||||
"""Share of the issued swords and shields a fight breaks: 10% in an easy win, 30% in a close one, 40% in defeat."""
|
||||
if ratio < 1:
|
||||
return 0.4
|
||||
return 0.1 + 0.2 * max(0.0, 2 - ratio)
|
||||
|
||||
|
||||
def wear_kit(world: dict, share: float, rng: random.Random) -> dict:
|
||||
"""Break `share` of the swords and shields handed to defenders, worst first. Spares are untouched."""
|
||||
stock = world["city"]["stockpile"]
|
||||
broken = {}
|
||||
for slot, stat in (("weapon", "power"), ("shield", "armour")):
|
||||
people = int(world["city"]["population"] // 2)
|
||||
issued = []
|
||||
for item in sorted((i for i in stock if ITEMS[i].get("slot") == slot), key=lambda i: -ITEMS[i].get(stat, 0)):
|
||||
take = min(people, stock[item])
|
||||
issued.append((item, take))
|
||||
people -= take
|
||||
if not people:
|
||||
break
|
||||
breaks = int(sum(take for _item, take in issued) * share + rng.random())
|
||||
for item, take in reversed(issued):
|
||||
lost = min(breaks, take)
|
||||
if lost:
|
||||
take_stock_qty(stock, item, lost)
|
||||
broken[item] = lost
|
||||
breaks -= lost
|
||||
return broken
|
||||
|
||||
|
||||
def _wear_lines(broken: dict, arrows: int) -> list[str]:
|
||||
lines = []
|
||||
if arrows:
|
||||
lines.append(f"The defenders used {amount('arrows', arrows)}.")
|
||||
if broken:
|
||||
lines.append(f"{upper_first(_listing(broken))} {'was' if sum(broken.values()) == 1 else 'were'} broken in the fighting.")
|
||||
return lines
|
||||
|
||||
|
||||
def equip_problem(player: dict, item: str) -> str | None:
|
||||
"""Why the player can't wear or use this yet, or None."""
|
||||
req = ITEMS[item].get("req")
|
||||
|
|
@ -1752,9 +1791,11 @@ def _resolve_raid(world: dict, event: dict, now: float, rng: random.Random) -> N
|
|||
loot = round(attack * 2)
|
||||
city["treasury"] += loot
|
||||
city["repelled"] += 1
|
||||
_use_up(stock, "arrows", 0.3)
|
||||
broken = wear_kit(world, wear_share(ratio), rng)
|
||||
arrows = _use_up(stock, "arrows", 0.3)
|
||||
report["title"] = f"Victory! {upper_first(event["group"])} were driven off"
|
||||
lines.append(f"The raiders fled and left {loot} gold worth of plunder behind.")
|
||||
lines.extend(_wear_lines(broken, arrows))
|
||||
for entry in garrison:
|
||||
player = world["players"][entry["id"]]
|
||||
player["favour"] += 25
|
||||
|
|
@ -1764,6 +1805,7 @@ def _resolve_raid(world: dict, event: dict, now: float, rng: random.Random) -> N
|
|||
log(world, now, "raid", f"{upper_first(event["group"])} were driven off")
|
||||
else:
|
||||
severity = min(1.0, 1 - ratio)
|
||||
broken = wear_kit(world, wear_share(ratio), rng)
|
||||
shelter = 1 - 0.08 * building_level(world, "walls")
|
||||
lost_people = int(city["population"] * (0.1 + 0.3 * severity))
|
||||
city["population"] = max(5.0, city["population"] - lost_people)
|
||||
|
|
@ -1778,9 +1820,10 @@ def _resolve_raid(world: dict, event: dict, now: float, rng: random.Random) -> N
|
|||
del stock[item]
|
||||
gold = int(city["treasury"] * share)
|
||||
city["treasury"] -= gold
|
||||
_use_up(stock, "arrows", 0.6)
|
||||
arrows = _use_up(stock, "arrows", 0.6)
|
||||
report["title"] = f"Defeat. {upper_first(event["group"])} broke into the city"
|
||||
lines.append(f"{lost_people} townsfolk were killed or fled.")
|
||||
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 "."))
|
||||
|
|
@ -1817,9 +1860,11 @@ def _resolve_dragon(world: dict, event: dict, now: float, rng: random.Random) ->
|
|||
if guard["total"] >= attack:
|
||||
city["repelled"] += 1
|
||||
city["treasury"] += round(attack)
|
||||
_use_up(stock, "arrows", 0.5)
|
||||
broken = wear_kit(world, min(0.5, wear_share(guard["total"] / attack) * 1.25), rng)
|
||||
arrows = _use_up(stock, "arrows", 0.5)
|
||||
report["title"] = f"Victory! {upper_first(event['group'])} was driven off"
|
||||
lines.append(f"It fled wounded, dropping a hoard worth {round(attack):,} gold.")
|
||||
lines.extend(_wear_lines(broken, arrows))
|
||||
for entry in garrison:
|
||||
player = world["players"][entry["id"]]
|
||||
player["favour"] += 40
|
||||
|
|
@ -1831,15 +1876,17 @@ def _resolve_dragon(world: dict, event: dict, now: float, rng: random.Random) ->
|
|||
log(world, now, "raid", report["title"])
|
||||
else:
|
||||
severity = min(1.0, 1 - guard["total"] / attack)
|
||||
broken = wear_kit(world, 0.5, rng)
|
||||
lost = int(city["population"] * (0.05 + 0.15 * severity))
|
||||
city["population"] = max(5.0, city["population"] - lost)
|
||||
share = min(0.8, 0.2 + 0.4 * severity)
|
||||
burned = _take_share(stock, WOOD, share * (1 - 0.25 * building_level(world, "lumber_shed")))
|
||||
burned.update(_take_share(stock, _food_items(stock), share * (1 - 0.25 * building_level(world, "granary"))))
|
||||
name = _damage_building(world, now, rng)
|
||||
_use_up(stock, "arrows", 0.7)
|
||||
arrows = _use_up(stock, "arrows", 0.7)
|
||||
report["title"] = f"{upper_first(event['group'])} set the city ablaze"
|
||||
lines.append(f"{lost} townsfolk were lost in the flames.")
|
||||
lines.extend(_wear_lines(broken, arrows))
|
||||
if burned:
|
||||
lines.append(f"It burned {_listing(burned)}.")
|
||||
if name:
|
||||
|
|
@ -1865,11 +1912,11 @@ def _work_problem(world: dict, player: dict, action: dict, now: float) -> None:
|
|||
_solve(world, problem, now, player["name"])
|
||||
|
||||
|
||||
def _use_up(stock: dict, item: str, share: float) -> None:
|
||||
if stock.get(item):
|
||||
stock[item] -= int(stock[item] * share)
|
||||
if not stock[item]:
|
||||
del stock[item]
|
||||
def _use_up(stock: dict, item: str, share: float) -> int:
|
||||
used = int(stock.get(item, 0) * share)
|
||||
if used:
|
||||
take_stock_qty(stock, item, used)
|
||||
return used
|
||||
|
||||
|
||||
# --- happenings: weather, seasons, hardships, fortunes, minor threats ---------
|
||||
|
|
|
|||
|
|
@ -861,7 +861,7 @@ function raidPrep(ev) {
|
|||
}).join("");
|
||||
return `<div class="cols">
|
||||
<div class="panel"><h2>Council requests</h2>${reqs}
|
||||
<p class="small muted">Arrows, swords and shields in the stockpile arm the defenders. Food keeps them fighting.</p></div>
|
||||
<p class="small muted">Arrows, swords and shields in the stockpile arm the defenders: one sword and shield each, best first. Every fight uses up arrows and breaks some of the gear, more when it's close. Food keeps them fighting.</p></div>
|
||||
<div class="panel"><h2>Defence</h2>
|
||||
<table class="parts">${Object.entries(d.parts).map(([k, v]) => `<tr><td>${names[k]}</td><td>${fmt(v)}</td></tr>`).join("")}
|
||||
${d.starving ? `<tr><td class="bad">Hungry (×0.8)</td><td></td></tr>` : ""}</table>
|
||||
|
|
|
|||
|
|
@ -1025,6 +1025,38 @@ class HappeningsTest(unittest.TestCase):
|
|||
|
||||
|
||||
|
||||
class WearTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.world = world_with(("fred", "warrior"))
|
||||
self.city = self.world["city"]
|
||||
self.city["population"] = 40.0 # 20 defenders
|
||||
|
||||
def test_wear_follows_how_close_the_fight_was(self):
|
||||
self.assertAlmostEqual(game.wear_share(3), 0.1)
|
||||
self.assertAlmostEqual(game.wear_share(1.5), 0.2)
|
||||
self.assertAlmostEqual(game.wear_share(1), 0.3)
|
||||
self.assertAlmostEqual(game.wear_share(0.5), 0.4)
|
||||
|
||||
def test_only_issued_gear_breaks_worst_first(self):
|
||||
self.city["stockpile"] = {"iron_sword": 10, "copper_sword": 30}
|
||||
rng = random.Random()
|
||||
rng.random = lambda: 0.0
|
||||
broken = game.wear_kit(self.world, 0.4, rng)
|
||||
# 20 defenders take 10 iron and 10 copper; 8 break, all copper.
|
||||
self.assertEqual(broken, {"copper_sword": 8})
|
||||
self.assertEqual(self.city["stockpile"], {"iron_sword": 10, "copper_sword": 22})
|
||||
|
||||
def test_a_won_raid_breaks_some_kit_and_reports_it(self):
|
||||
self.city["stockpile"] = {"copper_sword": 20, "arrows": 100}
|
||||
event = {"strength": 1, "sabotage": 0, "group": "the Red Hand"}
|
||||
game._resolve_raid(self.world, event, T0, random.Random(1))
|
||||
self.assertLess(self.city["stockpile"]["copper_sword"], 20)
|
||||
self.assertEqual(self.city["stockpile"]["arrows"], 70)
|
||||
text = " ".join(event["report"]["lines"])
|
||||
self.assertIn("used 30 arrows", text)
|
||||
self.assertIn("broken in the fighting", text)
|
||||
|
||||
|
||||
class FittingTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.world = world_with(("fred", "artisan"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue