Construction: buildings rise by labour; planks and hammers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4093245b8e
commit
a4e4bd4385
6 changed files with 180 additions and 47 deletions
97
game.py
97
game.py
|
|
@ -98,7 +98,7 @@ PIECE_ICONS = {"sword": "⚔️", "shield": "🛡️", "helmet": "🪖", "legs":
|
|||
COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body": "🥋"}
|
||||
# Gathering tools: one slot per skill, `speed` cuts the action time.
|
||||
TOOLS = {"mining": ("pickaxe", "⛏️"), "woodcutting": ("axe", "🪓"), "farming": ("hoe", ""),
|
||||
"fishing": ("fishing rod", "🎣"), "hunting": ("hunting knife", "🔪")}
|
||||
"fishing": ("fishing rod", "🎣"), "hunting": ("hunting knife", "🔪"), "construction": ("hammer", "🔨")}
|
||||
TOOL_SPEED = [0.15, 0.30, 0.38, 0.45, 0.52, 0.60]
|
||||
|
||||
# item id, name, icon, value, action id, verb
|
||||
|
|
@ -160,8 +160,11 @@ for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in
|
|||
"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]],
|
||||
}
|
||||
for _log, _name, _icon, _value, _action_id, _verb in TREES:
|
||||
PLANKS = [("plank", "Plank"), ("oak_plank", "Oak plank"), ("willow_plank", "Willow plank"),
|
||||
("maple_plank", "Maple plank"), ("yew_plank", "Yew plank"), ("magic_plank", "Magic plank")]
|
||||
for (_log, _name, _icon, _value, _action_id, _verb), (_plank, _plank_name) in zip(TREES, PLANKS):
|
||||
ITEMS[_log] = {"name": _name, "icon": _icon, "value": _value}
|
||||
ITEMS[_plank] = {"name": _plank_name, "icon": "🪚", "value": _value * 2 + 1}
|
||||
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]}
|
||||
|
|
@ -247,6 +250,7 @@ SKILLS = [
|
|||
{"id": "fishing", "name": "Fishing", "icon": "🎣", "group": "Gathering"},
|
||||
{"id": "smithing", "name": "Smithing", "icon": "🔨", "group": "Crafting"},
|
||||
{"id": "masonry", "name": "Masonry", "icon": "🧱", "group": "Crafting"},
|
||||
{"id": "construction", "name": "Construction", "icon": "🏗️", "group": "Crafting"},
|
||||
{"id": "cooking", "name": "Cooking", "icon": "🍳", "group": "Crafting"},
|
||||
{"id": "leatherworking", "name": "Leatherworking", "icon": "🪡", "group": "Crafting"},
|
||||
{"id": "herblore", "name": "Herblore", "icon": "🧪", "group": "Crafting"},
|
||||
|
|
@ -276,8 +280,8 @@ CLASSES = {
|
|||
"signature": "Masterwork: 1 in 10 crafts makes double",
|
||||
},
|
||||
"pioneer": {
|
||||
"name": "Pioneer", "icon": "🧭", "fast": ["mining", "woodcutting", "farming"],
|
||||
"bonus": "Mining, woodcutting and farming 20% faster",
|
||||
"name": "Pioneer", "icon": "🧭", "fast": ["mining", "woodcutting", "farming", "construction"],
|
||||
"bonus": "Mining, woodcutting, farming and construction 20% faster",
|
||||
"signature": "Surveying: 1 in 10 gathers yields double",
|
||||
},
|
||||
}
|
||||
|
|
@ -355,6 +359,10 @@ ACTIONS = [
|
|||
{"copper_bar": 1, "logs": 1, "feathers": 2}, {"arrows": 15}, group="Bars"),
|
||||
_action("lw_studded", "leatherworking", "Studded jerkin", 25, 50, 40,
|
||||
{"leather": 4, "iron_bar": 1}, {"studded_body": 1}, group="Leather"),
|
||||
_action("labour", "construction", "Work on the building site", 1, 30, 10, special="labour"),
|
||||
*[_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", "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_beam", "masonry", "Reinforced beam", 20, 45, 30,
|
||||
|
|
@ -399,47 +407,50 @@ BUILDINGS = {
|
|||
"houses": {
|
||||
"name": "Houses", "icon": "🏠", "max": 8, "gold": 200,
|
||||
"base": {"logs": 600, "bricks": 300}, "advanced": {"oak_logs": 200},
|
||||
"late": {"willow_logs": 150}, "final": {"maple_logs": 100},
|
||||
"late": {"willow_logs": 150, "oak_plank": 60}, "final": {"maple_logs": 100, "willow_plank": 60},
|
||||
"effect": "Room for 20 more people per level",
|
||||
},
|
||||
"walls": {
|
||||
"name": "Walls", "icon": "🏰", "max": 5, "gold": 300,
|
||||
"base": {"stone": 1200, "bricks": 500}, "advanced": {"cut_stone": 300, "beam": 40},
|
||||
"late": {"steel_bar": 60}, "final": {"mithril_bar": 40},
|
||||
"late": {"steel_bar": 60, "oak_plank": 40}, "final": {"mithril_bar": 40, "maple_plank": 40},
|
||||
"effect": "+20 defence per level, and raiders carry off less",
|
||||
},
|
||||
"barracks": {
|
||||
"name": "Barracks", "icon": "⛺", "max": 5, "gold": 300,
|
||||
"base": {"logs": 600, "bricks": 400, "copper_sword": 20}, "advanced": {"iron_sword": 15},
|
||||
"late": {"steel_sword": 10}, "final": {"mithril_sword": 6},
|
||||
"late": {"steel_sword": 10, "oak_plank": 50}, "final": {"mithril_sword": 6, "willow_plank": 50},
|
||||
"effect": "Trains townsfolk as militia: more defence per citizen",
|
||||
},
|
||||
"smithy": {
|
||||
"name": "Smithy", "icon": "⚒️", "max": 5, "gold": 250,
|
||||
"base": {"stone": 500, "logs": 600, "copper_bar": 150}, "advanced": {"iron_bar": 150},
|
||||
"late": {"steel_bar": 80}, "final": {"mithril_bar": 60},
|
||||
"late": {"steel_bar": 80, "oak_plank": 40}, "final": {"mithril_bar": 60, "maple_plank": 40},
|
||||
"effect": "Smithing 8% faster per level; each level unlocks a metal: iron, steel, mithril, adamant, rune",
|
||||
},
|
||||
"temple": {
|
||||
"name": "Temple", "icon": "⛪", "max": 3, "gold": 300,
|
||||
"base": {"stone": 800, "bricks": 400, "cut_stone": 100}, "advanced": {"beam": 20},
|
||||
"late": {"willow_logs": 150},
|
||||
"late": {"willow_logs": 150, "oak_plank": 60},
|
||||
"effect": "Offerings ward the city in raids; +1 prayer point per bone buried and +25% devotion per level",
|
||||
},
|
||||
"spymaster": {
|
||||
"name": "Spymaster's guild", "icon": "🕵️", "max": 3, "gold": 400,
|
||||
"base": {"logs": 500, "bricks": 300, "hide": 100, "feathers": 300}, "advanced": {"cut_stone": 150},
|
||||
"late": {"bear_leather": 40},
|
||||
"late": {"bear_leather": 40, "oak_plank": 40},
|
||||
"effect": "One more level of intel on every event per level",
|
||||
},
|
||||
"storehouse": {
|
||||
"name": "Storehouse", "icon": "📦", "max": 5, "gold": 200,
|
||||
"base": {"logs": 800, "stone": 800}, "advanced": {"oak_logs": 300, "cut_stone": 200},
|
||||
"late": {"willow_logs": 200}, "final": {"yew_logs": 80},
|
||||
"late": {"willow_logs": 200, "oak_plank": 80}, "final": {"yew_logs": 80, "yew_plank": 40},
|
||||
"effect": "+3 bank slots and +250 per stack for everyone, per level",
|
||||
},
|
||||
}
|
||||
BUILD_HOURS = 3
|
||||
# Buildings rise by labour: players working the site, and a slow trickle from
|
||||
# the townsfolk so nothing stalls completely while everyone is away.
|
||||
LABOUR_PER_LEVEL = 360 # a level-1 building: about three hours of one player's work
|
||||
TOWNSFOLK_LABOUR = 0.5 # per person per game hour
|
||||
REPAIR_SHARE = 0.3
|
||||
|
||||
# Population promotes the settlement, permanently. Each tier lifts how high
|
||||
|
|
@ -463,7 +474,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
|
|||
|
||||
# --- world ---------------------------------------------------------------
|
||||
|
||||
WORLD_VERSION = 6
|
||||
WORLD_VERSION = 7
|
||||
|
||||
|
||||
def new_world(now: float, speed: float = 1.0) -> dict:
|
||||
|
|
@ -529,6 +540,19 @@ def migrate(world: dict, now: float | None = None) -> dict:
|
|||
player["xp"].setdefault("herblore", 0)
|
||||
player.setdefault("potions", {})
|
||||
world["version"] = 6
|
||||
if world["version"] < 7:
|
||||
for player in world["players"].values():
|
||||
player["xp"].setdefault("construction", 0)
|
||||
player["equip"].setdefault("tool_construction", None)
|
||||
work = world["city"]["construction"]
|
||||
if work and "done_at" in work:
|
||||
# The old timer becomes labour, credited for the time already spent.
|
||||
level = world["city"]["buildings"][work["building"]]["level"]
|
||||
needed = labour_needed(level, work["repair"])
|
||||
share = min(1.0, max(0.0, (now - work["start"]) / max(1.0, work["done_at"] - work["start"])))
|
||||
del work["done_at"]
|
||||
work.update(labour=needed * share, needed=needed)
|
||||
world["version"] = 7
|
||||
return world
|
||||
|
||||
|
||||
|
|
@ -910,6 +934,8 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
|
|||
return "No bandit camp has been located"
|
||||
if raid["sabotage"] >= 0.3:
|
||||
return "The camp is already in ruins"
|
||||
if action.get("special") == "labour" and not world["city"]["construction"]:
|
||||
return "Nothing is being built"
|
||||
if action["skill"] == "combat" and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
|
||||
return "Too hurt to fight. Rest, or bring food"
|
||||
return None
|
||||
|
|
@ -983,6 +1009,13 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
event = upcoming_event(world)
|
||||
if event:
|
||||
event["recon"] += action["recon"]
|
||||
if action.get("special") == "labour":
|
||||
site = world["city"]["construction"]
|
||||
site["labour"] += labour_per_action(player)
|
||||
summary["labour"] += labour_per_action(player)
|
||||
if site["labour"] >= site["needed"]:
|
||||
_finish_construction(world, now)
|
||||
_start_next(world, now)
|
||||
bonus = 1 + brew.get("bones", 0)
|
||||
if action.get("special") == "bury":
|
||||
player["pp"] += bone_points(world, action) * bonus
|
||||
|
|
@ -1017,7 +1050,7 @@ def empty_summary() -> dict:
|
|||
return {"done": 0, "items": {}, "overflow": {}, "xp": {}, "levels": [], "eaten": {}, "damage": 0,
|
||||
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None,
|
||||
"sold": {}, "sold_gold": 0, "pp_spent": 0, "pp_gained": 0, "prayer_out": False, "second_wind": 0,
|
||||
"kept": 0, "potions_drunk": {}, "potion_out": []}
|
||||
"kept": 0, "potions_drunk": {}, "potion_out": [], "labour": 0}
|
||||
|
||||
|
||||
def process_player(world: dict, player: dict, now: float, rng: random.Random | None = None) -> dict:
|
||||
|
|
@ -1241,9 +1274,8 @@ def _start_next(world: dict, now: float) -> None:
|
|||
else:
|
||||
del building["fund"][item]
|
||||
repair = building["damaged"]
|
||||
hours = BUILD_HOURS * (building["level"] + 1) * (0.25 if repair else 1)
|
||||
city["construction"] = {"building": key, "repair": repair, "start": now,
|
||||
"done_at": now + hours * HOUR / speed(world)}
|
||||
city["construction"] = {"building": key, "repair": repair, "start": now, "labour": 0.0,
|
||||
"needed": labour_needed(building["level"], repair)}
|
||||
verb = "Repairs to" if repair else "Work on"
|
||||
log(world, now, "build", f"{verb} the {BUILDINGS[key]['name'].lower()} began")
|
||||
|
||||
|
|
@ -1339,9 +1371,9 @@ def tick(world: dict, now: float, rng: random.Random | None = None) -> bool:
|
|||
_city_hour(world, city["last_tick"])
|
||||
changed = True
|
||||
construction = city["construction"]
|
||||
if construction and construction["done_at"] <= now:
|
||||
_finish_construction(world, construction["done_at"])
|
||||
_start_next(world, construction["done_at"])
|
||||
if construction and construction["labour"] >= construction["needed"]:
|
||||
_finish_construction(world, now)
|
||||
_start_next(world, now)
|
||||
changed = True
|
||||
while city["tier"] + 1 < len(TIERS) and city["population"] >= TIERS[city["tier"] + 1]["pop"]:
|
||||
city["tier"] += 1
|
||||
|
|
@ -1368,6 +1400,8 @@ def tick(world: dict, now: float, rng: random.Random | None = None) -> bool:
|
|||
def _city_hour(world: dict, now: float) -> None:
|
||||
city = world["city"]
|
||||
pop = city["population"]
|
||||
if city["construction"]:
|
||||
city["construction"]["labour"] += pop * TOWNSFOLK_LABOUR
|
||||
city["food_due"] += pop * 0.5 / 24
|
||||
stock = city["stockpile"]
|
||||
for item in FOOD_ORDER:
|
||||
|
|
@ -1392,6 +1426,17 @@ def _city_hour(world: dict, now: float) -> None:
|
|||
city["treasury"] += pop * 0.5 / 24
|
||||
|
||||
|
||||
def labour_needed(level: int, repair: bool) -> float:
|
||||
"""Labour to raise a building from `level`, or to repair it at that level."""
|
||||
if repair:
|
||||
return LABOUR_PER_LEVEL * max(1, level) * 0.25
|
||||
return LABOUR_PER_LEVEL * (level + 1)
|
||||
|
||||
|
||||
def labour_per_action(player: dict) -> float:
|
||||
return 1 + skill_level(player, "construction") // 20 # one more labour every 20 levels
|
||||
|
||||
|
||||
def _finish_construction(world: dict, now: float) -> None:
|
||||
city = world["city"]
|
||||
work = city["construction"]
|
||||
|
|
@ -1616,7 +1661,7 @@ def _use_up(stock: dict, item: str, share: float) -> None:
|
|||
|
||||
# --- favour --------------------------------------------------------------
|
||||
|
||||
BUILDER_FAVOUR_PER_HOUR = 40
|
||||
BUILDER_FAVOUR_PER_LABOUR = 0.67
|
||||
INFORMANT_FAVOUR = 250
|
||||
RALLY_FAVOUR = 150
|
||||
RALLY_DEFENCE = 10
|
||||
|
|
@ -1626,14 +1671,14 @@ def favours(world: dict, now: float) -> dict:
|
|||
"""What favour can buy for the city right now: cost, and why not if it can't."""
|
||||
city = world["city"]
|
||||
work = city["construction"]
|
||||
hours = (work["done_at"] - now) * speed(world) / HOUR if work else 0
|
||||
remaining = max(0.0, work["needed"] - work["labour"]) if work else 0
|
||||
event = upcoming_event(world)
|
||||
raid = upcoming_raid(world)
|
||||
return {
|
||||
"builders": {
|
||||
"cost": max(25, math.ceil(hours * BUILDER_FAVOUR_PER_HOUR)),
|
||||
"why": None if work and hours > 0.05 else "Nothing is being built",
|
||||
"saves": hours / 2,
|
||||
"cost": max(25, math.ceil(remaining / 2 * BUILDER_FAVOUR_PER_LABOUR)),
|
||||
"why": None if work and remaining > 1 else "Nothing is being built",
|
||||
"labour": round(remaining / 2),
|
||||
},
|
||||
"informant": {
|
||||
"cost": INFORMANT_FAVOUR,
|
||||
|
|
@ -1661,7 +1706,7 @@ def spend_favour(world: dict, player: dict, kind: str, now: float) -> str | None
|
|||
city = world["city"]
|
||||
if kind == "builders":
|
||||
work = city["construction"]
|
||||
work["done_at"] = now + (work["done_at"] - now) / 2
|
||||
work["labour"] += (work["needed"] - work["labour"]) / 2
|
||||
name = BUILDINGS[work["building"]]["name"].lower()
|
||||
log(world, now, "favour", f"{player['name']} hired extra builders for the {name}", player["id"])
|
||||
elif kind == "informant":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue