Six tiers of ore, trees, hides and gear; dragons; tier tabs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a18e8e5bb7
commit
ba68a62080
6 changed files with 319 additions and 103 deletions
10
README.md
10
README.md
|
|
@ -14,9 +14,13 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
|
||||||
- **Combat:** fights cost HP (less when out-levelled or armoured); below half
|
- **Combat:** fights cost HP (less when out-levelled or armoured); below half
|
||||||
health you eat food from your bank, and out of food you retreat, losing
|
health you eat food from your bank, and out of food you retreat, losing
|
||||||
nothing but the fight. HP recovers 25% an hour.
|
nothing but the fight. HP recovers 25% an hour.
|
||||||
- **Gear and tools:** smiths make weapons, copper and iron armour, and tools
|
- **Tiers:** six RuneScape-style tiers (copper, iron, steel, mithril,
|
||||||
(pickaxe, axe, hoe, fishing rod, hunting knife) that make gathering 15–30%
|
adamant, rune at levels 1–75) of ore, trees (up to magic) and hides (deer,
|
||||||
faster; leatherworkers turn hides into leather armour.
|
boar and bear from hunting; green, blue and red dragonhide from fighting
|
||||||
|
dragons). Every metal tier has a sword, shield, helmet, greaves, cuirass and
|
||||||
|
five tools; every leather tier a cap, trousers and jerkin. Higher bars need
|
||||||
|
coal; each metal past copper needs a Smithy level (rune needs level 5, so a
|
||||||
|
city). Gear needs a combat level to wear, tools a skill level to use.
|
||||||
- **Classes:** warrior, scout, artisan, pioneer. Every class can train every
|
- **Classes:** warrior, scout, artisan, pioneer. Every class can train every
|
||||||
skill; a class makes some skills 20% faster and has one signature ability.
|
skill; a class makes some skills 20% faster and has one signature ability.
|
||||||
- **Bank:** 12 slots of up to 1,000 each to start. When something doesn't
|
- **Bank:** 12 slots of up to 1,000 each to start. When something doesn't
|
||||||
|
|
|
||||||
3
app.py
3
app.py
|
|
@ -361,6 +361,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
return "That can't be equipped"
|
return "That can't be equipped"
|
||||||
if player["bank"].get(item, 0) < 1:
|
if player["bank"].get(item, 0) < 1:
|
||||||
return "You don't have one"
|
return "You don't have one"
|
||||||
|
problem = game.equip_problem(player, item)
|
||||||
|
if problem:
|
||||||
|
return problem
|
||||||
game.bank_take(player, item, 1)
|
game.bank_take(player, item, 1)
|
||||||
current = player["equip"][spec["slot"]]
|
current = player["equip"][spec["slot"]]
|
||||||
if current:
|
if current:
|
||||||
|
|
|
||||||
313
game.py
313
game.py
|
|
@ -49,21 +49,11 @@ def level_for(xp: float) -> int:
|
||||||
|
|
||||||
ITEMS = {
|
ITEMS = {
|
||||||
"stone": {"name": "Stone", "icon": "🪨", "value": 1},
|
"stone": {"name": "Stone", "icon": "🪨", "value": 1},
|
||||||
"copper_ore": {"name": "Copper ore", "icon": "🟤", "value": 2},
|
|
||||||
"iron_ore": {"name": "Iron ore", "icon": "⚫", "value": 4},
|
|
||||||
"logs": {"name": "Logs", "icon": "🪵", "value": 1},
|
|
||||||
"oak_logs": {"name": "Oak logs", "icon": "🌳", "value": 4},
|
|
||||||
"wheat": {"name": "Wheat", "icon": "🌾", "value": 1},
|
"wheat": {"name": "Wheat", "icon": "🌾", "value": 1},
|
||||||
"vegetables": {"name": "Vegetables", "icon": "🥕", "value": 2},
|
"vegetables": {"name": "Vegetables", "icon": "🥕", "value": 2},
|
||||||
"feathers": {"name": "Feathers", "icon": "🪶", "value": 1},
|
"feathers": {"name": "Feathers", "icon": "🪶", "value": 1},
|
||||||
"raw_meat": {"name": "Raw meat", "icon": "🥩", "value": 2},
|
"raw_meat": {"name": "Raw meat", "icon": "🥩", "value": 2},
|
||||||
"hide": {"name": "Hide", "icon": "🟫", "value": 3},
|
|
||||||
"copper_bar": {"name": "Copper bar", "icon": "🟧", "value": 5},
|
|
||||||
"iron_bar": {"name": "Iron bar", "icon": "⬛", "value": 10},
|
|
||||||
"arrows": {"name": "Arrows", "icon": "🏹", "value": 1},
|
"arrows": {"name": "Arrows", "icon": "🏹", "value": 1},
|
||||||
"copper_sword": {"name": "Copper sword", "plural": "Copper swords", "icon": "🗡️", "value": 14, "slot": "weapon", "power": 4},
|
|
||||||
"iron_sword": {"name": "Iron sword", "plural": "Iron swords", "icon": "⚔️", "value": 30, "slot": "weapon", "power": 9},
|
|
||||||
"iron_shield": {"name": "Iron shield", "plural": "Iron shields", "icon": "🛡️", "value": 40, "slot": "shield", "power": 6},
|
|
||||||
"bricks": {"name": "Bricks", "icon": "🧱", "value": 3},
|
"bricks": {"name": "Bricks", "icon": "🧱", "value": 3},
|
||||||
"cut_stone": {"name": "Cut stone", "icon": "🔷", "value": 5},
|
"cut_stone": {"name": "Cut stone", "icon": "🔷", "value": 5},
|
||||||
"beam": {"name": "Reinforced beam", "plural": "Reinforced beams", "icon": "🪜", "value": 25},
|
"beam": {"name": "Reinforced beam", "plural": "Reinforced beams", "icon": "🪜", "value": 25},
|
||||||
|
|
@ -71,39 +61,120 @@ ITEMS = {
|
||||||
"bread": {"name": "Bread", "icon": "🍞", "value": 4, "food": 1, "heal": 8},
|
"bread": {"name": "Bread", "icon": "🍞", "value": 4, "food": 1, "heal": 8},
|
||||||
"cooked_meat": {"name": "Cooked meat", "icon": "🍖", "value": 5, "food": 1, "heal": 12},
|
"cooked_meat": {"name": "Cooked meat", "icon": "🍖", "value": 5, "food": 1, "heal": 12},
|
||||||
"stew": {"name": "Stew", "icon": "🍲", "value": 10, "food": 2, "heal": 20},
|
"stew": {"name": "Stew", "icon": "🍲", "value": 10, "food": 2, "heal": 20},
|
||||||
"raw_shrimp": {"name": "Raw shrimp", "icon": "🦐", "value": 1},
|
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 6,
|
||||||
"raw_trout": {"name": "Raw trout", "icon": "🐟", "value": 3},
|
"req": ["combat", 10]},
|
||||||
"raw_salmon": {"name": "Raw salmon", "icon": "🐠", "value": 6},
|
|
||||||
"raw_eel": {"name": "Raw eel", "icon": "🐍", "value": 10},
|
|
||||||
"cooked_shrimp": {"name": "Cooked shrimp", "icon": "🍤", "value": 4, "food": 1, "heal": 10},
|
|
||||||
"cooked_trout": {"name": "Grilled trout", "icon": "🍢", "value": 8, "food": 1, "heal": 18},
|
|
||||||
"cooked_salmon": {"name": "Baked salmon", "icon": "🍣", "value": 14, "food": 2, "heal": 28},
|
|
||||||
"smoked_eel": {"name": "Smoked eel", "icon": "🥓", "value": 22, "food": 2, "heal": 40},
|
|
||||||
"leather": {"name": "Leather", "icon": "🪢", "value": 5},
|
|
||||||
# `armour` lowers the damage a fight deals; `power` adds to fighting strength.
|
|
||||||
"leather_cap": {"name": "Leather cap", "icon": "🧢", "value": 12, "slot": "helmet", "armour": 1},
|
|
||||||
"leather_legs": {"name": "Leather trousers", "plural": "Leather trousers", "icon": "👖", "value": 18, "slot": "legs", "armour": 2},
|
|
||||||
"leather_body": {"name": "Leather jerkin", "icon": "🦺", "value": 24, "slot": "body", "armour": 3},
|
|
||||||
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 6},
|
|
||||||
"copper_helmet": {"name": "Copper helmet", "icon": "⛑️", "value": 12, "slot": "helmet", "armour": 2},
|
|
||||||
"copper_legs": {"name": "Copper greaves", "plural": "Copper greaves", "icon": "🦵", "value": 22, "slot": "legs", "armour": 3},
|
|
||||||
"copper_body": {"name": "Copper cuirass", "icon": "🥋", "value": 28, "slot": "body", "armour": 4},
|
|
||||||
"iron_helmet": {"name": "Iron helmet", "icon": "🪖", "value": 25, "slot": "helmet", "armour": 4},
|
|
||||||
"iron_legs": {"name": "Iron greaves", "plural": "Iron greaves", "icon": "🦿", "value": 45, "slot": "legs", "armour": 6},
|
|
||||||
"iron_body": {"name": "Iron cuirass", "icon": "🦾", "value": 55, "slot": "body", "armour": 8},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- tier ladders -----------------------------------------------------------
|
||||||
|
# Six tiers, RuneScape's metals with Melvor-ish level spacing. Each row of a
|
||||||
|
# ladder generates its items and recipes below; the copper and iron ids
|
||||||
|
# predate the ladders and are kept as they were.
|
||||||
|
|
||||||
|
TIER_LEVELS = [1, 15, 30, 45, 60, 75] # gathering / smelting level of each tier
|
||||||
|
EQUIP_LEVELS = [1, 10, 20, 30, 40, 50] # combat level to wear, skill level to use a tool
|
||||||
|
|
||||||
|
# key, name, ore (item id, name, icon, value), coal per bar, bar icon, bar value, smithy level, handle wood
|
||||||
|
METALS = [
|
||||||
|
("copper", "Copper", ("copper_ore", "Copper ore", "🟤", 2), 0, "🟧", 5, 0, "logs"),
|
||||||
|
("iron", "Iron", ("iron_ore", "Iron ore", "⚫", 4), 0, "⬛", 10, 1, "oak_logs"),
|
||||||
|
("steel", "Steel", None, 2, "🔲", 20, 2, "willow_logs"),
|
||||||
|
("mithril", "Mithril", ("mithril_ore", "Mithril ore", "🔵", 15), 3, "🟦", 40, 3, "maple_logs"),
|
||||||
|
("adamant", "Adamant", ("adamantite_ore", "Adamantite ore", "🟢", 30), 4, "🟩", 70, 4, "yew_logs"),
|
||||||
|
("rune", "Rune", ("runite_ore", "Runite ore", "💠", 55), 5, "🟪", 120, 5, "magic_logs"),
|
||||||
|
]
|
||||||
|
# piece, slot, display noun, bars, smithing level above the tier, power by tier, armour by tier
|
||||||
|
PIECES = [
|
||||||
|
("sword", "weapon", "sword", 2, 7, [4, 9, 14, 20, 27, 35], None),
|
||||||
|
("shield", "shield", "shield", 3, 10, [3, 6, 8, 11, 14, 18], [2, 4, 6, 9, 12, 15]),
|
||||||
|
("helmet", "helmet", "helmet", 2, 5, None, [2, 4, 6, 8, 11, 14]),
|
||||||
|
("legs", "legs", "greaves", 4, 9, None, [3, 6, 9, 12, 16, 21]),
|
||||||
|
("body", "body", "cuirass", 5, 11, None, [4, 8, 12, 16, 22, 28]),
|
||||||
|
]
|
||||||
|
PLURALS = {"sword": "swords", "shield": "shields", "helmet": "helmets", "greaves": "greaves", "cuirass": "cuirasses"}
|
||||||
|
PIECE_ICONS = {"sword": "⚔️", "shield": "🛡️", "helmet": "🪖", "legs": "🦿", "body": "🦾"}
|
||||||
|
COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body": "🥋"}
|
||||||
# Gathering tools: one slot per skill, `speed` cuts the action time.
|
# Gathering tools: one slot per skill, `speed` cuts the action time.
|
||||||
TOOLS = {"mining": ("pickaxe", "⛏️"), "woodcutting": ("axe", "🪓"), "farming": ("hoe", ""),
|
TOOLS = {"mining": ("pickaxe", "⛏️"), "woodcutting": ("axe", "🪓"), "farming": ("hoe", ""),
|
||||||
"fishing": ("fishing rod", "🎣"), "hunting": ("hunting knife", "🔪")}
|
"fishing": ("fishing rod", "🎣"), "hunting": ("hunting knife", "🔪")}
|
||||||
for _skill, (_tool, _icon) in TOOLS.items():
|
TOOL_SPEED = [0.15, 0.30, 0.38, 0.45, 0.52, 0.60]
|
||||||
for _metal, _value, _speed in (("copper", 14, 0.15), ("iron", 30, 0.3)):
|
|
||||||
ITEMS[f"{_metal}_{_tool.replace(' ', '_')}"] = {
|
# item id, name, icon, value, action id, verb
|
||||||
"name": f"{_metal.title()} {_tool}", "icon": _icon, "value": _value,
|
TREES = [
|
||||||
"slot": f"tool_{_skill}", "tool": _skill, "speed": _speed,
|
("logs", "Logs", "🪵", 1, "cut_logs", "Cut trees"),
|
||||||
|
("oak_logs", "Oak logs", "🌳", 4, "cut_oak", "Cut oaks"),
|
||||||
|
("willow_logs", "Willow logs", "🌿", 8, "cut_willow", "Cut willows"),
|
||||||
|
("maple_logs", "Maple logs", "🍁", 15, "cut_maple", "Cut maples"),
|
||||||
|
("yew_logs", "Yew logs", "🌲", 30, "cut_yew", "Cut yews"),
|
||||||
|
("magic_logs", "Magic logs", "🔮", 55, "cut_magic", "Cut magic trees"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# raw hide (id, name, icon, value), leather (id, name, value), armour id prefix, armour name prefix,
|
||||||
|
# leatherworking level, armour by piece
|
||||||
|
LEATHERS = [
|
||||||
|
(("hide", "Hide", "🟫", 3), ("leather", "Leather", 5), "leather", "Leather", 1, (1, 2, 3)),
|
||||||
|
(("boar_hide", "Boar hide", "🐗", 8), ("hard_leather", "Hard leather", 12), "hard_leather", "Hard leather", 20, (2, 4, 6)),
|
||||||
|
(("bear_pelt", "Bear pelt", "🐻", 16), ("bear_leather", "Bear leather", 24), "bear_leather", "Bear leather", 35, (4, 6, 9)),
|
||||||
|
(("green_dragonhide", "Green dragonhide", "🐉", 35), ("green_dragon_leather", "Green dragon leather", 50),
|
||||||
|
"green_dhide", "Green dragonhide", 50, (6, 9, 13)),
|
||||||
|
(("blue_dragonhide", "Blue dragonhide", "🐲", 60), ("blue_dragon_leather", "Blue dragon leather", 85),
|
||||||
|
"blue_dhide", "Blue dragonhide", 65, (8, 12, 17)),
|
||||||
|
(("red_dragonhide", "Red dragonhide", "🔥", 100), ("red_dragon_leather", "Red dragon leather", 140),
|
||||||
|
"red_dhide", "Red dragonhide", 80, (10, 15, 21)),
|
||||||
|
]
|
||||||
|
# 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, "🦺")]
|
||||||
|
|
||||||
|
# raw (id, name, icon, value), cooked (id, name, icon, value, heal, food), fishing level, fish action, verb
|
||||||
|
FISH = [
|
||||||
|
(("raw_shrimp", "Raw shrimp", "🦐", 1), ("cooked_shrimp", "Cooked shrimp", "🍤", 4, 10, 1), 1, "fish_shrimp", "Net shrimp"),
|
||||||
|
(("raw_trout", "Raw trout", "🐟", 3), ("cooked_trout", "Grilled trout", "🍢", 8, 18, 1), 15, "fish_trout", "Fly-fish trout"),
|
||||||
|
(("raw_salmon", "Raw salmon", "🐠", 6), ("cooked_salmon", "Baked salmon", "🍣", 14, 28, 2), 30, "fish_salmon", "Catch salmon"),
|
||||||
|
(("raw_eel", "Raw eel", "🐍", 10), ("smoked_eel", "Smoked eel", "🥓", 22, 40, 2), 45, "fish_eel", "Trap eels"),
|
||||||
|
(("raw_lobster", "Raw lobster", "🦞", 18), ("cooked_lobster", "Cooked lobster", "🍜", 35, 55, 3), 60, "fish_lobster", "Pot lobsters"),
|
||||||
|
(("raw_swordfish", "Raw swordfish", "🐬", 30), ("cooked_swordfish", "Grilled swordfish", "🍱", 55, 70, 3), 75, "fish_swordfish", "Harpoon swordfish"),
|
||||||
|
]
|
||||||
|
COOK_IDS = {"raw_shrimp": "cook_shrimp", "raw_trout": "cook_trout", "raw_salmon": "cook_salmon", "raw_eel": "cook_eel",
|
||||||
|
"raw_lobster": "cook_lobster", "raw_swordfish": "cook_swordfish"}
|
||||||
|
|
||||||
|
ITEMS["coal"] = {"name": "Coal", "icon": "🌑", "value": 6}
|
||||||
|
for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in enumerate(METALS):
|
||||||
|
if _ore:
|
||||||
|
ITEMS[_ore[0]] = {"name": _ore[1], "icon": _ore[2], "value": _ore[3]}
|
||||||
|
ITEMS[f"{_key}_bar"] = {"name": f"{_name} bar", "icon": _bar_icon, "value": _bar_value, "tier": _tier}
|
||||||
|
for _piece, _slot, _noun, _bars, _offset, _power, _armour in PIECES:
|
||||||
|
_item = {"name": f"{_name} {_noun}", "plural": f"{_name} {PLURALS[_noun]}",
|
||||||
|
"icon": COPPER_ICONS.get(_piece, PIECE_ICONS[_piece]) if _key == "copper" else PIECE_ICONS[_piece],
|
||||||
|
"value": round(_bars * _bar_value * 1.2), "slot": _slot, "tier": _tier,
|
||||||
|
"req": ["combat", EQUIP_LEVELS[_tier]]}
|
||||||
|
if _power:
|
||||||
|
_item["power"] = _power[_tier]
|
||||||
|
if _armour:
|
||||||
|
_item["armour"] = _armour[_tier]
|
||||||
|
ITEMS[f"{_key}_{_piece}"] = {k: v for k, v in _item.items() if v is not None}
|
||||||
|
for _skill, (_tool, _icon) in TOOLS.items():
|
||||||
|
ITEMS[f"{_key}_{_tool.replace(' ', '_')}"] = {
|
||||||
|
"name": f"{_name} {_tool}", "icon": _icon, "value": round(2 * _bar_value * 1.4), "tier": _tier,
|
||||||
|
"slot": f"tool_{_skill}", "tool": _skill, "speed": TOOL_SPEED[_tier], "req": [_skill, EQUIP_LEVELS[_tier]],
|
||||||
}
|
}
|
||||||
ITEMS["iron_shield"]["armour"] = 4
|
for _log, _name, _icon, _value, _action_id, _verb in TREES:
|
||||||
|
ITEMS[_log] = {"name": _name, "icon": _icon, "value": _value}
|
||||||
|
for _tier, (_raw, _leather, _prefix, _label, _level, _armour) in enumerate(LEATHERS):
|
||||||
|
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
|
||||||
|
ITEMS[_leather[0]] = {"name": _leather[1], "icon": "🪢", "value": _leather[2]}
|
||||||
|
for (_suffix, _slot, _noun, _used, _offset, _icon), _points in zip(LEATHER_PIECES, _armour):
|
||||||
|
ITEMS[f"{_prefix}_{_suffix}"] = {
|
||||||
|
"name": f"{_label} {_noun}", "icon": _icon, "value": round(_used * _leather[2] * 1.3),
|
||||||
|
"slot": _slot, "armour": _points, "tier": _tier, "req": ["combat", EQUIP_LEVELS[_tier]],
|
||||||
|
}
|
||||||
|
if _noun == "trousers":
|
||||||
|
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
|
||||||
|
for _raw, _cooked, _level, _action_id, _verb in FISH:
|
||||||
|
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
|
||||||
|
ITEMS[_cooked[0]] = {"name": _cooked[1], "icon": _cooked[2], "value": _cooked[3], "heal": _cooked[4], "food": _cooked[5]}
|
||||||
|
|
||||||
# What the city eats first: plain food before the good stuff.
|
# What the city eats first: plain food before the good stuff.
|
||||||
FOOD_ORDER = ("bread", "cooked_meat", "cooked_shrimp", "cooked_trout", "stew", "cooked_salmon", "smoked_eel")
|
FOOD_ORDER = tuple(sorted((item for item, spec in ITEMS.items() if "food" in spec), key=lambda i: ITEMS[i]["heal"]))
|
||||||
EQUIP_SLOTS = ["weapon", "shield", "helmet", "body", "legs"] + [f"tool_{skill}" for skill in TOOLS]
|
EQUIP_SLOTS = ["weapon", "shield", "helmet", "body", "legs"] + [f"tool_{skill}" for skill in TOOLS]
|
||||||
|
|
||||||
SKILLS = [
|
SKILLS = [
|
||||||
|
|
@ -153,55 +224,68 @@ def _action(id, skill, name, level, time, xp, inputs=None, outputs=None, **extra
|
||||||
"inputs": inputs or {}, "outputs": outputs or {}, **extra}
|
"inputs": inputs or {}, "outputs": outputs or {}, **extra}
|
||||||
|
|
||||||
|
|
||||||
|
def _tiered_actions() -> list[dict]:
|
||||||
|
"""Mining, woodcutting, smelting, smithing, tanning and leatherworking for every tier."""
|
||||||
|
actions = []
|
||||||
|
ore_xp = [7, 15, 30, 40, 55, 75]
|
||||||
|
ore_time = [25, 35, 50, 55, 65, 80]
|
||||||
|
bar_xp = [6, 14, 22, 32, 44, 58]
|
||||||
|
piece_xp = [6, 13, 20, 28, 37, 48] # per bar used
|
||||||
|
for tier, (key, name, ore, coal, _icon, _value, smithy, wood) in enumerate(METALS):
|
||||||
|
if ore:
|
||||||
|
actions.append(_action(f"mine_{key}" if key in ("copper", "iron") else f"mine_{ore[0].removesuffix('_ore')}",
|
||||||
|
"mining", f"Mine {ore[1].removesuffix(' ore').lower()}", TIER_LEVELS[tier],
|
||||||
|
ore_time[tier], ore_xp[tier], outputs={ore[0]: 1}))
|
||||||
|
if tier == 2:
|
||||||
|
actions.append(_action("mine_coal", "mining", "Mine coal", 30, 40, 22, outputs={"coal": 1}))
|
||||||
|
city = {"smithy": smithy} if smithy else {}
|
||||||
|
inputs = {"iron_ore" if key == "steel" else ore[0]: 1}
|
||||||
|
inputs.update({"coal": coal} if coal else {"logs": 1})
|
||||||
|
actions.append(_action(f"smith_{key}_bar", "smithing", f"{name} bar", TIER_LEVELS[tier], 20 + 4 * tier,
|
||||||
|
bar_xp[tier], inputs, {f"{key}_bar": 1}, group="Bars",
|
||||||
|
city={"smithy": smithy} if tier >= 2 else {}))
|
||||||
|
for piece, _slot, noun, bars, offset, _power, _armour in PIECES:
|
||||||
|
actions.append(_action(f"smith_{key}_{piece}", "smithing", f"{name} {noun}", TIER_LEVELS[tier] + offset,
|
||||||
|
25 + 5 * bars + 2 * tier, bars * piece_xp[tier], {f"{key}_bar": bars},
|
||||||
|
{f"{key}_{piece}": 1}, group=name, city=city))
|
||||||
|
for tool, _icon in TOOLS.values():
|
||||||
|
tool_id = f"{key}_{tool.replace(' ', '_')}"
|
||||||
|
actions.append(_action(f"smith_{tool_id}", "smithing", f"{name} {tool}", TIER_LEVELS[tier] + 3,
|
||||||
|
30 + 2 * tier, 2 * piece_xp[tier], {f"{key}_bar": 2, wood: 1}, {tool_id: 1},
|
||||||
|
group=name, city=city))
|
||||||
|
for tier, (log, _name, _icon, _value, action_id, verb) in enumerate(TREES):
|
||||||
|
actions.append(_action(action_id, "woodcutting", verb, TIER_LEVELS[tier], [20, 35, 40, 50, 60, 75][tier],
|
||||||
|
[5, 15, 24, 36, 52, 75][tier], outputs={log: 1}))
|
||||||
|
for tier, (raw, leather, prefix, label, level, _armour) in enumerate(LEATHERS):
|
||||||
|
actions.append(_action("tan_leather" if tier == 0 else f"tan_{leather[0]}", "leatherworking",
|
||||||
|
f"Tan {leather[1].lower()}", level, 25 + 3 * tier, bar_xp[tier],
|
||||||
|
{raw[0]: 1}, {leather[0]: 1}, group="Tanning"))
|
||||||
|
for suffix, _slot, noun, used, offset, _icon in LEATHER_PIECES:
|
||||||
|
actions.append(_action(f"lw_{suffix}" if tier == 0 else f"lw_{prefix}_{suffix}", "leatherworking",
|
||||||
|
f"{label} {noun}", level + offset, 25 + 5 * used + 2 * tier, used * piece_xp[tier],
|
||||||
|
{leather[0]: used}, {f"{prefix}_{suffix}": 1}, group=label))
|
||||||
|
for raw, cooked, level, fish_id, verb in FISH:
|
||||||
|
tier = TIER_LEVELS.index(level) if level in TIER_LEVELS else 3
|
||||||
|
actions.append(_action(fish_id, "fishing", verb, level, [25, 35, 45, 55, 65, 80][tier],
|
||||||
|
[6, 15, 28, 45, 62, 85][tier], outputs={raw[0]: 1}))
|
||||||
|
actions.append(_action(COOK_IDS[raw[0]], "cooking", cooked[1], level, [20, 30, 35, 40, 45, 50][tier],
|
||||||
|
[6, 15, 27, 42, 58, 80][tier], {raw[0]: 1, "logs": 1}, {cooked[0]: 1}))
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
ACTIONS = [
|
ACTIONS = [
|
||||||
_action("mine_stone", "mining", "Quarry stone", 1, 20, 5, outputs={"stone": 1}),
|
_action("mine_stone", "mining", "Quarry stone", 1, 20, 5, outputs={"stone": 1}),
|
||||||
_action("mine_copper", "mining", "Mine copper", 1, 25, 7, outputs={"copper_ore": 1}),
|
|
||||||
_action("mine_iron", "mining", "Mine iron", 15, 35, 15, outputs={"iron_ore": 1}),
|
|
||||||
_action("cut_logs", "woodcutting", "Cut trees", 1, 20, 5, outputs={"logs": 1}),
|
|
||||||
_action("cut_oak", "woodcutting", "Cut oaks", 15, 35, 15, outputs={"oak_logs": 1}),
|
|
||||||
_action("farm_wheat", "farming", "Grow wheat", 1, 25, 6, outputs={"wheat": 2}),
|
_action("farm_wheat", "farming", "Grow wheat", 1, 25, 6, outputs={"wheat": 2}),
|
||||||
_action("farm_veg", "farming", "Grow vegetables", 12, 35, 13, outputs={"vegetables": 2}),
|
_action("farm_veg", "farming", "Grow vegetables", 12, 35, 13, outputs={"vegetables": 2}),
|
||||||
_action("hunt_birds", "hunting", "Snare birds", 1, 25, 6, outputs={"feathers": 3}),
|
_action("hunt_birds", "hunting", "Snare birds", 1, 25, 6, outputs={"feathers": 3}),
|
||||||
_action("hunt_deer", "hunting", "Hunt deer", 8, 40, 13, outputs={"raw_meat": 2, "hide": 1}),
|
_action("hunt_deer", "hunting", "Hunt deer", 8, 40, 13, outputs={"raw_meat": 2, "hide": 1}),
|
||||||
_action("fish_shrimp", "fishing", "Net shrimp", 1, 25, 6, outputs={"raw_shrimp": 1}),
|
_action("hunt_boar", "hunting", "Hunt boar", 25, 50, 30, outputs={"raw_meat": 2, "boar_hide": 1}),
|
||||||
_action("fish_trout", "fishing", "Fly-fish trout", 15, 35, 15, outputs={"raw_trout": 1}),
|
_action("hunt_bear", "hunting", "Hunt bears", 40, 60, 48, outputs={"raw_meat": 3, "bear_pelt": 1}),
|
||||||
_action("fish_salmon", "fishing", "Catch salmon", 30, 45, 28, outputs={"raw_salmon": 1}),
|
*_tiered_actions(),
|
||||||
_action("fish_eel", "fishing", "Trap eels", 45, 55, 45, outputs={"raw_eel": 1}),
|
|
||||||
_action("smith_copper_bar", "smithing", "Copper bar", 1, 20, 6,
|
|
||||||
{"copper_ore": 1, "logs": 1}, {"copper_bar": 1}, group="Bars"),
|
|
||||||
_action("smith_iron_bar", "smithing", "Iron bar", 15, 30, 14,
|
|
||||||
{"iron_ore": 1, "logs": 1}, {"iron_bar": 1}, group="Bars"),
|
|
||||||
_action("smith_copper_sword", "smithing", "Copper sword", 8, 40, 18,
|
|
||||||
{"copper_bar": 3}, {"copper_sword": 1}, group="Weapons"),
|
|
||||||
_action("smith_iron_sword", "smithing", "Iron sword", 20, 45, 32,
|
|
||||||
{"iron_bar": 2}, {"iron_sword": 1}, city={"smithy": 1}, group="Weapons"),
|
|
||||||
_action("smith_iron_shield", "smithing", "Iron shield", 25, 50, 40,
|
|
||||||
{"iron_bar": 3}, {"iron_shield": 1}, city={"smithy": 2}, group="Weapons"),
|
|
||||||
_action("smith_copper_helmet", "smithing", "Copper helmet", 6, 35, 14,
|
|
||||||
{"copper_bar": 2}, {"copper_helmet": 1}, group="Armour"),
|
|
||||||
_action("smith_copper_legs", "smithing", "Copper greaves", 10, 40, 20,
|
|
||||||
{"copper_bar": 4}, {"copper_legs": 1}, group="Armour"),
|
|
||||||
_action("smith_copper_body", "smithing", "Copper cuirass", 12, 45, 26,
|
|
||||||
{"copper_bar": 5}, {"copper_body": 1}, group="Armour"),
|
|
||||||
_action("smith_iron_helmet", "smithing", "Iron helmet", 22, 40, 34,
|
|
||||||
{"iron_bar": 2}, {"iron_helmet": 1}, city={"smithy": 1}, group="Armour"),
|
|
||||||
_action("smith_iron_legs", "smithing", "Iron greaves", 27, 45, 46,
|
|
||||||
{"iron_bar": 4}, {"iron_legs": 1}, city={"smithy": 1}, group="Armour"),
|
|
||||||
_action("smith_iron_body", "smithing", "Iron cuirass", 30, 50, 56,
|
|
||||||
{"iron_bar": 5}, {"iron_body": 1}, city={"smithy": 2}, group="Armour"),
|
|
||||||
*[_action(f"smith_{metal}_{tool.replace(' ', '_')}", "smithing", f"{metal.title()} {tool}", level, time, xp,
|
|
||||||
{f"{metal}_bar": 2, wood: 1}, {f"{metal}_{tool.replace(' ', '_')}": 1}, group="Tools",
|
|
||||||
**({"city": {"smithy": 1}} if metal == "iron" else {}))
|
|
||||||
for metal, level, time, xp, wood in (("copper", 4, 30, 12, "logs"), ("iron", 20, 40, 30, "oak_logs"))
|
|
||||||
for tool, _icon in TOOLS.values()],
|
|
||||||
_action("smith_arrows", "smithing", "Arrows", 5, 30, 10,
|
_action("smith_arrows", "smithing", "Arrows", 5, 30, 10,
|
||||||
{"copper_bar": 1, "logs": 1, "feathers": 2}, {"arrows": 15}, group="Supplies"),
|
{"copper_bar": 1, "logs": 1, "feathers": 2}, {"arrows": 15}, group="Bars"),
|
||||||
_action("tan_leather", "leatherworking", "Tan leather", 1, 25, 6, {"hide": 1}, {"leather": 1}),
|
|
||||||
_action("lw_cap", "leatherworking", "Leather cap", 3, 30, 10, {"leather": 2}, {"leather_cap": 1}),
|
|
||||||
_action("lw_legs", "leatherworking", "Leather trousers", 8, 35, 16, {"leather": 3}, {"leather_legs": 1}),
|
|
||||||
_action("lw_body", "leatherworking", "Leather jerkin", 12, 40, 22, {"leather": 4}, {"leather_body": 1}),
|
|
||||||
_action("lw_studded", "leatherworking", "Studded jerkin", 25, 50, 40,
|
_action("lw_studded", "leatherworking", "Studded jerkin", 25, 50, 40,
|
||||||
{"leather": 4, "iron_bar": 1}, {"studded_body": 1}),
|
{"leather": 4, "iron_bar": 1}, {"studded_body": 1}, group="Leather"),
|
||||||
_action("mason_bricks", "masonry", "Bricks", 1, 25, 6, {"stone": 2, "logs": 1}, {"bricks": 2}),
|
_action("mason_bricks", "masonry", "Bricks", 1, 25, 6, {"stone": 2, "logs": 1}, {"bricks": 2}),
|
||||||
_action("mason_cut_stone", "masonry", "Cut stone", 10, 30, 12, {"stone": 3}, {"cut_stone": 1}),
|
_action("mason_cut_stone", "masonry", "Cut stone", 10, 30, 12, {"stone": 3}, {"cut_stone": 1}),
|
||||||
_action("mason_beam", "masonry", "Reinforced beam", 20, 45, 30,
|
_action("mason_beam", "masonry", "Reinforced beam", 20, 45, 30,
|
||||||
|
|
@ -210,10 +294,6 @@ ACTIONS = [
|
||||||
_action("cook_meat", "cooking", "Cooked meat", 5, 20, 7, {"raw_meat": 1, "logs": 1}, {"cooked_meat": 1}),
|
_action("cook_meat", "cooking", "Cooked meat", 5, 20, 7, {"raw_meat": 1, "logs": 1}, {"cooked_meat": 1}),
|
||||||
_action("cook_stew", "cooking", "Stew", 15, 35, 16,
|
_action("cook_stew", "cooking", "Stew", 15, 35, 16,
|
||||||
{"raw_meat": 1, "vegetables": 2, "logs": 1}, {"stew": 2}),
|
{"raw_meat": 1, "vegetables": 2, "logs": 1}, {"stew": 2}),
|
||||||
_action("cook_shrimp", "cooking", "Cooked shrimp", 1, 20, 6, {"raw_shrimp": 1, "logs": 1}, {"cooked_shrimp": 1}),
|
|
||||||
_action("cook_trout", "cooking", "Grilled trout", 15, 30, 15, {"raw_trout": 1, "logs": 1}, {"cooked_trout": 1}),
|
|
||||||
_action("cook_salmon", "cooking", "Baked salmon", 30, 35, 27, {"raw_salmon": 1, "logs": 1}, {"cooked_salmon": 1}),
|
|
||||||
_action("cook_eel", "cooking", "Smoked eel", 45, 40, 42, {"raw_eel": 1, "logs": 1}, {"smoked_eel": 1}),
|
|
||||||
_action("fight_wolves", "combat", "Wolves in the woods", 1, 60, 14, difficulty=2, damage=2, gold=2,
|
_action("fight_wolves", "combat", "Wolves in the woods", 1, 60, 14, difficulty=2, damage=2, gold=2,
|
||||||
drops=[["raw_meat", 1, 0.6], ["hide", 1, 0.5]]),
|
drops=[["raw_meat", 1, 0.6], ["hide", 1, 0.5]]),
|
||||||
_action("fight_scouts", "combat", "Bandit scouts", 10, 75, 30, difficulty=14, damage=6, gold=10,
|
_action("fight_scouts", "combat", "Bandit scouts", 10, 75, 30, difficulty=14, damage=6, gold=10,
|
||||||
|
|
@ -221,7 +301,15 @@ ACTIONS = [
|
||||||
_action("fight_ruins", "combat", "Watchtower ruins", 25, 90, 60, difficulty=30, damage=14, gold=25,
|
_action("fight_ruins", "combat", "Watchtower ruins", 25, 90, 60, difficulty=30, damage=14, gold=25,
|
||||||
drops=[["iron_bar", 1, 0.4], ["iron_sword", 1, 0.03]]),
|
drops=[["iron_bar", 1, 0.4], ["iron_sword", 1, 0.03]]),
|
||||||
_action("fight_stronghold", "combat", "Bandit stronghold", 40, 105, 95, difficulty=50, damage=25, gold=60,
|
_action("fight_stronghold", "combat", "Bandit stronghold", 40, 105, 95, difficulty=50, damage=25, gold=60,
|
||||||
drops=[["iron_bar", 2, 0.5], ["iron_shield", 1, 0.04]]),
|
drops=[["steel_bar", 1, 0.4], ["coal", 3, 0.5], ["iron_shield", 1, 0.04]]),
|
||||||
|
_action("fight_green_dragon", "combat", "Green dragon", 50, 120, 135, difficulty=90, damage=30, gold=80,
|
||||||
|
drops=[["green_dragonhide", 1, 0.9], ["mithril_ore", 2, 0.3]]),
|
||||||
|
_action("fight_blue_dragon", "combat", "Blue dragon", 62, 130, 175, difficulty=115, damage=40, gold=110,
|
||||||
|
drops=[["blue_dragonhide", 1, 0.9], ["adamantite_ore", 2, 0.3]]),
|
||||||
|
_action("fight_red_dragon", "combat", "Red dragon", 75, 140, 220, difficulty=140, damage=52, gold=150,
|
||||||
|
drops=[["red_dragonhide", 1, 0.9], ["runite_ore", 1, 0.25]]),
|
||||||
|
_action("fight_black_dragon", "combat", "Black dragon", 85, 150, 280, difficulty=165, damage=65, gold=220,
|
||||||
|
drops=[["red_dragonhide", 2, 0.9], ["rune_bar", 1, 0.1]]),
|
||||||
_action("sabotage", "combat", "Sabotage the bandit camp", 10, 120, 35, difficulty=12, damage=10,
|
_action("sabotage", "combat", "Sabotage the bandit camp", 10, 120, 35, difficulty=12, damage=10,
|
||||||
special="sabotage"),
|
special="sabotage"),
|
||||||
_action("recon_roads", "reconnaissance", "Scout the roads", 1, 60, 12, special="recon", recon=1),
|
_action("recon_roads", "reconnaissance", "Scout the roads", 1, 60, 12, special="recon", recon=1),
|
||||||
|
|
@ -233,31 +321,37 @@ BUILDINGS = {
|
||||||
"houses": {
|
"houses": {
|
||||||
"name": "Houses", "icon": "🏠", "max": 8, "gold": 200,
|
"name": "Houses", "icon": "🏠", "max": 8, "gold": 200,
|
||||||
"base": {"logs": 600, "bricks": 300}, "advanced": {"oak_logs": 200},
|
"base": {"logs": 600, "bricks": 300}, "advanced": {"oak_logs": 200},
|
||||||
|
"late": {"willow_logs": 150}, "final": {"maple_logs": 100},
|
||||||
"effect": "Room for 20 more people per level",
|
"effect": "Room for 20 more people per level",
|
||||||
},
|
},
|
||||||
"walls": {
|
"walls": {
|
||||||
"name": "Walls", "icon": "🏰", "max": 5, "gold": 300,
|
"name": "Walls", "icon": "🏰", "max": 5, "gold": 300,
|
||||||
"base": {"stone": 1200, "bricks": 500}, "advanced": {"cut_stone": 300, "beam": 40},
|
"base": {"stone": 1200, "bricks": 500}, "advanced": {"cut_stone": 300, "beam": 40},
|
||||||
|
"late": {"steel_bar": 60}, "final": {"mithril_bar": 40},
|
||||||
"effect": "+20 defence per level, and raiders carry off less",
|
"effect": "+20 defence per level, and raiders carry off less",
|
||||||
},
|
},
|
||||||
"barracks": {
|
"barracks": {
|
||||||
"name": "Barracks", "icon": "⛺", "max": 5, "gold": 300,
|
"name": "Barracks", "icon": "⛺", "max": 5, "gold": 300,
|
||||||
"base": {"logs": 600, "bricks": 400, "copper_sword": 20}, "advanced": {"iron_sword": 15},
|
"base": {"logs": 600, "bricks": 400, "copper_sword": 20}, "advanced": {"iron_sword": 15},
|
||||||
|
"late": {"steel_sword": 10}, "final": {"mithril_sword": 6},
|
||||||
"effect": "Trains townsfolk as militia: more defence per citizen",
|
"effect": "Trains townsfolk as militia: more defence per citizen",
|
||||||
},
|
},
|
||||||
"smithy": {
|
"smithy": {
|
||||||
"name": "Smithy", "icon": "⚒️", "max": 3, "gold": 250,
|
"name": "Smithy", "icon": "⚒️", "max": 5, "gold": 250,
|
||||||
"base": {"stone": 500, "logs": 600, "copper_bar": 150}, "advanced": {"iron_bar": 150},
|
"base": {"stone": 500, "logs": 600, "copper_bar": 150}, "advanced": {"iron_bar": 150},
|
||||||
"effect": "Smithing 8% faster per level; unlocks iron swords (1) and shields (2)",
|
"late": {"steel_bar": 80}, "final": {"mithril_bar": 60},
|
||||||
|
"effect": "Smithing 8% faster per level; each level unlocks a metal: iron, steel, mithril, adamant, rune",
|
||||||
},
|
},
|
||||||
"spymaster": {
|
"spymaster": {
|
||||||
"name": "Spymaster's guild", "icon": "🕵️", "max": 3, "gold": 400,
|
"name": "Spymaster's guild", "icon": "🕵️", "max": 3, "gold": 400,
|
||||||
"base": {"logs": 500, "bricks": 300, "hide": 100, "feathers": 300}, "advanced": {"cut_stone": 150},
|
"base": {"logs": 500, "bricks": 300, "hide": 100, "feathers": 300}, "advanced": {"cut_stone": 150},
|
||||||
|
"late": {"bear_leather": 40},
|
||||||
"effect": "One more level of intel on every event per level",
|
"effect": "One more level of intel on every event per level",
|
||||||
},
|
},
|
||||||
"storehouse": {
|
"storehouse": {
|
||||||
"name": "Storehouse", "icon": "📦", "max": 5, "gold": 200,
|
"name": "Storehouse", "icon": "📦", "max": 5, "gold": 200,
|
||||||
"base": {"logs": 800, "stone": 800}, "advanced": {"oak_logs": 300, "cut_stone": 200},
|
"base": {"logs": 800, "stone": 800}, "advanced": {"oak_logs": 300, "cut_stone": 200},
|
||||||
|
"late": {"willow_logs": 200}, "final": {"yew_logs": 80},
|
||||||
"effect": "+3 bank slots and +250 per stack for everyone, per level",
|
"effect": "+3 bank slots and +250 per stack for everyone, per level",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -367,9 +461,11 @@ def building_level(world: dict, key: str) -> int:
|
||||||
def build_cost(key: str, level: int) -> dict:
|
def build_cost(key: str, level: int) -> dict:
|
||||||
spec = BUILDINGS[key]
|
spec = BUILDINGS[key]
|
||||||
cost = {item: qty * level for item, qty in spec["base"].items()}
|
cost = {item: qty * level for item, qty in spec["base"].items()}
|
||||||
for item, qty in spec["advanced"].items():
|
# Later levels add materials from higher tiers: advanced from level 2, late from 3, final from 5.
|
||||||
if level > 1:
|
for key, first in (("advanced", 2), ("late", 3), ("final", 5)):
|
||||||
cost[item] = cost.get(item, 0) + qty * (level - 1)
|
for item, qty in spec.get(key, {}).items():
|
||||||
|
if level >= first:
|
||||||
|
cost[item] = cost.get(item, 0) + qty * (level - first + 1)
|
||||||
cost["gold"] = spec["gold"] * level
|
cost["gold"] = spec["gold"] * level
|
||||||
return cost
|
return cost
|
||||||
|
|
||||||
|
|
@ -889,20 +985,43 @@ def population_view(world: dict) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)):
|
||||||
|
take = min(people, stock[item])
|
||||||
|
total += take * ITEMS[item].get(stat, 0)
|
||||||
|
people -= take
|
||||||
|
if not people:
|
||||||
|
break
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def kit_count(stock: dict, slot: str) -> int:
|
||||||
|
return sum(qty for item, qty in stock.items() if ITEMS[item].get("slot") == slot)
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
if req and skill_level(player, req[0]) < req[1]:
|
||||||
|
name = next(s["name"] for s in SKILLS if s["id"] == req[0])
|
||||||
|
return f"Needs {name.lower()} level {req[1]}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def defence(world: dict) -> dict:
|
def defence(world: dict) -> dict:
|
||||||
"""The city's defence right now, part by part."""
|
"""The city's defence right now, part by part."""
|
||||||
city = world["city"]
|
city = world["city"]
|
||||||
pop = city["population"]
|
pop = city["population"]
|
||||||
stock = city["stockpile"]
|
stock = city["stockpile"]
|
||||||
armed = int(pop // 2)
|
armed = int(pop // 2)
|
||||||
swords = min(armed, stock.get("copper_sword", 0) + stock.get("iron_sword", 0))
|
|
||||||
iron = min(swords, stock.get("iron_sword", 0))
|
|
||||||
parts = {
|
parts = {
|
||||||
"townsfolk": 10,
|
"townsfolk": 10,
|
||||||
"walls": 20 * building_level(world, "walls"),
|
"walls": 20 * building_level(world, "walls"),
|
||||||
"militia": round(0.3 * building_level(world, "barracks") * pop, 1),
|
"militia": round(0.3 * building_level(world, "barracks") * pop, 1),
|
||||||
"weapons": round(3 * iron + 1.5 * (swords - iron), 1),
|
"weapons": round(0.35 * best_kit(stock, "weapon", "power", armed), 1),
|
||||||
"shields": 2 * min(armed, stock.get("iron_shield", 0)),
|
"shields": round(0.5 * best_kit(stock, "shield", "armour", armed), 1),
|
||||||
"arrows": min(100, stock.get("arrows", 0) // 10),
|
"arrows": min(100, stock.get("arrows", 0) // 10),
|
||||||
"rallied": city.get("rally", 0),
|
"rallied": city.get("rally", 0),
|
||||||
"garrison": 0,
|
"garrison": 0,
|
||||||
|
|
@ -1093,16 +1212,18 @@ def council_requests(world: dict, event: dict) -> list[dict]:
|
||||||
("copper_sword", min(armed, max(5, math.ceil(strength / 6)))),
|
("copper_sword", min(armed, max(5, math.ceil(strength / 6)))),
|
||||||
("bread", math.ceil(city["population"] * 1.5)),
|
("bread", math.ceil(city["population"] * 1.5)),
|
||||||
]
|
]
|
||||||
if building_level(world, "smithy") >= 2:
|
if building_level(world, "smithy") >= 1:
|
||||||
wants.append(("iron_shield", min(armed, math.ceil(strength / 10))))
|
wants.append(("iron_shield", min(armed, math.ceil(strength / 10))))
|
||||||
requests = []
|
requests = []
|
||||||
for item, qty in wants:
|
for item, qty in wants:
|
||||||
have = stock.get(item, 0)
|
have = stock.get(item, 0)
|
||||||
if item == "copper_sword":
|
if item in ("copper_sword", "iron_shield"):
|
||||||
have += stock.get("iron_sword", 0)
|
have = kit_count(stock, ITEMS[item]["slot"])
|
||||||
if item == "bread":
|
if item == "bread":
|
||||||
have = sum(stock.get(food, 0) * ITEMS[food]["food"] for food in FOOD_ORDER)
|
have = sum(stock.get(food, 0) * ITEMS[food]["food"] for food in FOOD_ORDER)
|
||||||
requests.append({"item": item, "want": qty, "have": have})
|
label = {"copper_sword": "Swords (any)", "iron_shield": "Shields (any)", "bread": "Food (any)"}[item] \
|
||||||
|
if item in ("copper_sword", "iron_shield", "bread") else ITEMS[item]["name"]
|
||||||
|
requests.append({"item": item, "label": label, "want": qty, "have": have})
|
||||||
return requests
|
return requests
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ let skew = 0; // server clock minus client clock, seconds
|
||||||
let page = decodeURIComponent(location.hash.slice(1)) || remember("page") || "town";
|
let page = decodeURIComponent(location.hash.slice(1)) || remember("page") || "town";
|
||||||
let selected = null; // bank item
|
let selected = null; // bank item
|
||||||
let pickedClass = null; // registration
|
let pickedClass = null; // registration
|
||||||
|
const tabs = {}; // skill -> selected recipe tab
|
||||||
let refreshing = false;
|
let refreshing = false;
|
||||||
let refreshedFor = null; // action start a completion refresh was sent for
|
let refreshedFor = null; // action start a completion refresh was sent for
|
||||||
let version = null; // server build this page was loaded from
|
let version = null; // server build this page was loaded from
|
||||||
|
|
@ -315,8 +316,18 @@ function skillPage(id) {
|
||||||
if (id === "combat") html += combatPanel();
|
if (id === "combat") html += combatPanel();
|
||||||
if (`tool_${id}` in me.equip) html += toolPanel(id);
|
if (`tool_${id}` in me.equip) html += toolPanel(id);
|
||||||
if (id === "reconnaissance") html += `<div class="panel small muted">Every ${8} reconnaissance points on an upcoming event raises its intel by one level, on top of what the spymaster's guild provides. The council & threats page shows the progress.</div>`;
|
if (id === "reconnaissance") html += `<div class="panel small muted">Every ${8} reconnaissance points on an upcoming event raises its intel by one level, on top of what the spymaster's guild provides. The council & threats page shows the progress.</div>`;
|
||||||
|
const actions = C.actions.filter((a) => a.skill === id);
|
||||||
|
const groups = [...new Set(actions.map((a) => a.group).filter(Boolean))];
|
||||||
|
if (groups.length > 2) {
|
||||||
|
// Many tiers: one tab per tier, opening on the highest one you can work.
|
||||||
|
const open = (g) => actions.some((a) => a.group === g && !isLocked(a));
|
||||||
|
let current = tabs[id] || remember(`tab.${id}`);
|
||||||
|
if (!groups.includes(current)) current = [...groups].reverse().find(open) || groups[0];
|
||||||
|
const bar = groups.map((g) => `<button class="tab ${g === current ? "on" : ""} ${open(g) ? "" : "dim"}" data-tab="${id}" data-group="${esc(g)}">${esc(g)}</button>`).join("");
|
||||||
|
return html + `<div class="tabs">${bar}</div><div class="grid">${actions.filter((a) => a.group === current).map(actionCard).join("")}</div>`;
|
||||||
|
}
|
||||||
let group = null, cards = "";
|
let group = null, cards = "";
|
||||||
for (const a of C.actions.filter((a) => a.skill === id)) {
|
for (const a of actions) {
|
||||||
if (a.group && a.group !== group) {
|
if (a.group && a.group !== group) {
|
||||||
group = a.group;
|
group = a.group;
|
||||||
cards += `<div class="section-title grid-head">${esc(group)}</div>`;
|
cards += `<div class="section-title grid-head">${esc(group)}</div>`;
|
||||||
|
|
@ -326,6 +337,12 @@ function skillPage(id) {
|
||||||
return html + `<div class="grid">${cards}</div>`;
|
return html + `<div class="grid">${cards}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Locked: missing a level, class or building, as opposed to just out of materials.
|
||||||
|
function isLocked(a) {
|
||||||
|
const why = S.me.blocked[a.id];
|
||||||
|
return Boolean(why && (why.startsWith("Needs") || why.startsWith("Only")));
|
||||||
|
}
|
||||||
|
|
||||||
function toolPanel(skill) {
|
function toolPanel(skill) {
|
||||||
const tool = S.me.equip[`tool_${skill}`];
|
const tool = S.me.equip[`tool_${skill}`];
|
||||||
return `<div class="panel tool-panel">${tool
|
return `<div class="panel tool-panel">${tool
|
||||||
|
|
@ -365,7 +382,7 @@ function actionCard(a) {
|
||||||
const me = S.me;
|
const me = S.me;
|
||||||
const active = me.action?.id === a.id;
|
const active = me.action?.id === a.id;
|
||||||
const why = me.blocked[a.id];
|
const why = me.blocked[a.id];
|
||||||
const locked = why && (why.startsWith("Needs") || why.startsWith("Only"));
|
const locked = isLocked(a);
|
||||||
const out = Object.keys(a.outputs)[0];
|
const out = Object.keys(a.outputs)[0];
|
||||||
const icon = out ? item(out).icon : skillOf(a.skill).icon;
|
const icon = out ? item(out).icon : skillOf(a.skill).icon;
|
||||||
const time = me.times[a.id];
|
const time = me.times[a.id];
|
||||||
|
|
@ -440,7 +457,7 @@ function itemPanel(id) {
|
||||||
<div class="ico">${it.icon}</div>
|
<div class="ico">${it.icon}</div>
|
||||||
<h3>${esc(it.name)}</h3>
|
<h3>${esc(it.name)}</h3>
|
||||||
<div class="muted small">You have ${fmt(have)} · worth ${price % 1 ? price.toFixed(1) : price} gold each${merchant ? " (merchant in town!)" : ""}</div>
|
<div class="muted small">You have ${fmt(have)} · worth ${price % 1 ? price.toFixed(1) : price} gold each${merchant ? " (merchant in town!)" : ""}</div>
|
||||||
${it.slot ? `<div class="row"><button class="btn gold" data-equip="${id}">${equipLabel(it)}</button></div>` : ""}
|
${it.slot ? equipRow(id, it) : ""}
|
||||||
${it.heal ? `<div class="row"><div class="small muted">Heals ${it.heal} in fights.</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>` : ""}
|
${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 two days in the city granary.</div>` : ""}
|
||||||
|
|
@ -451,6 +468,13 @@ function itemPanel(id) {
|
||||||
</div>${goldPanel()}`;
|
</div>${goldPanel()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function equipRow(id, it) {
|
||||||
|
const [skill, level] = it.req || [];
|
||||||
|
const short = skill && S.me.levels[skill] < level;
|
||||||
|
return `<div class="row"><button class="btn gold" data-equip="${id}" ${short ? "disabled" : ""}>${equipLabel(it)}</button>
|
||||||
|
${skill ? `<div class="small ${short ? "why" : "muted"}">Needs ${esc(skillOf(skill).name.toLowerCase())} level ${level}</div>` : ""}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function equipLabel(it) {
|
function equipLabel(it) {
|
||||||
if (it.tool) return `Use as ${skillOf(it.tool).name.toLowerCase()} tool (${Math.round(it.speed * 100)}% faster)`;
|
if (it.tool) return `Use as ${skillOf(it.tool).name.toLowerCase()} tool (${Math.round(it.speed * 100)}% faster)`;
|
||||||
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`].filter(Boolean).join(", ");
|
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`].filter(Boolean).join(", ");
|
||||||
|
|
@ -609,7 +633,7 @@ function raidPrep(ev) {
|
||||||
const names = { rallied: "Rallied militia (favour)", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Militia (barracks)", weapons: "Swords in the armoury", shields: "Shields in the armoury", arrows: "Arrows", garrison: "Garrison" };
|
const names = { rallied: "Rallied militia (favour)", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Militia (barracks)", weapons: "Swords in the armoury", shields: "Shields in the armoury", arrows: "Arrows", garrison: "Garrison" };
|
||||||
const reqs = (ev.requests || []).map((r) => {
|
const reqs = (ev.requests || []).map((r) => {
|
||||||
const mine = me.bank[r.item] || 0;
|
const mine = me.bank[r.item] || 0;
|
||||||
return `<div class="req"><span>${item(r.item).icon}</span><span>${r.item === "bread" ? "Food (any)" : esc(item(r.item).name)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
|
return `<div class="req"><span>${item(r.item).icon}</span><span>${esc(r.label)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
|
||||||
${mine ? `<button class="btn sm gold" data-donate="${r.item}" data-qty="all">Give ${fmt(mine)}</button>` : "<span></span>"}
|
${mine ? `<button class="btn sm gold" data-donate="${r.item}" data-qty="all">Give ${fmt(mine)}</button>` : "<span></span>"}
|
||||||
<div class="bar"><span style="width:${Math.min(100, r.have / r.want * 100)}%"></span></div></div>`;
|
<div class="bar"><span style="width:${Math.min(100, r.have / r.want * 100)}%"></span></div></div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
@ -812,6 +836,7 @@ document.addEventListener("click", (e) => {
|
||||||
if (d.food) return act("/api/food", { item: d.food });
|
if (d.food) return act("/api/food", { item: d.food });
|
||||||
if (d.overflow) return act("/api/settings", { overflow: d.overflow });
|
if (d.overflow) return act("/api/settings", { overflow: d.overflow });
|
||||||
if (d.class) { pickedClass = d.class; render(); return; }
|
if (d.class) { pickedClass = d.class; render(); return; }
|
||||||
|
if (d.tab) { tabs[d.tab] = d.group; remember(`tab.${d.tab}`, d.group); render(); return; }
|
||||||
if (d.devAdvance) return act("/api/dev/advance", { hours: Number(d.devAdvance) });
|
if (d.devAdvance) return act("/api/dev/advance", { hours: Number(d.devAdvance) });
|
||||||
if ("devEvent" in d) return act("/api/dev/event", {});
|
if ("devEvent" in d) return act("/api/dev/event", {});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -370,3 +370,7 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
||||||
.favour-option:first-of-type { border-top: 0; }
|
.favour-option:first-of-type { border-top: 0; }
|
||||||
.favour-option .ico { font-size: 24px; width: 30px; text-align: center; }
|
.favour-option .ico { font-size: 24px; width: 30px; text-align: center; }
|
||||||
.favour-option .grow { display: flex; flex-direction: column; }
|
.favour-option .grow { display: flex; flex-direction: column; }
|
||||||
|
.tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
|
||||||
|
.tab { border: 1px solid var(--line); background: var(--panel); border-radius: 999px; padding: 5px 14px; font-weight: 600; font-size: 14px; }
|
||||||
|
.tab.on { background: var(--gold); border-color: var(--gold); color: var(--bg); }
|
||||||
|
.tab.dim:not(.on) { opacity: .5; }
|
||||||
|
|
|
||||||
|
|
@ -514,5 +514,64 @@ class FavourTest(unittest.TestCase):
|
||||||
self.assertEqual(game.spend_favour(self.world, self.p, "builders", T0), "Needs 40 favour")
|
self.assertEqual(game.spend_favour(self.world, self.p, "builders", T0), "Needs 40 favour")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TierTest(unittest.TestCase):
|
||||||
|
def test_steel_needs_coal_and_the_smithy(self):
|
||||||
|
world = world_with(("kay", "artisan"))
|
||||||
|
p = world["players"]["kay"]
|
||||||
|
p["xp"]["smithing"] = game.XP_TABLE[30]
|
||||||
|
p["bank"] = {"iron_ore": 5, "coal": 10}
|
||||||
|
self.assertEqual(game.start_action(world, p, "smith_steel_bar", T0), "Needs Smithy level 2")
|
||||||
|
world["city"]["buildings"]["smithy"]["level"] = 2
|
||||||
|
self.assertIsNone(game.start_action(world, p, "smith_steel_bar", T0))
|
||||||
|
game.process_player(world, p, T0 + 1000, random.Random(1))
|
||||||
|
self.assertGreaterEqual(p["bank"]["steel_bar"], 5)
|
||||||
|
self.assertNotIn("iron_ore", p["bank"])
|
||||||
|
|
||||||
|
def test_every_tier_has_full_kit(self):
|
||||||
|
for key, *_ in game.METALS:
|
||||||
|
for piece in ("sword", "shield", "helmet", "legs", "body", "pickaxe", "axe", "hoe", "fishing_rod", "hunting_knife"):
|
||||||
|
self.assertIn(f"{key}_{piece}", game.ITEMS)
|
||||||
|
self.assertIn(f"smith_{key}_{piece}", game.ACTION_BY_ID)
|
||||||
|
for *_, prefix, _label, _level, _armour in game.LEATHERS:
|
||||||
|
for piece in ("cap", "legs", "body"):
|
||||||
|
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
|
||||||
|
self.assertEqual(game.ITEMS["rune_body"]["plural"], "Rune cuirasses")
|
||||||
|
|
||||||
|
def test_gear_needs_levels_to_wear(self):
|
||||||
|
world = world_with(("wil", "warrior"))
|
||||||
|
p = world["players"]["wil"]
|
||||||
|
self.assertEqual(game.equip_problem(p, "rune_sword"), "Needs combat level 50")
|
||||||
|
self.assertEqual(game.equip_problem(p, "steel_pickaxe"), "Needs mining level 20")
|
||||||
|
self.assertIsNone(game.equip_problem(p, "copper_sword"))
|
||||||
|
p["xp"]["combat"] = game.XP_TABLE[50]
|
||||||
|
self.assertIsNone(game.equip_problem(p, "rune_sword"))
|
||||||
|
|
||||||
|
def test_militia_takes_the_best_weapons_first(self):
|
||||||
|
world = world_with()
|
||||||
|
world["city"]["stockpile"] = {"copper_sword": 50, "rune_sword": 2, "iron_shield": 3}
|
||||||
|
parts = game.defence(world)["parts"]
|
||||||
|
armed = int(world["city"]["population"] // 2) # 10
|
||||||
|
self.assertEqual(parts["weapons"], round(0.35 * (2 * 35 + (armed - 2) * 4), 1))
|
||||||
|
self.assertEqual(parts["shields"], 0.5 * 3 * 4)
|
||||||
|
|
||||||
|
def test_higher_building_levels_need_higher_tiers(self):
|
||||||
|
self.assertNotIn("steel_bar", game.build_cost("walls", 2))
|
||||||
|
self.assertEqual(game.build_cost("walls", 3)["steel_bar"], 60)
|
||||||
|
self.assertEqual(game.build_cost("walls", 5)["mithril_bar"], 40)
|
||||||
|
self.assertEqual(game.build_cost("walls", 5)["steel_bar"], 180)
|
||||||
|
|
||||||
|
def test_dragons_drop_dragonhide(self):
|
||||||
|
world = world_with(("wil", "warrior"))
|
||||||
|
p = world["players"]["wil"]
|
||||||
|
p["xp"]["combat"] = game.XP_TABLE[80]
|
||||||
|
p["equip"].update(weapon="rune_sword", shield="rune_shield", helmet="rune_helmet", body="rune_body", legs="rune_legs")
|
||||||
|
p["hp"] = game.max_hp(p)
|
||||||
|
p["bank"] = {"cooked_swordfish": 200}
|
||||||
|
game.start_action(world, p, "fight_green_dragon", T0)
|
||||||
|
game.process_player(world, p, T0 + 3600 * 2, random.Random(3))
|
||||||
|
self.assertGreater(p["bank"].get("green_dragonhide", 0), 30)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue