diff --git a/README.md b/README.md
index fc3aad3..ec9953c 100644
--- a/README.md
+++ b/README.md
@@ -51,10 +51,14 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
area. Rings for offence (swing speed, crit, power), amulets for defence (max
HP, ward, regen), trinkets for the rest (XP, double gathering,
masterwork).
-- **Stations and storage:** builders make a station for every crafting
- skill (anvil stand, jeweller's bench, hearth, alembic, tanning rack, loom),
- as fast as the smith's tools for gathering, from the tier's planks and
- hardware and another trade's bars, leather or cloth. Builders also make
+- **Tools and stations:** smiths make every tool: one per gathering skill and
+ one per crafting skill (chisel, gem drill, hammer, cleaver, mortar and
+ pestle, awl, shears). Builders make every station, a second slot for each
+ crafting skill (anvil stand, jeweller's bench, workbench, hearth, alembic,
+ tanning rack, loom), from the tier's planks and hardware and another
+ trade's bars, leather or cloth. Both run 15โ60% faster by tier, and a
+ station speeds what's made at it, not the building site.
+- **Storage:** builders make
strongboxes (+2 to +14 bank slots) and homesteaders sew bags (+250 to
+2,000 per stack); each has one slot on the Bank page, and a smaller one
can't go on while the bank holds more than it would.
diff --git a/game.py b/game.py
index b0adba1..aa73bdf 100644
--- a/game.py
+++ b/game.py
@@ -120,14 +120,18 @@ def _armour_stats(weight: str, slot: str, tier: int) -> dict:
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.
+# Tools: one slot per skill, `speed` cuts the action time. Smiths make every tool.
TOOLS = {"mining": ("pickaxe", "โ๏ธ"), "woodcutting": ("axe", "๐ช"), "farming": ("hoe", "๐ช"),
- "fishing": ("fishing rod", "๐ฃ"), "hunting": ("hunting knife", "๐ช"), "construction": ("hammer", "๐จ")}
+ "fishing": ("fishing rod", "๐ฃ"), "hunting": ("hunting knife", "๐ช"), "construction": ("hammer", "๐จ"),
+ "smithing": ("chisel", "๐ง"), "jewelcrafting": ("gem drill", "๐ช"), "cooking": ("cleaver", "๐ช"),
+ "herblore": ("mortar and pestle", "๐ฅฃ"), "leatherworking": ("awl", "๐"), "tailoring": ("shears", "โ๏ธ")}
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)
+# Crafting stations: a second slot for every crafting skill, beside its tool, and as fast.
+# Builders make every station, from the tier's planks and hardware and a material from
+# another trade. A station speeds what's made at it, not work like the building site.
+# skill: (id suffix, noun, glyph, other material)
STATIONS = {
+ "construction": ("workbench", "workbench", "toolbox", "bar"),
"smithing": ("anvil_stand", "anvil stand", "anvil-impact", "leather"),
"jewelcrafting": ("jewellers_bench", "jeweller's bench", "gem-chain", "cloth"),
"cooking": ("hearth", "hearth", "campfire", "bar"),
@@ -315,7 +319,7 @@ def station_material(kind: str, tier: int) -> str:
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,
+ "name": f"{WOOD_NAMES[_tier]} {_noun}", "icon": "๐ช", "tier": _tier, "slot": f"station_{_skill}", "station": _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}
@@ -433,7 +437,7 @@ 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", "storage", "bag"]
- + [f"tool_{skill}" for skill in [*TOOLS, *STATIONS]])
+ + [f"tool_{skill}" for skill in TOOLS] + [f"station_{skill}" for skill in STATIONS])
SKILLS = [
{"id": "mining", "name": "Mining", "icon": "โ๏ธ", "group": "Gathering"},
@@ -1090,7 +1094,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
-WORLD_VERSION = 22
+WORLD_VERSION = 23
def new_world(now: float, speed: float = 1.0) -> dict:
@@ -1257,6 +1261,17 @@ def migrate(world: dict, now: float | None = None) -> dict:
for player in world["players"].values():
player.setdefault("tracked", None)
world["version"] = 22
+ if world["version"] < 23:
+ # Stations got their own slot: crafting skills have a tool and a station now.
+ for player in world["players"].values():
+ equip = player["equip"]
+ for slot in EQUIP_SLOTS:
+ equip.setdefault(slot, None)
+ for skill in STATIONS:
+ item = equip.get(f"tool_{skill}")
+ if item and ITEMS[item].get("station"):
+ equip[f"tool_{skill}"], equip[f"station_{skill}"] = None, item
+ world["version"] = 23
return world
@@ -1884,6 +1899,9 @@ def action_time(world: dict, player: dict, action: dict) -> float:
tool = player["equip"].get(f"tool_{action['skill']}")
if tool:
seconds *= 1 - ITEMS[tool]["speed"]
+ station = player["equip"].get(f"station_{action['skill']}")
+ if station and action["outputs"]:
+ seconds *= 1 - ITEMS[station]["speed"]
if action["skill"] == "smithing":
seconds *= 1 - 0.08 * building_level(world, "smithy")
seconds *= 0.9 if fitted(world, "bellows") else 1
@@ -3990,7 +4008,8 @@ HERB_TINTS = {"guam_leaf": "#7fbf5a", "marrentill": "#e7c84a", "tarromin": "#d05
PIECE_GLYPHS = {"sword": "pointy-sword", "shield": "round-shield", "helmet": "visored-helm",
"legs": "leg-armor", "body": "breastplate"}
TOOL_GLYPHS = {"pickaxe": "war-pick", "axe": "wood-axe", "hoe": "sickle", "fishing rod": "fishing-pole",
- "hunting knife": "bowie-knife", "hammer": "claw-hammer"}
+ "hunting knife": "bowie-knife", "hammer": "claw-hammer", "chisel": "chisel", "gem drill": "drill",
+ "cleaver": "meat-cleaver", "mortar and pestle": "mortar", "awl": "needle-drill", "shears": "shears"}
LEATHER_GLYPHS = {"cap": "hood", "legs": "trousers", "body": "leather-vest"}
CLOTH_GLYPHS = {"hood": "pointy-hat", "legs": "trousers", "body": "robe"}
FIBRE_GLYPHS = {"cotton": "cotton-flower", "silk_cocoons": "butterfly", "moonpetal": "lotus-flower",
diff --git a/static/app.js b/static/app.js
index bcbf497..62ae17f 100644
--- a/static/app.js
+++ b/static/app.js
@@ -523,16 +523,18 @@ function skillGear(skill) {
: `No potion`}
${others ? `
${others}
` : ""}`);
}
- if (`tool_${skill}` in me.equip) {
- const id = me.equip[`tool_${skill}`], it = id && item(id), noTool = C.stations.includes(skill) ? "No station" : "No tool";
- const others = Object.keys(me.bank).filter((x) => item(x).tool === skill).sort((a, b) => item(b).speed - item(a).speed).map((x) => {
+ for (const [kind, none] of [["tool", "No tool"], ["station", "No station"]]) {
+ const slot = `${kind}_${skill}`;
+ if (!(slot in me.equip)) continue;
+ const id = me.equip[slot], it = id && item(id);
+ const others = Object.keys(me.bank).filter((x) => item(x)[kind] === skill).sort((a, b) => item(b).speed - item(a).speed).map((x) => {
const [s, level] = item(x).req || [], why = s && me.levels[s] < level ? `Needs ${skillOf(s).name.toLowerCase()} ${level}` : "";
return ``;
}).join("");
cards.push(`