diff --git a/game.py b/game.py index 28bade8..fbfbacc 100644 --- a/game.py +++ b/game.py @@ -206,6 +206,10 @@ LEATHERS = [ # piece suffix, slot, noun, leather used, level above the tier, icon LEATHER_PIECES = [("cap", "helmet", "cap", 2, 2, "🧢"), ("legs", "legs", "trousers", 3, 7, "👖"), ("body", "body", "jerkin", 4, 11, "🦺")] +# Guard jerkins: city kit, not worn by players. Only the tier's leather, so a hunter can turn +# their own hides into them; every paid guard wears the best one in the stockpile. +GUARD_JERKIN_LEATHER = 12 +JERKIN_DEFENCE = 0.2 # defence per point of armour, per guard # Tailoring: a fibre from Farming, the cloth woven from it, and the light armour sewn from that. # fibre (id, name, icon, value), farming level, seconds, xp, cloth (id, name, value), armour id prefix, armour name prefix CLOTHS = [ @@ -375,6 +379,11 @@ for _tier, (_raw, _leather, _prefix, _label) in enumerate(LEATHERS): } if _noun == "trousers": ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"] + ITEMS[f"{_prefix}_guard_jerkin"] = { + "name": f"{_label} guard jerkin", "icon": "🦺", "kit": "jerkin", "tier": _tier, + "armour": _armour_stats("medium", "body", _tier)["armour"], + "value": round(GUARD_JERKIN_LEATHER * _leather[2] * 1.3), + } for _tier, (_fibre, _level, _time, _xp, _cloth, _prefix, _label) in enumerate(CLOTHS): ITEMS.setdefault(_fibre[0], {"name": _fibre[1], "icon": _fibre[2], "value": _fibre[3]}) ITEMS[_cloth[0]] = {"name": _cloth[1], "icon": "🧶", "value": _cloth[2]} @@ -608,6 +617,10 @@ def _tiered_actions() -> list[dict]: 25 + 5 * used + 2 * tier, used * piece_xp[tier], {leather[0]: used, **({reagent("medium", tier): REAGENT_COUNTS[slot]} if tier else {})}, {f"{prefix}_{suffix}": 1}, group=label)) + actions.append(_action(f"lw_{prefix}_guard_jerkin", "leatherworking", f"{label} guard jerkin", + TIER_LEVELS[tier] + 9, 25 + 5 * GUARD_JERKIN_LEATHER + 2 * tier, 4 * piece_xp[tier], + {leather[0]: GUARD_JERKIN_LEATHER}, {f"{prefix}_guard_jerkin": 1}, + group=label)) for tier, (fibre, level, seconds, fibre_xp, cloth, prefix, label) in enumerate(CLOTHS): if tier: # flax is grown with the other building materials actions.append(_action(f"grow_{fibre[0]}", "farming", FIBRE_VERBS.get(fibre[0], f"Grow {fibre[1].lower()}"), @@ -2087,7 +2100,7 @@ def city_uses(item: str) -> list[str]: spec = ITEMS[item] if "food" in spec: uses.append("the granary") - if spec.get("slot") in ("weapon", "shield") or item == "arrows": + if spec.get("slot") in ("weapon", "shield") or "kit" in spec or item == "arrows": uses.append("the armoury") if item == "water_barrel": uses.append("the fire brigade") @@ -4184,10 +4197,15 @@ def population_view(world: dict) -> dict: } +def kit_slot(item: str) -> str | None: + """Where a defender carries it: a gear slot, or city-only kit like guard jerkins.""" + return ITEMS[item].get("slot") or ITEMS[item].get("kit") + + def best_kit(stock: dict, slot: str, stat: str, people: int) -> float: """Total `stat` when the best `slot` items in the stockpile go to `people` defenders.""" total = 0 - for item in sorted((i for i in stock if ITEMS[i].get("slot") == slot), key=lambda i: -ITEMS[i].get(stat, 0)): + for item in sorted((i for i in stock if kit_slot(i) == slot), key=lambda i: -ITEMS[i].get(stat, 0)): take = min(people, stock[item]) total += take * ITEMS[item].get(stat, 0) people -= take @@ -4197,26 +4215,26 @@ def best_kit(stock: dict, slot: str, stat: str, people: int) -> float: def kit_count(stock: dict, slot: str) -> int: - return sum(qty for item, qty in stock.items() if ITEMS[item].get("slot") == slot) + return sum(qty for item, qty in stock.items() if kit_slot(item) == 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.""" + """Share of the issued swords, shields and jerkins 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.""" + """Break `share` of the swords, shields and jerkins handed to defenders, worst first. Spares are untouched.""" stock = world["city"]["stockpile"] broken = {} - for slot, stat in (("weapon", "power"), ("shield", "armour")): + for slot, stat in (("weapon", "power"), ("shield", "armour"), ("jerkin", "armour")): people = guards_on_duty(world) issued = [] if not people: continue - for item in sorted((i for i in stock if ITEMS[i].get("slot") == slot), key=lambda i: -ITEMS[i].get(stat, 0)): + for item in sorted((i for i in stock if kit_slot(i) == slot), key=lambda i: -ITEMS[i].get(stat, 0)): take = min(people, stock[item]) issued.append((item, take)) people -= take @@ -4277,6 +4295,7 @@ def defence(world: dict, guards: int | None = None) -> dict: "militia": round(militia * armed / posts, 1) if posts else 0, "weapons": round(0.35 * best_kit(stock, "weapon", "power", armed), 1), "shields": round(0.5 * best_kit(stock, "shield", "armour", armed), 1), + "jerkins": round(JERKIN_DEFENCE * best_kit(stock, "jerkin", "armour", armed), 1), "arrows": min(100, stock.get("arrows", 0) // 10), "drilled": round(drilled(world), 1), "rallied": city.get("rally", 0), # favour bought before drilling existed; gone after the next raid @@ -6133,6 +6152,7 @@ def _assign_glyphs() -> None: ITEMS[leather[0]].update(glyph="rolled-cloth", tint=tint) for suffix, glyph in LEATHER_GLYPHS.items(): ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint) + ITEMS[f"{prefix}_guard_jerkin"].update(glyph="leather-armor", tint=tint) for trophy, (_name, line, tier) in TROPHIES.items(): ITEMS[trophy].update(glyph=TROPHY_GLYPHS[line], tint=TIER_TINTS[tier - 1]) for item, (weight, tier) in REAGENTS.items(): @@ -6220,6 +6240,7 @@ ORDER_RECIPES, ORDER_RAW = _order_sources() # line here (new / balance / fix), and a new release a new entry on top. PATCH_NOTES = [ {"date": "2026-10-27", "title": "Prayer", "notes": [ + ("new", "Added resource dumps for Leatherworking: Guard Jerkins"), ("fix", "+Q button: rounded corners."), ("fix", "Make box: number field sits in front of +Q."), ("new", "Crafting queue menu: tap ⏳ in the top bar to reorder, change counts, skip or clear."), @@ -6347,7 +6368,7 @@ def content() -> dict: "buildings": buildings, "xp_table": XP_TABLE, "offline_cap": OFFLINE_CAP, "tiers": TIERS, "city_uses": {item: city_uses(item) for item in ITEMS}, "prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS, - "potion_charges": POTION_CHARGES, "order_recipes": ORDER_RECIPES, "order_raw": ORDER_RAW, + "potion_charges": POTION_CHARGES, "jerkin_defence": JERKIN_DEFENCE, "order_recipes": ORDER_RECIPES, "order_raw": ORDER_RAW, "order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "make_max": MAKE_MAX, "queue_max": QUEUE_MAX, "input_share": INPUT_SHARE, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()), "equip_slots": EQUIP_SLOTS, "class_speed": CLASS_SPEED, "rest_rate": REST_RATE, "bite": BITE, "eat_below": EAT_BELOW, "eat_choices": EAT_CHOICES, "abilities": combat.ABILITIES, "enemy_abilities": combat.ENEMY_ABILITIES, "unlock": combat.UNLOCK, "auto_cd": combat.AUTO_CD, "watch": combat.WATCH, "gcd": combat.GCD, diff --git a/static/app.js b/static/app.js index f39a581..bfda724 100644 --- a/static/app.js +++ b/static/app.js @@ -244,7 +244,7 @@ function replayBattle(id) { // strong as its share of the total. Theirs: the raiders' strength split into bands, or the one dragon. // `every` is how often each swings, in seconds of the replay; walls and the ram only take blows. const SQUADS = [ - { id: "guards", parts: ["militia", "weapons", "shields"], name: "Guards", glyph: "swordman", dmg: "slash", every: 2 }, + { id: "guards", parts: ["militia", "weapons", "shields", "jerkins"], name: "Guards", glyph: "swordman", dmg: "slash", every: 2 }, { id: "archers", parts: ["arrows"], name: "Archers", glyph: "bow-arrow", dmg: "pierce", every: 1.8 }, { id: "walls", parts: ["walls"], name: "Walls", glyph: "stone-wall", dmg: "blunt", still: true }, { id: "folk", parts: ["townsfolk", "drilled", "rallied"], name: "Townsfolk", glyph: "farmer", dmg: "blunt", every: 2.8 }, @@ -1689,7 +1689,8 @@ function itemStats(id) { const st = C.styles[it.style], trade = Object.entries(C.class_style).find(([, s]) => s === it.style)?.[0]; parts.push(`${it.two_handed ? "two-handed " : ""}${st.name}, ${st.interval}s${trade ? ` (${C.classes[trade].name.toLowerCase()})` : ""}`); } - if (it.armour || it.ward) parts.push(gearStats({ ...it, power: 0 })); + if (it.kit) parts.push(`+${Math.round(it.armour * C.jerkin_defence * 10) / 10} city defence per paid guard`); + else if (it.armour || it.ward) parts.push(gearStats({ ...it, power: 0 })); if (it.reagent) parts.push(it.reagent === "jewel" ? "Reagent for jewellery" : `Reagent for ${C.weights[it.reagent].name.toLowerCase()} armour`); if (it.jewel && !["power", "ward"].includes(it.jewel)) parts.push(jewelText(it)); if (it.tool || it.station) parts.push(`${Math.round(it.speed * 100)}% faster ${skillOf(it.tool || it.station).name.toLowerCase()}`); @@ -2250,7 +2251,7 @@ function favourSpends() { function raidPrep(ev) { const city = S.city, me = S.me, d = city.defence; - const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", drilled: "Drilled militia", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Paid guards (barracks)", weapons: "Swords the guards hold", shields: "Shields the guards hold", arrows: "Arrows", garrison: "Garrison" }; + const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", drilled: "Drilled militia", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Paid guards (barracks)", weapons: "Swords the guards hold", shields: "Shields the guards hold", jerkins: "Jerkins the guards wear", arrows: "Arrows", garrison: "Garrison" }; const full = d.posts * d.wage, owed = Math.max(0, full - d.wages); const reqs = (ev.requests || []).map((r) => { const mine = me.bank[r.item] || 0; @@ -2267,7 +2268,7 @@ function raidPrep(ev) {
Only paid guards stand, and only they hold the armoury's swords and shields. Paying earns favour.
+Only paid guards stand, and only they get the armoury's swords, shields and jerkins. Paying earns favour.
| ${names[k]} | ${fmt(v)} |
| Hungry (×0.8) |