Crafting stations, strongboxes and bags

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 21:00:31 +01:00
parent 636123e454
commit 722cf0b382
6 changed files with 182 additions and 14 deletions

94
game.py
View file

@ -124,6 +124,20 @@ COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body":
TOOLS = {"mining": ("pickaxe", "⛏️"), "woodcutting": ("axe", "🪓"), "farming": ("hoe", "🪏"),
"fishing": ("fishing rod", "🎣"), "hunting": ("hunting knife", "🔪"), "construction": ("hammer", "🔨")}
TOOL_SPEED = [0.15, 0.30, 0.38, 0.45, 0.52, 0.60]
# Crafting stations: the builder's tools for every crafting skill, as fast as the smith's
# tools for gathering. Each takes the tier's planks and hardware and a material from
# another trade. skill: (id suffix, noun, glyph, other material)
STATIONS = {
"smithing": ("anvil_stand", "anvil stand", "anvil-impact", "leather"),
"jewelcrafting": ("jewellers_bench", "jeweller's bench", "gem-chain", "cloth"),
"cooking": ("hearth", "hearth", "campfire", "bar"),
"herblore": ("alembic", "alembic", "erlenmeyer", "bar"),
"leatherworking": ("tanning_rack", "tanning rack", "animal-hide", "cloth"),
"tailoring": ("loom", "loom", "spinning-wheel", "leather"),
}
# Storage: a builder's strongbox adds bank slots, a homesteader's bag a bigger stack.
STRONGBOX_SLOTS = [2, 4, 6, 8, 11, 14]
BAG_STACK = [250, 500, 750, 1000, 1500, 2000]
# item id, name, icon, value, action id, verb
TREES = [
@ -290,6 +304,24 @@ for _tier, _metal in enumerate(METALS):
ITEMS[f"{WOODS[_tier]}_staff"] = {"name": STAVES[_tier], "plural": STAVES[_tier].replace("staff", "staves"),
"icon": "🪄", "style": "staff", **_weapon}
ITEMS[f"{WOODS[_tier]}_bow"] = {"name": BOWS[_tier], "icon": "🏹", "style": "bow", **_weapon}
WOOD_NAMES = ["Wooden", "Oak", "Willow", "Maple", "Yew", "Enchanted"]
def station_material(kind: str, tier: int) -> str:
"""The other trade's part of a station: the tier's bar, leather or cloth."""
return {"bar": f"{METALS[tier][0]}_bar", "leather": LEATHERS[tier][1][0], "cloth": CLOTHS[tier][4][0]}[kind]
for _tier, _wood in enumerate(WOODS):
for _skill, (_suffix, _noun, _glyph, _kind) in STATIONS.items():
ITEMS[f"{_wood}_{_suffix}"] = {
"name": f"{WOOD_NAMES[_tier]} {_noun}", "icon": "🪚", "tier": _tier, "slot": f"tool_{_skill}", "tool": _skill,
"speed": TOOL_SPEED[_tier], "req": [_skill, EQUIP_LEVELS[_tier]], "value": 0}
ITEMS[f"{_wood}_strongbox"] = {"name": f"{WOOD_NAMES[_tier]} strongbox", "plural": f"{WOOD_NAMES[_tier]} strongboxes",
"icon": "🧰", "tier": _tier, "slot": "storage", "slots": STRONGBOX_SLOTS[_tier], "value": 0}
for _tier, (_fibre, _level, _time, _xp, _cloth, _prefix, _label) in enumerate(CLOTHS):
ITEMS[f"{_prefix}_bag"] = {"name": f"{_label} bag", "icon": "👜", "tier": _tier, "slot": "bag",
"stack": BAG_STACK[_tier], "value": 0}
for _tier, (_raw, _leather, _prefix, _label, _level) 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]}
@ -400,7 +432,8 @@ for _id, _name, _level, _herb, _second, _skill, _effect in POTIONS:
# What the city eats first: plain food before the good stuff.
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", "ring", "amulet", "trinket"] + [f"tool_{skill}" for skill in TOOLS]
EQUIP_SLOTS = (["weapon", "shield", "helmet", "body", "legs", "ring", "amulet", "trinket", "storage", "bag"]
+ [f"tool_{skill}" for skill in [*TOOLS, *STATIONS]])
SKILLS = [
{"id": "mining", "name": "Mining", "icon": "⛏️", "group": "Gathering"},
@ -789,6 +822,30 @@ def _weapon_actions() -> list[dict]:
return actions
def _storage_actions() -> list[dict]:
"""Crafting stations and strongboxes (Construction), and bags (Tailoring)."""
actions = []
for tier, wood in enumerate(WOODS):
plank, (hardware, *_rest, made) = PLANKS[tier][0], HARDWARE[tier]
for skill, (suffix, _noun, _glyph, kind) in STATIONS.items():
inputs = {plank: 3, hardware: made, station_material(kind, tier): 2}
actions.append(_action(f"make_{wood}_{suffix}", "construction", ITEMS[f"{wood}_{suffix}"]["name"],
TIER_LEVELS[tier] + 4, 40 + 3 * tier, [14, 30, 48, 68, 90, 115][tier], inputs,
{f"{wood}_{suffix}": 1}, group="Stations"))
inputs = {plank: 4, hardware: 2 * made, LEATHERS[tier][1][0]: 2}
actions.append(_action(f"make_{wood}_strongbox", "construction", ITEMS[f"{wood}_strongbox"]["name"],
TIER_LEVELS[tier] + 8, 45 + 3 * tier, [18, 38, 60, 85, 112, 142][tier], inputs,
{f"{wood}_strongbox": 1}, group="Storage"))
for tier, (_fibre, _level, _time, _xp, cloth, prefix, label) in enumerate(CLOTHS):
actions.append(_action(f"sew_{prefix}_bag", "tailoring", f"{label} bag", TIER_LEVELS[tier] + 8, 45 + 3 * tier,
[18, 38, 60, 85, 112, 142][tier], {cloth[0]: 4, LEATHERS[tier][1][0]: 1},
{f"{prefix}_bag": 1}, group=label))
for action in actions:
made_item = ITEMS[next(iter(action["outputs"]))]
made_item["value"] = round(sum(ITEMS[item]["value"] * qty for item, qty in action["inputs"].items()) * 1.3)
return actions
def _jewel_actions() -> list[dict]:
"""Silver and gold, prospecting ore for gems, and every tier's jewellery."""
actions = [
@ -1033,7 +1090,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
WORLD_VERSION = 20
WORLD_VERSION = 21
def new_world(now: float, speed: float = 1.0) -> dict:
@ -1190,6 +1247,12 @@ def migrate(world: dict, now: float | None = None) -> dict:
if world["version"] < 20:
_precision_to_haste(world)
world["version"] = 20
if world["version"] < 21:
# Crafting stations, strongboxes and bags: their slots.
for player in world["players"].values():
for slot in EQUIP_SLOTS:
player["equip"].setdefault(slot, None)
world["version"] = 21
return world
@ -1459,22 +1522,29 @@ def skill_level(player: dict, skill: str) -> int:
def bank_slots(world: dict, player: dict) -> int:
return (BASE_SLOTS + 3 * building_level(world, "storehouse") + TIER_SLOTS * world["city"]["tier"]
+ player["perks"]["bank_slot"] + fitted(world, "shelving"))
+ player["perks"]["bank_slot"] + fitted(world, "shelving") + int(gear_bonus(player, "slots")))
def stack_size(world: dict) -> int:
return BASE_STACK + 250 * building_level(world, "storehouse") + 500 * fitted(world, "enchanted_chests")
def stack_size(world: dict, player: dict | None = None) -> int:
"""How many of one item fit in a bank slot: the storehouse's, plus your bag's."""
bag = int(gear_bonus(player, "stack")) if player else 0
return BASE_STACK + 250 * building_level(world, "storehouse") + 500 * fitted(world, "enchanted_chests") + bag
def overfull(world: dict, player: dict) -> int:
"""Bank slots in use past what the bank holds (after a smaller strongbox or bag, say)."""
return slots_used(world, player) - bank_slots(world, player)
def slots_used(world: dict, player: dict) -> int:
"""Bank slots taken: an item spills into another slot every full stack."""
stack = stack_size(world)
stack = stack_size(world, player)
return sum(-(-qty // stack) for qty in player["bank"].values())
def bank_room(world: dict, player: dict, item: str) -> int:
"""How many more of `item` fit: the rest of its last stack, plus any free slots."""
stack = stack_size(world)
stack = stack_size(world, player)
have = player["bank"].get(item, 0)
partial = -have % stack # room left in the item's last, part-filled stack
free = max(0, bank_slots(world, player) - slots_used(world, player))
@ -3320,7 +3390,7 @@ FITTINGS = [
for _weapon in _weapon_actions():
ACTIONS.append(_weapon)
ACTION_BY_ID[_weapon["id"]] = _weapon
for _jewel in _jewel_actions():
for _jewel in [*_jewel_actions(), *_storage_actions()]:
ACTIONS.append(_jewel)
ACTION_BY_ID[_jewel["id"]] = _jewel
FITTING_BY_ID = {}
@ -4026,6 +4096,12 @@ def _assign_glyphs() -> None:
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
for herb, tint in HERB_TINTS.items():
ITEMS[herb].update(glyph="herbs-bundle", tint=tint)
for tier, (wood, tint) in enumerate(zip(WOODS, WOOD_TINTS)):
for suffix, _noun, glyph, _kind in STATIONS.values():
ITEMS[f"{wood}_{suffix}"].update(glyph=glyph, tint=tint)
ITEMS[f"{wood}_strongbox"].update(glyph="locked-chest", tint=tint)
for cloth, tint in zip(CLOTHS, CLOTH_TINTS):
ITEMS[f"{cloth[5]}_bag"].update(glyph="shoulder-bag", tint=tint)
for gem, _name, _value, tint, glyph in GEMS:
ITEMS[gem].update(glyph=glyph, tint=tint)
for key, *_rest, piece_glyph in JEWELLERY:
@ -4088,6 +4164,6 @@ def content() -> dict:
"order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
"styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE,
"resistant": RESISTANT, "jewel_stats": JEWEL_STATS, "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
"resistant": RESISTANT, "jewel_stats": JEWEL_STATS, "stations": list(STATIONS), "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
"regen_per_hour": REGEN_PER_HOUR,
}