Livelier frontier: 2-day raids, more and shorter happenings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bf013a1648
commit
db1a00e8aa
5 changed files with 201 additions and 48 deletions
189
game.py
189
game.py
|
|
@ -737,6 +737,12 @@ WOLVES = [("Grey wolf", "skirmisher", "slash", "pierce", "blunt"),
|
|||
GOBLINS = [("Goblin brute", "brute", "blunt", "blunt", "slash"),
|
||||
("Goblin rogue", "skirmisher", "pierce", "magic", "blunt"),
|
||||
("Goblin warlock", "caster", "magic", "slash", "magic")]
|
||||
SPIDERS = [("Cave spider", "skirmisher", "pierce", "blunt", "pierce"),
|
||||
("Bloated spider", "brute", "magic", "slash", "blunt"),
|
||||
("Spider matriarch", "elite", "pierce", "magic", "slash")]
|
||||
SMUGGLERS = [("Smuggler", "skirmisher", "slash", "blunt", "slash"),
|
||||
("Smuggler bruiser", "brute", "blunt", "pierce", "blunt"),
|
||||
("Smuggler hexer", "caster", "magic", "slash", "pierce")]
|
||||
|
||||
# Expedition areas, tiers II to VI: name, level, the reagent prefix, and four races.
|
||||
# Each race: (name, trophy, weapon line its trophy goes into, level offset, roster).
|
||||
|
|
@ -1161,7 +1167,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
|
|||
|
||||
# --- world ---------------------------------------------------------------
|
||||
|
||||
WORLD_VERSION = 26
|
||||
WORLD_VERSION = 27
|
||||
|
||||
|
||||
def new_world(now: float, speed: float = 1.0) -> dict:
|
||||
|
|
@ -1366,6 +1372,16 @@ def migrate(world: dict, now: float | None = None) -> dict:
|
|||
for party in world["parties"].values():
|
||||
party["fight"] = None
|
||||
world["version"] = 26
|
||||
if world["version"] < 27:
|
||||
# Livelier happenings: tracks waiting longer than their new cooldowns are brought forward,
|
||||
# and a city with no threat coming hears rumours of the next one straight away.
|
||||
for track, (_low, high) in TRACK_HOURS.items():
|
||||
key = f"next_{track}_at"
|
||||
if world.get(key):
|
||||
world[key] = min(world[key], now + high * HOUR / speed(world))
|
||||
if world["next_event_at"] and not upcoming_event(world):
|
||||
world["next_event_at"] = min(world["next_event_at"], now)
|
||||
world["version"] = 27
|
||||
return world
|
||||
|
||||
|
||||
|
|
@ -2397,7 +2413,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
outputs[item] = outputs.get(item, 0) + qty
|
||||
if action.get("special") == "sabotage":
|
||||
raid = upcoming_raid(world)
|
||||
raid["sabotage"] = min(0.3, raid["sabotage"] + 0.03)
|
||||
raid["sabotage"] = min(0.3, raid["sabotage"] + 0.01)
|
||||
if action.get("special") == "problem":
|
||||
_work_problem(world, player, action, now)
|
||||
if action.get("special") == "recon":
|
||||
|
|
@ -3457,16 +3473,17 @@ def tick(world: dict, now: float, rng: random.Random | None = None) -> bool:
|
|||
_queue_if_funded(world, key, now)
|
||||
_start_next(world, now)
|
||||
changed = True
|
||||
if world["next_event_at"] and world["next_event_at"] <= now and not upcoming_event(world):
|
||||
_announce(world, now, rng)
|
||||
changed = True
|
||||
for event in world["events"]:
|
||||
if event["status"] == "upcoming" and event["fire_at"] <= now:
|
||||
for player in world["players"].values():
|
||||
process_player(world, player, event["fire_at"], rng)
|
||||
_fire(world, event, now, rng)
|
||||
world["next_event_at"] = now + rng.uniform(8, 24) * HOUR / speed(world)
|
||||
# The next threat is rumoured as soon as this one is over.
|
||||
world["next_event_at"] = now
|
||||
changed = True
|
||||
if world["next_event_at"] and world["next_event_at"] <= now and not upcoming_event(world):
|
||||
_announce(world, now, rng)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
|
|
@ -3534,16 +3551,33 @@ def _finish_construction(world: dict, now: float) -> None:
|
|||
|
||||
# --- events --------------------------------------------------------------
|
||||
|
||||
# Threats keep a two-day rhythm: one lands every THREAT_EVERY game hours, give or
|
||||
# take THREAT_WOBBLE, and the next is rumoured the moment it's over. The first day
|
||||
# is for scouting, drilling and building; on the second the camp can be found.
|
||||
THREAT_EVERY = 48
|
||||
THREAT_WOBBLE = 2
|
||||
|
||||
|
||||
def threat_slot(world: dict, now: float) -> float:
|
||||
"""When the next threat is due: two days after the last one's slot, never sooner than about two days off."""
|
||||
hours = HOUR / speed(world)
|
||||
last = next((e["slot"] for e in reversed(world["events"]) if e["kind"] in THREATS and "slot" in e), None)
|
||||
soonest = now + (THREAT_EVERY - THREAT_WOBBLE) * hours
|
||||
return max(last + THREAT_EVERY * hours, soonest) if last else now + THREAT_EVERY * hours
|
||||
|
||||
|
||||
def _announce(world: dict, now: float, rng: random.Random) -> None:
|
||||
city = world["city"]
|
||||
hours = HOUR / speed(world)
|
||||
first = not any(e["kind"] in ("raid", "dragon") for e in world["events"])
|
||||
slot = threat_slot(world, now)
|
||||
day = (now - world["created"]) * speed(world) / DAY
|
||||
# The major-threat track: mostly bandits, sometimes (after the first days) a dragon.
|
||||
kind = "dragon" if not first and day >= 5 and rng.random() < 0.15 else "raid"
|
||||
event = {
|
||||
"id": world["next_event_id"], "kind": kind, "status": "upcoming",
|
||||
"announced": now, "recon": 0, "sabotage": 0.0, "jitter": rng.uniform(-1, 1),
|
||||
"slot": slot, "fire_at": slot + rng.uniform(-THREAT_WOBBLE, THREAT_WOBBLE) * hours,
|
||||
}
|
||||
world["next_event_id"] += 1
|
||||
# Bigger, richer and older cities draw bigger threats; every win draws a bolder one.
|
||||
|
|
@ -3551,7 +3585,6 @@ def _announce(world: dict, now: float, rng: random.Random) -> None:
|
|||
strength = (15 + 4 * day + 0.8 * city["population"] + 1.2 * math.sqrt(wealth)) * (1 + 0.08 * city["repelled"])
|
||||
if kind == "dragon":
|
||||
event.update({
|
||||
"fire_at": now + rng.uniform(30, 54) * hours,
|
||||
"strength": round(strength * 1.5 * rng.uniform(0.9, 1.15), 1),
|
||||
"group": rng.choice(DRAGONS),
|
||||
"trait": rng.choice(DRAGON_TRAITS),
|
||||
|
|
@ -3559,7 +3592,6 @@ def _announce(world: dict, now: float, rng: random.Random) -> None:
|
|||
log(world, now, "event", "Herders report scorched pastures in the hills")
|
||||
elif kind == "raid":
|
||||
event.update({
|
||||
"fire_at": now + rng.uniform(30, 54) * hours,
|
||||
"strength": round(strength * rng.uniform(0.85, 1.2), 1),
|
||||
"group": rng.choice(RAID_GROUPS),
|
||||
"trait": rng.choice(RAID_TRAITS),
|
||||
|
|
@ -3574,7 +3606,7 @@ def _announce(world: dict, now: float, rng: random.Random) -> None:
|
|||
# knowable as the threat draws near, however much scouting is done.
|
||||
INTEL_POINTS = [15, 30, 45] # recon for intel levels 1, 2 and 3 against a small band
|
||||
INTEL_SIZE = [1.0, 1.33, 1.67, 2.0] # small, medium, large, huge
|
||||
INTEL_OPENS = [None, 24, 8] # game hours before arrival that levels 2 and 3 open, ±25% per threat
|
||||
INTEL_OPENS = [None, 36, 24] # game hours before arrival that levels 2 (±25% per threat) and 3 open
|
||||
SPYMASTER_RECON = 0.25 # more recon per guild level
|
||||
SPYMASTER_AGENTS = 0.5 # recon per guild level per game hour, with nobody scouting
|
||||
INFORMANT_RECON = 30 # boosted recon per informant: doubled recon and XP for the scout who works it
|
||||
|
|
@ -3593,11 +3625,12 @@ def intel_costs(event: dict) -> list[int]:
|
|||
|
||||
|
||||
def intel_opens(world: dict, event: dict, level: int) -> float:
|
||||
"""When an intel level becomes knowable. The ±25% is fixed per threat, so it can't be timed."""
|
||||
"""When an intel level becomes knowable. Level 2's ±25% is fixed per threat, so it can't be timed;
|
||||
the camp (level 3) can always be found a day before the threat lands."""
|
||||
hours = INTEL_OPENS[level - 1]
|
||||
if hours is None:
|
||||
return 0
|
||||
wobble = random.Random(f"{event['id']}-{level}").uniform(0.75, 1.25)
|
||||
wobble = random.Random(f"{event['id']}-{level}").uniform(0.75, 1.25) if level < 3 else 1
|
||||
return event["fire_at"] - hours * wobble * HOUR / speed(world)
|
||||
|
||||
|
||||
|
|
@ -3612,7 +3645,7 @@ def intel_level(world: dict, event: dict, now: float | None = None) -> int:
|
|||
|
||||
|
||||
SIZE_LABELS = [(30, "small"), (60, "medium"), (100, "large"), (math.inf, "huge")]
|
||||
WINDOW_HOURS = [24, 8, 2, 0]
|
||||
WINDOW_HOURS = [4, 3, 2, 0]
|
||||
|
||||
|
||||
THREATS = ("raid", "dragon")
|
||||
|
|
@ -3926,14 +3959,24 @@ PROBLEMS = {
|
|||
"rockslide": {"name": "Rockslide on the quarry road", "icon": "🪨", "work": 25, "action": "clear_rockslide",
|
||||
"block": {"mining": "Quarry road blocked"}, "only": ["mine_stone"],
|
||||
"text": "No stone until builders clear it."},
|
||||
"flood": {"name": "Flooded fishing grounds", "icon": "🌊", "speed": {"fishing": 1.5}, "need": {"plank": 20}, "lasts": 36,
|
||||
"text": "Fishing slow for 36h, or until dammed with planks."},
|
||||
"flood": {"name": "Flooded fishing grounds", "icon": "🌊", "speed": {"fishing": 1.5}, "need": {"plank": 20}, "lasts": 18,
|
||||
"text": "Fishing slow for 18h, or until dammed with planks."},
|
||||
"boars": {"name": "Boars trampling the crops", "icon": "🐗", "speed": {"farming": 1.5}, "work": 10, "action": "cull_boars",
|
||||
"text": "Farming slower until hunters cull them."},
|
||||
"rats": {"name": "Rats in the granary", "icon": "🐀", "steal_food": 0.03, "work": 12, "action": "catch_rats",
|
||||
"text": "Eat city food hourly until hunters catch them."},
|
||||
"spiders": {"name": "Spiders in the mine", "icon": "🕷️", "speed": {"mining": 1.5}, "work": 10, "action": "clear_spiders",
|
||||
"text": "Mining much slower until fighters clear them out."},
|
||||
"smugglers": {"name": "Smugglers in the caves", "icon": "🏴☠️", "steal_gold": 0.01, "work": 10, "action": "catch_smugglers",
|
||||
"text": "Skim 1% of the treasury hourly until fighters catch them."},
|
||||
"bridge": {"name": "Bridge washed out", "icon": "🌉", "need": {"plank": 25},
|
||||
"block": {"hunting": "The bridge to the hunting grounds is out"},
|
||||
"text": "No hunting until the bridge is rebuilt with planks."},
|
||||
"hornets": {"name": "Hornets in the woods", "icon": "🐝", "speed": {"woodcutting": 1.4}, "work": 8, "action": "smoke_hornets",
|
||||
"text": "Woodcutting slower until herbalists smoke them out."},
|
||||
}
|
||||
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
|
||||
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars",
|
||||
"spiders", "smugglers", "bridge", "hornets")
|
||||
PROBLEM_ACTIONS = [
|
||||
_encounter("clear_wolves", "Wolf pack", "Frontier", 4, WOLVES, label="Drive off the wolves",
|
||||
special="problem", problem="wolves"),
|
||||
|
|
@ -3943,6 +3986,11 @@ PROBLEM_ACTIONS = [
|
|||
_action("cull_boars", "hunting", "Cull the boar herd", 1, 45, 14, special="problem", problem="boars"),
|
||||
_action("catch_rats", "hunting", "Catch the rats", 1, 30, 8, special="problem", problem="rats"),
|
||||
_action("clear_rockslide", "construction", "Clear the rockslide", 1, 30, 10, special="problem", problem="rockslide"),
|
||||
_encounter("clear_spiders", "Mine spiders", "Frontier", 6, SPIDERS, label="Clear the spiders",
|
||||
special="problem", problem="spiders"),
|
||||
_encounter("catch_smugglers", "Smugglers", "Frontier", 8, SMUGGLERS, gold=6, label="Catch the smugglers",
|
||||
special="problem", problem="smugglers"),
|
||||
_action("smoke_hornets", "herblore", "Smoke out the hornets", 1, 40, 12, special="problem", problem="hornets"),
|
||||
]
|
||||
ACTIONS.extend(PROBLEM_ACTIONS)
|
||||
ACTION_BY_ID.update({action["id"]: action for action in PROBLEM_ACTIONS})
|
||||
|
|
@ -4002,11 +4050,16 @@ for _id, _name, _building, _blevel, _level, _inputs, _effect, _glyph, _tint in F
|
|||
|
||||
# (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]}
|
||||
"rats": [2, 2, 2, 2], "fire": [1, 2, 1, 2], "winter": [0, 0, 0, 4], "drought": [0, 4, 1, 0],
|
||||
"blight": [2, 1, 3, 0], "tremors": [1, 1, 1, 1], "taxman": [1, 1, 2, 1]}
|
||||
FORTUNES = {"merchant": [12] * 4, "refugees": [10] * 4, "master": [16] * 4, "harvest": [10, 10, 12, 0],
|
||||
"rich_vein": [10] * 4, "salmon": [10, 8, 4, 0], "festival": [8] * 4, "noble": [8] * 4,
|
||||
"pilgrims": [8] * 4, "mercenaries": [6] * 4, "cargo": [10] * 4}
|
||||
TRACK_HOURS = {"minor": (48, 96), "hardship": (48, 96), "fortune": (36, 72)}
|
||||
"pilgrims": [8] * 4, "mercenaries": [6] * 4, "cargo": [10] * 4,
|
||||
"timber": [8, 6, 10, 8], "deer": [8, 10, 10, 4], "masons": [10, 10, 8, 6], "artisans": [8] * 4,
|
||||
"trapper": [10] * 4}
|
||||
TRACK_HOURS = {"minor": (16, 32), "hardship": (20, 40), "fortune": (10, 20)}
|
||||
CRAFTS = ("smithing", "jewelcrafting", "cooking", "leatherworking", "tailoring", "herblore")
|
||||
TRAPPER_RECON = 15 # boosted recon a trapper's leads are worth, like an informant's
|
||||
|
||||
|
||||
def calendar_day(world: dict, now: float) -> int:
|
||||
|
|
@ -4160,7 +4213,7 @@ def _hardship(world: dict, now: float, rng: random.Random, kind: str | None = No
|
|||
kind = kind or _pick(weights, rng)
|
||||
granary, shed, well = (building_level(world, k) for k in ("granary", "lumber_shed", "well"))
|
||||
if kind == "sickness":
|
||||
hours = 72 * (1 - 0.2 * building_level(world, "temple"))
|
||||
hours = 36 * (1 - 0.2 * building_level(world, "temple"))
|
||||
_add_effect(world, now, hours, kind="sickness", name="Sickness", icon="🤒", food=2.0,
|
||||
text="City eats twice as much food")
|
||||
_record(world, now, "hardship", kind, "🤒", "Sickness is spreading through the city",
|
||||
|
|
@ -4208,28 +4261,46 @@ def _hardship(world: dict, now: float, rng: random.Random, kind: str | None = No
|
|||
lines.append("Water barrels in the stockpile (five per fire) would have put it out.")
|
||||
_record(world, now, "hardship", kind, "🔥", "Fire swept through part of the city", lines)
|
||||
elif kind == "winter":
|
||||
_add_effect(world, now, 48, kind="winter", name="Harsh winter", icon="🥶", firewood=0.1,
|
||||
_add_effect(world, now, 24, kind="winter", name="Harsh winter", icon="🥶", firewood=0.1,
|
||||
text="Burns stockpile logs hourly")
|
||||
_record(world, now, "hardship", kind, "🥶", "A harsh cold snap has set in",
|
||||
["For two days the city burns logs from the stockpile to keep warm.",
|
||||
["For a day the city burns logs from the stockpile to keep warm.",
|
||||
"Without firewood, the cold makes everyone eat 50% more food."])
|
||||
elif kind == "blight":
|
||||
_add_effect(world, now, 12, kind="blight", name="Blight", icon="🥀", **{"yield": {"farming": 0.5}},
|
||||
text="Farming yield halved")
|
||||
_record(world, now, "hardship", kind, "🥀", "Blight has struck the fields",
|
||||
["For 12 hours, farming yields half."])
|
||||
elif kind == "tremors":
|
||||
_add_effect(world, now, 12, kind="tremors", name="Tremors", icon="🫨", speed={"mining": 1.3},
|
||||
text="Mining 30% slower")
|
||||
_record(world, now, "hardship", kind, "🫨", "The ground is shaking under the mines",
|
||||
["For 12 hours, mining is 30% slower."])
|
||||
elif kind == "taxman":
|
||||
gold = int(city["treasury"] * 0.15)
|
||||
city["treasury"] -= gold
|
||||
_record(world, now, "hardship", kind, "📜", "The crown's tax collector came by",
|
||||
[f"The collector took {gold:,} gold (15%) from the treasury." if gold else "The treasury was empty, so the collector left grumbling."])
|
||||
else:
|
||||
_add_effect(world, now, 24, kind="drought", name="Drought", icon="🏜️", speed={"farming": 1.3, "fishing": 1.2},
|
||||
_add_effect(world, now, 12, kind="drought", name="Drought", icon="🏜️", speed={"farming": 1.3, "fishing": 1.2},
|
||||
text="Farming 30% and fishing 20% slower")
|
||||
_record(world, now, "hardship", kind, "🏜️", "Drought has dried the fields and streams",
|
||||
["For a day, farming is 30% and fishing 20% slower."])
|
||||
["For 12 hours, farming is 30% and fishing 20% slower."])
|
||||
|
||||
|
||||
def _fortune(world: dict, now: float, rng: random.Random, kind: str | None = None) -> None:
|
||||
city = world["city"]
|
||||
weights = {kind: w[season(world, now)["index"]] for kind, w in FORTUNES.items()}
|
||||
if not upcoming_raid(world):
|
||||
threat = upcoming_raid(world)
|
||||
if not threat:
|
||||
weights["mercenaries"] = 0
|
||||
if not threat or threat["recon"] >= intel_costs(threat)[-1]:
|
||||
weights["trapper"] = 0
|
||||
kind = kind or _pick(weights, rng)
|
||||
if kind == "merchant":
|
||||
city["merchant_until"] = now + 12 * HOUR / speed(world)
|
||||
city["merchant_until"] = now + 6 * HOUR / speed(world)
|
||||
_record(world, now, "fortune", kind, "🐪", "A merchant caravan has arrived",
|
||||
["For the next 12 hours, everything sells for 50% more."])
|
||||
["For the next 6 hours, everything sells for 50% more."])
|
||||
elif kind == "refugees":
|
||||
people = rng.randint(6, 12)
|
||||
taken = min(max(0, housing(world) - int(city["population"])), people)
|
||||
|
|
@ -4241,30 +4312,30 @@ def _fortune(world: dict, now: float, rng: random.Random, kind: str | None = Non
|
|||
elif kind == "master":
|
||||
skill = rng.choice([s for s in SKILL_IDS if s != "reconnaissance"])
|
||||
name = next(s["name"] for s in SKILLS if s["id"] == skill)
|
||||
_add_effect(world, now, 12, kind="master", name=f"A master of {name.lower()}", icon="🎓",
|
||||
_add_effect(world, now, 6, kind="master", name=f"A master of {name.lower()}", icon="🎓",
|
||||
xp={skill: 1.5}, text=f"{name} +50% XP")
|
||||
_record(world, now, "fortune", kind, "🎓", f"A travelling master of {name.lower()} is visiting",
|
||||
[f"For 12 hours, {name.lower()} earns 50% more XP. Learn while you can."])
|
||||
[f"For 6 hours, {name.lower()} earns 50% more XP. Learn while you can."])
|
||||
elif kind == "harvest":
|
||||
_add_effect(world, now, 12, kind="harvest", name="Bountiful harvest", icon="🌾", **{"yield": {"farming": 2}},
|
||||
_add_effect(world, now, 6, kind="harvest", name="Bountiful harvest", icon="🌾", **{"yield": {"farming": 2}},
|
||||
text="Farming yield doubled")
|
||||
_record(world, now, "fortune", kind, "🌾", "The fields are bursting: a bountiful harvest",
|
||||
["Farming yields are doubled for 12 hours."])
|
||||
["Farming yields are doubled for 6 hours."])
|
||||
elif kind == "rich_vein":
|
||||
_add_effect(world, now, 12, kind="rich_vein", name="Rich vein", icon="💎", **{"yield": {"mining": 1.5}},
|
||||
_add_effect(world, now, 6, kind="rich_vein", name="Rich vein", icon="💎", **{"yield": {"mining": 1.5}},
|
||||
text="Mining yield +50%")
|
||||
_record(world, now, "fortune", kind, "💎", "Miners struck a rich vein",
|
||||
["Mining yields 50% more for 12 hours."])
|
||||
["Mining yields 50% more for 6 hours."])
|
||||
elif kind == "salmon":
|
||||
_add_effect(world, now, 12, kind="salmon", name="Salmon run", icon="🐟", **{"yield": {"fishing": 2}},
|
||||
_add_effect(world, now, 6, kind="salmon", name="Salmon run", icon="🐟", **{"yield": {"fishing": 2}},
|
||||
text="Fishing yield doubled")
|
||||
_record(world, now, "fortune", kind, "🐟", "The salmon are running",
|
||||
["Fishing yields are doubled for 12 hours."])
|
||||
["Fishing yields are doubled for 6 hours."])
|
||||
elif kind == "festival":
|
||||
_add_effect(world, now, 24, kind="festival", name="Festival", icon="🎉", growth=2, favour=1.5,
|
||||
_add_effect(world, now, 12, kind="festival", name="Festival", icon="🎉", growth=2, favour=1.5,
|
||||
text="Growth doubled, +50% favour from donations")
|
||||
_record(world, now, "fortune", kind, "🎉", "The city is holding a festival",
|
||||
["For a day, the population grows twice as fast and donations earn 50% more favour."])
|
||||
["For 12 hours, the population grows twice as fast and donations earn 50% more favour."])
|
||||
elif kind == "noble":
|
||||
levels = sum(b["level"] for b in city["buildings"].values())
|
||||
gold = 100 + 40 * levels
|
||||
|
|
@ -4283,10 +4354,34 @@ def _fortune(world: dict, now: float, rng: random.Random, kind: str | None = Non
|
|||
_record(world, now, "fortune", kind, "🙏", "Pilgrims passed through the city", lines)
|
||||
elif kind == "mercenaries":
|
||||
cost = 150 + 30 * season(world, now)["index"] + int(city["population"]) * 5
|
||||
_add_effect(world, now, 24, kind="mercenaries", name="Mercenaries for hire", icon="🗡️", offer=True,
|
||||
_add_effect(world, now, 12, kind="mercenaries", name="Mercenaries for hire", icon="🗡️", offer=True,
|
||||
cost=cost, defence=25, text="+25 defence for the coming threat")
|
||||
_record(world, now, "fortune", kind, "🗡️", "A sellsword company is camped outside the walls",
|
||||
[f"For a day they'll join the next defence for {cost:,} gold from the treasury."])
|
||||
[f"For 12 hours they'll join the next defence for {cost:,} gold from the treasury."])
|
||||
elif kind == "timber":
|
||||
_add_effect(world, now, 6, kind="timber", name="Storm-felled timber", icon="🪵", **{"yield": {"woodcutting": 1.5}},
|
||||
text="Woodcutting yield +50%")
|
||||
_record(world, now, "fortune", kind, "🪵", "A storm felled trees all over the woods",
|
||||
["Woodcutting yields 50% more for 6 hours."])
|
||||
elif kind == "deer":
|
||||
_add_effect(world, now, 6, kind="deer", name="Deer migration", icon="🦌", **{"yield": {"hunting": 2}},
|
||||
text="Hunting yield doubled")
|
||||
_record(world, now, "fortune", kind, "🦌", "Herds are migrating past the city",
|
||||
["Hunting yields are doubled for 6 hours."])
|
||||
elif kind == "masons":
|
||||
_add_effect(world, now, 6, kind="masons", name="Masons' guild", icon="🧱", speed={"construction": 0.75},
|
||||
text="Construction 25% faster")
|
||||
_record(world, now, "fortune", kind, "🧱", "A masons' guild is passing through",
|
||||
["For 6 hours, construction is 25% faster."])
|
||||
elif kind == "artisans":
|
||||
_add_effect(world, now, 6, kind="artisans", name="Artisans' fair", icon="🎨", speed={s: 0.85 for s in CRAFTS},
|
||||
text="Smithing, jewelcrafting, cooking, leatherworking, tailoring and herblore 15% faster")
|
||||
_record(world, now, "fortune", kind, "🎨", "Artisans from afar opened a fair",
|
||||
["For 6 hours, every craft but construction and firekeeping is 15% faster."])
|
||||
elif kind == "trapper":
|
||||
city["recon_boost"] = city.get("recon_boost", 0) + TRAPPER_RECON
|
||||
_record(world, now, "fortune", kind, "🗺️", "A trapper brought word from the hills",
|
||||
[f"The next {TRAPPER_RECON} recon scouts gather counts double."])
|
||||
else:
|
||||
goods = rng.sample([("stone", 200), ("logs", 200), ("bricks", 80), ("bread", 60), ("arrows", 150), ("plank", 40)], 2)
|
||||
for item, qty in goods:
|
||||
|
|
@ -4390,6 +4485,8 @@ def _happenings_hour(world: dict, now: float, rng: random.Random) -> None:
|
|||
_take_share(stock, [i for i in stock if i not in _food_items(stock)], spec["steal"])
|
||||
if spec.get("steal_food"):
|
||||
_take_share(stock, _food_items(stock), spec["steal_food"] * (1 - 0.25 * building_level(world, "granary")))
|
||||
if spec.get("steal_gold"):
|
||||
city["treasury"] *= 1 - spec["steal_gold"]
|
||||
|
||||
|
||||
def _happenings_tracks(world: dict, now: float, rng: random.Random) -> bool:
|
||||
|
|
@ -4637,6 +4734,8 @@ ACTION_GLYPHS = {
|
|||
"labour": ("hammer-nails", "#d1a46a"), "clear_wolves": ("wolf-head", "#9aa1ab"),
|
||||
"catch_goblins": ("goblin-head", "#8fbf6a"), "drive_bear": ("bear-head", "#8a5a32"), "cull_boars": ("boar", "#a06a4a"),
|
||||
"catch_rats": ("rat", "#9aa1ab"), "clear_rockslide": ("falling-rocks", "#a8a29e"),
|
||||
"clear_spiders": ("spider-alt", "#9a7fb0"), "catch_smugglers": ("pirate-hat", "#a08060"),
|
||||
"smoke_hornets": ("wasp-sting", "#e3c060"),
|
||||
"hunt_elk": ("deer", "#a07a50"), "hunt_sabrecat": ("saber-toothed-cat-head", "#e0a050"),
|
||||
"hunt_mammoth": ("mammoth", "#8a6a50"),
|
||||
}
|
||||
|
|
@ -4783,6 +4882,20 @@ ORDER_RECIPES, ORDER_RAW = _order_sources()
|
|||
# What changed, shown on the Patch notes page. Newest first; every shipped change adds a
|
||||
# line here (new / balance / fix), and a new release a new entry on top.
|
||||
PATCH_NOTES = [
|
||||
{"date": "2026-10-24", "title": "Livelier frontier", "notes": [
|
||||
("balance", "A threat lands every 2 days (±2h), and the next one is rumoured as soon as the last lands. "
|
||||
"Day 1: scout, drill, build. Day 2: the camp is found (intel III opens 24h out), with the exact hour."),
|
||||
("balance", "Sabotage: -1% raid strength per win (was -3%), still max 30%."),
|
||||
("balance", "Happenings ~3× as often: fortunes every 10–20h (was 36–72h), hardships 20–40h (was 48–96h), "
|
||||
"minor threats 16–32h (was 48–96h)."),
|
||||
("balance", "Shorter happenings: 12h boosts → 6h; festival, sellswords, drought 24h → 12h; cold snap 48h → 24h; "
|
||||
"sickness 72h → 36h; flood 36h → 18h."),
|
||||
("new", "Fortunes: storm-felled timber (woodcutting yield +50%), deer migration (hunting yield ×2), masons' guild "
|
||||
"(construction 25% faster), artisans' fair (crafts 15% faster), a trapper's leads (next 15 recon doubled)."),
|
||||
("new", "Hardships: blight (farming yield ½), tremors (mining 30% slower), the tax collector (-15% treasury)."),
|
||||
("new", "Minor threats: spiders in the mine and smugglers skimming 1% treasury/h (fighters), a washed-out "
|
||||
"bridge that blocks hunting (25 planks), hornets slowing woodcutting (herbalists smoke them out)."),
|
||||
]},
|
||||
{"date": "2026-10-24", "title": "Combat rework", "notes": [
|
||||
("new", "Fights are against packs: as many enemies as there are of you, often one more (each 35% weaker). "
|
||||
"A rare elite still comes alone."),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue