Firekeeping: shared guild fires

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-21 12:16:22 +01:00
parent c64e28fc38
commit 54ae3ae641
6 changed files with 657 additions and 34 deletions

243
game.py
View file

@ -123,7 +123,8 @@ COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body":
TOOLS = {"mining": ("pickaxe", "⛏️"), "woodcutting": ("axe", "🪓"), "farming": ("hoe", "🪏"),
"fishing": ("fishing rod", "🎣"), "hunting": ("hunting knife", "🔪"), "construction": ("hammer", "🔨"),
"smithing": ("chisel", "🔧"), "jewelcrafting": ("gem drill", "🪛"), "cooking": ("cleaver", "🔪"),
"herblore": ("mortar and pestle", "🥣"), "leatherworking": ("awl", "📍"), "tailoring": ("shears", "✂️")}
"herblore": ("mortar and pestle", "🥣"), "leatherworking": ("awl", "📍"), "tailoring": ("shears", "✂️"),
"firekeeping": ("bellows", "🪭")}
TOOL_SPEED = [0.15, 0.30, 0.38, 0.45, 0.52, 0.60]
# 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
@ -151,6 +152,38 @@ TREES = [
("yew_logs", "Yew logs", "🌲", 30, "cut_yew", "Cut yews"),
("magic_logs", "Enchanted logs", "🔮", 55, "cut_magic", "Cut enchanted trees"),
]
FIREWOOD = tuple(tree[0] for tree in TREES)
# The guild's fires, shared by everyone. A craft at a fire takes one log of its tier as
# fuel, and a fire burning that tier's flame or hotter pays it (the forge a coal too).
# Firekeepers feed them: fuel burns hottest first, and a better log pushes cheaper fuel
# out of a full fire. Level gives more minutes per log and a bigger bank.
FIRES = {
"forge": {"name": "Forge", "icon": "⚒️", "skill": "smithing", "glyph": "anvil-impact"},
"kitchen": {"name": "Kitchen fire", "icon": "🍳", "skill": "cooking", "glyph": "cooking-pot"},
"kiln": {"name": "Kiln", "icon": "🧱", "skill": "construction", "glyph": "brick-pile"},
"still": {"name": "Still", "icon": "⚗️", "skill": "herblore", "glyph": "erlenmeyer"},
}
FLAMES = [("Red", "#ff6a2a"), ("Gold", "#ffcb3d"), ("White", "#eeeaf8"),
("Blue", "#4fa3ff"), ("Green", "#3ed37a"), ("Violet", "#b07cff")]
TEND_XP = [2, 3, 4.5, 5.5, 6.5, 7.5] # per game minute of fire fed, by the log's tier
WARMTH = 0.1 # of a craft's XP, to whoever fed the fuel that paid for it
FIRE_LOW = 30 * 60 # game seconds left when a fire counts as low
def fire_minutes(level: int) -> float:
"""Game minutes of fire one log gives at this Firekeeping level."""
return 3 + 0.12 * level
def fire_bank(level: int) -> float:
"""The most fuel, in game minutes, a keeper of this level can bank in a fire."""
return 60 + 9 * level
def _fuelled(inputs: dict, tier: int) -> dict:
"""A fire craft's inputs: its materials and one log of its tier to burn."""
return {**inputs, FIREWOOD[tier]: inputs.get(FIREWOOD[tier], 0) + 1}
# Every tier's hide comes from Hunting. raw hide (id, name, icon, value), leather (id, name, value),
# armour id prefix, armour name prefix, leatherworking level
@ -452,6 +485,7 @@ SKILLS = [
{"id": "leatherworking", "name": "Leatherworking", "icon": "🪡", "group": "Crafting"},
{"id": "tailoring", "name": "Tailoring", "icon": "🧵", "group": "Crafting"},
{"id": "herblore", "name": "Herblore", "icon": "🧪", "group": "Crafting"},
{"id": "firekeeping", "name": "Firekeeping", "icon": "🔥", "group": "Crafting"},
{"id": "combat", "name": "Combat", "icon": "⚔️", "group": "Combat"},
{"id": "prayer", "name": "Prayer", "icon": "🙏", "group": "Combat"},
{"id": "reconnaissance", "name": "Reconnaissance", "icon": "🔭", "group": "Combat"},
@ -473,8 +507,8 @@ CLASSES = {
"combat": "Warhammer: +15% damage",
},
"builder": {
"name": "Builder", "icon": "🏗️", "fast": ["woodcutting", "construction"], "craft": ["construction"],
"bonus": "Woodcutting and construction 20% faster",
"name": "Builder", "icon": "🏗️", "fast": ["woodcutting", "construction", "firekeeping"], "craft": ["construction"],
"bonus": "Woodcutting, construction and firekeeping 20% faster",
"masterwork": "Masterwork: 10% double construction crafts",
"signature": "Foreman: +1 labour per site action",
"combat": "Sword: +15% damage",
@ -520,11 +554,10 @@ def _tiered_actions() -> list[dict]:
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})
inputs = {"iron_ore" if key == "steel" else ore[0]: 1, **({"coal": coal} if coal else {})}
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 {}))
bar_xp[tier], _fuelled(inputs, tier), {f"{key}_bar": 1}, group="Bars",
city={"smithy": smithy} if tier >= 2 else {}, fire="forge", fuel=tier))
hardware, _name, _plural, bars, made = HARDWARE[tier]
actions.append(_action(f"smith_{hardware}", "smithing", _plural, TIER_LEVELS[tier] + 2, 25 + 2 * tier,
bars * piece_xp[tier], {f"{key}_bar": bars}, {hardware: made}, group="Hardware",
@ -576,7 +609,8 @@ def _tiered_actions() -> list[dict]:
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}, group="Fish"))
[6, 15, 27, 42, 58, 80][tier], _fuelled({raw[0]: 1}, tier), {cooked[0]: 1}, group="Fish",
fire="kitchen", fuel=tier))
return actions
@ -934,7 +968,8 @@ ACTIONS = [
*[_action(f"grow_{herb}", "farming", f"Grow {name.lower()}", level, seconds, xp, outputs={herb: 1}, group="Herbs")
for herb, name, _icon, level, seconds, xp, _value in HERBS],
*[_action(f"brew_{potion}", "herblore", name, level, 30 + level // 3, round(4 + level * 1.6),
{herb: 1, second: 1}, {potion: 1}, group=f"Tier {ROMAN[POTION_TIER[potion]]}")
_fuelled({herb: 1, second: 1}, POTION_TIER[potion]), {potion: 1}, group=f"Tier {ROMAN[POTION_TIER[potion]]}",
fire="still", fuel=POTION_TIER[potion])
for potion, name, level, herb, second, skill, _effect in POTIONS],
_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}),
@ -957,14 +992,17 @@ ACTIONS = [
*[_action(f"saw_{plank}", "construction", f"Saw {name.lower()}s", TIER_LEVELS[tier], 25 + 3 * tier,
[6, 14, 22, 32, 44, 58][tier], {TREES[tier][0]: 2}, {plank: 1}, group="Planks")
for tier, (plank, name) in enumerate(PLANKS)],
_action("mason_bricks", "construction", "Bricks", 1, 25, 6, {"stone": 2, "logs": 1}, {"bricks": 2}, group="Masonry"),
_action("mason_bricks", "construction", "Bricks", 1, 25, 6, {"stone": 2, "logs": 1}, {"bricks": 2}, group="Masonry",
fire="kiln", fuel=0),
_action("mason_cut_stone", "construction", "Cut stone", 10, 30, 12, {"stone": 3}, {"cut_stone": 1}, group="Masonry"),
_action("mason_beam", "construction", "Reinforced beam", 20, 45, 30,
{"oak_logs": 2, "iron_nails": 5}, {"beam": 1}, group="Masonry"),
_action("cook_bread", "cooking", "Bread", 1, 20, 5, {"wheat": 2, "logs": 1}, {"bread": 1}, group="Meals"),
_action("cook_meat", "cooking", "Cooked meat", 5, 20, 7, {"raw_meat": 1, "logs": 1}, {"cooked_meat": 1}, group="Meals"),
_action("cook_bread", "cooking", "Bread", 1, 20, 5, {"wheat": 2, "logs": 1}, {"bread": 1}, group="Meals",
fire="kitchen", fuel=0),
_action("cook_meat", "cooking", "Cooked meat", 5, 20, 7, {"raw_meat": 1, "logs": 1}, {"cooked_meat": 1}, group="Meals",
fire="kitchen", fuel=0),
_action("cook_stew", "cooking", "Stew", 15, 35, 16,
{"raw_meat": 1, "vegetables": 2, "logs": 1}, {"stew": 2}, group="Meals"),
{"raw_meat": 1, "vegetables": 2, "logs": 1}, {"stew": 2}, group="Meals", fire="kitchen", fuel=0),
_action("pack_bread", "cooking", "Pack bread rations", 1, 20, 6, {"bread": 2}, {"provisions": 1}, group="Rations"),
_action("pack_meat", "cooking", "Pack meat rations", 5, 20, 7, {"cooked_meat": 2}, {"provisions": 1}, group="Rations"),
_action("pack_fish", "cooking", "Pack fish rations", 1, 20, 6, {"cooked_shrimp": 2}, {"provisions": 1}, group="Rations"),
@ -980,6 +1018,10 @@ ACTIONS = [
devotion=20, city={"temple": 2}, group="Offer at the temple"),
_action("recon_roads", "reconnaissance", "Scout the roads", 1, 60, 12, special="recon", recon=1),
_action("recon_camps", "reconnaissance", "Shadow the war bands", 15, 90, 28, special="recon", recon=2),
# Tending: one log a go into one fire; better logs need more Firekeeping.
*[_action(f"tend_{fire}_{log}", "firekeeping", f"Tend the {spec['name'].lower()}", TIER_LEVELS[tier], 30, 0,
{log: 1}, special="tend", tend=fire, tier=tier, group=spec["name"])
for fire, spec in FIRES.items() for tier, log in enumerate(FIREWOOD)],
]
ACTION_BY_ID = {action["id"]: action for action in ACTIONS}
for _id, _gem in GEM_ROCKS.items():
@ -1095,7 +1137,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
WORLD_VERSION = 23
WORLD_VERSION = 24
def new_world(now: float, speed: float = 1.0) -> dict:
@ -1118,6 +1160,7 @@ def new_world(now: float, speed: float = 1.0) -> dict:
"food_due": 0.0,
"starving": False,
"repelled": 0,
"fires": {key: new_fire(now) for key in FIRES},
},
"events": [],
"next_event_at": now + 12 * HOUR / speed,
@ -1273,6 +1316,15 @@ def migrate(world: dict, now: float | None = None) -> dict:
if item and ITEMS[item].get("station"):
equip[f"tool_{skill}"], equip[f"station_{skill}"] = None, item
world["version"] = 23
if world["version"] < 24:
# Firekeeping: a new skill, its bellows slot, and the guild's fires.
world["city"].setdefault("fires", {})
for key in FIRES:
world["city"]["fires"].setdefault(key, new_fire(now))
for player in world["players"].values():
player["xp"].setdefault("firekeeping", 0)
player["equip"].setdefault("tool_firekeeping", None)
world["version"] = 24
return world
@ -2000,6 +2052,106 @@ def _use_charge(player: dict, skill: str, summary: dict) -> None:
summary["potion_out"].append(active["item"])
# --- fires -----------------------------------------------------------------
# Each fire is a list of fuel: {tier, left (game seconds), by (who fed it)}. It burns on
# game time, hottest fuel first and the oldest of that first, so the flame's colour is
# the best fuel still in it.
def new_fire(now: float) -> dict:
return {"fuel": [], "at": now, "saved": 0}
def burn_fires(world: dict, now: float) -> None:
"""Burn every fire down to `now`. Never runs backwards."""
for fire in world["city"]["fires"].values():
if now <= fire["at"]:
continue
seconds = (now - fire["at"]) * speed(world)
fire["at"] = now
fuel = fire["fuel"]
while seconds > 0 and fuel:
chunk = burning(fire)
used = min(seconds, chunk["left"])
chunk["left"] -= used
seconds -= used
if chunk["left"] <= 1e-6:
fuel.remove(chunk)
def burning(fire: dict) -> dict | None:
"""The fuel burning right now: the hottest, and the oldest of those."""
return max(fire["fuel"], key=lambda chunk: chunk["tier"], default=None)
def heat(world: dict, fire: str) -> int:
"""The tier of a fire's flame, or -1 when it's out."""
chunk = burning(world["city"]["fires"][fire])
return chunk["tier"] if chunk else -1
def _room(fire: dict, tier: int, level: int) -> float:
"""Game seconds of cheaper fuel a log of `tier` would push out, or -1 if it won't fit."""
short = sum(chunk["left"] for chunk in fire["fuel"]) + fire_minutes(level) * 60 - fire_bank(level) * 60
cheaper = sum(chunk["left"] for chunk in fire["fuel"] if chunk["tier"] < tier)
return -1 if short > cheaper + 1e-6 else max(0.0, short)
def feed_fire(world: dict, player: dict, fire_id: str, tier: int) -> float:
"""Put one log in a fire, pushing out the cheapest fuel if it's full. Returns the minutes added."""
fire, level = world["city"]["fires"][fire_id], skill_level(player, "firekeeping")
short = _room(fire, tier, level)
while short > 1e-6:
cheapest = min((chunk for chunk in fire["fuel"] if chunk["tier"] < tier), key=lambda chunk: chunk["tier"])
out = min(short, cheapest["left"])
cheapest["left"] -= out
short -= out
if cheapest["left"] <= 1e-6:
fire["fuel"].remove(cheapest)
minutes = fire_minutes(level)
last = fire["fuel"][-1] if fire["fuel"] else None
if last and last["tier"] == tier and last["by"] == player["id"]:
last["left"] += minutes * 60
else:
fire["fuel"].append({"tier": tier, "left": minutes * 60, "by": player["id"]})
return minutes
def fire_inputs(world: dict, action: dict) -> dict:
"""What a craft takes right now: a hot enough fire pays its log, and the forge a coal (never the last)."""
fire = action.get("fire")
if not fire or heat(world, fire) < action["fuel"]:
return action["inputs"]
inputs = dict(action["inputs"])
log = FIREWOOD[action["fuel"]]
inputs[log] -= 1
if not inputs[log]:
del inputs[log]
if fire == "forge" and inputs.get("coal", 0) > 1:
inputs["coal"] -= 1
return inputs
def _warm(world: dict, crafter: dict, action: dict, xp: float, now: float) -> None:
"""A fire paid for a craft: count it, and give its feeder a share of the XP."""
fire = world["city"]["fires"][action["fire"]]
fire["saved"] = fire.get("saved", 0) + 1
keeper = world["players"].get(burning(fire)["by"])
if keeper and keeper is not crafter:
gain_xp(world, keeper, "firekeeping", xp * WARMTH, now, keeper.setdefault("away", empty_summary()))
def fires_view(world: dict, now: float) -> dict:
burn_fires(world, now)
users = {}
for player in world["players"].values():
action = player["action"] and ACTION_BY_ID.get(player["action"]["id"])
if action and (action.get("fire") or action.get("tend")):
users.setdefault(action.get("fire") or action["tend"], []).append(
{"id": player["id"], "name": player["name"], "action": action["id"]})
return {key: {**fire, "heat": heat(world, key), "speed": speed(world), "users": users.get(key, [])}
for key, fire in world["city"]["fires"].items()}
def upcoming_raid(world: dict) -> dict | None:
"""The coming major threat: a bandit raid or a dragon."""
for event in world["events"]:
@ -2026,12 +2178,15 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
if order is not None or "order" in (player["action"] or {}) and player["action"]["id"] == action["id"]:
if not order or order["status"] != "open" or order["claimed"] != player["id"]:
return "Work order done" if order and order["status"] == "done" else "Work order closed"
for item, qty in action["inputs"].items():
for item, qty in fire_inputs(world, action).items():
if order and item in order["provided"]:
if order["provided"][item] < qty:
return f"Out of {ITEMS[item]['name'].lower()}"
elif player["bank"].get(item, 0) < qty:
return f"Out of {ITEMS[item]['name'].lower()}"
if action.get("special") == "tend" and _room(world["city"]["fires"][action["tend"]], action["tier"],
skill_level(player, "firekeeping")) < 0:
return f"The {FIRES[action['tend']]['name'].lower()} is full"
if action.get("special") == "sabotage":
raid = upcoming_raid(world)
if not raid or intel_level(world, raid) < 3:
@ -2061,9 +2216,11 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
def _complete(world: dict, player: dict, action: dict, now: float, rng: random.Random, summary: dict) -> None:
brew = potion_effect(player, action["skill"])
order = working_order(world, player, action)
own = {item: qty for item, qty in action["inputs"].items() if not order or item not in order["provided"]}
inputs = fire_inputs(world, action)
warmed = inputs is not action["inputs"]
own = {item: qty for item, qty in inputs.items() if not order or item not in order["provided"]}
if order:
for item, qty in action["inputs"].items():
for item, qty in inputs.items():
if item in order["provided"]:
order["provided"][item] -= qty
if own and rng.random() < brew.get("keep", 0):
@ -2164,6 +2321,10 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
city["drill_boost"] = city.get("drill_boost", 0) - boost
xp = DRILL_XP * (gain + boost) * mods["xp"]
threat["drilled"] = min(drill_needed(threat), threat.get("drilled", 0) + gain + boost)
if action.get("special") == "tend":
xp = TEND_XP[action["tier"]] * feed_fire(world, player, action["tend"], action["tier"]) * mods["xp"]
if warmed:
_warm(world, player, action, xp, now)
if action.get("special") == "labour":
city = world["city"]
site = city["construction"]
@ -2249,6 +2410,7 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
duration = action_time(world, player, action)
if current["start"] + duration > limit:
break
burn_fires(world, current["start"] + duration)
reason = can_start(world, player, action)
if reason and reason.startswith("Too hurt"):
if blessing(player)["wind"] and not current.get("wind_used"):
@ -2304,6 +2466,43 @@ def stop(player: dict, reason: str | None = None) -> None:
player["stopped"] = reason
def catch_up(world: dict, now: float, rng: random.Random | None = None) -> bool:
"""Run everyone's actions up to now, so the shared fires see feeds and crafts as they
happen. What each player missed waits in their `away` summary for their next visit."""
changed = False
for player in world["players"].values():
summary = process_player(world, player, now, rng)
if summary["done"] or summary["stopped"]:
merge_summary(player.setdefault("away", empty_summary()), summary)
changed = True
return changed
def merge_summary(into: dict, more: dict) -> dict:
"""Fold a later summary into an earlier one."""
for key, value in more.items():
if isinstance(value, bool):
into[key] = into.get(key) or value
elif isinstance(value, (int, float)):
into[key] = into.get(key, 0) + value
elif isinstance(value, dict):
for item, qty in value.items():
into.setdefault(key, {})[item] = into[key].get(item, 0) + qty
elif key == "levels":
into[key] = [entry for entry in into.get(key, []) if entry[0] not in {s for s, _ in value}] + value
elif isinstance(value, list):
into[key] = into.get(key, []) + value
elif value is not None:
into[key] = value
return into
def take_summary(player: dict, summary: dict) -> dict:
"""Everything that happened since the player last looked: the catch-up's and this request's."""
away = player.pop("away", None)
return merge_summary(away, summary) if away else summary
# --- work orders -----------------------------------------------------------
# A player asks the guild for something. A crafting order names a recipe: the
# requester can put in some of its reagents, one crafter claims it and makes it
@ -2876,6 +3075,7 @@ def tick(world: dict, now: float, rng: random.Random | None = None) -> bool:
rng = rng or random.Random()
city = world["city"]
changed = False
burn_fires(world, now)
step = HOUR / speed(world)
# Never replay more than two weeks if the server was off.
city["last_tick"] = max(city["last_tick"], now - 14 * DAY / speed(world))
@ -3348,7 +3548,6 @@ SEASON_WEATHER = [
{"clear": 30, "rain": 30, "storm": 25, "fog": 15},
{"clear": 30, "snow": 45, "fog": 15, "storm": 10},
]
FIREWOOD = ("logs", "oak_logs", "willow_logs", "maple_logs", "yew_logs", "magic_logs")
WOOD = FIREWOOD + tuple(plank for plank, _name in PLANKS)
# Minor threats: they slow or block a skill until someone deals with them,
@ -4022,7 +4221,8 @@ PIECE_GLYPHS = {"sword": "pointy-sword", "shield": "round-shield", "helmet": "vi
"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", "chisel": "chisel", "gem drill": "drill",
"cleaver": "meat-cleaver", "mortar and pestle": "mortar", "awl": "needle-drill", "shears": "shears"}
"cleaver": "meat-cleaver", "mortar and pestle": "mortar", "awl": "needle-drill", "shears": "shears",
"bellows": "bellows"}
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",
@ -4057,6 +4257,7 @@ SKILL_GLYPHS = {
"cooking": ("cooking-pot", "#e0703a"), "leatherworking": ("sewing-needle", "#d6a26e"),
"herblore": ("cauldron", "#7fbf5a"), "combat": ("crossed-swords", "#e05a5a"), "prayer": ("prayer", "#e9dfc6"),
"reconnaissance": ("spyglass", "#6f8fec"), "tailoring": ("sewing-machine", "#c8a0e0"),
"firekeeping": ("campfire", "#ff7a2a"),
}
CLASS_GLYPHS = {"miner": ("miner", "#d07a3c"), "builder": ("crane", "#d1a46a"),
"homesteader": ("farmer", "#8fbf6a"), "ranger": ("bow-arrow", "#5fae8a")}
@ -4098,6 +4299,9 @@ ACTION_GLYPHS.update({
for _index, (_area, _level, _prefix, _races) in enumerate(AREAS):
for _race, *_rest in _races:
ACTION_GLYPHS[f"fight_{_slug(_race)}"] = (RACE_GLYPHS[_race], TIER_TINTS[_index])
for _tend in ACTIONS:
if _tend.get("special") == "tend":
ACTION_GLYPHS[_tend["id"]] = ("campfire", FLAMES[_tend["tier"]][1])
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield", "stopwatch", "swordman", "biceps", "cancel", "scroll-unfurled", "striking-diamonds",
"archery-target", "dodging", "shield-reflect",
"cape", "gem-pendant", "gauntlet", "ring", "quiver", "leather-boot", "crystal-ball", "hot-meal", "knife-fork", "round-bottom-flask", "visored-helm", "breastplate",
@ -4211,4 +4415,5 @@ def content() -> dict:
"styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE,
"resistant": RESISTANT, "jewel_stats": JEWEL_STATS, "stations": list(STATIONS), "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
"regen_per_hour": REGEN_PER_HOUR,
"fires": FIRES, "flames": FLAMES, "firewood": FIREWOOD, "tend_xp": TEND_XP, "fire_low": FIRE_LOW,
}