Shorter descriptions throughout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 08:01:57 +01:00
parent 5898e2006f
commit 61d8b2fce3
2 changed files with 146 additions and 153 deletions

124
game.py
View file

@ -281,14 +281,14 @@ def describe_potion(skill: str, effect: dict) -> str:
if "speed" in effect:
return f"{name.title()} {round(effect['speed'] * 100)}% faster"
if "double" in effect:
return f"{name.title()}: {round(effect['double'] * 100)}% chance of double yield"
return f"{name.title()}: {round(effect['double'] * 100)}% double yield"
if "keep" in effect:
return f"{name.title()}: {round(effect['keep'] * 100)}% chance to keep your materials"
return f"{name.title()}: {round(effect['keep'] * 100)}% save materials"
if "bones" in effect:
return f"+{round(effect['bones'] * 100)}% prayer points and devotion from bones"
return f"+{round(effect['bones'] * 100)}% from bones"
if "power" in effect:
return f"+{effect['power']} combat power"
return f"-{round(effect['reduce'] * 100)}% damage taken in fights"
return f"+{effect['power']} power"
return f"-{round(effect['reduce'] * 100)}% damage taken"
for _id, _name, _icon, _level, _time, _xp, _value in HERBS:
@ -328,31 +328,31 @@ CLASSES = {
"miner": {
"name": "Miner", "icon": "⛏️", "fast": ["mining", "smithing"], "craft": ["smithing"],
"bonus": "Mining and smithing 20% faster",
"masterwork": "Masterwork: 1 in 10 smithing crafts make double",
"signature": "Prospector: 1 in 10 mining trips bring back double",
"combat": "Warhammer: +15% damage with one. Slow, heavy, crushes golems",
"masterwork": "Masterwork: 10% double smithing crafts",
"signature": "Prospector: 10% double ore",
"combat": "Warhammer: +15% damage",
},
"builder": {
"name": "Builder", "icon": "🏗️", "fast": ["woodcutting", "construction"], "craft": ["construction"],
"bonus": "Woodcutting and construction (masonry included) 20% faster",
"masterwork": "Masterwork: 1 in 10 construction crafts make double",
"signature": "Foreman: +1 labour every time you work a building site",
"combat": "Sword and shield: +15% damage with a sword. The shield blocks, and trolls fall to blades",
"bonus": "Woodcutting and construction 20% faster",
"masterwork": "Masterwork: 10% double construction crafts",
"signature": "Foreman: +1 labour per site action",
"combat": "Sword: +15% damage",
},
"homesteader": {
"name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore"], "craft": ["cooking", "herblore"],
"bonus": "Cooking and herblore 20% faster. Two plots: farm one crop 30% faster, or two crops at once",
"masterwork": "Masterwork: 1 in 10 cooking and herblore crafts make double",
"signature": "Steady hand: potions you drink last 25% longer",
"combat": "Staff: +15% damage with one. Rarely misses, ignores armour, and spirits fear it",
"bonus": "Cooking and herblore 20% faster, two farm plots",
"masterwork": "Masterwork: 10% double cooking and herblore crafts",
"signature": "Steady hand: potions last 25% longer",
"combat": "Staff: +15% damage",
},
"ranger": {
"name": "Ranger", "icon": "🏹", "fast": ["hunting", "fishing", "leatherworking", "cooking"],
"craft": ["leatherworking", "cooking"],
"bonus": "Hunting, fishing, leatherworking and cooking 20% faster",
"masterwork": "Masterwork: 1 in 10 leatherworking and cooking crafts make double",
"signature": "Pathfinder: your scouting gathers 50% more reconnaissance",
"combat": "Bow: +15% damage with one. Fast, strikes first, and brings down flyers",
"masterwork": "Masterwork: 10% double leatherworking and cooking crafts",
"signature": "Pathfinder: +50% recon",
"combat": "Bow: +15% damage",
},
}
# Classes from before the trades, as a starting point until their player re-picks.
@ -639,7 +639,7 @@ BUILDINGS = {
"advanced": {"oak_logs": 100, "leather": 80, "rope": 40, "iron_nails": 150},
"late": {"willow_logs": 80, "oak_plank": 35, "hide_glue": 50, "steel_hinges": 20},
"final": {"maple_logs": 50, "willow_plank": 30, "hard_leather": 60, "mithril_brackets": 15},
"effect": "Room for 20 more people per level",
"effect": "+20 homes per level",
},
"walls": {
"name": "Walls", "icon": "🏰", "max": 5, "gold": 300,
@ -647,7 +647,7 @@ BUILDINGS = {
"advanced": {"cut_stone": 150, "beam": 20, "rope": 50, "iron_nails": 200},
"late": {"steel_bar": 60, "oak_plank": 25, "hide_glue": 50, "steel_hinges": 30},
"final": {"mithril_brackets": 60, "maple_plank": 20, "hide_glue": 60, "adamant_chain": 10},
"effect": "+20 defence per level, and raiders carry off less",
"effect": "+20 defence per level, less plunder lost",
},
"barracks": {
"name": "Barracks", "icon": "⛺", "max": 5, "gold": 300,
@ -655,7 +655,7 @@ BUILDINGS = {
"advanced": {"iron_bar": 60, "hard_leather": 40},
"late": {"steel_bar": 40, "oak_plank": 30, "hide_glue": 40},
"final": {"mithril_bar": 25, "willow_plank": 25, "bear_leather": 40},
"effect": "Trains townsfolk as militia: more defence per citizen",
"effect": "More defence per townsperson",
},
"smithy": {
"name": "Smithy", "icon": "⚒️", "max": 5, "gold": 250,
@ -663,42 +663,42 @@ BUILDINGS = {
"advanced": {"iron_bar": 180, "leather": 70},
"late": {"steel_bar": 100, "oak_plank": 25, "hide_glue": 40},
"final": {"mithril_bar": 70, "maple_plank": 20, "bear_leather": 35},
"effect": "Smithing 8% faster per level; each level unlocks a metal: iron, steel, mithril, adamant, rune",
"effect": "Smithing 8% faster and one more metal per level",
},
"temple": {
"name": "Temple", "icon": "⛪", "max": 3, "gold": 300,
"base": {"stone": 700, "bricks": 200, "cut_stone": 60, "provisions": 60},
"advanced": {"beam": 15, "rope": 40, "iron_nails": 150},
"late": {"willow_logs": 80, "oak_plank": 35, "hide_glue": 40, "steel_hinges": 20},
"effect": "Offerings ward the city in raids; +1 prayer point per bone buried and +25% devotion per level",
"effect": "Per level: +1 prayer point per bone, +25% devotion",
},
"granary": {
"name": "Granary", "icon": "🌾", "max": 3, "gold": 150,
"base": {"logs": 200, "bricks": 100, "plank": 40, "thatch": 300, "provisions": 50, "copper_rivets": 40},
"advanced": {"cut_stone": 40, "rope": 40, "iron_nails": 100},
"late": {"oak_plank": 35, "hide_glue": 35, "steel_hinges": 15},
"effect": "Protects a quarter of the food from locusts and rats per level",
"effect": "Guards 25% of food from pests per level",
},
"lumber_shed": {
"name": "Lumber shed", "icon": "🪵", "max": 3, "gold": 120,
"base": {"logs": 200, "stone": 250, "plank": 50, "thatch": 180, "provisions": 40, "copper_rivets": 40},
"advanced": {"oak_plank": 40, "rope": 40, "iron_nails": 100},
"late": {"willow_plank": 25, "hide_glue": 35},
"effect": "Keeps a quarter of the stored wood safe from rot per level",
"effect": "Guards 25% of wood from rot per level",
},
"well": {
"name": "Well", "icon": "🪣", "max": 3, "gold": 120,
"base": {"stone": 500, "bricks": 80, "rope": 50, "provisions": 40},
"advanced": {"cut_stone": 40, "leather": 60, "iron_nails": 125},
"late": {"oak_plank": 25, "hide_glue": 35, "steel_hinges": 15},
"effect": "Each level cuts fire losses by a quarter and may save the building",
"effect": "Fire damage -25% per level",
},
"spymaster": {
"name": "Spymaster's guild", "icon": "🕵️", "max": 3, "gold": 400,
"base": {"logs": 200, "bricks": 150, "hide": 120, "feathers": 300, "provisions": 60},
"advanced": {"cut_stone": 80, "hard_leather": 40, "iron_nails": 100},
"late": {"bear_leather": 50, "oak_plank": 25, "steel_hinges": 15},
"effect": "+25% reconnaissance per level, and its agents gather some every hour on their own",
"effect": "+25% recon per level, plus some each hour",
},
"storehouse": {
"name": "Storehouse", "icon": "📦", "max": 5, "gold": 200,
@ -706,7 +706,7 @@ BUILDINGS = {
"advanced": {"oak_logs": 150, "cut_stone": 100, "rope": 50, "iron_nails": 150},
"late": {"willow_logs": 120, "oak_plank": 45, "hide_glue": 45, "steel_hinges": 25},
"final": {"yew_logs": 40, "yew_plank": 20, "bear_leather": 40, "rune_lock": 6},
"effect": "+3 bank slots and +250 per stack for everyone, per level",
"effect": "Per level: +3 bank slots, +250 stack size",
},
}
# Buildings rise by labour: players working the site, and a slow trickle from
@ -1285,9 +1285,9 @@ def describe_prayer(prayer: dict) -> str:
if prayer.get("power"):
parts.append(f"+{prayer['power']} power")
if prayer.get("regen"):
parts.append("HP recovers twice as fast while fighting")
parts.append("HP recovers twice as fast")
if prayer.get("wind"):
parts.append("once per outing, a retreat becomes a second wind: back to half health")
parts.append("once per outing, revive at half HP")
return ", ".join(parts)
@ -2269,7 +2269,7 @@ def event_view(world: dict, event: dict, now: float) -> dict:
view["sabotage"] = round(event["sabotage"] * 100)
view["requests"] = council_requests(world, event)
elif event["kind"] == "merchant":
view["headline"] = "A merchant caravan is on its way. Everything sells for half again as much while it's here."
view["headline"] = "A merchant caravan is on its way. Sell prices +50% while it's here."
else:
view["headline"] = f"About {event['people']} refugees are walking towards the city, hoping for a roof."
return view
@ -2481,15 +2481,15 @@ SEASON_DAYS = 7
# `speed` multiplies action time (below 1 is faster), `yield` multiplies output,
# `food` multiplies what the townsfolk eat, `firewood` is logs per person per hour.
WEATHER = {
"clear": {"name": "Clear skies", "icon": "🌤️", "text": "No effect on work or the city"},
"clear": {"name": "Clear skies", "icon": "🌤️", "text": "No effects"},
"rain": {"name": "Rain", "icon": "🌧️", "speed": {"farming": 0.9}, "text": "Farming 10% faster"},
"heat": {"name": "Heat wave", "icon": "🥵", "speed": {"farming": 0.85}, "food": 1.15,
"text": "Farming 15% faster, but food spoils: the city eats 15% more"},
"text": "Farming 15% faster, city eats 15% more"},
"storm": {"name": "Storm", "icon": "⛈️", "speed": {"fishing": 1.25}, "text": "Fishing 25% slower"},
"fog": {"name": "Fog", "icon": "🌫️", "speed": {"reconnaissance": 1.3, "hunting": 1.1},
"text": "Scouting 30% and hunting 10% slower"},
"snow": {"name": "Snow", "icon": "🌨️", "speed": {"hunting": 1.2}, "firewood": 0.05,
"text": "Hunting 20% slower, and the city burns firewood"},
"text": "Hunting 20% slower, burns firewood"},
}
SEASON_WEATHER = [
{"clear": 35, "rain": 40, "storm": 10, "fog": 15},
@ -2504,23 +2504,23 @@ WOOD = FIREWOOD + tuple(plank for plank, _name in PLANKS)
# either by working a special action (`work` completions) or by donating (`need`).
PROBLEMS = {
"collapse": {"name": "Mine collapse", "icon": "⛰️", "need": {"plank": 30},
"block": {"mining": "The mine has collapsed: shore it up with planks"}, "allow": ["mine_stone"],
"text": "A shaft caved in. Ore can't be mined until the mine is shored up with planks."},
"block": {"mining": "Mine collapsed"}, "allow": ["mine_stone"],
"text": "No ore until it's shored up with planks."},
"wolves": {"name": "Wolves at the farms", "icon": "🐺", "yield": {"farming": 0.5}, "work": 10, "action": "clear_wolves",
"text": "A pack is harrying the fields. Farming yields are halved until fighters drive them off."},
"text": "Farming yield halved until fighters drive them off."},
"bear": {"name": "Bear in the woods", "icon": "🐻", "speed": {"woodcutting": 1.5}, "work": 8, "action": "drive_bear",
"text": "Woodcutters won't go near it. Woodcutting is much slower until hunters drive it off."},
"text": "Woodcutting much slower until hunters drive it off."},
"goblins": {"name": "Goblin thieves", "icon": "👺", "steal": 0.01, "work": 10, "action": "catch_goblins",
"text": "Goblins raid the stockpile every hour until fighters catch them."},
"text": "Steal from the stockpile hourly until fighters catch them."},
"rockslide": {"name": "Rockslide on the quarry road", "icon": "🪨", "work": 25, "action": "clear_rockslide",
"block": {"mining": "The quarry road is blocked by a rockslide"}, "only": ["mine_stone"],
"text": "No stone can be quarried until builders clear the road."},
"block": {"mining": "Quarry road blocked"}, "only": ["mine_stone"],
"text": "No stone until builders clear it."},
"flood": {"name": "Flooded fishing grounds", "icon": "🌊", "speed": {"fishing": 1.5}, "need": {"plank": 20}, "lasts": 36,
"text": "The river burst its banks. Fishing is slow until it drops in a day and a half, or a dam of planks holds it back."},
"text": "Fishing slow for 36h, or until dammed with planks."},
"boars": {"name": "Boars trampling the crops", "icon": "🐗", "yield": {"farming": 0.6}, "work": 10, "action": "cull_boars",
"text": "A herd is trampling the fields. Farming yields are down until hunters cull them."},
"text": "Farming yield down until hunters cull them."},
"rats": {"name": "Rats in the granary", "icon": "🐀", "steal_food": 0.03, "work": 12, "action": "catch_rats",
"text": "Rats eat into the city's food every hour until someone catches them."},
"text": "Eat city food hourly until hunters catch them."},
}
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
PROBLEM_ACTIONS = [
@ -2547,15 +2547,15 @@ ACTION_BY_ID[_barrel["id"]] = _barrel
# id, name, building, building level, construction level, inputs, effect, glyph, tint
FITTINGS = [
("grain_bins", "Grain bins", "granary", 1, 15, {"oak_plank": 20, "iron_nails": 25},
"The city eats 10% less food", "grain-bundle", "#e3c060"),
"City eats 10% less", "grain-bundle", "#e3c060"),
("saw_benches", "Saw benches", "lumber_shed", 1, 15, {"oak_plank": 20, "iron_nails": 40},
"Sawing planks is 20% faster", "hand-saw", "#8f5e30"),
"Planks 20% faster", "hand-saw", "#8f5e30"),
("shelving", "Shelving", "storehouse", 2, 20, {"oak_plank": 40, "iron_nails": 40},
"+1 bank slot for everyone", "bookshelf", "#8f5e30"),
"+1 bank slot", "bookshelf", "#8f5e30"),
("map_table", "Map table", "spymaster", 1, 30, {"willow_plank": 25, "hard_leather": 10, "steel_hinges": 4},
"Scouting gathers one more point of reconnaissance", "treasure-map", "#a3b86c"),
"+1 recon per scout", "treasure-map", "#a3b86c"),
("well_pump", "Well pump", "well", 2, 30, {"willow_plank": 25, "steel_hinges": 10},
"Half of all fires are put out before they spread", "water-tank", "#5aa0d8"),
"Stops half of all fires", "water-tank", "#5aa0d8"),
("furniture", "Furniture", "houses", 2, 35, {"willow_plank": 40, "leather": 30, "steel_hinges": 10},
"Population grows 25% faster", "sofa", "#a3b86c"),
("weapon_racks", "Weapon racks", "barracks", 2, 40, {"willow_plank": 30, "steel_hinges": 16},
@ -2563,13 +2563,13 @@ FITTINGS = [
("hoardings", "Hoardings", "walls", 3, 45, {"maple_plank": 40, "mithril_brackets": 20},
"+25 defence", "palisade", "#d0663a"),
("bellows", "Great bellows", "smithy", 3, 50, {"maple_plank": 30, "bear_leather": 15, "mithril_brackets": 6},
"Smithing another 10% faster", "bellows", "#d0663a"),
"Smithing 10% faster", "bellows", "#d0663a"),
("carved_altar", "Carved altar", "temple", 2, 60, {"yew_plank": 30, "cut_stone": 60, "adamant_chain": 4},
"+1 prayer point per bone buried, and offerings can ward 15 more", "star-altar", "#6f9a4a"),
"+1 prayer point per bone, ward +15", "star-altar", "#6f9a4a"),
("enchanted_chests", "Enchanted chests", "storehouse", 4, 75, {"magic_plank": 40, "rune_lock": 6},
"+500 per bank stack for everyone", "open-treasure-chest", "#a77be8"),
"+500 stack size", "open-treasure-chest", "#a77be8"),
("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "red_dragon_leather": 5, "adamant_chain": 6},
"Everyone recovers HP twice as fast", "fireplace", "#a77be8"),
"HP recovers twice as fast", "fireplace", "#a77be8"),
]
# Builders' shields, homesteaders' staves and rangers' bows, after each skill's other tabs.
for _weapon in _weapon_actions():
@ -2748,7 +2748,7 @@ def _hardship(world: dict, now: float, rng: random.Random, kind: str | None = No
if kind == "sickness":
hours = 72 * (1 - 0.2 * building_level(world, "temple"))
_add_effect(world, now, hours, kind="sickness", name="Sickness", icon="🤒", food=1.5,
text="The sick need more food: the city eats half again as much")
text="City eats 50% more")
_record(world, now, "hardship", kind, "🤒", "Sickness is spreading through the city",
[f"For about {round(hours)} hours the townsfolk eat half again as much.",
"A bigger temple shortens it."])
@ -2795,7 +2795,7 @@ def _hardship(world: dict, now: float, rng: random.Random, kind: str | None = No
_record(world, now, "hardship", kind, "🔥", "Fire swept through part of the city", lines)
elif kind == "winter":
_add_effect(world, now, 48, kind="winter", name="Harsh winter", icon="🥶", firewood=0.1,
text="The city burns firewood from the stockpile every hour; without it, people go cold and hungry")
text="Burns stockpile logs hourly")
_record(world, now, "hardship", kind, "🥶", "A harsh cold snap has set in",
["For two days the city burns logs from the stockpile to keep warm.",
"Without firewood, the cold makes everyone eat half again as much."])
@ -2828,27 +2828,27 @@ def _fortune(world: dict, now: float, rng: random.Random, kind: str | None = Non
skill = rng.choice([s for s in SKILL_IDS if s != "reconnaissance"])
name = next(s["name"] for s in SKILLS if s["id"] == skill)
_add_effect(world, now, 12, kind="master", name=f"A master of {name.lower()}", icon="🎓",
xp={skill: 1.5}, text=f"{name} earns 50% more XP")
xp={skill: 1.5}, text=f"{name} +50% XP")
_record(world, now, "fortune", kind, "🎓", f"A travelling master of {name.lower()} is visiting",
[f"For 12 hours, {name.lower()} earns 50% more XP. Learn while you can."])
elif kind == "harvest":
_add_effect(world, now, 12, kind="harvest", name="Bountiful harvest", icon="🌾", **{"yield": {"farming": 2}},
text="Farming yields doubled")
text="Farming yield doubled")
_record(world, now, "fortune", kind, "🌾", "The fields are bursting: a bountiful harvest",
["Farming yields are doubled for 12 hours."])
elif kind == "rich_vein":
_add_effect(world, now, 12, kind="rich_vein", name="Rich vein", icon="💎", **{"yield": {"mining": 1.5}},
text="Mining yields 50% more")
text="Mining yield +50%")
_record(world, now, "fortune", kind, "💎", "Miners struck a rich vein",
["Mining yields 50% more for 12 hours."])
elif kind == "salmon":
_add_effect(world, now, 12, kind="salmon", name="Salmon run", icon="🐟", **{"yield": {"fishing": 2}},
text="Fishing yields doubled")
text="Fishing yield doubled")
_record(world, now, "fortune", kind, "🐟", "The salmon are running",
["Fishing yields are doubled for 12 hours."])
elif kind == "festival":
_add_effect(world, now, 24, kind="festival", name="Festival", icon="🎉", growth=2, favour=1.5,
text="The population grows twice as fast, and giving to the city earns 50% more favour")
text="Growth doubled, +50% favour from donations")
_record(world, now, "fortune", kind, "🎉", "The city is holding a festival",
["For a day, the population grows twice as fast and donations earn 50% more favour."])
elif kind == "noble":
@ -2870,7 +2870,7 @@ def _fortune(world: dict, now: float, rng: random.Random, kind: str | None = Non
elif kind == "mercenaries":
cost = 150 + 30 * season(world, now)["index"] + int(city["population"]) * 5
_add_effect(world, now, 24, kind="mercenaries", name="Mercenaries for hire", icon="🗡️", offer=True,
cost=cost, defence=25, text=f"A sellsword company offers +25 defence for the coming threat for {cost:,} gold from the treasury")
cost=cost, defence=25, text="+25 defence for the coming threat")
_record(world, now, "fortune", kind, "🗡️", "A sellsword company is camped outside the walls",
[f"For a day they'll join the next defence for {cost:,} gold from the treasury."])
else:

View file

@ -150,8 +150,8 @@ function showSummary(sum) {
if (sum.stopped) toast(`Stopped: ${sum.stopped}`, "bad");
for (const id of sum.potion_out || []) toast(`🧪 Your ${item(id).name.toLowerCase()} ran out`, "bad");
for (const id of Object.keys(sum.potions_drunk || {})) toast(`🧪 Drank another ${item(id).name.toLowerCase()}`);
if (sum.prayer_out) toast("🙏 Out of prayer points: your prayers have gone quiet", "bad");
if (sum.second_wind) toast("🌬️ Second Wind! Back on your feet at half health", "level");
if (sum.prayer_out) toast("🙏 Out of prayer points", "bad");
if (sum.second_wind) toast("🌬️ Second Wind! Back to half HP", "level");
if (sum.resumed) toast(`Back to ${C.actions.find((x) => x.id === sum.resumed).name.toLowerCase()}`);
}
@ -169,8 +169,8 @@ function awayModal(sum) {
${sum.gold ? `<p class="gold">+${fmt(sum.gold)} gold</p>` : ""}
${xp ? `<ul>${xp}</ul>` : ""}
${levels ? `<ul>${levels}</ul>` : ""}
${sold ? `<p class="muted small">Your bank was full, so this was sold for ${fmt(sum.sold_gold)} gold:</p><ul>${sold}</ul>` : ""}
${spilled ? `<p class="muted small">Your bank was full, so this went to the city stockpile (for half the favour of giving it yourself):</p><ul>${spilled}</ul>` : ""}
${sold ? `<p class="muted small">Bank full, sold for ${fmt(sum.sold_gold)} gold:</p><ul>${sold}</ul>` : ""}
${spilled ? `<p class="muted small">Bank full, sent to the stockpile:</p><ul>${spilled}</ul>` : ""}
${sum.stopped ? `<p class="tag bad">Stopped: ${esc(sum.stopped)}</p>` : ""}
<p><button class="btn primary" data-close>Back to work</button></p>`);
}
@ -230,7 +230,7 @@ function sidebar() {
<div class="me">
<div class="who">${esc(me.name)}</div>
<div class="cls">${cls.icon} ${cls.name} · ${HEART} <span data-live-hp>${fmt(me.hp)}</span>/${me.max_hp}</div>
<div class="purse"><span class="gold" title="Gold: from fights and selling. Fund buildings with it or give it to the treasury">${COIN} ${fmt(me.gold)}</span><span class="favour" title="Favour: earned by giving to the city. Spend it on the Council page, or on extra bank slots">${FAVOUR} ${fmt(me.favour)}</span></div>
<div class="purse"><span class="gold" title="Gold">${COIN} ${fmt(me.gold)}</span><span class="favour" title="Favour: earned by donating">${FAVOUR} ${fmt(me.favour)}</span></div>
</div>
<div class="navhead">City</div>
${nav("town", "🏘️", "Town square", `${S.city.population}`, "", `Population ${S.city.population}`)}
@ -342,11 +342,11 @@ function skillPage(id) {
if (id === "combat") html += fightPanel() + combatPanel();
if (id === "prayer") html += prayerPanel();
if (id === "construction") html += S.city.construction ? siteBanner()
: `<div class="panel muted">Nothing is being built right now. Fund a building on the Town square and its site opens here.</div>`;
: `<div class="panel muted">Nothing under construction.</div>`;
if (id === "farming" && me.cls === "homesteader") html += plotsPanel();
html += potionPanel(id);
if (`tool_${id}` in me.equip) html += toolPanel(id);
if (id === "reconnaissance") html += `<div class="panel small muted">Reconnaissance on a coming threat raises its intel. Each level takes more than the last, a bigger band takes longer to read, and the sharpest news (the exact day and their camp) only comes in the last hours before they strike. The spymaster's guild adds to every point and gathers some on its own. The council & threats page shows the progress.</div>`;
if (id === "reconnaissance") html += `<div class="panel small muted">Raises intel on the coming threat. Higher levels open as it nears.</div>`;
html += skillHappenings(id);
const live = new Set(S.happenings.problems.map((p) => p.kind));
const listed = C.actions.filter((a) => a.skill === id && (a.special !== "problem" || live.has(a.problem)));
@ -384,9 +384,9 @@ function isLocked(a) {
function plotsPanel() {
const a = S.me.action, farming = a && C.actions.find((x) => x.id === a.id)?.skill === "farming";
const name = (id) => esc(C.actions.find((x) => x.id === id).name);
const status = !farming ? `<span class="muted">Start a crop to plant your first plot.</span>`
: a.plot2 ? `Plot 1: <b>${name(a.id)}</b> · Plot 2: <b>${name(a.plot2)}</b> <span class="muted">· harvests alternate, together 20% faster than one crop</span>`
: `Plot 1: <b>${name(a.id)}</b> · Plot 2: <span class="muted">empty, so you're focused: 30% faster. Use "Plot 2" on another crop to grow both.</span>`;
const status = !farming ? `<span class="muted">Start a crop to plant plot 1.</span>`
: a.plot2 ? `Plot 1: <b>${name(a.id)}</b> · Plot 2: <b>${name(a.plot2)}</b> <span class="muted">· alternating, 20% faster</span>`
: `Plot 1: <b>${name(a.id)}</b> · Plot 2: <span class="muted">empty · focused, 30% faster</span>`;
return `<div class="panel tool-panel"><span>🌱 ${status}</span></div>`;
}
@ -402,7 +402,7 @@ function potionPanel(skill) {
? `${item(active.item).icon} <b>${esc(item(active.item).name)}</b> <span class="muted">${esc(item(active.item).potion.text)} · ${fmt(active.charges)} uses left${me.bank[active.item] ? `, ${fmt(me.bank[active.item])} more in the bank` : ""}</span>`
: `<span class="muted">No potion.</span>`}
${!active && drink ? `<span class="btns">Drink: ${drink}</span>` : ""}
${!active && !drink ? `<span class="small muted">Herblore can brew ${kinds.length === 1 ? "one" : "some"} for ${skill === "combat" ? "fighting" : esc(skillOf(skill).name.toLowerCase())}.</span>` : ""}
${!active && !drink ? `<span class="small muted">Brewed in Herblore.</span>` : ""}
</div>`;
}
@ -413,7 +413,7 @@ function prayerPanel() {
const on = me.prayers.includes(p.id), locked = level < p.level;
const full = !on && me.prayers.length >= C.max_prayers;
return `<button class="prayer ${on ? "on" : ""} ${locked ? "locked" : ""}" data-pray="${p.id}" ${locked ? "disabled" : ""}
title="${locked ? `Needs prayer level ${p.level}` : full ? `Only ${C.max_prayers} at once: turn one off first` : ""}">
title="${locked ? `Needs prayer level ${p.level}` : full ? `Max ${C.max_prayers} active` : ""}">
<span class="ico">${p.icon}</span>
<span class="grow"><b>${esc(p.name)}</b><span class="small muted">${esc(p.text)}</span></span>
<span class="small ${locked ? "why" : "muted"}">${locked ? `Lv ${p.level}` : `${p.cost} pt${p.cost > 1 ? "s" : ""}/fight`}</span>
@ -422,14 +422,14 @@ function prayerPanel() {
const temple = city.buildings.temple.level;
return `<div class="cols">
<div class="panel"><h2>Prayers · ${me.prayers.length} / ${C.max_prayers} active</h2>
<div class="prayer-points">${skillOf("prayer").icon} <b>${fmt(me.pp)}</b> prayer points${cost ? ` · ${cost} per fight, enough for about ${fmt(me.pp / cost)} fights` : ""}</div>
<div class="prayer-points">${skillOf("prayer").icon} <b>${fmt(me.pp)}</b> prayer points${cost ? ` · ${cost} per fight (~${fmt(me.pp / cost)} fights)` : ""}</div>
<div class="prayers">${cards}</div>
<p class="small muted">Active prayers work in every fight while you have the points to pay for them, and each point spent trains Prayer. Bury bones for more points.</p>
<p class="small muted">Prayers cost points each fight. Bury bones for more.</p>
</div>
<div class="panel"><h2>Temple</h2>
${temple ? `<p>Devotion <b>${fmt(city.devotion)}</b> · divine ward <b>+${city.ward}</b> defence for the next raid (up to +${15 * temple} at temple level ${temple}).</p>
<p class="small muted">Offering bones builds devotion; it wards the city through the next raid, then starts again. The temple also adds ${temple} prayer point${temple > 1 ? "s" : ""} to every bone you bury.</p>`
: `<p class="muted">No temple yet. Build one on the Town square to offer bones for a divine ward in raids, and to get more prayer points from bones.</p>`}
${temple ? `<p>Devotion <b>${fmt(city.devotion)}</b> · ward <b>+${city.ward}</b> defence (max +${15 * temple})</p>
<p class="small muted">Offerings ward the next raid. +${temple} prayer point${temple > 1 ? "s" : ""} per bone buried.</p>`
: `<p class="muted">No temple yet.</p>`}
</div></div>`;
}
@ -438,7 +438,7 @@ function toolPanel(skill) {
return `<div class="panel tool-panel">${tool
? `${item(tool).icon} <b>${esc(item(tool).name)}</b> <span class="muted">${Math.round(item(tool).speed * 100)}% faster</span>
<button class="btn sm" data-unequip="tool_${skill}">Unequip</button>`
: `<span class="muted">No tool. A smith can make you one that speeds this up.</span>`}</div>`;
: `<span class="muted">No tool.</span>`}</div>`;
}
// What a monster is weak to and shrugs off, and how your weapon fares against it.
@ -525,13 +525,13 @@ function animateFight(now) {
function styleNote(me) {
const weapon = me.equip.weapon, styleId = (weapon && item(weapon).style) || "sword", st = C.styles[styleId];
const own = C.class_style[me.cls], traits = {
sword: `a swing every ${st.interval}s${me.equip.shield ? `, and your shield blocks ${Math.round(C.block_chance * 100)}% of hits` : ""}`,
hammer: `slow (${st.interval}s) heavy blows, twice the crits`,
staff: `a cast every ${st.interval}s that rarely misses and goes through armour`,
bow: `fast (${st.interval}s), and you shoot first`,
sword: `${st.interval}s${me.equip.shield ? `, shield blocks ${Math.round(C.block_chance * 100)}%` : ""}`,
hammer: `${st.interval}s, double crits`,
staff: `${st.interval}s, accurate, ignores armour`,
bow: `${st.interval}s, shoots first`,
};
return `<p class="small">Fighting with ${weapon ? `a ${esc(st.name)}` : "your fists"}: ${traits[styleId]}.
${own === styleId ? `<b class="good">Your trade's weapon: +15% damage.</b>` : `As a ${esc(C.classes[me.cls].name.toLowerCase())} you hit 15% harder with a ${esc(C.styles[own].name)}.`}</p>`;
return `<p class="small">${weapon ? st.name[0].toUpperCase() + st.name.slice(1) : "Fists"}: ${traits[styleId]}.
${own === styleId ? `<b class="good">Trade weapon: +15% damage.</b>` : `Trade weapon: ${esc(C.styles[own].name)} (+15%).`}</p>`;
}
function combatPanel() {
@ -551,15 +551,14 @@ function combatPanel() {
<div class="bar thick hp"><span data-live-hpbar style="width:${hpPct}%"></span></div>
<div class="row" style="margin-top:12px"><label class="small muted">Food in fights
<select data-food-pick>${`<option value="">Cheapest first</option>` + foods}</select></label></div>
<p class="small">${skillOf("prayer").icon} ${me.prayers.length ? `${me.prayers.map((id) => esc(C.prayers.find((p) => p.id === id).name)).join(" + ")} · ${fmt(me.pp)} points${me.blessing.cost ? "" : ` <b class="warn">(not enough points: prayers are off)</b>`}` : `<span class="muted">No prayers active.</span>`}
<p class="small">${skillOf("prayer").icon} ${me.prayers.length ? `${me.prayers.map((id) => esc(C.prayers.find((p) => p.id === id).name)).join(" + ")} · ${fmt(me.pp)} points${me.blessing.cost ? "" : ` <b class="warn">(out of points)</b>`}` : `<span class="muted">No prayers active.</span>`}
<button class="linkish" data-nav="skill:prayer">Prayers</button></p>
<p class="small muted">Below half health you eat from your bank automatically.
${me.edible.length ? "" : `<b class="warn">You have no food in your bank.</b>`}
Out of food and badly hurt, you retreat: the fight is lost, but nothing else is.</p>
<p class="small muted">Eats below half HP, retreats when out of food.
${me.edible.length ? "" : `<b class="warn">No food in your bank.</b>`}</p>
</div>
<div class="panel"><h2>Equipment</h2>
<div class="gear">${slot("weapon", "weapon")}${slot("shield", "shield")}${slot("helmet", "helmet")}${slot("body", "body armour")}${slot("legs", "leg armour")}</div>
<p class="small muted">Power ${me.power} (level + weapon + shield + half your armour) · armour ${me.armour} takes ${Math.round(me.reduction * 100)}% off the damage you take. Equip gear from the bank.</p>
<p class="small muted">Power ${me.power} · armour ${me.armour} (-${Math.round(me.reduction * 100)}% damage)</p>
${styleNote(me)}
</div></div>`;
}
@ -569,20 +568,20 @@ function itemStats(id) {
const it = item(id);
const parts = [];
if (it.heal) parts.push(`❤️ Heals ${it.heal}`);
if (it.food) parts.push(`feeds ${it.food} ${it.food === 1 ? "person" : "people"} for two days`);
if (it.food) parts.push(`feeds ${it.food} for 2 days`);
if (it.power) parts.push(`+${it.power} power`);
if (it.style) {
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}, a swing every ${st.interval}s${trade ? ` (the ${C.classes[trade].name.toLowerCase()}'s weapon)` : ""}`);
parts.push(`${it.two_handed ? "two-handed " : ""}${st.name}, ${st.interval}s${trade ? ` (${C.classes[trade].name.toLowerCase()})` : ""}`);
}
if (it.armour) parts.push(`+${it.armour} armour`);
if (it.tool) parts.push(`${Math.round(it.speed * 100)}% faster ${skillOf(it.tool).name.toLowerCase()}`);
if (it.req && it.req[1] > 1) parts.push(`needs ${skillOf(it.req[0]).name.toLowerCase()} ${it.req[1]}`);
if (it.potion) parts.push(`${it.potion.text}, ${C.potion_charges} uses`);
if (it.trophy) parts.push(`An expedition trophy: stronger weapons will need these`);
if (it.trophy) parts.push(`Trophy for stronger weapons`);
if (it.fitting) {
const f = C.fittings.find((x) => x.id === id), done = S.city.buildings[f.building].fittings.includes(id);
parts.push(`${C.buildings[f.building].icon} For the ${C.buildings[f.building].name.toLowerCase()} (Lv ${f.level}): ${f.effect}${done ? " · already fitted" : ""}`);
parts.push(`${C.buildings[f.building].icon} ${C.buildings[f.building].name} Lv ${f.level}: ${f.effect}${done ? " · fitted" : ""}`);
}
return parts.join(", ");
}
@ -609,23 +608,22 @@ function actionCard(a) {
if (a.enemy) {
const o = me.fight_odds[a.id], e = a.enemy, pct = (x) => `${Math.round(x * 100)}%`;
extra = `<div class="small">${HEART} ${fmt(e.hp)} HP · hits up to <b>${o.their_max}</b> every ${e.interval}s${e.boss ? ` · <span class="tag warn">Boss</span>` : ""}</div>
<div class="small">You land <b>${pct(o.hit)}</b> of swings (up to ${o.max_hit}) · it lands <b>${pct(o.their_hit)}</b></div>
<div class="small">About <b>${dur(o.kill_time)}</b> a kill, <b>${Math.round(o.damage)}</b> damage taken${a.gold ? ` · ${a.gold} gold` : ""}</div>
<div class="small">You hit <b>${pct(o.hit)}</b> (max ${o.max_hit}) · it hits <b>${pct(o.their_hit)}</b></div>
<div class="small">~<b>${dur(o.kill_time)}</b> a kill · <b>${Math.round(o.damage)}</b> damage taken${a.gold ? ` · ${a.gold} gold` : ""}</div>
${weaknessLine(e.kind, o)}`;
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">-3% raid strength per win (max 30%)</div>`;
}
if (a.special === "drill") {
const s = S.favours.sergeant, boost = s.boost;
extra = !s.needed ? `<div class="small muted">Fighters drill the townsfolk while a raid is coming. A fully drilled militia adds one defence per townsperson.</div>`
: `<div class="small makes">${skillOf("combat").icon} +${+me.drill_per_action.toFixed(2)} drill · ${fmt(s.points)} / ${fmt(s.needed)} points, worth ${fmt(s.defence)} of ${fmt(s.cap)} defence${boost ? ` <b class="boosted">×2 drill and XP for the next ${fmt(boost)}</b>` : ""}</div>
<div class="small muted">A bigger band needs more drilling. A fully drilled militia adds one defence per townsperson.</div>`;
extra = !s.needed ? `<div class="small muted">Only while a raid is coming.</div>`
: `<div class="small makes">${skillOf("combat").icon} +${+me.drill_per_action.toFixed(2)} drill · ${fmt(s.points)} / ${fmt(s.needed)} points, worth ${fmt(s.defence)} of ${fmt(s.cap)} defence${boost ? ` <b class="boosted">×2 drill and XP for the next ${fmt(boost)}</b>` : ""}</div>`;
}
if (a.special === "recon") {
const boost = S.favours.informant.boost;
const ev = S.upcoming, full = ev && ev.recon >= ev.recon_full;
extra = full ? `<div class="small muted">The coming threat is fully scouted: this only trains the skill for now.</div>`
: `<div class="small makes">${skillOf("reconnaissance").icon} +${+me.recon_gain[a.id].toFixed(2)} recon on the coming threat${boost ? ` <b class="boosted">×2 recon and XP for the next ${fmt(boost)}</b>` : ""}</div>`;
extra = full ? `<div class="small muted">Fully scouted: XP only.</div>`
: `<div class="small makes">${skillOf("reconnaissance").icon} +${+me.recon_gain[a.id].toFixed(2)} recon${boost ? ` <b class="boosted">×2 recon and XP for the next ${fmt(boost)}</b>` : ""}</div>`;
}
const temple = S.city.buildings.temple?.level || 0;
if (a.special === "labour") {
@ -633,7 +631,7 @@ function actionCard(a) {
extra = `<div class="small makes">${skillOf("construction").icon} +${me.labour_per_action} labour per action${boost ? ` <b class="boosted">×2 labour and XP for the next ${fmt(boost)}</b>` : ""}</div>`;
}
if (a.special === "bury") extra = `<div class="small makes">${skillOf("prayer").icon} +${a.pp + temple} prayer points</div>`;
if (a.special === "offer") extra = `<div class="small makes">${C.buildings.temple.icon} +${+(a.devotion * (1 + 0.25 * temple)).toFixed(1)} devotion for the city's ward</div>`;
if (a.special === "offer") extra = `<div class="small makes">${C.buildings.temple.icon} +${+(a.devotion * (1 + 0.25 * temple)).toFixed(1)} devotion</div>`;
const made = Object.keys(a.outputs).map(itemStats).filter(Boolean);
if (made.length) extra += `<div class="small makes">${made.join(" · ")}</div>`;
return `
@ -652,8 +650,8 @@ function actionCard(a) {
<span class="why">${!active && !second && why ? esc(why) : ""}</span>
${second ? `<button class="btn" data-plot="">Clear plot 2</button>`
: active ? `<button class="btn danger" data-stop>Stop</button>`
: `<span class="btns"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Do this once, then go back to what you were doing">${a.enemy ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</button>
${canPlot ? `<button class="btn" data-plot="${a.id}" ${why ? "disabled" : ""} title="Grow this in your second plot as well">Plot 2</button>` : ""}
: `<span class="btns"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Once, then resume">${a.enemy ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</button>
${canPlot ? `<button class="btn" data-plot="${a.id}" ${why ? "disabled" : ""} title="Grow in plot 2">Plot 2</button>` : ""}
<button class="btn primary" data-start="${a.id}" ${why ? "disabled" : ""}>Start</button></span>`}
</div>
</div>`;
@ -682,11 +680,10 @@ function bank() {
<div class="panel">
<h2>Bank · ${me.slots_used} / ${me.slots} slots · ${fmt(me.stack)} per stack</h2>
<div class="slots">${slots}</div>
<p class="small muted">When an item doesn't fit:
<p class="small muted">Bank full:
<button class="btn sm ${me.overflow === "city" ? "gold" : ""}" data-overflow="city">Send it to the city</button>
<button class="btn sm ${me.overflow === "stop" ? "gold" : ""}" data-overflow="stop">Stop working</button>
<br>Raids never touch your bank. Overflow sent to the city earns half the favour of giving it yourself;
anything the city has no use for is sold instead.</p>
<br>Overflow earns half favour; what the city can't use is sold.</p>
</div>
${favourPanel()}
</div>
@ -711,11 +708,11 @@ function itemPanel(id) {
${S.me.potions[it.potion.skill] && S.me.potions[it.potion.skill].item !== id ? `<div class="small why">Replaces your ${esc(item(S.me.potions[it.potion.skill].item).name.toLowerCase())} (${S.me.potions[it.potion.skill].charges} uses left)</div>` : ""}</div>` : ""}
${it.heal ? `<div class="row"><div class="small muted">Heals ${it.heal} in fights.</div>
${S.me.food === id ? `<span class="tag good">Your fighting food</span>` : `<button class="btn sm" data-food="${id}">Eat this first in fights</button>`}</div>` : ""}
${it.food ? `<div class="row small muted">Feeds ${it.food} townsfolk for two days in the city granary.</div>` : ""}
${it.food ? `<div class="row small muted">Feeds ${it.food} townsfolk for 2 days.</div>` : ""}
${C.city_uses[id].length
? `<div class="row"><div class="k">Donate to the city · +${it.value} favour each</div><div class="btns">${qtyBtns("data-donate")}</div>
<div class="small muted">Used for ${esc(listing(C.city_uses[id]))}.</div></div>`
: `<div class="row small muted">The city has no use for this. Sell it, or give it to someone who can use it.</div>`}
: `<div class="row small muted">The city has no use for this.</div>`}
${others.length ? `<div class="row"><div class="k">Give to <select data-keep="give-to">${others.map((p) => `<option value="${esc(p.id)}">${esc(p.name)}</option>`).join("")}</select></div><div class="btns">${qtyBtns("data-give")}</div></div>` : ""}
<div class="row"><div class="k">Sell</div><div class="btns">${qtyBtns("data-sell")}</div></div>
<div class="row"><button class="btn sm" data-select="">Close</button></div>
@ -743,7 +740,7 @@ function goldPanel() {
return `<div class="panel"><h2>Gold</h2>
<p class="gold">${COIN} ${fmt(S.me.gold)}</p>
<form data-form="gold" class="btns"><input type="number" min="1" data-keep="gold" placeholder="Amount" style="width:120px"><button class="btn gold">Donate to treasury</button></form>
<p class="small muted">The treasury pays the gold part of new buildings. 1 favour per gold.</p></div>`;
<p class="small muted">1 favour per gold.</p></div>`;
}
function favourPanel() {
@ -751,7 +748,7 @@ function favourPanel() {
return `<div class="panel"><h2>Favour with the council</h2>
<p class="favour">${FAVOUR} ${fmt(me.favour)} favour · ${fmt(me.donated)} given in total</p>
<div class="btns"><button class="btn" data-perk="bank_slot" ${me.favour < me.perk_costs.bank_slot ? "disabled" : ""}>+1 bank slot (${fmt(me.perk_costs.bank_slot)} favour)</button></div>
<p class="small muted">Favour can also hire master builders, informants and drill sergeants: see <button class="linkish" data-nav="council">Council &amp; threats</button>.</p></div>`;
<p class="small muted">More to spend on in <button class="linkish" data-nav="council">Council &amp; threats</button>.</p></div>`;
}
// --- town -----------------------------------------------------------------
@ -761,8 +758,8 @@ function town() {
const growth = city.starving ? `<span class="bad">Hungry: ${city.growth} a day</span>`
: city.growth > 0 ? `+${city.growth} a day`
: city.growth < 0 ? `<span class="bad">Overcrowded: ${city.growth} a day</span>`
: `<span class="warn">Full. Build more houses to grow</span>`;
const next = city.next_tier ? `${city.next_tier.name} at ${city.next_tier.pop} people` : "The largest a settlement can be";
: `<span class="warn">Full: build houses</span>`;
const next = city.next_tier ? `${city.next_tier.name} at ${city.next_tier.pop} people` : "Largest tier";
let html = `<div class="stats">
<div class="stat"><div class="k">Settlement</div><div class="v">${city.tier_name}</div><div class="note">${next}</div></div>
<div class="stat"><div class="k">Population</div><div class="v">${city.population} <small>/ ${city.housing} homes</small></div><div class="note">${growth}</div></div>
@ -772,16 +769,16 @@ function town() {
${weatherCard()}
</div>`;
html += happeningsPanel();
if (city.starving) html += `<div class="banner bad">🍞<div class="grow"><b>The granary is empty.</b> People are leaving and the city defends worse. Donate bread, cooked meat or stew.</div></div>`;
if (city.merchant_until > serverNow()) html += `<div class="banner">🐪<div class="grow"><b>A merchant caravan is in town.</b> Everything sells for 50% more for another ${countdown(city.merchant_until)}.</div></div>`;
if (city.starving) html += `<div class="banner bad">🍞<div class="grow"><b>Granary empty.</b> People are leaving, defence is down. Donate food.</div></div>`;
if (city.merchant_until > serverNow()) html += `<div class="banner">🐪<div class="grow"><b>Merchant in town.</b> Sell prices +50% for ${countdown(city.merchant_until)}.</div></div>`;
if (city.construction) html += siteBanner();
html += `<div class="section-title">Buildings</div>
<p class="small muted">Each building collects its own fund. Materials in a fund are safe from raiders, and building starts on its own once the fund is complete.</p>
<p class="small muted">Funds are safe from raiders. Building starts once a fund is full.</p>
<div class="grid wide">${Object.entries(city.buildings).map(([key, b]) => buildingCard(key, b)).join("")}</div>`;
html += `<div class="section-title">City stockpile</div><div class="panel">`;
const stock = Object.entries(city.stockpile);
html += stock.length ? `<div class="stock">${stock.map(([id, q]) => itemChip(id, q)).join("")}</div>` : `<p class="muted">Empty.</p>`;
html += `<p class="small muted">Food, weapons and anything not yet in a fund. The granary and armoury draw from here, and so do raiders.</p></div>`;
html += `<p class="small muted">Feeds the granary and armoury. Raiders can loot it.</p></div>`;
return html;
}
@ -798,10 +795,10 @@ function siteBanner() {
<span class="muted small">${fmt(work.labour)} / ${fmt(work.needed)} labour</span>
<span class="btns">
${working ? `<span class="btn sm working">Working</span>` : `<button class="btn sm primary" data-start="labour">Work on it</button>`}
${hire.why ? "" : `<button class="btn sm gold" data-favour="builders" ${me.favour < hire.cost ? "disabled" : ""} title="The next ${hire.labour} labour on the site counts double, with double XP">Master builder ${FAVOUR} ${fmt(hire.cost)}</button>`}
${hire.why ? "" : `<button class="btn sm gold" data-favour="builders" ${me.favour < hire.cost ? "disabled" : ""} title="×2 labour and XP for the next ${hire.labour}">Master builder ${FAVOUR} ${fmt(hire.cost)}</button>`}
</span></div>
<div class="bar gold" style="margin-top:6px"><span style="width:${Math.min(100, work.labour / work.needed * 100)}%"></span></div>
<div class="small muted" style="margin-top:6px">${builders.length ? `On site: ${builders.join(", ")}. ` : ""}${hire.boost ? `<b class="boosted">Master builder on site: the next ${fmt(hire.boost)} labour counts double, with double XP.</b> ` : ""}The townsfolk add about ${fmt(city.townsfolk_labour)} labour an hour.${queued ? ` Next: ${queued}.` : ""}</div>
<div class="small muted" style="margin-top:6px">${builders.length ? `On site: ${builders.join(", ")}. ` : ""}${hire.boost ? `<b class="boosted">Master builder: ×2 for the next ${fmt(hire.boost)}.</b> ` : ""}Townsfolk +${fmt(city.townsfolk_labour)}/h.${queued ? ` Next: ${queued}.` : ""}</div>
</div></div>`;
}
@ -812,7 +809,7 @@ function weatherTip(w) {
function weatherCard() {
const h = S.happenings, w = h.weather;
return `<div class="stat" title="${esc(`${weatherTip(w)} Weather changes every few hours; each season lasts a week.`)}"><div class="k">${h.season.icon} ${h.season.name}, day ${h.season.day}</div>
return `<div class="stat" title="${esc(weatherTip(w))}"><div class="k">${h.season.icon} ${h.season.name}, day ${h.season.day}</div>
<div class="v">${w.icon} <small>${esc(w.name)}</small></div>
<div class="note">${w.next_icon} ${esc(w.next_name)} in ${countdown(w.until)}</div></div>`;
}
@ -822,7 +819,7 @@ function happeningsPanel() {
const h = S.happenings, me = S.me, city = S.city;
const rows = [];
if (h.weather.kind !== "clear") rows.push(`<div class="happening"><span class="ico">${h.weather.icon}</span><span class="grow"><b>${esc(h.weather.name)}</b><span class="small muted">${esc(h.weather.text)}</span></span></div>`);
if (h.cold) rows.push(`<div class="happening bad"><span class="ico">🥶</span><span class="grow"><b>Out of firewood</b><span class="small muted">People are cold and eating half again as much. Donate logs.</span></span></div>`);
if (h.cold) rows.push(`<div class="happening bad"><span class="ico">🥶</span><span class="grow"><b>Out of firewood</b><span class="small muted">City eats 50% more. Donate logs.</span></span></div>`);
for (const e of h.effects) {
const hire = e.offer ? `<button class="btn sm gold" data-hire ${city.treasury < e.cost ? "disabled" : ""}>Hire for ${fmt(e.cost)} gold</button>` : "";
rows.push(`<div class="happening"><span class="ico">${e.icon}</span><span class="grow"><b>${esc(e.name)}</b>
@ -860,7 +857,7 @@ function skillHappenings(skill) {
for (const p of h.problems) {
if (!C.problems[p.kind] || !touches(C.problems[p.kind])) continue;
const fix = p.action && C.actions.find((x) => x.id === p.action);
note(p.icon, p.name, !fix ? p.text : fix.skill === skill ? "Help clear it below." : `${skillOf(fix.skill).name} can clear it: see the Town square.`);
note(p.icon, p.name, !fix ? p.text : fix.skill === skill ? "Clear it below." : `${skillOf(fix.skill).name} clears it (Town square).`);
}
return lines.length ? `<div class="panel tool-panel skill-happenings">${lines.join("")}</div>` : "";
}
@ -869,9 +866,9 @@ function buildingCard(key, b) {
const spec = C.buildings[key], city = S.city, me = S.me;
const tags = {
building: `<span class="tag gold">Under construction</span>`,
queued: `<span class="tag good">Funded · waiting for builders</span>`,
queued: `<span class="tag good">Funded · queued</span>`,
max: `<span class="tag good">Fully built</span>`,
capped: `<span class="tag">Next level when the settlement grows</span>`,
capped: `<span class="tag">Needs a bigger settlement</span>`,
};
let body = tags[b.status] ? `<div>${tags[b.status]}</div>` : "";
if (b.status === "funding" || b.status === "queued") {
@ -888,10 +885,10 @@ function buildingCard(key, b) {
}).join("")}</div>`;
}
body += fittingRows(key, b);
const levelText = b.cap < spec.max ? `Lv ${b.level} · max ${b.cap} for a ${city.tier_name.toLowerCase()}` : `Lv ${b.level} / ${spec.max}`;
const levelText = b.cap < spec.max ? `Lv ${b.level} / ${b.cap} (${city.tier_name.toLowerCase()} cap)` : `Lv ${b.level} / ${spec.max}`;
return `<div class="action building ${b.damaged ? "damaged" : ""}">
<div class="top"><div class="ico">${spec.icon}</div><div>
<div class="name">${esc(spec.name)} <span class="lv muted">${levelText}</span> ${b.damaged ? `<span class="tag bad">Damaged: repairs needed</span>` : ""}</div>
<div class="name">${esc(spec.name)} <span class="lv muted">${levelText}</span> ${b.damaged ? `<span class="tag bad">Damaged</span>` : ""}</div>
<div class="sub">${esc(spec.effect)}</div>
</div></div>${body}</div>`;
}
@ -927,17 +924,17 @@ function pips(level) {
function reconText(ev) {
const points = `${fmt(ev.recon)} / ${fmt(ev.recon_full)} points`;
if (ev.intel >= 3) return `${points} · nothing more to learn`;
const boost = S.favours.informant.boost ? ` · <b class="boosted">informant's leads: ×2 for the next ${fmt(S.favours.informant.boost)}</b>` : "";
if (ev.recon_next <= 0) return `${points} · enough for the next level, but they're too far off to read yet${boost}`;
return `${points} · ${fmt(ev.recon_next)} more for the next intel level${boost}`;
if (ev.intel >= 3) return `${points} · complete`;
const boost = S.favours.informant.boost ? ` · <b class="boosted">informant: ×2 for the next ${fmt(S.favours.informant.boost)}</b>` : "";
if (ev.recon_next <= 0) return `${points} · next level opens closer to the threat${boost}`;
return `${points} · ${fmt(ev.recon_next)} to the next level${boost}`;
}
function council() {
const ev = S.upcoming, city = S.city, me = S.me;
let html = "";
if (!ev) {
html += `<div class="panel"><h2>All quiet</h2><p class="muted">No news on the roads. The spymasters will report as soon as something stirs.</p></div>`;
html += `<div class="panel"><h2>All quiet</h2><p class="muted">No news on the roads.</p></div>`;
} else {
const threat = ["raid", "dragon"].includes(ev.kind);
html += `<div class="panel ${threat ? "threat-card" : ""}">
@ -947,10 +944,9 @@ function council() {
<span class="k">Expected</span><span>${eventWhen(ev)}</span>
${ev.range ? `<span class="k">Strength</span><span>${ev.strength ?? `${ev.range[0]}–${ev.range[1]}`}</span>` : ""}
${ev.trait ? `<span class="k">Scouts say</span><span>${esc(ev.trait)}</span>` : ""}
${ev.camp ? `<span class="k">Their camp</span><span>Located. Fighters can sabotage it from the combat page${ev.sabotage ? ` (weakened ${ev.sabotage}% so far)` : ""}.</span>` : ""}
${ev.camp ? `<span class="k">Their camp</span><span>Found · sabotage it in Combat${ev.sabotage ? ` (-${ev.sabotage}% so far)` : ""}</span>` : ""}
<span class="k">Reconnaissance</span><span>${reconText(ev)}</span>
</div>
${ev.intel < 3 ? `<p class="small muted">Better intel comes from the spymaster's guild and anyone's scouting (rangers are best at it), and the sharpest only as the threat draws near.</p>` : ""}`;
</div>`;
if (threat) {
const strength = ev.strength ?? (ev.range ? `${ev.range[0]}–${ev.range[1]}` : "?");
html += `<div class="vs"><div class="side"><span class="muted small">Our defence</span><b>${fmt(city.defence.total)}</b></div><span class="muted">vs</span><div class="side"><span class="muted small">Raiders</span><b>${strength}</b></div></div>`;
@ -988,9 +984,9 @@ function favourSpends() {
</div>`;
};
return `<div class="panel"><h2>Spend favour on the city · you have ${FAVOUR} ${fmt(me.favour)}</h2>
${option("builders", "🏗️", "Hire a master builder", f.builders.why ? esc(f.builders.why) + "." : `The next ${f.builders.labour} labour anyone puts into the site counts double, with double Construction XP${f.builders.boost ? ` (${fmt(f.builders.boost)} still to use)` : ""}. Costs more each time on the same site.`)}
${option("informant", "🕵️", "Pay an informant", f.informant.why ? esc(f.informant.why) + "." : `Leads to follow up: the next ${f.informant.recon} reconnaissance on the coming threat counts double, with double XP${f.informant.boost ? ` (${fmt(f.informant.boost)} still to use)` : ""}. Costs more each time for the same threat.`)}
${option("sergeant", "📯", "Hire a drill sergeant", f.sergeant.why ? esc(f.sergeant.why) + "." : `The next ${f.sergeant.drill} drill points fighters put into the militia count double, with double combat XP${f.sergeant.boost ? ` (${fmt(f.sergeant.boost)} still to use)` : ""}. Costs more each time for the same threat.`)}
${option("builders", "🏗️", "Hire a master builder", f.builders.why ? esc(f.builders.why) + "." : `×2 labour and XP for the next ${f.builders.labour}${f.builders.boost ? ` (${fmt(f.builders.boost)} left)` : ""}. Price rises per hire.`)}
${option("informant", "🕵️", "Pay an informant", f.informant.why ? esc(f.informant.why) + "." : `×2 recon and XP for the next ${f.informant.recon}${f.informant.boost ? ` (${fmt(f.informant.boost)} left)` : ""}. Price rises per hire.`)}
${option("sergeant", "📯", "Hire a drill sergeant", f.sergeant.why ? esc(f.sergeant.why) + "." : `×2 drill and XP for the next ${f.sergeant.drill}${f.sergeant.boost ? ` (${fmt(f.sergeant.boost)} left)` : ""}. Price rises per hire.`)}
</div>`;
}
@ -1005,15 +1001,15 @@ 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: 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>
<p class="small muted">Stockpile gear arms the defenders, best first. Fights use up arrows and break some gear.</p></div>
<div class="panel"><h2>Defence</h2>
<table class="parts">${Object.entries(d.parts).filter(([k, v]) => k !== "rallied" || v).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>
<h2 style="margin-top:16px">Garrison</h2>
<p class="small">${d.garrison.length ? d.garrison.map((g) => `${esc(g.name)} (${g.power})`).join(", ") : `<span class="muted">Nobody has signed up yet.</span>`}</p>
<p class="small">${d.garrison.length ? d.garrison.map((g) => `${esc(g.name)} (${g.power})`).join(", ") : `<span class="muted">Nobody yet.</span>`}</p>
${me.garrison ? `<button class="btn" data-garrison="0">Leave the garrison</button>`
: `<button class="btn primary" data-garrison="1">Stand guard (+${me.power})</button>`}
<p class="small muted">You can keep working while you're signed up. Victory earns the garrison favour and combat XP.</p>
<p class="small muted">Keep working while on guard. Victory earns favour and combat XP.</p>
</div></div>`;
}
@ -1026,7 +1022,7 @@ function board() {
const ledger = [...S.ledger].reverse().map((l) => `<li>${esc(l.text)}<span class="when">${ago(l.at)}</span></li>`).join("");
return `<div class="cols">
<div class="panel"><h2>Town board</h2>
<div class="msgs">${msgs || `<p class="muted">No messages yet. Say hello, or ask for that iron you need.</p>`}</div>
<div class="msgs">${msgs || `<p class="muted">No messages yet.</p>`}</div>
<form data-form="board"><textarea data-keep="board" maxlength="500" placeholder="Write to the guild…"></textarea>
<p><button class="btn primary">Post</button></p></form></div>
<div class="panel"><h2>Town ledger</h2><ul class="ledger">${ledger || `<li class="muted">Nothing recorded yet.</li>`}</ul></div>
@ -1117,7 +1113,7 @@ function characterSheet(uid) {
<div class="sheet-gear">${Object.entries(SLOT_NAMES).map(([slot, label]) => gearSlot(slot, label, "Nothing")).join("")}</div>
<h2 style="margin-top:16px">Tools</h2>
<div class="sheet-gear">${C.skills.filter((s) => `tool_${s.id}` in p.equip).map((s) => gearSlot(`tool_${s.id}`, s.name, "No tool")).join("")}</div>
${mine ? `<p class="small muted">Equip gear and tools from the <button class="linkish" data-nav="bank">bank</button>.</p>` : ""}
${mine ? `<p class="small muted">Equip from the <button class="linkish" data-nav="bank">bank</button>.</p>` : ""}
</div>
</div>
<div class="panel"><h2>Skills</h2><div class="sheet-skills">${skillRows}</div></div>
@ -1137,12 +1133,10 @@ function classCards() {
function repickPage() {
return `<div class="welcome">
<h2>Pick your trade</h2>
<p class="muted">Classes are now whole trades, from gathering through to the finished goods, so you can play
the quiet gathering side and the busier crafting side yourself. You keep every level and item. This choice
is free, once.</p>
<p class="muted">Classes are now trades, gathering through crafting. You keep every level and item. Free, once.</p>
<div class="classes">${classCards()}</div>
<div class="btns"><button class="btn primary" data-repick ${pickedClass ? "" : "disabled"}>Take up this trade</button></div>
<p class="small muted">Every class can still train every skill, scout and drill. A trade makes its skills faster and better.</p>
<p class="small muted">Any class can train any skill.</p>
</div>`;
}
@ -1150,15 +1144,14 @@ function register() {
const cards = classCards();
return `<div class="welcome">
<h2>A new city on the frontier</h2>
<p class="muted">You're one of a handful of adventurers settling a young city at the edge of the kingdom.
Train your skills, supply the council, and keep the place standing when the bandits come.
Your adventurer keeps working while you're away, for up to a day.</p>
<p class="muted">Settle a frontier city together. Train, build, and hold off the bandits.
You keep working while away, for up to a day.</p>
<div class="classes">${cards}</div>
<form data-form="register" class="btns">
<input type="text" data-keep="name" maxlength="20" placeholder="Your adventurer's name" style="flex:1;min-width:200px">
<button class="btn primary" ${pickedClass ? "" : "disabled"}>Enter the city</button>
</form>
<p class="small muted">Every class can train every skill. Your class is a trade: its skills are faster, its crafts sometimes make double, and it has one bonus of its own.</p>
<p class="small muted">Any class can train any skill.</p>
</div>`;
}
@ -1236,7 +1229,7 @@ document.addEventListener("click", (e) => {
if (d.devHappen) return act("/api/dev/happen", { track: d.devHappen });
if (d.pray) {
const on = S.me.prayers.includes(d.pray);
if (!on && S.me.prayers.length >= C.max_prayers) return toast(`Only ${C.max_prayers} prayers at once: turn one off first`, "bad");
if (!on && S.me.prayers.length >= C.max_prayers) return toast(`Max ${C.max_prayers} prayers active`, "bad");
return act("/api/prayers", { ids: on ? S.me.prayers.filter((x) => x !== d.pray) : [...S.me.prayers, d.pray] });
}
if (d.overflow) return act("/api/settings", { overflow: d.overflow });