Fittings: Construction-made building upgrades from planks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb4b8bf7c8
commit
cdb109e554
7 changed files with 218 additions and 14 deletions
119
game.py
119
game.py
|
|
@ -496,7 +496,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
|
|||
|
||||
# --- world ---------------------------------------------------------------
|
||||
|
||||
WORLD_VERSION = 9
|
||||
WORLD_VERSION = 10
|
||||
|
||||
|
||||
def new_world(now: float, speed: float = 1.0) -> dict:
|
||||
|
|
@ -509,7 +509,7 @@ def new_world(now: float, speed: float = 1.0) -> dict:
|
|||
"population": 20.0,
|
||||
"treasury": 0.0,
|
||||
"stockpile": {"bread": 40},
|
||||
"buildings": {key: {"level": 0, "damaged": False, "fund": {}} for key in BUILDINGS},
|
||||
"buildings": {key: {"level": 0, "damaged": False, "fund": {}, "fittings": []} for key in BUILDINGS},
|
||||
"construction": None,
|
||||
"queue": [],
|
||||
"tier": 0,
|
||||
|
|
@ -599,6 +599,10 @@ def migrate(world: dict, now: float | None = None) -> dict:
|
|||
message["id"] = number
|
||||
world["next_message_id"] = len(world["board"]) + 1
|
||||
world["version"] = 9
|
||||
if world["version"] < 10:
|
||||
for building in world["city"]["buildings"].values():
|
||||
building.setdefault("fittings", [])
|
||||
world["version"] = 10
|
||||
return world
|
||||
|
||||
|
||||
|
|
@ -644,6 +648,12 @@ def building_level(world: dict, key: str) -> int:
|
|||
return 0 if building["damaged"] else building["level"]
|
||||
|
||||
|
||||
def fitted(world: dict, fitting: str) -> bool:
|
||||
"""Whether a fitting is installed and working (its building isn't damaged)."""
|
||||
key = FITTING_BY_ID[fitting]["building"]
|
||||
return fitting in world["city"]["buildings"][key]["fittings"] and building_level(world, key) > 0
|
||||
|
||||
|
||||
def build_cost(key: str, level: int) -> dict:
|
||||
spec = BUILDINGS[key]
|
||||
cost = {item: qty * level for item, qty in spec["base"].items()}
|
||||
|
|
@ -711,11 +721,11 @@ 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"])
|
||||
+ player["perks"]["bank_slot"] + fitted(world, "shelving"))
|
||||
|
||||
|
||||
def stack_size(world: dict) -> int:
|
||||
return BASE_STACK + 250 * building_level(world, "storehouse")
|
||||
return BASE_STACK + 250 * building_level(world, "storehouse") + 500 * fitted(world, "enchanted_chests")
|
||||
|
||||
|
||||
def slots_used(world: dict, player: dict) -> int:
|
||||
|
|
@ -744,6 +754,8 @@ def city_uses(item: str) -> list[str]:
|
|||
uses.append("the armoury")
|
||||
if item == "water_barrel":
|
||||
uses.append("the fire brigade")
|
||||
if "fitting" in spec:
|
||||
uses.append(BUILDINGS[spec["fitting"]]["name"])
|
||||
return uses
|
||||
|
||||
|
||||
|
|
@ -802,6 +814,7 @@ def regen(world: dict, player: dict, now: float, boost: float = 1) -> None:
|
|||
"""Heal for the time since HP was last settled."""
|
||||
if now > player["hp_at"]:
|
||||
top = max_hp(player)
|
||||
boost *= 2 if fitted(world, "enchanted_hearths") else 1
|
||||
per_second = top * REGEN_PER_HOUR * boost * speed(world) / HOUR
|
||||
player["hp"] = min(top, player["hp"] + (now - player["hp_at"]) * per_second)
|
||||
player["hp_at"] = now
|
||||
|
|
@ -876,12 +889,13 @@ def set_prayers(player: dict, ids: list) -> str | None:
|
|||
|
||||
|
||||
def bone_points(world: dict, action: dict) -> int:
|
||||
return action["pp"] + building_level(world, "temple")
|
||||
return action["pp"] + building_level(world, "temple") + fitted(world, "carved_altar")
|
||||
|
||||
|
||||
def ward(world: dict) -> float:
|
||||
"""Defence the temple's devotion adds to the next raid."""
|
||||
return round(min(world["city"].get("devotion", 0) / 4, 15 * building_level(world, "temple")), 1)
|
||||
cap = 15 * building_level(world, "temple") + 15 * fitted(world, "carved_altar")
|
||||
return round(min(world["city"].get("devotion", 0) / 4, cap), 1)
|
||||
|
||||
|
||||
def edible(player: dict) -> list[str]:
|
||||
|
|
@ -913,6 +927,9 @@ def action_time(world: dict, player: dict, action: dict) -> float:
|
|||
seconds *= 1 - ITEMS[tool]["speed"]
|
||||
if action["skill"] == "smithing":
|
||||
seconds *= 1 - 0.08 * building_level(world, "smithy")
|
||||
seconds *= 0.9 if fitted(world, "bellows") else 1
|
||||
if action["id"].startswith("saw_") and fitted(world, "saw_benches"):
|
||||
seconds *= 0.8
|
||||
seconds *= 1 - potion_effect(player, action["skill"]).get("speed", 0)
|
||||
seconds *= skill_mods(world, action)["speed"]
|
||||
return seconds / speed(world)
|
||||
|
|
@ -1080,7 +1097,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
if action.get("special") == "recon":
|
||||
event = upcoming_event(world)
|
||||
if event:
|
||||
event["recon"] += action["recon"]
|
||||
event["recon"] += action["recon"] + fitted(world, "map_table")
|
||||
if action.get("special") == "problem" and action["skill"] != "combat":
|
||||
_work_problem(world, player, action, now)
|
||||
if action.get("special") == "labour":
|
||||
|
|
@ -1319,6 +1336,36 @@ def fund(world: dict, player: dict, key: str, item: str, qty: int | str, source:
|
|||
return None
|
||||
|
||||
|
||||
def install(world: dict, player: dict, fitting: str, source: str, now: float) -> str | None:
|
||||
"""Fit a building with a fitting from the player's bank (earns favour) or the stockpile."""
|
||||
spec = FITTING_BY_ID.get(fitting)
|
||||
if not spec:
|
||||
return "No such fitting"
|
||||
city = world["city"]
|
||||
building = city["buildings"][spec["building"]]
|
||||
name = BUILDINGS[spec["building"]]["name"].lower()
|
||||
if fitting in building["fittings"]:
|
||||
return f"The {name} already {'have' if name.endswith('s') else 'has'} that"
|
||||
if building["level"] < spec["level"]:
|
||||
return f"Needs {BUILDINGS[spec['building']]['name']} level {spec['level']}"
|
||||
if source == "bank":
|
||||
if not player["bank"].get(fitting):
|
||||
return "You don't have one"
|
||||
bank_take(player, fitting, 1)
|
||||
gain = ITEMS[fitting]["value"]
|
||||
player["favour"] += gain * city_rate(world, "favour")
|
||||
player["donated"] += gain
|
||||
elif source == "city":
|
||||
if not city["stockpile"].get(fitting):
|
||||
return "There isn't one in the stockpile"
|
||||
take_stock(city, fitting, 1)
|
||||
else:
|
||||
return "Bad source"
|
||||
building["fittings"].append(fitting)
|
||||
log(world, now, "build", f"{player['name']} fitted the {name} with {spec['name'].lower()}", player["id"])
|
||||
return None
|
||||
|
||||
|
||||
def take_stock(city: dict, item: str, qty: int) -> None:
|
||||
left = city["stockpile"][item] - qty
|
||||
if left > 0:
|
||||
|
|
@ -1418,8 +1465,8 @@ def defence(world: dict) -> dict:
|
|||
armed = int(pop // 2)
|
||||
parts = {
|
||||
"townsfolk": 10,
|
||||
"walls": 20 * building_level(world, "walls"),
|
||||
"militia": round(0.3 * building_level(world, "barracks") * pop, 1),
|
||||
"walls": 20 * building_level(world, "walls") + 25 * fitted(world, "hoardings"),
|
||||
"militia": round(0.3 * building_level(world, "barracks") * pop * (1.5 if fitted(world, "weapon_racks") else 1), 1),
|
||||
"weapons": round(0.35 * best_kit(stock, "weapon", "power", armed), 1),
|
||||
"shields": round(0.5 * best_kit(stock, "shield", "armour", armed), 1),
|
||||
"arrows": min(100, stock.get("arrows", 0) // 10),
|
||||
|
|
@ -1486,7 +1533,8 @@ def _city_hour(world: dict, now: float) -> None:
|
|||
pop = city["population"]
|
||||
if city["construction"]:
|
||||
city["construction"]["labour"] += pop * TOWNSFOLK_LABOUR
|
||||
city["food_due"] += pop * 0.5 / 24 * city_rate(world, "food") * (1.5 if city.get("cold") else 1)
|
||||
city["food_due"] += (pop * 0.5 / 24 * city_rate(world, "food") * (1.5 if city.get("cold") else 1)
|
||||
* (0.9 if fitted(world, "grain_bins") else 1))
|
||||
stock = city["stockpile"]
|
||||
for item in FOOD_ORDER:
|
||||
while city["food_due"] >= ITEMS[item]["food"] and stock.get(item, 0) > 0:
|
||||
|
|
@ -1504,7 +1552,8 @@ def _city_hour(world: dict, now: float) -> None:
|
|||
else:
|
||||
room = housing(world)
|
||||
if pop < room:
|
||||
city["population"] = min(room, pop + max(0.5, pop * GROWTH_PER_DAY) * city_rate(world, "growth") / 24)
|
||||
growth = max(0.5, pop * GROWTH_PER_DAY) * city_rate(world, "growth") * (1.25 if fitted(world, "furniture") else 1)
|
||||
city["population"] = min(room, pop + growth / 24)
|
||||
elif pop > room:
|
||||
city["population"] = max(room, pop - pop * 0.05 / 24)
|
||||
city["treasury"] += pop * 0.5 / 24
|
||||
|
|
@ -1894,6 +1943,47 @@ _barrel = _action("cooper_barrel", "construction", "Water barrel", 10, 35, 18,
|
|||
ACTIONS.append(_barrel)
|
||||
ACTION_BY_ID[_barrel["id"]] = _barrel
|
||||
|
||||
# Fittings: Construction makes them, mostly from the better planks, and each
|
||||
# building takes one of each once it's high enough. A fitting works while its
|
||||
# building stands; damage pauses it like the rest of the building.
|
||||
# id, name, building, building level, construction level, inputs, effect, glyph, tint
|
||||
FITTINGS = [
|
||||
("grain_bins", "Grain bins", "granary", 1, 15, {"oak_plank": 20, "iron_bar": 5},
|
||||
"The city eats 10% less food", "grain-bundle", "#e3c060"),
|
||||
("saw_benches", "Saw benches", "lumber_shed", 1, 15, {"oak_plank": 20, "iron_bar": 10},
|
||||
"Sawing planks is 20% faster", "hand-saw", "#8f5e30"),
|
||||
("shelving", "Shelving", "storehouse", 2, 20, {"oak_plank": 40, "iron_bar": 10},
|
||||
"+1 bank slot for everyone", "bookshelf", "#8f5e30"),
|
||||
("map_table", "Map table", "spymaster", 1, 30, {"willow_plank": 25, "hard_leather": 10},
|
||||
"Scouting gathers one more point of reconnaissance", "treasure-map", "#a3b86c"),
|
||||
("well_pump", "Well pump", "well", 2, 30, {"willow_plank": 25, "steel_bar": 10},
|
||||
"Half of all fires are put out before they spread", "water-tank", "#5aa0d8"),
|
||||
("furniture", "Furniture", "houses", 2, 35, {"willow_plank": 40, "leather": 30},
|
||||
"Population grows 25% faster", "sofa", "#a3b86c"),
|
||||
("weapon_racks", "Weapon racks", "barracks", 2, 40, {"willow_plank": 30, "steel_bar": 20},
|
||||
"Militia defence +50%", "spears", "#d4dae2"),
|
||||
("hoardings", "Hoardings", "walls", 3, 45, {"maple_plank": 40, "mithril_bar": 10},
|
||||
"+25 defence", "palisade", "#d0663a"),
|
||||
("bellows", "Great bellows", "smithy", 3, 50, {"maple_plank": 30, "bear_leather": 15},
|
||||
"Smithing another 10% faster", "bellows", "#d0663a"),
|
||||
("carved_altar", "Carved altar", "temple", 2, 60, {"yew_plank": 30, "cut_stone": 60},
|
||||
"+1 prayer point per bone buried, and offerings can ward 15 more", "star-altar", "#6f9a4a"),
|
||||
("enchanted_chests", "Enchanted chests", "storehouse", 4, 75, {"magic_plank": 40, "rune_bar": 5},
|
||||
"+500 per bank stack for everyone", "open-treasure-chest", "#a77be8"),
|
||||
("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "red_dragon_leather": 5},
|
||||
"Everyone recovers HP twice as fast", "fireplace", "#a77be8"),
|
||||
]
|
||||
FITTING_BY_ID = {}
|
||||
for _id, _name, _building, _blevel, _level, _inputs, _effect, _glyph, _tint in FITTINGS:
|
||||
FITTING_BY_ID[_id] = {"id": _id, "name": _name, "building": _building, "level": _blevel, "effect": _effect}
|
||||
ITEMS[_id] = {"name": _name, "plural": _name, "icon": "🪚", "value": sum(ITEMS[i]["value"] * q for i, q in _inputs.items()),
|
||||
"fitting": _building}
|
||||
_fitting = _action(f"make_{_id}", "construction", _name, _level, 120 + _level * 2, 40 + _level * 4,
|
||||
_inputs, {_id: 1}, group="Fittings")
|
||||
ACTIONS.append(_fitting)
|
||||
ACTION_BY_ID[_fitting["id"]] = _fitting
|
||||
ITEMS[_id].update(glyph=_glyph, tint=_tint)
|
||||
|
||||
# (weights by season, spring to winter)
|
||||
HARDSHIPS = {"sickness": [2, 1, 2, 3], "locusts": [1, 3, 2, 0], "rot": [3, 1, 3, 1], "storm": [1, 1, 3, 1],
|
||||
"rats": [2, 2, 2, 2], "fire": [1, 2, 1, 2], "winter": [0, 0, 0, 4], "drought": [0, 4, 1, 0]}
|
||||
|
|
@ -2061,7 +2151,10 @@ def _hardship(world: dict, now: float, rng: random.Random, kind: str | None = No
|
|||
["They'll eat into the food every hour until hunters catch them."])
|
||||
elif kind == "fire":
|
||||
barrels = stock.get("water_barrel", 0)
|
||||
if barrels >= 5:
|
||||
if fitted(world, "well_pump") and rng.random() < 0.5:
|
||||
_record(world, now, "hardship", kind, "🔥", "A fire broke out, and the well's pump put it out",
|
||||
["Nothing was lost."])
|
||||
elif barrels >= 5:
|
||||
take_stock_qty(stock, "water_barrel", 5)
|
||||
_record(world, now, "hardship", kind, "🔥", "A fire broke out, and the water barrels put it out",
|
||||
["Five barrels were used up. Nothing else was lost."])
|
||||
|
|
@ -2515,6 +2608,6 @@ def content() -> dict:
|
|||
"buildings": buildings, "xp_table": XP_TABLE, "offline_cap": OFFLINE_CAP, "tiers": TIERS,
|
||||
"city_uses": {item: city_uses(item) for item in ITEMS},
|
||||
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
|
||||
"potion_charges": POTION_CHARGES, "problems": PROBLEMS,
|
||||
"potion_charges": POTION_CHARGES, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
|
||||
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "regen_per_hour": REGEN_PER_HOUR,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue