Townsfolk mood: needs, kindling, clothes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
002e990dc4
commit
58a3e66454
6 changed files with 385 additions and 30 deletions
1
app.py
1
app.py
|
|
@ -192,6 +192,7 @@ def city_view(world: dict, now: float) -> dict:
|
||||||
"day": game.calendar_day(world, now) + 1,
|
"day": game.calendar_day(world, now) + 1,
|
||||||
"fires": game.fires_view(world, now),
|
"fires": game.fires_view(world, now),
|
||||||
"monuments": game.monuments_view(world),
|
"monuments": game.monuments_view(world),
|
||||||
|
"happiness": game.happiness(world, now),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
220
game.py
220
game.py
|
|
@ -514,6 +514,38 @@ for _tier, (_id, _name, *_rest) in enumerate(HERBS):
|
||||||
ITEMS[f"{_id}_salve"] = {"name": f"{_name.removesuffix(' leaf')} salve", "plural": f"{_name.removesuffix(' leaf')} salves", "icon": "🩹", "kit": "salve",
|
ITEMS[f"{_id}_salve"] = {"name": f"{_name.removesuffix(' leaf')} salve", "plural": f"{_name.removesuffix(' leaf')} salves", "icon": "🩹", "kit": "salve",
|
||||||
"tier": _tier, "mend": _tier + 1, "value": 0}
|
"tier": _tier, "mend": _tier + 1, "value": 0}
|
||||||
|
|
||||||
|
# --- townsfolk needs -----------------------------------------------------------
|
||||||
|
# The townsfolk's mood (0 to 1) follows what the city gives them, a little each hour.
|
||||||
|
# Each settlement tier adds a need: a hamlet only wants food, a village firewood too,
|
||||||
|
# a town clothes for the season as well.
|
||||||
|
NEEDS = {"food": 0.4, "warmth": 0.3, "clothes": 0.3} # weight in the mood
|
||||||
|
NEEDS_BY_TIER = [("food",), ("food", "warmth"), ("food", "warmth", "clothes"), ("food", "warmth", "clothes")]
|
||||||
|
FOOD_DAYS = [2, 4, 7, 10] # days of food stored for full marks, by tier
|
||||||
|
FOOD_KINDS = 4 # kinds of food, each enough for a day, for full marks
|
||||||
|
FUEL_DAYS = 2 # days of fuel stored for full marks
|
||||||
|
HEARTH_BURN = [0.01, 0.005, 0.015, 0.025] # fuel per person per game hour, spring to winter
|
||||||
|
KINDLING_YIELD = [2, 3, 4, 5, 6, 8] # from one log of each tier
|
||||||
|
KINDLING_XP = [3, 7, 15, 23, 33, 45]
|
||||||
|
SEASON_CLOTHES = ["smock", "smock", "cloak", "cloak"] # each person wears out one a season
|
||||||
|
MOOD_DRIFT = 0.5 / 48 # per game hour: Content to either end in two days
|
||||||
|
PROBLEM_MOOD = 0.05 # off the mood for each minor threat left unsolved
|
||||||
|
# Sudden news moves the mood at once, then it drifts back.
|
||||||
|
MOOD_SHOCKS = {"raid_won": 0.05, "raid_lost": -0.2, "dragon_won": 0.05, "dragon_lost": -0.25,
|
||||||
|
"fire": -0.05, "storm": -0.05, "festival": 0.1, "trouble": -0.03}
|
||||||
|
# Up to `below`: name, icon, growth, guards (militia), townsfolk labour and taxes, leaving a day
|
||||||
|
MOODS = [
|
||||||
|
{"below": 0.2, "name": "Miserable", "icon": "😠", "growth": 0, "militia": 0.8, "work": 0.5, "leave": 0.03},
|
||||||
|
{"below": 0.4, "name": "Unhappy", "icon": "😟", "growth": 0, "militia": 0.9, "work": 0.8, "leave": 0},
|
||||||
|
{"below": 0.6, "name": "Content", "icon": "😐", "growth": 1.0, "militia": 1.0, "work": 1.0, "leave": 0},
|
||||||
|
{"below": 0.8, "name": "Happy", "icon": "🙂", "growth": 1.1, "militia": 1.05, "work": 1.1, "leave": 0},
|
||||||
|
{"below": 9.0, "name": "Joyful", "icon": "😄", "growth": 1.25, "militia": 1.1, "work": 1.25, "leave": 0},
|
||||||
|
]
|
||||||
|
TROUBLE_ODDS = 1 / 24 # an hour, while Miserable: about once a day
|
||||||
|
FUEL = ("kindling", *FIREWOOD) # what the hearths burn, kindling first; one each an hour's warmth
|
||||||
|
ITEMS["kindling"] = {"name": "Kindling", "plural": "Kindling", "icon": "🪵", "value": 1}
|
||||||
|
ITEMS["smock"] = {"name": "Summer smock", "plural": "Summer smocks", "icon": "👕", "value": 12}
|
||||||
|
ITEMS["cloak"] = {"name": "Winter cloak", "plural": "Winter cloaks", "icon": "🧥", "value": 24}
|
||||||
|
|
||||||
# Health potions: the one heal you take into a fight, drunk once a fight when you're low.
|
# Health potions: the one heal you take into a fight, drunk once a fight when you're low.
|
||||||
# Each tier is the tier's herb and its cooked fish.
|
# Each tier is the tier's herb and its cooked fish.
|
||||||
HEALTH_HEAL = [20, 40, 65, 90, 115, 140]
|
HEALTH_HEAL = [20, 40, 65, 90, 115, 140]
|
||||||
|
|
@ -1367,6 +1399,12 @@ ACTIONS = [
|
||||||
*[_action(f"tend_all_{log}", "firekeeping", "Tend all fires", TIER_LEVELS[tier], 30, 0,
|
*[_action(f"tend_all_{log}", "firekeeping", "Tend all fires", TIER_LEVELS[tier], 30, 0,
|
||||||
{log: 1}, special="tend", tend=None, tier=tier, group="All fires")
|
{log: 1}, special="tend", tend=None, tier=tier, group="All fires")
|
||||||
for tier, log in enumerate(FIREWOOD)],
|
for tier, log in enumerate(FIREWOOD)],
|
||||||
|
# Kindling for the townsfolk's hearths: better logs split into more.
|
||||||
|
*[_action(f"split_{log}", "firekeeping", f"Split {ITEMS[log]['name'].lower()}", TIER_LEVELS[tier], 25,
|
||||||
|
KINDLING_XP[tier], {log: 1}, {"kindling": KINDLING_YIELD[tier]}, group="Kindling")
|
||||||
|
for tier, log in enumerate(FIREWOOD)],
|
||||||
|
_action("sew_smock", "tailoring", "Summer smock", 3, 30, 12, {"linen": 2}, {"smock": 1}, group="Townsfolk"),
|
||||||
|
_action("sew_cloak", "tailoring", "Winter cloak", 8, 40, 24, {"linen": 4}, {"cloak": 1}, group="Townsfolk"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# --- dungeon loot --------------------------------------------------------------
|
# --- dungeon loot --------------------------------------------------------------
|
||||||
|
|
@ -1586,7 +1624,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
|
||||||
|
|
||||||
# --- world ---------------------------------------------------------------
|
# --- world ---------------------------------------------------------------
|
||||||
|
|
||||||
WORLD_VERSION = 34
|
WORLD_VERSION = 35
|
||||||
|
|
||||||
|
|
||||||
def new_world(now: float, speed: float = 1.0) -> dict:
|
def new_world(now: float, speed: float = 1.0) -> dict:
|
||||||
|
|
@ -1611,6 +1649,7 @@ def new_world(now: float, speed: float = 1.0) -> dict:
|
||||||
"repelled": 0,
|
"repelled": 0,
|
||||||
"fires": {key: new_fire(now) for key in FIRES},
|
"fires": {key: new_fire(now) for key in FIRES},
|
||||||
"wages": 0, "monuments": {}, "built": [], "carving": None,
|
"wages": 0, "monuments": {}, "built": [], "carving": None,
|
||||||
|
"happiness": 0.5, "clothes_due": 0.0, "unheated": False,
|
||||||
},
|
},
|
||||||
"events": [],
|
"events": [],
|
||||||
"next_event_at": now + 12 * HOUR / speed,
|
"next_event_at": now + 12 * HOUR / speed,
|
||||||
|
|
@ -1847,6 +1886,10 @@ def migrate(world: dict, now: float | None = None) -> dict:
|
||||||
for player in world["players"].values():
|
for player in world["players"].values():
|
||||||
player.setdefault("gave", {})
|
player.setdefault("gave", {})
|
||||||
world["version"] = 34
|
world["version"] = 34
|
||||||
|
if world["version"] < 35:
|
||||||
|
# Townsfolk needs: everyone starts Content and drifts to what the city gives them.
|
||||||
|
world["city"].update(happiness=0.5, clothes_due=0.0, unheated=False)
|
||||||
|
world["version"] = 35
|
||||||
return world
|
return world
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2155,6 +2198,10 @@ def city_uses(item: str) -> list[str]:
|
||||||
spec = ITEMS[item]
|
spec = ITEMS[item]
|
||||||
if "food" in spec:
|
if "food" in spec:
|
||||||
uses.append("the granary")
|
uses.append("the granary")
|
||||||
|
if item in FUEL:
|
||||||
|
uses.append("the hearths")
|
||||||
|
if item in SEASON_CLOTHES:
|
||||||
|
uses.append("the townsfolk")
|
||||||
if spec.get("slot") in ("weapon", "shield") or "kit" in spec or item == "arrows":
|
if spec.get("slot") in ("weapon", "shield") or "kit" in spec or item == "arrows":
|
||||||
uses.append("the armoury")
|
uses.append("the armoury")
|
||||||
if item == "water_barrel":
|
if item == "water_barrel":
|
||||||
|
|
@ -4282,7 +4329,7 @@ def growth_per_day(world: dict) -> float:
|
||||||
"""New townsfolk a day while there's room: settlers for the homes, plus families."""
|
"""New townsfolk a day while there's room: settlers for the homes, plus families."""
|
||||||
pop = world["city"]["population"]
|
pop = world["city"]["population"]
|
||||||
growth = SETTLERS_PER_EMPTY_HOME * max(0.0, housing(world) - pop) + FAMILY_GROWTH * pop
|
growth = SETTLERS_PER_EMPTY_HOME * max(0.0, housing(world) - pop) + FAMILY_GROWTH * pop
|
||||||
return growth * city_rate(world, "growth") * (1.25 if fitted(world, "furniture") else 1)
|
return growth * city_rate(world, "growth") * (1.25 if fitted(world, "furniture") else 1) * mood(world)["growth"]
|
||||||
|
|
||||||
|
|
||||||
def population_view(world: dict) -> dict:
|
def population_view(world: dict) -> dict:
|
||||||
|
|
@ -4298,17 +4345,137 @@ def population_view(world: dict) -> dict:
|
||||||
growth = -pop * 0.05
|
growth = -pop * 0.05
|
||||||
else:
|
else:
|
||||||
growth = 0.0
|
growth = 0.0
|
||||||
|
leaving = 0.0 if city["starving"] else pop * mood(world)["leave"]
|
||||||
food = sum(city["stockpile"].get(item, 0) * ITEMS[item]["food"] for item in FOOD_ORDER)
|
food = sum(city["stockpile"].get(item, 0) * ITEMS[item]["food"] for item in FOOD_ORDER)
|
||||||
tier = city["tier"]
|
tier = city["tier"]
|
||||||
following = TIERS[tier + 1] if tier + 1 < len(TIERS) else None
|
following = TIERS[tier + 1] if tier + 1 < len(TIERS) else None
|
||||||
return {
|
return {
|
||||||
"population": int(pop), "housing": room, "growth": round(growth, 1),
|
"population": int(pop), "housing": room, "growth": round(growth - leaving, 1), "leaving": round(leaving, 1),
|
||||||
|
"halted": not mood(world)["growth"],
|
||||||
"food": food, "food_days": round(food / (pop * 0.5), 1) if pop else None,
|
"food": food, "food_days": round(food / (pop * 0.5), 1) if pop else None,
|
||||||
"tier": tier, "tier_name": TIERS[tier]["name"],
|
"tier": tier, "tier_name": TIERS[tier]["name"],
|
||||||
"next_tier": following and {"name": following["name"], "pop": following["pop"]},
|
"next_tier": following and {"name": following["name"], "pop": following["pop"]},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mood(world: dict) -> dict:
|
||||||
|
"""The townsfolk's mood band right now, with what it does."""
|
||||||
|
score = world["city"]["happiness"]
|
||||||
|
return next(band for band in MOODS if score < band["below"])
|
||||||
|
|
||||||
|
|
||||||
|
def _meals(stock: dict, item: str) -> float:
|
||||||
|
return stock.get(item, 0) * ITEMS[item]["food"]
|
||||||
|
|
||||||
|
|
||||||
|
def hearth_burn(world: dict, now: float) -> tuple[float, float]:
|
||||||
|
"""Fuel the city burns an hour: (homes, weather and hardships). Hamlets don't keep hearths."""
|
||||||
|
pop = world["city"]["population"]
|
||||||
|
homes = pop * HEARTH_BURN[season(world, now)["index"]] if "warmth" in NEEDS_BY_TIER[world["city"]["tier"]] else 0.0
|
||||||
|
return homes, pop * sum(source.get("firewood", 0) for source in _modifier_sources(world))
|
||||||
|
|
||||||
|
|
||||||
|
def happiness(world: dict, now: float) -> dict:
|
||||||
|
"""Each need scored 0..1 with its have / need rows, the modifiers, and the mood they pull towards."""
|
||||||
|
city = world["city"]
|
||||||
|
pop = max(1.0, city["population"])
|
||||||
|
stock = city["stockpile"]
|
||||||
|
tier = city["tier"]
|
||||||
|
day = pop * 0.5 # meals a day
|
||||||
|
meals = sum(_meals(stock, item) for item in FOOD_ORDER)
|
||||||
|
kinds = sum(1 for item in FOOD_ORDER if _meals(stock, item) >= day)
|
||||||
|
want_meals = math.ceil(FOOD_DAYS[tier] * day)
|
||||||
|
supply = min(1.0, meals / want_meals)
|
||||||
|
variety = min(1.0, kinds / FOOD_KINDS)
|
||||||
|
needs = {"food": {"score": 0.0 if city["starving"] else 0.5 * supply + 0.5 * variety, "rows": [
|
||||||
|
{"item": "bread", "label": "Food (any)", "have": int(meals), "want": want_meals, "any": "food"},
|
||||||
|
{"item": "stew", "label": "Kinds of food", "have": kinds, "want": FOOD_KINDS, "kinds": True},
|
||||||
|
]}}
|
||||||
|
season_now = season(world, now)["index"]
|
||||||
|
burn = 24 * sum(hearth_burn(world, now))
|
||||||
|
fuel = sum(stock.get(item, 0) for item in FUEL)
|
||||||
|
want_fuel = math.ceil(FUEL_DAYS * burn)
|
||||||
|
needs["warmth"] = {"score": 0.0 if city.get("unheated") else min(1.0, fuel / want_fuel) if want_fuel else 1.0,
|
||||||
|
"rows": [{"item": "kindling", "label": "Firewood (any)", "have": fuel, "want": want_fuel,
|
||||||
|
"any": "fuel"}]}
|
||||||
|
garment, later = SEASON_CLOTHES[season_now], SEASON_CLOTHES[(season_now + 1) % 4]
|
||||||
|
rows = [{"item": garment, "label": ITEMS[garment]["plural"], "have": stock.get(garment, 0), "want": math.ceil(pop)}]
|
||||||
|
if later != garment:
|
||||||
|
rows.append({"item": later, "label": f"{ITEMS[later]['plural']} (next season)", "have": stock.get(later, 0),
|
||||||
|
"want": math.ceil(pop), "next": True})
|
||||||
|
needs["clothes"] = {"score": min(1.0, stock.get(garment, 0) / pop), "rows": rows}
|
||||||
|
active = NEEDS_BY_TIER[tier]
|
||||||
|
base = sum(NEEDS[key] * needs[key]["score"] for key in active) / sum(NEEDS[key] for key in active)
|
||||||
|
problems = sum(1 for problem in city["problems"] if problem["kind"] in MINOR_THREATS)
|
||||||
|
mods = [{"name": "Unsolved problems", "value": -PROBLEM_MOOD * problems}] if problems else []
|
||||||
|
mods += [{"name": effect["name"], "value": effect["mood"]} for effect in city["effects"] if effect.get("mood")]
|
||||||
|
target = max(0.0, min(1.0, base + sum(m["value"] for m in mods)))
|
||||||
|
band = mood(world)
|
||||||
|
return {
|
||||||
|
"score": round(city["happiness"], 3), "target": round(target, 3), "name": band["name"], "icon": band["icon"],
|
||||||
|
"heading": next(b["name"] for b in MOODS if target < b["below"]),
|
||||||
|
"effects": {key: band[key] for key in ("growth", "militia", "work", "leave")},
|
||||||
|
"needs": {key: {**needs[key], "score": round(needs[key]["score"], 3), "weight": NEEDS[key]} for key in active},
|
||||||
|
"locked": [key for key in NEEDS if key not in active],
|
||||||
|
"mods": mods,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def shock_mood(world: dict, now: float, kind: str) -> None:
|
||||||
|
"""News that moves the townsfolk at once: a raid, a fire, a festival."""
|
||||||
|
city = world["city"]
|
||||||
|
before = mood(world)["name"]
|
||||||
|
city["happiness"] = max(0.0, min(1.0, city["happiness"] + MOOD_SHOCKS[kind]))
|
||||||
|
_mood_news(world, now, before)
|
||||||
|
|
||||||
|
|
||||||
|
def _mood_news(world: dict, now: float, before: str) -> None:
|
||||||
|
band = mood(world)
|
||||||
|
if band["name"] != before:
|
||||||
|
log(world, now, "city", f"{band['icon']} The townsfolk are {band['name'].lower()}.")
|
||||||
|
|
||||||
|
|
||||||
|
def _townsfolk_hour(world: dict, now: float, rng: random.Random) -> None:
|
||||||
|
"""Clothes wear out, the mood drifts towards what the city gives, and misery makes trouble."""
|
||||||
|
city = world["city"]
|
||||||
|
stock = city["stockpile"]
|
||||||
|
pop = city["population"]
|
||||||
|
if "clothes" in NEEDS_BY_TIER[city["tier"]]:
|
||||||
|
garment = SEASON_CLOTHES[season(world, now)["index"]]
|
||||||
|
city["clothes_due"] = city.get("clothes_due", 0) + pop / (SEASON_DAYS * 24)
|
||||||
|
while city["clothes_due"] >= 1 and stock.get(garment):
|
||||||
|
take_stock_qty(stock, garment, 1)
|
||||||
|
city["clothes_due"] -= 1
|
||||||
|
city["clothes_due"] = min(city["clothes_due"], 1) # nothing to wear out: no debt piles up
|
||||||
|
before = mood(world)["name"]
|
||||||
|
gap = happiness(world, now)["target"] - city["happiness"]
|
||||||
|
city["happiness"] += max(-MOOD_DRIFT, min(MOOD_DRIFT, gap))
|
||||||
|
_mood_news(world, now, before)
|
||||||
|
if mood(world)["name"] == "Miserable" and rng.random() < TROUBLE_ODDS:
|
||||||
|
_trouble(world, now, rng)
|
||||||
|
|
||||||
|
|
||||||
|
def _trouble(world: dict, now: float, rng: random.Random) -> None:
|
||||||
|
"""What miserable townsfolk do: down tools, help themselves to the stockpile, or riot."""
|
||||||
|
city = world["city"]
|
||||||
|
kind = rng.choice(["strike", "theft", "riot"])
|
||||||
|
if kind == "strike":
|
||||||
|
_add_effect(world, now, 12, kind="strike", name="Strike", icon="✊", labour=0, text="No townsfolk labour")
|
||||||
|
_record(world, now, "hardship", "strike", "✊", "The townsfolk have downed tools",
|
||||||
|
["For 12 hours they won't work on the building site."])
|
||||||
|
elif kind == "theft":
|
||||||
|
stock = city["stockpile"]
|
||||||
|
taken = _take_share(stock, [item for item in stock if item not in _food_items(stock)], 0.05)
|
||||||
|
_record(world, now, "hardship", "theft", "🫳", "Townsfolk helped themselves to the stockpile",
|
||||||
|
[f"They took {_listing(taken)}." if taken else "There was little worth taking."])
|
||||||
|
else:
|
||||||
|
name = _damage_building(world, now, rng)
|
||||||
|
_record(world, now, "hardship", "riot", "🔥", "A riot broke out in the streets",
|
||||||
|
[f"The {name.lower()} {'were' if name.endswith('s') else 'was'} damaged and needs repairs." if name
|
||||||
|
else "Nothing was badly damaged."])
|
||||||
|
shock_mood(world, now, "trouble")
|
||||||
|
|
||||||
|
|
||||||
def kit_slot(item: str) -> str | None:
|
def kit_slot(item: str) -> str | None:
|
||||||
"""Where a defender carries it: a gear slot, or city-only kit like guard jerkins."""
|
"""Where a defender carries it: a gear slot, or city-only kit like guard jerkins."""
|
||||||
return ITEMS[item].get("slot") or ITEMS[item].get("kit")
|
return ITEMS[item].get("slot") or ITEMS[item].get("kit")
|
||||||
|
|
@ -4423,7 +4590,8 @@ def defence(world: dict, guards: int | None = None) -> dict:
|
||||||
stock = city["stockpile"]
|
stock = city["stockpile"]
|
||||||
posts = guard_posts(world)
|
posts = guard_posts(world)
|
||||||
armed = guards_on_duty(world) if guards is None else min(guards, posts)
|
armed = guards_on_duty(world) if guards is None else min(guards, posts)
|
||||||
militia = 0.3 * building_level(world, "barracks") * pop * (1.5 if fitted(world, "weapon_racks") else 1)
|
militia = (0.3 * building_level(world, "barracks") * pop * (1.5 if fitted(world, "weapon_racks") else 1)
|
||||||
|
* mood(world)["militia"])
|
||||||
parts = {
|
parts = {
|
||||||
"townsfolk": 10,
|
"townsfolk": 10,
|
||||||
"walls": 20 * building_level(world, "walls") + 25 * fitted(world, "hoardings"),
|
"walls": 20 * building_level(world, "walls") + 25 * fitted(world, "hoardings"),
|
||||||
|
|
@ -4541,15 +4709,17 @@ def tick(world: dict, now: float, rng: random.Random | None = None) -> bool:
|
||||||
def _city_hour(world: dict, now: float) -> None:
|
def _city_hour(world: dict, now: float) -> None:
|
||||||
city = world["city"]
|
city = world["city"]
|
||||||
pop = city["population"]
|
pop = city["population"]
|
||||||
|
band = mood(world)
|
||||||
if city["construction"]:
|
if city["construction"]:
|
||||||
city["construction"]["labour"] += pop * TOWNSFOLK_LABOUR
|
city["construction"]["labour"] += pop * TOWNSFOLK_LABOUR * band["work"] * city_rate(world, "labour")
|
||||||
threat = upcoming_raid(world)
|
threat = upcoming_raid(world)
|
||||||
if threat and building_level(world, "spymaster"):
|
if threat and building_level(world, "spymaster"):
|
||||||
threat["recon"] = min(intel_costs(threat)[-1], threat["recon"] + SPYMASTER_AGENTS * building_level(world, "spymaster"))
|
threat["recon"] = min(intel_costs(threat)[-1], threat["recon"] + SPYMASTER_AGENTS * building_level(world, "spymaster"))
|
||||||
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))
|
* (0.9 if fitted(world, "grain_bins") else 1))
|
||||||
stock = city["stockpile"]
|
stock = city["stockpile"]
|
||||||
for item in FOOD_ORDER:
|
# The biggest pile goes first, so the smaller kinds last and the table stays varied.
|
||||||
|
for item in sorted(FOOD_ORDER, key=lambda i: -_meals(stock, i)):
|
||||||
while city["food_due"] >= ITEMS[item]["food"] and stock.get(item, 0) > 0:
|
while city["food_due"] >= ITEMS[item]["food"] and stock.get(item, 0) > 0:
|
||||||
stock[item] -= 1
|
stock[item] -= 1
|
||||||
if not stock[item]:
|
if not stock[item]:
|
||||||
|
|
@ -4568,7 +4738,9 @@ def _city_hour(world: dict, now: float) -> None:
|
||||||
city["population"] = min(room, pop + growth_per_day(world) / 24)
|
city["population"] = min(room, pop + growth_per_day(world) / 24)
|
||||||
elif pop > room:
|
elif pop > room:
|
||||||
city["population"] = max(room, pop - pop * 0.05 / 24)
|
city["population"] = max(room, pop - pop * 0.05 / 24)
|
||||||
city["treasury"] += pop * 0.5 / 24
|
if band["leave"]:
|
||||||
|
city["population"] = max(5.0, city["population"] - pop * band["leave"] / 24)
|
||||||
|
city["treasury"] += pop * 0.5 / 24 * band["work"]
|
||||||
|
|
||||||
|
|
||||||
def labour_needed(level: int, repair: bool) -> float:
|
def labour_needed(level: int, repair: bool) -> float:
|
||||||
|
|
@ -4869,6 +5041,7 @@ def _resolve_raid(world: dict, event: dict, now: float, rng: random.Random) -> N
|
||||||
if garrison:
|
if garrison:
|
||||||
lines.append("Everyone in the garrison earned 25 favour and combat experience.")
|
lines.append("Everyone in the garrison earned 25 favour and combat experience.")
|
||||||
log(world, now, "raid", f"{upper_first(event["group"])} were driven off")
|
log(world, now, "raid", f"{upper_first(event["group"])} were driven off")
|
||||||
|
shock_mood(world, now, "raid_won")
|
||||||
else:
|
else:
|
||||||
severity = min(1.0, 1 - ratio)
|
severity = min(1.0, 1 - ratio)
|
||||||
broken = wear_kit(world, wear_share(ratio), rng)
|
broken = wear_kit(world, wear_share(ratio), rng)
|
||||||
|
|
@ -4907,6 +5080,7 @@ def _resolve_raid(world: dict, event: dict, now: float, rng: random.Random) -> N
|
||||||
for entry in garrison:
|
for entry in garrison:
|
||||||
gain_xp(world, world["players"][entry["id"]], "combat", round(attack), now)
|
gain_xp(world, world["players"][entry["id"]], "combat", round(attack), now)
|
||||||
log(world, now, "raid", f"{upper_first(event["group"])} broke into the city")
|
log(world, now, "raid", f"{upper_first(event["group"])} broke into the city")
|
||||||
|
shock_mood(world, now, "raid_lost")
|
||||||
report["lines"] = lines
|
report["lines"] = lines
|
||||||
event["report"] = report
|
event["report"] = report
|
||||||
_pay_guards(world, report)
|
_pay_guards(world, report)
|
||||||
|
|
@ -4945,6 +5119,7 @@ def _resolve_dragon(world: dict, event: dict, now: float, rng: random.Random) ->
|
||||||
if garrison:
|
if garrison:
|
||||||
lines.append("Everyone in the garrison earned 40 favour, combat experience, dragon bones and Ironhill reagents.")
|
lines.append("Everyone in the garrison earned 40 favour, combat experience, dragon bones and Ironhill reagents.")
|
||||||
log(world, now, "raid", report["title"])
|
log(world, now, "raid", report["title"])
|
||||||
|
shock_mood(world, now, "dragon_won")
|
||||||
else:
|
else:
|
||||||
severity = min(1.0, 1 - guard["total"] / attack)
|
severity = min(1.0, 1 - guard["total"] / attack)
|
||||||
broken = wear_kit(world, 0.5, rng)
|
broken = wear_kit(world, 0.5, rng)
|
||||||
|
|
@ -4966,6 +5141,7 @@ def _resolve_dragon(world: dict, event: dict, now: float, rng: random.Random) ->
|
||||||
for entry in garrison:
|
for entry in garrison:
|
||||||
gain_xp(world, world["players"][entry["id"]], "combat", round(attack), now)
|
gain_xp(world, world["players"][entry["id"]], "combat", round(attack), now)
|
||||||
log(world, now, "raid", report["title"])
|
log(world, now, "raid", report["title"])
|
||||||
|
shock_mood(world, now, "dragon_lost")
|
||||||
report["lines"] = lines
|
report["lines"] = lines
|
||||||
event["report"] = report
|
event["report"] = report
|
||||||
_pay_guards(world, report)
|
_pay_guards(world, report)
|
||||||
|
|
@ -5324,6 +5500,7 @@ def _hardship(world: dict, now: float, rng: random.Random, kind: str | None = No
|
||||||
if taken:
|
if taken:
|
||||||
lines.append(f"The wind scattered {_listing(taken)}.")
|
lines.append(f"The wind scattered {_listing(taken)}.")
|
||||||
_record(world, now, "hardship", kind, "🌪️", "A wild storm battered the city", lines)
|
_record(world, now, "hardship", kind, "🌪️", "A wild storm battered the city", lines)
|
||||||
|
shock_mood(world, now, "storm")
|
||||||
elif kind == "rats":
|
elif kind == "rats":
|
||||||
city["problems"].append({"kind": "rats", "started": now, "progress": {}, "work": 0})
|
city["problems"].append({"kind": "rats", "started": now, "progress": {}, "work": 0})
|
||||||
_record(world, now, "hardship", kind, "🐀", "Rats have got into the granary",
|
_record(world, now, "hardship", kind, "🐀", "Rats have got into the granary",
|
||||||
|
|
@ -5346,6 +5523,7 @@ def _hardship(world: dict, now: float, rng: random.Random, kind: str | None = No
|
||||||
lines.append(f"The {name.lower()} {'were' if name.endswith('s') else 'was'} damaged and needs repairs.")
|
lines.append(f"The {name.lower()} {'were' if name.endswith('s') else 'was'} damaged and needs repairs.")
|
||||||
lines.append("Water barrels in the stockpile (five per fire) would have put it out.")
|
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)
|
_record(world, now, "hardship", kind, "🔥", "Fire swept through part of the city", lines)
|
||||||
|
shock_mood(world, now, "fire")
|
||||||
elif kind == "winter":
|
elif kind == "winter":
|
||||||
_add_effect(world, now, 24, 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")
|
text="Burns stockpile logs hourly")
|
||||||
|
|
@ -5418,8 +5596,9 @@ def _fortune(world: dict, now: float, rng: random.Random, kind: str | None = Non
|
||||||
_record(world, now, "fortune", kind, "🐟", "The salmon are running",
|
_record(world, now, "fortune", kind, "🐟", "The salmon are running",
|
||||||
["Fishing yields are doubled for 6 hours."])
|
["Fishing yields are doubled for 6 hours."])
|
||||||
elif kind == "festival":
|
elif kind == "festival":
|
||||||
_add_effect(world, now, 12, 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, mood=0.1,
|
||||||
text="Growth doubled, +50% favour from donations")
|
text="Growth doubled, +50% favour from donations, +10% mood")
|
||||||
|
shock_mood(world, now, "festival")
|
||||||
_record(world, now, "fortune", kind, "🎉", "The city is holding a festival",
|
_record(world, now, "fortune", kind, "🎉", "The city is holding a festival",
|
||||||
["For 12 hours, 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":
|
elif kind == "noble":
|
||||||
|
|
@ -5549,20 +5728,26 @@ def _happenings_hour(world: dict, now: float, rng: random.Random) -> None:
|
||||||
if problem.get("until") and problem["until"] <= now:
|
if problem.get("until") and problem["until"] <= now:
|
||||||
_solve(world, problem, now)
|
_solve(world, problem, now)
|
||||||
stock = city["stockpile"]
|
stock = city["stockpile"]
|
||||||
burn = city["population"] * sum(source.get("firewood", 0) for source in _modifier_sources(world))
|
homes, weather = hearth_burn(world, now)
|
||||||
city["firewood_due"] = city.get("firewood_due", 0) + burn
|
city["firewood_due"] = city.get("firewood_due", 0) + homes + weather
|
||||||
cold = False
|
short = False
|
||||||
while city["firewood_due"] >= 1:
|
while city["firewood_due"] >= 1:
|
||||||
log_item = next((item for item in FIREWOOD if stock.get(item)), None)
|
fuel = next((item for item in FUEL if stock.get(item)), None)
|
||||||
if not log_item:
|
if not fuel:
|
||||||
cold = True
|
short = True
|
||||||
city["firewood_due"] = 0
|
city["firewood_due"] = 0
|
||||||
break
|
break
|
||||||
take_stock_qty(stock, log_item, 1)
|
take_stock_qty(stock, fuel, 1)
|
||||||
city["firewood_due"] -= 1
|
city["firewood_due"] -= 1
|
||||||
|
# Cold homes make for unhappy townsfolk; in snow or a cold snap they also eat more to keep warm.
|
||||||
|
cold = short and weather > 0
|
||||||
|
unheated = homes + weather > 0 and not any(stock.get(item) for item in FUEL)
|
||||||
if cold and not city.get("cold"):
|
if cold and not city.get("cold"):
|
||||||
log(world, now, "city", "The firewood has run out. People are cold and eating more to stay warm.")
|
log(world, now, "city", "The firewood has run out. People are cold and eating more to stay warm.")
|
||||||
|
elif unheated and not city.get("unheated"):
|
||||||
|
log(world, now, "city", "The firewood has run out. Homes are cold.")
|
||||||
city["cold"] = cold
|
city["cold"] = cold
|
||||||
|
city["unheated"] = unheated
|
||||||
for problem in city["problems"]:
|
for problem in city["problems"]:
|
||||||
spec = PROBLEMS[problem["kind"]]
|
spec = PROBLEMS[problem["kind"]]
|
||||||
if spec.get("steal"):
|
if spec.get("steal"):
|
||||||
|
|
@ -5571,6 +5756,7 @@ def _happenings_hour(world: dict, now: float, rng: random.Random) -> None:
|
||||||
_take_share(stock, _food_items(stock), spec["steal_food"] * (1 - 0.25 * building_level(world, "granary")))
|
_take_share(stock, _food_items(stock), spec["steal_food"] * (1 - 0.25 * building_level(world, "granary")))
|
||||||
if spec.get("steal_gold"):
|
if spec.get("steal_gold"):
|
||||||
city["treasury"] *= 1 - spec["steal_gold"]
|
city["treasury"] *= 1 - spec["steal_gold"]
|
||||||
|
_townsfolk_hour(world, now, rng)
|
||||||
|
|
||||||
|
|
||||||
def _happenings_tracks(world: dict, now: float, rng: random.Random) -> bool:
|
def _happenings_tracks(world: dict, now: float, rng: random.Random) -> bool:
|
||||||
|
|
@ -6160,6 +6346,7 @@ REAGENT_GLYPHS = {"heavy": "chain-mail", "medium": "scale-mail", "light": "yarn"
|
||||||
WEAPON_GLYPHS = {"hammer": "warhammer", "staff": "wizard-staff", "bow": "pocket-bow"}
|
WEAPON_GLYPHS = {"hammer": "warhammer", "staff": "wizard-staff", "bow": "pocket-bow"}
|
||||||
HARDWARE_GLYPHS = ["screw", "nails", "door-handle", "metal-plate", "crossed-chains", "padlock"]
|
HARDWARE_GLYPHS = ["screw", "nails", "door-handle", "metal-plate", "crossed-chains", "padlock"]
|
||||||
GLYPHS = {
|
GLYPHS = {
|
||||||
|
"kindling": ("wood-pile", "#c9a06a"), "smock": ("shirt", "#d8d0b8"), "cloak": ("cloak", "#8a6fd0"),
|
||||||
"stone": ("stone-block", "#a8a29e"), "wheat": ("wheat", "#e3c060"), "vegetables": ("carrot", "#f08a3c"),
|
"stone": ("stone-block", "#a8a29e"), "wheat": ("wheat", "#e3c060"), "vegetables": ("carrot", "#f08a3c"),
|
||||||
"feathers": ("feather", "#e8e8ee"), "raw_meat": ("meat", "#e06a6a"), "arrows": ("arrow-cluster", "#c9b28a"),
|
"feathers": ("feather", "#e8e8ee"), "raw_meat": ("meat", "#e06a6a"), "arrows": ("arrow-cluster", "#c9b28a"),
|
||||||
"bricks": ("brick-pile", "#c66a45"), "cut_stone": ("stone-stack", "#b8b2aa"), "beam": ("wood-beam", "#9a6a3a"),
|
"bricks": ("brick-pile", "#c66a45"), "cut_stone": ("stone-stack", "#b8b2aa"), "beam": ("wood-beam", "#9a6a3a"),
|
||||||
|
|
@ -6390,6 +6577,7 @@ PATCH_NOTES = [
|
||||||
{"date": "2026-10-28", "title": "Queue", "notes": [
|
{"date": "2026-10-28", "title": "Queue", "notes": [
|
||||||
("new", "Queue: ▲ on #1 swaps it with the current make."),
|
("new", "Queue: ▲ on #1 swaps it with the current make."),
|
||||||
("new", "Updated Guard kit, added recipes for crafting professions to contribute to defence of town without needing combat-related loot."),
|
("new", "Updated Guard kit, added recipes for crafting professions to contribute to defence of town without needing combat-related loot."),
|
||||||
|
("new", "Townsfolk mood: food, firewood and seasonal clothes. Kindling (Firekeeping), smocks and cloaks (Tailoring)."),
|
||||||
]},
|
]},
|
||||||
{"date": "2026-10-27", "title": "Prayer", "notes": [
|
{"date": "2026-10-27", "title": "Prayer", "notes": [
|
||||||
("new", "Gathering can be queued."),
|
("new", "Gathering can be queued."),
|
||||||
|
|
|
||||||
|
|
@ -934,12 +934,16 @@ function cityNeed(id) {
|
||||||
const it = item(id), fund = fundNeeds()[id];
|
const it = item(id), fund = fundNeeds()[id];
|
||||||
let qty = fund?.qty || 0;
|
let qty = fund?.qty || 0;
|
||||||
const why = [...(fund?.why || [])];
|
const why = [...(fund?.why || [])];
|
||||||
for (const r of S.upcoming?.requests || []) {
|
const townsfolk = Object.values(S.city.happiness.needs).flatMap((n) => n.rows).filter((r) => !r.kinds);
|
||||||
const short = r.want - r.have;
|
for (const [source, rows] of [["Raid readiness", S.upcoming?.requests || []], ["Townsfolk", townsfolk]]) {
|
||||||
const counts = r.any === "food" ? it.food : r.any ? (it.slot || it.kit) === r.any : id === r.item;
|
for (const r of rows) {
|
||||||
if (short <= 0 || !counts) continue;
|
const short = r.want - r.have;
|
||||||
qty += r.any === "food" ? Math.ceil(short / it.food) : short;
|
const counts = r.any === "food" ? it.food : r.any === "fuel" ? id === "kindling" || C.firewood.includes(id)
|
||||||
why.push(`Raid readiness: ${r.label} · ${fmt(r.have)} / ${fmt(r.want)}`);
|
: r.any ? (it.slot || it.kit) === r.any : id === r.item;
|
||||||
|
if (short <= 0 || !counts) continue;
|
||||||
|
qty += r.any === "food" ? Math.ceil(short / it.food) : short;
|
||||||
|
why.push(`${source}: ${r.label} · ${fmt(r.have)} / ${fmt(r.want)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return qty ? { qty, why } : null;
|
return qty ? { qty, why } : null;
|
||||||
}
|
}
|
||||||
|
|
@ -1140,7 +1144,9 @@ function firekeepingPage() {
|
||||||
<span class="tag" title="Most fuel you can bank in a fire">${glyph("campfire", "#98a1b4")} ${dur(fk.bank * 60 / speed)}</span>
|
<span class="tag" title="Most fuel you can bank in a fire">${glyph("campfire", "#98a1b4")} ${dur(fk.bank * 60 / speed)}</span>
|
||||||
${tendingAll ? `<button class="btn danger" data-stop>Stop</button>`
|
${tendingAll ? `<button class="btn danger" data-stop>Stop</button>`
|
||||||
: `<button class="btn primary" data-start="${all}" ${allWhy ? "disabled" : ""} title="${esc(allWhy || "Each log goes on the lowest fire with room in your pile")}">🔥 Tend all</button>`}</div>
|
: `<button class="btn primary" data-start="${all}" ${allWhy ? "disabled" : ""} title="${esc(allWhy || "Each log goes on the lowest fire with room in your pile")}">🔥 Tend all</button>`}</div>
|
||||||
<div class="fire-grid">${Object.keys(C.fires).map((id) => fireCard(id, pick, tendAllTarget())).join("")}</div>`;
|
<div class="fire-grid">${Object.keys(C.fires).map((id) => fireCard(id, pick, tendAllTarget())).join("")}</div>
|
||||||
|
<div class="section-title">Kindling</div>
|
||||||
|
<div class="grid">${C.actions.filter((a) => a.skill === "firekeeping" && a.group === "Kindling").map(actionCard).join("")}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fireCard(id, pick, target) {
|
function fireCard(id, pick, target) {
|
||||||
|
|
@ -1972,18 +1978,22 @@ function town() {
|
||||||
const city = S.city;
|
const city = S.city;
|
||||||
const growth = city.starving ? `<span class="bad">Hungry: ${city.growth} a day</span>`
|
const growth = city.starving ? `<span class="bad">Hungry: ${city.growth} a day</span>`
|
||||||
: city.growth > 0 ? `+${city.growth} a day`
|
: city.growth > 0 ? `+${city.growth} a day`
|
||||||
|
: city.growth < 0 && city.leaving ? `<span class="bad">Leaving: ${city.growth} a day</span>`
|
||||||
: city.growth < 0 ? `<span class="bad">Overcrowded: ${city.growth} a day</span>`
|
: city.growth < 0 ? `<span class="bad">Overcrowded: ${city.growth} a day</span>`
|
||||||
|
: city.halted ? `<span class="warn">Halted: mood</span>`
|
||||||
: `<span class="warn">Full: build houses</span>`;
|
: `<span class="warn">Full: build houses</span>`;
|
||||||
const next = city.next_tier ? `${city.next_tier.name} at ${city.next_tier.pop} people` : "Largest tier";
|
const next = city.next_tier ? `${city.next_tier.name} at ${city.next_tier.pop} people` : "Largest tier";
|
||||||
let html = `<div class="stats">
|
let html = `<div class="stats">
|
||||||
<div class="stat"><div class="k">Settlement</div><div class="v">${city.tier_name}</div><div class="note">${next}</div></div>
|
<div class="stat"><div class="k">Settlement</div><div class="v">${city.tier_name}</div><div class="note">${next}</div></div>
|
||||||
<div class="stat"><div class="k">Population</div><div class="v">${city.population} <small>/ ${city.housing} homes</small></div><div class="note">${growth}</div></div>
|
<div class="stat"><div class="k">Population</div><div class="v">${city.population} <small>/ ${city.housing} homes</small></div><div class="note">${growth}</div></div>
|
||||||
<div class="stat"><div class="k">Food</div><div class="v ${city.food_days < 1 ? "bad" : ""}">${city.food_days ?? 0} <small>days</small></div><div class="note">${fmt(city.food)} meals in the granary</div></div>
|
<div class="stat"><div class="k">Food</div><div class="v ${city.food_days < 1 ? "bad" : ""}">${city.food_days ?? 0} <small>days</small></div><div class="note">${fmt(city.food)} meals in the granary</div></div>
|
||||||
|
${moodStat()}
|
||||||
<div class="stat"><div class="k">Treasury</div><div class="v gold">${fmt(city.treasury)}</div><div class="note">Day ${city.day}</div></div>
|
<div class="stat"><div class="k">Treasury</div><div class="v gold">${fmt(city.treasury)}</div><div class="note">Day ${city.day}</div></div>
|
||||||
<div class="stat"><div class="k">Defence</div><div class="v">${fmt(city.defence.total)}</div><div class="note">${city.repelled} raid${city.repelled === 1 ? "" : "s"} repelled</div></div>
|
<div class="stat"><div class="k">Defence</div><div class="v">${fmt(city.defence.total)}</div><div class="note">${city.repelled} raid${city.repelled === 1 ? "" : "s"} repelled</div></div>
|
||||||
${weatherCard()}
|
${weatherCard()}
|
||||||
</div>`;
|
</div>`;
|
||||||
html += happeningsPanel();
|
html += happeningsPanel();
|
||||||
|
html += moodPanel();
|
||||||
html += caravanPanel();
|
html += caravanPanel();
|
||||||
if (city.starving) html += `<div class="banner bad">🍞<div class="grow"><b>Granary empty.</b> People are leaving, defence is down. Donate food.</div></div>`;
|
if (city.starving) html += `<div class="banner bad">🍞<div class="grow"><b>Granary empty.</b> People are leaving, defence is down. Donate food.</div></div>`;
|
||||||
if (city.merchant_until > serverNow()) html += `<div class="banner">🐪<div class="grow"><b>Merchant in town.</b> Sell prices +50% for ${countdown(city.merchant_until)}.</div></div>`;
|
if (city.merchant_until > serverNow()) html += `<div class="banner">🐪<div class="grow"><b>Merchant in town.</b> Sell prices +50% for ${countdown(city.merchant_until)}.</div></div>`;
|
||||||
|
|
@ -1999,6 +2009,35 @@ function town() {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- townsfolk ---------------------------------------------------------------
|
||||||
|
const NEED_NAMES = { food: ["🍲", "Food"], warmth: ["🔥", "Firewood"], clothes: ["👕", "Clothes"] };
|
||||||
|
const NEED_TIERS = { warmth: "Village", clothes: "Town" };
|
||||||
|
const pct = (x) => `${Math.round(x * 100)}%`;
|
||||||
|
|
||||||
|
function moodStat() {
|
||||||
|
const h = S.city.happiness;
|
||||||
|
const trend = Math.abs(h.target - h.score) < .005 ? "Steady" : `${h.target > h.score ? "↗" : "↘"} ${h.heading}`;
|
||||||
|
return `<div class="stat" title="${esc(`Mood ${pct(h.score)}, heading to ${pct(h.target)}`)}"><div class="k">Mood ${h.icon}</div>
|
||||||
|
<div class="v ${h.score < .4 ? "bad" : ""}">${h.name}</div><div class="note">${trend}</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moodPanel() {
|
||||||
|
const h = S.city.happiness, e = h.effects;
|
||||||
|
const x = (v) => `×${+v.toFixed(2)}`;
|
||||||
|
const needs = Object.entries(h.needs).map(([key, n]) => `<div class="need">
|
||||||
|
<div class="need-head"><b>${NEED_NAMES[key][0]} ${NEED_NAMES[key][1]}</b><span class="muted small">${pct(n.score)}</span></div>
|
||||||
|
${n.rows.map(reqRow).join("")}</div>`).join("");
|
||||||
|
const locked = h.locked.map((key) => `<span class="tag" title="Needed from ${NEED_TIERS[key]}">🔒 ${NEED_NAMES[key][1]} · ${NEED_TIERS[key]}</span>`).join("");
|
||||||
|
const mods = h.mods.map((m) => `<span class="tag ${m.value < 0 ? "bad" : "good"}">${esc(m.name)} ${m.value > 0 ? "+" : "−"}${pct(Math.abs(m.value))}</span>`).join("");
|
||||||
|
const effects = [`🌱 Growth ${x(e.growth)}`, `🛡️ Guards ${x(e.militia)}`, `🔨 Labour ${x(e.work)}`, `${COIN} Taxes ${x(e.work)}`,
|
||||||
|
...(e.leave ? [`<span class="bad">🚶 ${pct(e.leave)} leave a day</span>`] : [])];
|
||||||
|
return `<div class="panel mood"><h2>${h.icon} Townsfolk · ${h.name}</h2>
|
||||||
|
<div class="mood-bar" title="${esc(`Now ${pct(h.score)} · heading to ${pct(h.target)}`)}"><span style="width:${h.score * 100}%;--c:var(--${h.score < .4 ? "bad" : h.score < .6 ? "warn" : "good"})"></span><i style="left:${h.target * 100}%"></i></div>
|
||||||
|
<div class="mood-effects small">${effects.join(" · ")}</div>
|
||||||
|
<div class="needs">${needs}</div>
|
||||||
|
${mods || locked ? `<div class="mood-tags">${mods}${locked}</div>` : ""}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function lookChip(id) {
|
function lookChip(id) {
|
||||||
const c = C.cosmetics[id];
|
const c = C.cosmetics[id];
|
||||||
if (c.kind === "colour") return `<span class="swatch" style="background:${c.value}"></span>`;
|
if (c.kind === "colour") return `<span class="swatch" style="background:${c.value}"></span>`;
|
||||||
|
|
@ -2285,16 +2324,19 @@ function favourSpends() {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A have / need row with its bar, and a Give button when you hold the item.
|
||||||
|
function reqRow(r) {
|
||||||
|
const mine = !r.kinds && S.me.bank[r.item] || 0;
|
||||||
|
return `<div class="req ${r.next ? "dim" : ""}"><span>${item(r.item).icon}</span><span>${esc(r.label)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
|
||||||
|
${mine ? `<button class="btn sm gold" data-donate="${r.item}" data-qty="all">Give ${fmt(mine)}</button>` : "<span></span>"}
|
||||||
|
<div class="bar"><span style="width:${r.want ? Math.min(100, r.have / r.want * 100) : 100}%"></span></div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function raidPrep(ev) {
|
function raidPrep(ev) {
|
||||||
const city = S.city, me = S.me, d = city.defence;
|
const city = S.city, me = S.me, d = city.defence;
|
||||||
const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", drilled: "Drilled militia", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Paid guards (barracks)", weapons: "Weapons the guards hold", shields: "Shields the guards hold", jerkins: "Body armour the guards wear", salves: "Salves", arrows: "Arrows", garrison: "Garrison" };
|
const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", drilled: "Drilled militia", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Paid guards (barracks)", weapons: "Weapons the guards hold", shields: "Shields the guards hold", jerkins: "Body armour the guards wear", salves: "Salves", arrows: "Arrows", garrison: "Garrison" };
|
||||||
const full = d.posts * d.wage, owed = Math.max(0, full - d.wages);
|
const full = d.posts * d.wage, owed = Math.max(0, full - d.wages);
|
||||||
const reqs = (ev.requests || []).map((r) => {
|
const reqs = (ev.requests || []).map(reqRow).join("");
|
||||||
const mine = me.bank[r.item] || 0;
|
|
||||||
return `<div class="req"><span>${item(r.item).icon}</span><span>${esc(r.label)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
|
|
||||||
${mine ? `<button class="btn sm gold" data-donate="${r.item}" data-qty="all">Give ${fmt(mine)}</button>` : "<span></span>"}
|
|
||||||
<div class="bar"><span style="width:${Math.min(100, r.have / r.want * 100)}%"></span></div></div>`;
|
|
||||||
}).join("");
|
|
||||||
return `<div class="cols">
|
return `<div class="cols">
|
||||||
<div class="panel"><h2>Raid readiness</h2>${reqs}
|
<div class="panel"><h2>Raid readiness</h2>${reqs}
|
||||||
<p class="small muted">Stockpile gear arms the defenders, best first. Fights use up arrows and break some gear.</p></div>
|
<p class="small muted">Stockpile gear arms the defenders, best first. Fights use up arrows and break some gear.</p></div>
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
<symbol id="checked-shield" viewBox="0 0 512 512"><path fill="currentColor" d="M48.906 19.656v10.782c0 103.173 10.53 206.07 41.313 289.53c30.78 83.463 82.763 148.094 164.53 170.563l2.188.626l2.25-.5c89.686-19.12 142.322-84.028 171.187-168.344S465.78 133.657 465.78 30.438v-10.78l-10.655 1.53C323.26 39.954 191.452 40 59.595 21.188l-10.69-1.53zM67.75 41.03c63.242 8.536 126.495 12.792 189.75 12.782v184.532h174.78c-4.905 27.572-11.31 53.747-19.592 77.937c-27.348 79.884-73.757 137.33-155.157 155.564c-.008-.003-.02.003-.03 0v-233.5H86.53c-12.87-60.99-18.277-128.81-18.78-197.313z"/></symbol>
|
<symbol id="checked-shield" viewBox="0 0 512 512"><path fill="currentColor" d="M48.906 19.656v10.782c0 103.173 10.53 206.07 41.313 289.53c30.78 83.463 82.763 148.094 164.53 170.563l2.188.626l2.25-.5c89.686-19.12 142.322-84.028 171.187-168.344S465.78 133.657 465.78 30.438v-10.78l-10.655 1.53C323.26 39.954 191.452 40 59.595 21.188l-10.69-1.53zM67.75 41.03c63.242 8.536 126.495 12.792 189.75 12.782v184.532h174.78c-4.905 27.572-11.31 53.747-19.592 77.937c-27.348 79.884-73.757 137.33-155.157 155.564c-.008-.003-.02.003-.03 0v-233.5H86.53c-12.87-60.99-18.277-128.81-18.78-197.313z"/></symbol>
|
||||||
<symbol id="chisel" viewBox="0 0 512 512"><path fill="currentColor" d="M426.2 30.65L405 51.82l55.2 55.08l21.2-21.1zM173.6 55.12c-13.3 19.1-31.3 28.5-50 31.6c-12.3 2-24.91 1.5-37.41.4c10.5 10.6 19.91 22.18 24.21 36.18c36.7.2 69.2-9.9 98.6-36.28c-8.9-13.4-21.3-23.1-35.4-31.9m218.7 9.47L231.6 225.3c0 10.1-.1 20.4-1.5 30c-1.1 7.8-3.1 15.4-7 21.9l11.7 11.7c6.5-3.9 14.1-5.9 21.9-7c9.6-1.4 19.9-1.5 30-1.5l160.7-160.7zM188.6 268.2l-21.2 21.2l55.2 55.2l21.2-21.2zm-24.8 43c-28.8 11.1-48.7 30.5-59.4 41.2l-79.14 79.2c-.71.7-1.72 2.6-1.98 6.2c-.26 3.7.28 8.5 1.52 13.5c2.48 9.9 8.22 20.5 11.79 24.1c3.55 3.6 14.15 9.3 24.08 11.8c4.97 1.2 9.83 1.8 13.45 1.5c3.61-.2 5.59-1.3 6.3-2l79.18-79.2c10.7-10.6 30.1-30.5 41.2-59.3zM314 375.6c-17.2 13-25.1 26.3-27 37.2c-2.1 11.1 1.5 19.7 8.2 25.5l40.2 13.7c-3.9-9.7-6.8-19.9-5.8-30.8c.8-9.2 4.8-18.5 12.6-26.2c3.2-3.2 7-6 11.3-8.7zm85.4 40.5c-17.7 1.8-36.1 5-52 9.7c.2 7.8 3.6 17.2 7.9 27.1c22.7-9.5 54.8-17.6 87.9-19.6c-15.9-8.6-28.4-13.7-43.8-17.2"/></symbol>
|
<symbol id="chisel" viewBox="0 0 512 512"><path fill="currentColor" d="M426.2 30.65L405 51.82l55.2 55.08l21.2-21.1zM173.6 55.12c-13.3 19.1-31.3 28.5-50 31.6c-12.3 2-24.91 1.5-37.41.4c10.5 10.6 19.91 22.18 24.21 36.18c36.7.2 69.2-9.9 98.6-36.28c-8.9-13.4-21.3-23.1-35.4-31.9m218.7 9.47L231.6 225.3c0 10.1-.1 20.4-1.5 30c-1.1 7.8-3.1 15.4-7 21.9l11.7 11.7c6.5-3.9 14.1-5.9 21.9-7c9.6-1.4 19.9-1.5 30-1.5l160.7-160.7zM188.6 268.2l-21.2 21.2l55.2 55.2l21.2-21.2zm-24.8 43c-28.8 11.1-48.7 30.5-59.4 41.2l-79.14 79.2c-.71.7-1.72 2.6-1.98 6.2c-.26 3.7.28 8.5 1.52 13.5c2.48 9.9 8.22 20.5 11.79 24.1c3.55 3.6 14.15 9.3 24.08 11.8c4.97 1.2 9.83 1.8 13.45 1.5c3.61-.2 5.59-1.3 6.3-2l79.18-79.2c10.7-10.6 30.1-30.5 41.2-59.3zM314 375.6c-17.2 13-25.1 26.3-27 37.2c-2.1 11.1 1.5 19.7 8.2 25.5l40.2 13.7c-3.9-9.7-6.8-19.9-5.8-30.8c.8-9.2 4.8-18.5 12.6-26.2c3.2-3.2 7-6 11.3-8.7zm85.4 40.5c-17.7 1.8-36.1 5-52 9.7c.2 7.8 3.6 17.2 7.9 27.1c22.7-9.5 54.8-17.6 87.9-19.6c-15.9-8.6-28.4-13.7-43.8-17.2"/></symbol>
|
||||||
<symbol id="claw-hammer" viewBox="0 0 512 512"><path fill="currentColor" d="M215.97 27.813c-2.218.04-4.35.177-6.407.375l-.532.062c-2.774.266-5.796 1.01-9.124 1.844c-4.418 1.11-8.096 2.607-11.125 4.406l77.782 59.313l7.375 5.656l-5.593 7.405l-13.72 18.156l67.595 51.626l14.217-18.812l9.47-12.53l6.5 14.342l19.656 43.47l60.593-80.095l-47.187-7.124l-15.533-2.312l9.47-12.563l14.28-18.843l-67.593-51.624L310.78 50.78l-5.03 6.657l-7.188-4.25c-28.635-16.917-58.02-25.157-80.343-25.374a77 77 0 0 0-2.25 0zM453 127.124l-71.47 94.438l34.564 26.187c44.24-20.22 64.01-52.594 71.312-94.625zm-193.688 25.03c-27.17 33.09-55.308 64.706-82.812 96.22l47.406 39.875c23.11-35.343 46.336-71.61 71.938-108.22l-36.53-27.874zM164.25 262.47c-11.355 13.115-22.564 26.254-33.5 39.5l48.375 52.717c11.707-16.6 23.107-33.515 34.438-50.687zm-45.438 54.124a1112 1112 0 0 0-23.093 29.72l33.468 73.842c13.586-16.22 26.434-32.893 38.843-49.937zm-35.75 47.03c-12.108 16.978-23.57 34.335-34.124 52.282L98.53 454.47a688 688 0 0 0 17.22-18.72zm-43.437 68.688a546 546 0 0 0-14.75 29l37.375 28.313a611 611 0 0 0 23.125-21.72z"/></symbol>
|
<symbol id="claw-hammer" viewBox="0 0 512 512"><path fill="currentColor" d="M215.97 27.813c-2.218.04-4.35.177-6.407.375l-.532.062c-2.774.266-5.796 1.01-9.124 1.844c-4.418 1.11-8.096 2.607-11.125 4.406l77.782 59.313l7.375 5.656l-5.593 7.405l-13.72 18.156l67.595 51.626l14.217-18.812l9.47-12.53l6.5 14.342l19.656 43.47l60.593-80.095l-47.187-7.124l-15.533-2.312l9.47-12.563l14.28-18.843l-67.593-51.624L310.78 50.78l-5.03 6.657l-7.188-4.25c-28.635-16.917-58.02-25.157-80.343-25.374a77 77 0 0 0-2.25 0zM453 127.124l-71.47 94.438l34.564 26.187c44.24-20.22 64.01-52.594 71.312-94.625zm-193.688 25.03c-27.17 33.09-55.308 64.706-82.812 96.22l47.406 39.875c23.11-35.343 46.336-71.61 71.938-108.22l-36.53-27.874zM164.25 262.47c-11.355 13.115-22.564 26.254-33.5 39.5l48.375 52.717c11.707-16.6 23.107-33.515 34.438-50.687zm-45.438 54.124a1112 1112 0 0 0-23.093 29.72l33.468 73.842c13.586-16.22 26.434-32.893 38.843-49.937zm-35.75 47.03c-12.108 16.978-23.57 34.335-34.124 52.282L98.53 454.47a688 688 0 0 0 17.22-18.72zm-43.437 68.688a546 546 0 0 0-14.75 29l37.375 28.313a611 611 0 0 0 23.125-21.72z"/></symbol>
|
||||||
|
<symbol id="cloak" viewBox="0 0 512 512"><path fill="currentColor" d="M256 23.316c-27.177 0-58.578 5.75-79.525 17.967c-20.688 13.774-43.22 60.756-43.22 60.756a333 333 0 0 0-9.16 16.376c-36.372 69.627-61.496 175.763-62.4 317.686c46.593 26.853 97.436 44.53 142.05 52.582c-21.736-14.917-40.667-38.325-55.18-67.618c36.913-4.56 78.545-9.817 107.314-9.818c29.802 0 73.456 5.63 111.32 10.29c-14.484 29.072-33.326 52.308-54.946 67.144c44.615-8.052 91.458-25.727 138.05-52.58c-.903-141.923-26.027-248.06-62.4-317.686a331 331 0 0 0-9.16-16.375v-.003s-22.53-46.98-43.22-60.754c-23.52-11.95-52.347-17.967-79.524-17.967zm-.045 16.22c30.187 3.09 57.525 4.198 76.307 24.67c-18.46 21.81-45.975 49.5-76.29 49.503c-30.302.002-66.432-30.935-76.27-49.468c17.72-19.566 53.18-24.537 76.253-24.707zM170.57 80c5.43 12.588 52.522 49.167 85.375 49.25C289.09 129.333 336 91.16 342.32 80c0 0 12.28 18.68 18.594 31.057l4.55 10.906c20.518 45.148 32.614 92.753 32.614 158.1c0 45.65-8.408 88.144-22.873 123.724c-32.45-4.387-73.958-7.39-119.205-7.39c-43.513 0-83.54 2.78-115.408 6.896c-14.34-35.48-22.67-77.792-22.67-123.23c0-65.347 8.096-112.952 28.613-158.1l4.55-10.906C157.378 98.725 170.57 80 170.57 80"/></symbol>
|
||||||
<symbol id="clover" viewBox="0 0 512 512"><path fill="currentColor" d="M256 16c-24 0-72 24-72 48s48 24 72 96c24-72 72-72 72-96s-48-48-72-48M64 184c-24 0-48 48-48 72s24 72 48 72s24-48 96-72c-72-24-72-72-96-72m384 0c-24 0-24 48-96 72c72 24 72 72 96 72s48-48 48-72s-24-72-48-72m-189.906.03A72 72 0 0 0 184 256a72 72 0 0 0 144 0a72 72 0 0 0-69.906-71.97M256 352c-24 72-72 72-72 96s48 48 72 48s72-24 72-48s-48-24-72-96"/></symbol>
|
<symbol id="clover" viewBox="0 0 512 512"><path fill="currentColor" d="M256 16c-24 0-72 24-72 48s48 24 72 96c24-72 72-72 72-96s-48-48-72-48M64 184c-24 0-48 48-48 72s24 72 48 72s24-48 96-72c-72-24-72-72-96-72m384 0c-24 0-24 48-96 72c72 24 72 72 96 72s48-48 48-72s-24-72-48-72m-189.906.03A72 72 0 0 0 184 256a72 72 0 0 0 144 0a72 72 0 0 0-69.906-71.97M256 352c-24 72-72 72-72 96s48 48 72 48s72-24 72-48s-48-24-72-96"/></symbol>
|
||||||
<symbol id="cooking-pot" viewBox="0 0 512 512"><path fill="currentColor" d="M256 100c-8 0-16 4-32 12l10.848 32.543C179.665 147.226 112.76 160.04 98.68 183h314.625c-7.012-11.422-27.093-20.334-52.305-26.738V135h-18v17.266c-21.203-4.107-44.4-6.68-65.848-7.723L288 112c-16-8-24-12-32-12M96 201v14H55v18h41v151c0 16 16 32 32 32h256c16 0 32-16 32-32V233h41v-18h-41v-14z"/></symbol>
|
<symbol id="cooking-pot" viewBox="0 0 512 512"><path fill="currentColor" d="M256 100c-8 0-16 4-32 12l10.848 32.543C179.665 147.226 112.76 160.04 98.68 183h314.625c-7.012-11.422-27.093-20.334-52.305-26.738V135h-18v17.266c-21.203-4.107-44.4-6.68-65.848-7.723L288 112c-16-8-24-12-32-12M96 201v14H55v18h41v151c0 16 16 32 32 32h256c16 0 32-16 32-32V233h41v-18h-41v-14z"/></symbol>
|
||||||
<symbol id="cotton-flower" viewBox="0 0 512 512"><path fill="currentColor" d="M146.507 115.11c9.57 5.297 18.566 12.903 26.037 21.379c10.833 12.29 28.59 45.178 16.488 56.211c-4.728-13.9-21.103-34.226-29.992-44.311s-21.578-18.9-52.738-28.575c-31.16-9.674-69.148 19.903-76.52 51.505c-2.914 12.495.51 28.852 8.485 46.326s20.208 35.816 32.984 52.318a478 478 0 0 0 9.41 11.756c18.983 4.276 37.443 15.428 55.809 26.63c20.962 12.787 41.92 25.934 61.137 31.946c5.485-36.352 27.15-69.3 61.484-97.972l11.166-9.325l3.36 14.155c7.24 30.507 11.35 65.134-1.391 99.884c40.212-3.17 90.187-3.23 131.701 10.72c15.05-13.313 25.29-25.987 17.26-43.76c-51.41 12.81-96.615-21.713-110.274-43.09c35.856 22.168 58.87 35.061 96.428 27.488c29.382-7.439 56.674-9.419 65.94-39.385c16.521-53.932 14.432-125.682-21.018-166.254c-17.638-20.186-39.924-35.822-62.8-41.017c-22.878-5.196-46.394-.887-66.583 16.095c-20.188 16.982-29.956 33.382-33.648 48.041c-3.693 14.66 8.225 45.176 10.042 52.874c-19.041-15.644-32.472-37.512-27.495-57.27c4.468-17.742 15.862-34.596 30.91-47.748c-6.982-14.43-19.913-23.282-34.713-28.453c-61.606-16.768-134.47 5.879-121.469 79.832m112.725 151.232c-27.875 26.527-43.112 55.04-44.983 85.74l-.619 10.162l-10.01-1.86c-27.054-5.03-52.438-21.976-76.523-36.667c-16.28-9.93-31.842-18.507-45.717-22.818c10.695 15.797 22.65 33.306 38.1 49.05c21.19 21.595 46.663 37.596 79.838 35.805l14.314-.773c-13.651 31.451-22.935 66.172-22.822 94.023l15.404 2.96c6.81-36.11 13.404-62.107 29.902-86.911c52.267 7.413 107.26-4.614 147.467-24.219c-39.897-9.577-88.254-8.25-124.73-4.54l-16.412 1.667l7.48-14.703c14.8-29.092 14.524-58.461 9.31-86.916z"/></symbol>
|
<symbol id="cotton-flower" viewBox="0 0 512 512"><path fill="currentColor" d="M146.507 115.11c9.57 5.297 18.566 12.903 26.037 21.379c10.833 12.29 28.59 45.178 16.488 56.211c-4.728-13.9-21.103-34.226-29.992-44.311s-21.578-18.9-52.738-28.575c-31.16-9.674-69.148 19.903-76.52 51.505c-2.914 12.495.51 28.852 8.485 46.326s20.208 35.816 32.984 52.318a478 478 0 0 0 9.41 11.756c18.983 4.276 37.443 15.428 55.809 26.63c20.962 12.787 41.92 25.934 61.137 31.946c5.485-36.352 27.15-69.3 61.484-97.972l11.166-9.325l3.36 14.155c7.24 30.507 11.35 65.134-1.391 99.884c40.212-3.17 90.187-3.23 131.701 10.72c15.05-13.313 25.29-25.987 17.26-43.76c-51.41 12.81-96.615-21.713-110.274-43.09c35.856 22.168 58.87 35.061 96.428 27.488c29.382-7.439 56.674-9.419 65.94-39.385c16.521-53.932 14.432-125.682-21.018-166.254c-17.638-20.186-39.924-35.822-62.8-41.017c-22.878-5.196-46.394-.887-66.583 16.095c-20.188 16.982-29.956 33.382-33.648 48.041c-3.693 14.66 8.225 45.176 10.042 52.874c-19.041-15.644-32.472-37.512-27.495-57.27c4.468-17.742 15.862-34.596 30.91-47.748c-6.982-14.43-19.913-23.282-34.713-28.453c-61.606-16.768-134.47 5.879-121.469 79.832m112.725 151.232c-27.875 26.527-43.112 55.04-44.983 85.74l-.619 10.162l-10.01-1.86c-27.054-5.03-52.438-21.976-76.523-36.667c-16.28-9.93-31.842-18.507-45.717-22.818c10.695 15.797 22.65 33.306 38.1 49.05c21.19 21.595 46.663 37.596 79.838 35.805l14.314-.773c-13.651 31.451-22.935 66.172-22.822 94.023l15.404 2.96c6.81-36.11 13.404-62.107 29.902-86.911c52.267 7.413 107.26-4.614 147.467-24.219c-39.897-9.577-88.254-8.25-124.73-4.54l-16.412 1.667l7.48-14.703c14.8-29.092 14.524-58.461 9.31-86.916z"/></symbol>
|
||||||
|
|
@ -216,6 +217,7 @@
|
||||||
<symbol id="shield" viewBox="0 0 512 512"><path fill="currentColor" d="M256 16c25 24 100 72 150 72v96c0 96-75 240-150 312c-75-72-150-216-150-312V88c50 0 125-48 150-72"/></symbol>
|
<symbol id="shield" viewBox="0 0 512 512"><path fill="currentColor" d="M256 16c25 24 100 72 150 72v96c0 96-75 240-150 312c-75-72-150-216-150-312V88c50 0 125-48 150-72"/></symbol>
|
||||||
<symbol id="shield-bash" viewBox="0 0 512 512"><path fill="currentColor" d="M274.663 63.02L90.792 80.26l154.193 19.273c5.063-13.339 12.952-24.341 22.541-31.828a52 52 0 0 1 7.137-4.683zm19.832 12.803c-5.092.166-10.492 2.296-15.879 6.502c-7.835 6.118-15.009 16.575-18.83 29.688s-3.477 26.099-.289 35.927c3.188 9.829 8.73 16.071 15.633 18.395s14.766.596 22.601-5.522c7.835-6.117 15.01-16.574 18.83-29.687c3.822-13.113 3.48-26.1.292-35.928s-8.73-16.07-15.633-18.394a19 19 0 0 0-6.725-.98zm166.85 9.485c-24.113 13.949-46.193 20.298-87.233 17.252L340.48 228.452c-.675 2.682-.318 6 1.922 10.87c2.243 4.876 6.355 10.89 11.836 17.607c9.99 12.242 24.527 27.16 39.573 44.238c14.56-5.5 28.23-12.828 38.972-20.19c11.841-8.113 20.234-16.95 21.965-19.939l42.027-118.22c-16.748-14.613-29.471-33.974-35.43-57.51m-288.07 51.261L23.652 158.331l89.309 12.988l2.158-5.052zm286.265 2.325l16.941 6.078l-39.123 109.037l-37.212 19.181l-8.247-15.998l30.913-15.933zm-259.842 4.394l-70.586 36.043l-29.222 68.422l19.218 8.809l24.905-57.764l59.299-22.973l-14.702 75.955l-.963 1.477c-32.725 50.18-71.654 93.41-118.464 134.28L42.722 432.98l17.021 7.245l31.875-43.989l1.38-.906c45.476-29.872 75.93-62.333 112.255-94.492l4.533-4.012l5.426 2.686c23.365 11.571 42.934 24.117 62.107 37.705l8.924 6.324l-69.006 65.643l24.649 39.794l17.67-10.308l-20.078-28.477l8.224-5.004c29.884-18.186 49.986-39.43 71.938-66.039c-23.653-35.6-42.006-49.433-71.592-71.267l9.908-7.227c34.703-25.312 38.132-54.476 41.61-79.449c-9.203 4.441-19.498 5.772-29.473 2.414c-13.488-4.54-22.924-16.472-27.465-30.473c-.17-.522-.321-1.054-.479-1.584zm116.62 45.04c-1.355 7.027-3.324 14.17-6.092 21.349l14.056 9.666l5.938-22.223zm-174.243 97.476l-126.85 17.953l99.67 14.105a599 599 0 0 0 27.18-32.058m91.781 82.73l-95.892 21.432l59.406 13.277z"/></symbol>
|
<symbol id="shield-bash" viewBox="0 0 512 512"><path fill="currentColor" d="M274.663 63.02L90.792 80.26l154.193 19.273c5.063-13.339 12.952-24.341 22.541-31.828a52 52 0 0 1 7.137-4.683zm19.832 12.803c-5.092.166-10.492 2.296-15.879 6.502c-7.835 6.118-15.009 16.575-18.83 29.688s-3.477 26.099-.289 35.927c3.188 9.829 8.73 16.071 15.633 18.395s14.766.596 22.601-5.522c7.835-6.117 15.01-16.574 18.83-29.687c3.822-13.113 3.48-26.1.292-35.928s-8.73-16.07-15.633-18.394a19 19 0 0 0-6.725-.98zm166.85 9.485c-24.113 13.949-46.193 20.298-87.233 17.252L340.48 228.452c-.675 2.682-.318 6 1.922 10.87c2.243 4.876 6.355 10.89 11.836 17.607c9.99 12.242 24.527 27.16 39.573 44.238c14.56-5.5 28.23-12.828 38.972-20.19c11.841-8.113 20.234-16.95 21.965-19.939l42.027-118.22c-16.748-14.613-29.471-33.974-35.43-57.51m-288.07 51.261L23.652 158.331l89.309 12.988l2.158-5.052zm286.265 2.325l16.941 6.078l-39.123 109.037l-37.212 19.181l-8.247-15.998l30.913-15.933zm-259.842 4.394l-70.586 36.043l-29.222 68.422l19.218 8.809l24.905-57.764l59.299-22.973l-14.702 75.955l-.963 1.477c-32.725 50.18-71.654 93.41-118.464 134.28L42.722 432.98l17.021 7.245l31.875-43.989l1.38-.906c45.476-29.872 75.93-62.333 112.255-94.492l4.533-4.012l5.426 2.686c23.365 11.571 42.934 24.117 62.107 37.705l8.924 6.324l-69.006 65.643l24.649 39.794l17.67-10.308l-20.078-28.477l8.224-5.004c29.884-18.186 49.986-39.43 71.938-66.039c-23.653-35.6-42.006-49.433-71.592-71.267l9.908-7.227c34.703-25.312 38.132-54.476 41.61-79.449c-9.203 4.441-19.498 5.772-29.473 2.414c-13.488-4.54-22.924-16.472-27.465-30.473c-.17-.522-.321-1.054-.479-1.584zm116.62 45.04c-1.355 7.027-3.324 14.17-6.092 21.349l14.056 9.666l5.938-22.223zm-174.243 97.476l-126.85 17.953l99.67 14.105a599 599 0 0 0 27.18-32.058m91.781 82.73l-95.892 21.432l59.406 13.277z"/></symbol>
|
||||||
<symbol id="shield-reflect" viewBox="0 0 512 512"><path fill="currentColor" d="m19.36 19.457l-.38 27.84l267.127 154.75l-58.703 19.916l57.602 19.36L142.12 383.546l-19.214-19.213l-51.47 114.324l123.125-43.482l-17.492-18.95l151.868-151.87l12.433 43.078l22.976-68.5l56.675-16.565l-57.475-15.492l-22.934-79.828l-14.145 48.266l-268.65-155.41l-38.454-.446zM370.614 81.94q-6.796.03-13.51.302c-3.275.134-6.528.32-9.76.547l31.25 108.786l104.05 28.045c6.204-39.085 8.902-80.275 9.18-120.075C456.37 88.82 415.098 82.257 375.15 81.95a404 404 0 0 0-4.537-.01zm-36.87 2.1c-31.99 3.653-61.013 12.153-83.014 25.87l64.493 37.31l18.52-63.18zm148.18 139.997L378.958 254.13l-38.93 116.054l-20.268-70.225l-39.07 39.067c18.814 35.364 46.1 62.287 86.175 77.604c47.473-17.834 78.566-62.795 98.014-121.6c7.323-22.14 12.905-46.11 17.042-70.993z"/></symbol>
|
<symbol id="shield-reflect" viewBox="0 0 512 512"><path fill="currentColor" d="m19.36 19.457l-.38 27.84l267.127 154.75l-58.703 19.916l57.602 19.36L142.12 383.546l-19.214-19.213l-51.47 114.324l123.125-43.482l-17.492-18.95l151.868-151.87l12.433 43.078l22.976-68.5l56.675-16.565l-57.475-15.492l-22.934-79.828l-14.145 48.266l-268.65-155.41l-38.454-.446zM370.614 81.94q-6.796.03-13.51.302c-3.275.134-6.528.32-9.76.547l31.25 108.786l104.05 28.045c6.204-39.085 8.902-80.275 9.18-120.075C456.37 88.82 415.098 82.257 375.15 81.95a404 404 0 0 0-4.537-.01zm-36.87 2.1c-31.99 3.653-61.013 12.153-83.014 25.87l64.493 37.31l18.52-63.18zm148.18 139.997L378.958 254.13l-38.93 116.054l-20.268-70.225l-39.07 39.067c18.814 35.364 46.1 62.287 86.175 77.604c47.473-17.834 78.566-62.795 98.014-121.6c7.323-22.14 12.905-46.11 17.042-70.993z"/></symbol>
|
||||||
|
<symbol id="shirt" viewBox="0 0 512 512"><path fill="currentColor" d="M255.973 22.965c-43.313 0-95.117 14.71-126.66 30.12c-33.538 13.63-60.84 30.408-80.33 60.542c-26.22 40.536-38.948 103.153-30.278 202.797c24.886 8.635 47.288 6.054 70.272-1.73c-7.203-32.898-6.04-65.776 2.072-92.645c8.872-29.395 26.72-53.085 52.886-57.17l11.56-1.804l.305 11.697c.226 8.523.38 17.397.485 26.434l.113 26.338c-.155 36.707-1.593 76.298-5.328 116.182c23.522 7.64 61.78 12.326 99.99 12.597l19.18-.244c35.59-1.145 69.858-6.043 90.4-12.78c-4.562-61.128-4.866-122.812-6.34-172.464l-.407-13.7l13.267 3.44c26.03 6.746 44.022 30.776 53.246 60.528c8.425 27.166 9.814 59.957 2.668 92.833c17.938 5.68 35.678 7.15 53.602 4.926l18.355-2.453c4.31-78.49-5.98-156.066-26.157-193.04L450.48 96.767c-19.39-22.354-44.318-35.044-73.71-46.207c-28.146-14.427-76.6-27.595-120.797-27.595m0 12.724c27.095 0 55.783 8.103 80.42 18.845c-13.03 11.505-29.087 19.462-46.176 24.008l-32.764 72.228l-28.53-70.442c-19.852-3.88-38.642-12.34-53.34-25.76c25.763-11.032 54.585-18.88 80.39-18.88zm2.416 119.08l14.655 22.226l-14.59 21.084l-14.725-21.084zm0 49.603l14.655 22.23l-14.59 21.083l-14.725-21.084l14.66-22.23zm0 48.957l14.655 22.227l-14.59 21.086l-14.725-21.086zm0 49.553l14.655 22.228l-14.59 21.085l-14.725-21.084l14.66-22.227zm104.645 57.226c-27.583 8.572-66.045 12.127-105.467 12.314c-39.638.188-79.132-3.107-108.078-12.07a919 919 0 0 1-2.828 22.312c24.902 8.897 68.203 14.22 110.56 14.016c42.322-.205 84.93-6.02 108.116-14.32a1007 1007 0 0 1-2.303-22.253zm5.71 37.913c-28.3 9.63-68.984 13.61-110.698 13.81c-12.246.06-24.475-.22-36.402-.89l-17.637-1.314c-22.358-2.09-43.007-5.768-59.787-11.507c-3.742 22.658-8.426 44.944-14.243 66.354c8.833 4.422 18.403 8.212 28.52 11.404l16.564 4.575c66.132 15.875 149.713 8.995 206.963-14.658c-5.656-21.24-9.96-44.108-13.28-67.774"/></symbol>
|
||||||
<symbol id="shoulder-bag" viewBox="0 0 512 512"><path fill="currentColor" d="M256 18.75c14.4 0 27.4 5.89 38.2 15.27s19.7 22.17 27.5 37.08C337.2 101 348 139.6 355.8 178c11.6 56.7 16.5 113 18.3 138.8H356c-1.8-25.8-6.7-80.5-17.8-135.2c-7.7-37.6-18.4-75.1-32.5-102.2c-7.1-13.59-15-24.55-23.3-31.79c-8.4-7.25-16.8-10.86-26.4-10.86s-18 3.61-26.4 10.86c-8.3 7.24-16.2 18.2-23.3 31.79c-14.1 27.1-24.8 64.6-32.5 102.2c-11.1 54.7-16 109.4-17.8 135.2h-18.1c1.8-25.8 6.7-82.1 18.3-138.8c7.8-38.4 18.6-77 34.1-106.9c7.8-14.91 16.7-27.7 27.5-37.08s23.8-15.27 38.2-15.27M374.9 334.8q0 16.95-.3 32.1c-80.2 18.9-159.1 18.1-237.2-.1c-.2-10.2-.3-20.7-.3-32zm-1 50.6c-2.1 38.4-8.7 68.1-29.1 94.9v.1l-.1.1c.1-.1-2.7 2.1-7.8 4c-5.2 1.8-12.4 3.5-20.8 4.8c-16.8 2.6-38.5 4-60.1 4s-43.3-1.4-60.1-4c-8.4-1.3-15.6-3-20.8-4.8c-5.1-1.9-7.9-4.1-7.8-4l-.1-.1v-.1c-20.4-26.8-27-56.7-29.1-94.9c77.6 17.4 156.3 18.2 235.8 0"/></symbol>
|
<symbol id="shoulder-bag" viewBox="0 0 512 512"><path fill="currentColor" d="M256 18.75c14.4 0 27.4 5.89 38.2 15.27s19.7 22.17 27.5 37.08C337.2 101 348 139.6 355.8 178c11.6 56.7 16.5 113 18.3 138.8H356c-1.8-25.8-6.7-80.5-17.8-135.2c-7.7-37.6-18.4-75.1-32.5-102.2c-7.1-13.59-15-24.55-23.3-31.79c-8.4-7.25-16.8-10.86-26.4-10.86s-18 3.61-26.4 10.86c-8.3 7.24-16.2 18.2-23.3 31.79c-14.1 27.1-24.8 64.6-32.5 102.2c-11.1 54.7-16 109.4-17.8 135.2h-18.1c1.8-25.8 6.7-82.1 18.3-138.8c7.8-38.4 18.6-77 34.1-106.9c7.8-14.91 16.7-27.7 27.5-37.08s23.8-15.27 38.2-15.27M374.9 334.8q0 16.95-.3 32.1c-80.2 18.9-159.1 18.1-237.2-.1c-.2-10.2-.3-20.7-.3-32zm-1 50.6c-2.1 38.4-8.7 68.1-29.1 94.9v.1l-.1.1c.1-.1-2.7 2.1-7.8 4c-5.2 1.8-12.4 3.5-20.8 4.8c-16.8 2.6-38.5 4-60.1 4s-43.3-1.4-60.1-4c-8.4-1.3-15.6-3-20.8-4.8c-5.1-1.9-7.9-4.1-7.8-4l-.1-.1v-.1c-20.4-26.8-27-56.7-29.1-94.9c77.6 17.4 156.3 18.2 235.8 0"/></symbol>
|
||||||
<symbol id="shouting" viewBox="0 0 512 512"><path fill="currentColor" d="M256.813 34.094c-32.67 0-62.826 19.624-85.375 53.125c-22.55 33.5-36.844 80.558-36.844 132.686s14.294 99.187 36.844 132.688s52.705 53.125 85.375 53.125s62.825-19.626 85.375-53.126s36.875-80.56 36.875-132.688s-14.326-99.187-36.875-132.687c-22.55-33.502-52.706-53.126-85.375-53.126zM64.405 127.47c-37.863 55.188-52.23 125.55-33.562 195.218c23.816 88.89 95.022 152.122 179.25 170.25C145.894 460.577 90.12 390.486 65.874 300c-16.337-60.975-15.608-121.328-1.468-172.53zm384.813 0c14.14 51.202 14.836 111.555-1.5 172.53c-24.246 90.486-79.988 160.578-144.19 192.938c84.23-18.128 155.403-81.36 179.22-170.25c18.668-69.668 4.332-140.03-33.53-195.22zM118.5 167.78c-27.588 40.212-38.04 91.49-24.438 142.25c17.355 64.767 69.225 110.823 130.594 124.033C177.88 410.485 137.26 359.43 119.594 293.5c-11.905-44.428-11.397-88.41-1.094-125.72m276.594 0c10.303 37.31 10.81 81.292-1.094 125.72c-17.666 65.93-58.285 116.985-105.063 140.563c61.37-13.21 113.24-59.266 130.594-124.032c13.603-50.76 3.152-102.038-24.436-142.25zm-197.813 2.095c20.065-.2 38.86 8.59 52 25.313l-14.718 11.53c-16.02-20.384-38.817-24.964-64.625-10.312l-9.218-16.25c12.02-6.824 24.523-10.16 36.56-10.28zm117.157.53c.625-.01 1.25-.005 1.875 0c12.04.122 24.543 3.49 36.563 10.314l-9.22 16.25c-25.806-14.653-48.603-10.103-64.624 10.28l-14.686-11.53c12.73-16.2 30.74-24.965 50.094-25.314zm-57.625 66.69c42.196 0 76.375 34.21 76.375 76.405c0 42.193-34.18 76.375-76.375 76.375s-76.406-34.18-76.406-76.375s34.21-76.406 76.406-76.406z"/></symbol>
|
<symbol id="shouting" viewBox="0 0 512 512"><path fill="currentColor" d="M256.813 34.094c-32.67 0-62.826 19.624-85.375 53.125c-22.55 33.5-36.844 80.558-36.844 132.686s14.294 99.187 36.844 132.688s52.705 53.125 85.375 53.125s62.825-19.626 85.375-53.126s36.875-80.56 36.875-132.688s-14.326-99.187-36.875-132.687c-22.55-33.502-52.706-53.126-85.375-53.126zM64.405 127.47c-37.863 55.188-52.23 125.55-33.562 195.218c23.816 88.89 95.022 152.122 179.25 170.25C145.894 460.577 90.12 390.486 65.874 300c-16.337-60.975-15.608-121.328-1.468-172.53zm384.813 0c14.14 51.202 14.836 111.555-1.5 172.53c-24.246 90.486-79.988 160.578-144.19 192.938c84.23-18.128 155.403-81.36 179.22-170.25c18.668-69.668 4.332-140.03-33.53-195.22zM118.5 167.78c-27.588 40.212-38.04 91.49-24.438 142.25c17.355 64.767 69.225 110.823 130.594 124.033C177.88 410.485 137.26 359.43 119.594 293.5c-11.905-44.428-11.397-88.41-1.094-125.72m276.594 0c10.303 37.31 10.81 81.292-1.094 125.72c-17.666 65.93-58.285 116.985-105.063 140.563c61.37-13.21 113.24-59.266 130.594-124.032c13.603-50.76 3.152-102.038-24.436-142.25zm-197.813 2.095c20.065-.2 38.86 8.59 52 25.313l-14.718 11.53c-16.02-20.384-38.817-24.964-64.625-10.312l-9.218-16.25c12.02-6.824 24.523-10.16 36.56-10.28zm117.157.53c.625-.01 1.25-.005 1.875 0c12.04.122 24.543 3.49 36.563 10.314l-9.22 16.25c-25.806-14.653-48.603-10.103-64.624 10.28l-14.686-11.53c12.73-16.2 30.74-24.965 50.094-25.314zm-57.625 66.69c42.196 0 76.375 34.21 76.375 76.405c0 42.193-34.18 76.375-76.375 76.375s-76.406-34.18-76.406-76.375s34.21-76.406 76.406-76.406z"/></symbol>
|
||||||
<symbol id="shrimp" viewBox="0 0 512 512"><path fill="currentColor" d="M376.4 19.14c-24.4-.17-55.5 6.54-89 13.02c-33.4 6.48-68.9 12.8-100.2 12.56c-31.2-.24-57.4-6.91-74.6-24.83L99.67 32.35C128 59.39 166 63.49 200.2 62.49c-8.4 1.48-17.2 2.59-26.1 3.21c-36.7 2.54-75.23-2.61-92.58-13.15l-9.34 15.38C96.57 82.72 136.4 86.36 175.3 83.65c38.6-2.68 75.7-11.02 94.7-29.9c7.1-1.28 14.1-2.59 20.9-3.92c33.6-6.51 64.5-12.84 85.3-12.69c10.5.1 18 1.91 21.9 4.57c2.3 1.53 3.9 3.15 4.8 5.78h18.3c-1.7-8.79-6.3-16.19-13-20.74c-8.8-5.86-19.6-7.52-31.8-7.61m51.7 46.1c-44.9-.24-134 8.46-209.4 29.2c1.1-.11 2.2-.17 3.3-.17c18.1 0 33 14.93 33 33.03s-14.9 33-33 33s-33-14.9-33-33c0-13 7.7-24.3 18.7-29.71c-27.9 8.31-53.4 18.31-73.4 30.21c51.7 27.3 77.4 63 99.9 97.3c73.4-20.4 119.8-42.8 174.9-95.9c-24.1 5.9-35.3 6.5-56.2 5.7c28.1-10.5 97-43.23 118.7-55.9c-29.4 2.28-74.5 12.38-100.9 18.44c14.9-13.43 51.3-28.42 64.4-32.08c-2.2-.1-4.5-.1-7-.12M222 112.3c-8.4 0-15 6.6-15 15s6.6 15 15 15s15-6.6 15-15s-6.6-15-15-15m-120.5 20.2c-17.58 26.8-31.42 52.3-41.44 76.2c25.1 34.2 57.14 63.2 115.54 80.3c10-17.5 24.4-35.6 43-54.6c-25.3-38.9-51.8-75.5-117.1-101.9m-48.93 96c-12.03 35.4-15.18 67.4-9.37 96.1c32.35 23.5 59.1 38.1 115.5 25.9q-1.5-21.75 8.7-45.3c-54.9-16.7-89.29-44.9-114.83-76.7M231 247.2c-4.5 4.6-8.7 9.1-12.6 13.6c31.6 17.7 54.7 38.7 77.8 70.8l14.6-10.6c-23.4-32.4-48-55.2-79.8-73.8m-28.3 32.9c-3.8 5.2-7.2 10.3-10.1 15.3c8.6 3.6 21.7 10.7 33.8 19c15 10.4 28.9 23.4 33.6 32.6l16-8.4c-7.7-14.6-23.2-27.7-39.3-38.9c-11.9-8.3-23.8-15.1-34-19.6m-20.1 35.5c-2.5 6.2-4.1 12.2-5.1 17.9c4.7 1 12.9 4 20.8 9.1c10.7 7.1 21.5 17.3 27 26.1l15.2-9.6c-7.6-12-19.7-23.2-32.3-31.5c-8.6-5.6-17-10.4-25.6-12M52 352.1c15.4 34.5 46.57 64.6 95.2 89.5c11.6-1 20.9-2.1 28.7-5.9c6.9-3.4 13.3-9 19.9-20.9c-16.9-13.7-28.4-29.2-33.8-46.7c-49.2 10.4-81.56.9-110-16m370.1 72.6c-10.4-.1-43.1 4.2-75.8 11c-23.5 5-47.6 11.5-64.9 18c11.7-3.1 25.8-4.3 41.3-4.7c6.1-.2 12.5-.2 18.9-.1c19.2.4 39.2 2.2 57 4.8c12.3 1.9 23.4 3.9 32.9 6.5c6.9-8.3 12.4-18.2 12.9-24.1c.5-10.9-16.7-11.3-22.3-11.4m-211.7.7c-9.7 14.2-19.1 24.7-34.1 29.5c21.2 8.8 44.9 16.8 71.4 24c8.3-7.3 16-17.2 14.3-27.7c-19.8-7.8-37.1-16.3-51.6-25.8m63.3 50.9l-9.2 8.3c13.3 4 34.8 9 59.2 8.2c36.3-1.3 84.3 2.2 114.8-10.5c-48.8-12.3-126.4-27.6-164.8-6"/></symbol>
|
<symbol id="shrimp" viewBox="0 0 512 512"><path fill="currentColor" d="M376.4 19.14c-24.4-.17-55.5 6.54-89 13.02c-33.4 6.48-68.9 12.8-100.2 12.56c-31.2-.24-57.4-6.91-74.6-24.83L99.67 32.35C128 59.39 166 63.49 200.2 62.49c-8.4 1.48-17.2 2.59-26.1 3.21c-36.7 2.54-75.23-2.61-92.58-13.15l-9.34 15.38C96.57 82.72 136.4 86.36 175.3 83.65c38.6-2.68 75.7-11.02 94.7-29.9c7.1-1.28 14.1-2.59 20.9-3.92c33.6-6.51 64.5-12.84 85.3-12.69c10.5.1 18 1.91 21.9 4.57c2.3 1.53 3.9 3.15 4.8 5.78h18.3c-1.7-8.79-6.3-16.19-13-20.74c-8.8-5.86-19.6-7.52-31.8-7.61m51.7 46.1c-44.9-.24-134 8.46-209.4 29.2c1.1-.11 2.2-.17 3.3-.17c18.1 0 33 14.93 33 33.03s-14.9 33-33 33s-33-14.9-33-33c0-13 7.7-24.3 18.7-29.71c-27.9 8.31-53.4 18.31-73.4 30.21c51.7 27.3 77.4 63 99.9 97.3c73.4-20.4 119.8-42.8 174.9-95.9c-24.1 5.9-35.3 6.5-56.2 5.7c28.1-10.5 97-43.23 118.7-55.9c-29.4 2.28-74.5 12.38-100.9 18.44c14.9-13.43 51.3-28.42 64.4-32.08c-2.2-.1-4.5-.1-7-.12M222 112.3c-8.4 0-15 6.6-15 15s6.6 15 15 15s15-6.6 15-15s-6.6-15-15-15m-120.5 20.2c-17.58 26.8-31.42 52.3-41.44 76.2c25.1 34.2 57.14 63.2 115.54 80.3c10-17.5 24.4-35.6 43-54.6c-25.3-38.9-51.8-75.5-117.1-101.9m-48.93 96c-12.03 35.4-15.18 67.4-9.37 96.1c32.35 23.5 59.1 38.1 115.5 25.9q-1.5-21.75 8.7-45.3c-54.9-16.7-89.29-44.9-114.83-76.7M231 247.2c-4.5 4.6-8.7 9.1-12.6 13.6c31.6 17.7 54.7 38.7 77.8 70.8l14.6-10.6c-23.4-32.4-48-55.2-79.8-73.8m-28.3 32.9c-3.8 5.2-7.2 10.3-10.1 15.3c8.6 3.6 21.7 10.7 33.8 19c15 10.4 28.9 23.4 33.6 32.6l16-8.4c-7.7-14.6-23.2-27.7-39.3-38.9c-11.9-8.3-23.8-15.1-34-19.6m-20.1 35.5c-2.5 6.2-4.1 12.2-5.1 17.9c4.7 1 12.9 4 20.8 9.1c10.7 7.1 21.5 17.3 27 26.1l15.2-9.6c-7.6-12-19.7-23.2-32.3-31.5c-8.6-5.6-17-10.4-25.6-12M52 352.1c15.4 34.5 46.57 64.6 95.2 89.5c11.6-1 20.9-2.1 28.7-5.9c6.9-3.4 13.3-9 19.9-20.9c-16.9-13.7-28.4-29.2-33.8-46.7c-49.2 10.4-81.56.9-110-16m370.1 72.6c-10.4-.1-43.1 4.2-75.8 11c-23.5 5-47.6 11.5-64.9 18c11.7-3.1 25.8-4.3 41.3-4.7c6.1-.2 12.5-.2 18.9-.1c19.2.4 39.2 2.2 57 4.8c12.3 1.9 23.4 3.9 32.9 6.5c6.9-8.3 12.4-18.2 12.9-24.1c.5-10.9-16.7-11.3-22.3-11.4m-211.7.7c-9.7 14.2-19.1 24.7-34.1 29.5c21.2 8.8 44.9 16.8 71.4 24c8.3-7.3 16-17.2 14.3-27.7c-19.8-7.8-37.1-16.3-51.6-25.8m63.3 50.9l-9.2 8.3c13.3 4 34.8 9 59.2 8.2c36.3-1.3 84.3 2.2 114.8-10.5c-48.8-12.3-126.4-27.6-164.8-6"/></symbol>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 445 KiB After Width: | Height: | Size: 448 KiB |
|
|
@ -139,7 +139,7 @@ main { padding: 24px; max-width: 1200px; width: 100%; }
|
||||||
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr)); }
|
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr)); }
|
||||||
.grid.wide { grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr)); }
|
.grid.wide { grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr)); }
|
||||||
.cols { display: grid; gap: 18px; grid-template-columns: 1fr 1fr; }
|
.cols { display: grid; gap: 18px; grid-template-columns: 1fr 1fr; }
|
||||||
.stats { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); margin-bottom: 18px; }
|
.stats { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); margin-bottom: 18px; }
|
||||||
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; }
|
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; }
|
||||||
.stat .k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
|
.stat .k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
|
||||||
.stat .v { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
.stat .v { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
||||||
|
|
@ -871,3 +871,14 @@ button.swatch { width: 36px; height: 36px; }
|
||||||
.qp-row input::-webkit-inner-spin-button, .qp-row input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
|
.qp-row input::-webkit-inner-spin-button, .qp-row input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
|
||||||
.qp-then { display: inline-flex; align-items: center; gap: 4px; }
|
.qp-then { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
.qp-then .x { background: none; border: 0; color: var(--muted); cursor: pointer; font-size: 11px; padding: 0 2px; }
|
.qp-then .x { background: none; border: 0; color: var(--muted); cursor: pointer; font-size: 11px; padding: 0 2px; }
|
||||||
|
|
||||||
|
/* Townsfolk mood */
|
||||||
|
.mood-bar { position: relative; height: 12px; border-radius: 99px; margin: 4px 0 8px;
|
||||||
|
background: linear-gradient(90deg, #e2645c33, #e3a04333 40%, #5cbf6a33); }
|
||||||
|
.mood-bar > span { display: block; height: 100%; border-radius: 99px; background: var(--c); }
|
||||||
|
.mood-bar > i { position: absolute; top: -3px; bottom: -3px; width: 2px; margin-left: -1px; background: var(--text); border-radius: 2px; }
|
||||||
|
.mood-effects { color: var(--muted); margin-bottom: 12px; }
|
||||||
|
.needs { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
||||||
|
.need-head { display: flex; justify-content: space-between; margin-bottom: 8px; }
|
||||||
|
.req.dim { opacity: .6; }
|
||||||
|
.mood-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||||
|
|
|
||||||
|
|
@ -3242,5 +3242,116 @@ class PartyTest(unittest.TestCase):
|
||||||
self.assertEqual(world["parties"], {})
|
self.assertEqual(world["parties"], {})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TownsfolkTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.world = world_with()
|
||||||
|
self.city = self.world["city"]
|
||||||
|
self.city["population"] = 100.0
|
||||||
|
self.city["stockpile"] = {}
|
||||||
|
self.now = self.city["last_tick"]
|
||||||
|
|
||||||
|
def test_a_new_city_starts_content(self):
|
||||||
|
self.assertEqual(self.city["happiness"], 0.5)
|
||||||
|
self.assertEqual(game.mood(self.world)["name"], "Content")
|
||||||
|
|
||||||
|
def test_each_tier_adds_a_need(self):
|
||||||
|
for tier, keys in enumerate([["food"], ["food", "warmth"], ["food", "warmth", "clothes"]]):
|
||||||
|
self.city["tier"] = tier
|
||||||
|
self.assertEqual(list(game.happiness(self.world, self.now)["needs"]), keys)
|
||||||
|
|
||||||
|
def test_food_counts_supply_and_kinds(self):
|
||||||
|
# 100 people eat 50 meals a day; a hamlet wants 2 days stored and 4 kinds.
|
||||||
|
self.city["stockpile"] = {"bread": 100, "cooked_meat": 49}
|
||||||
|
food = game.happiness(self.world, self.now)["needs"]["food"]
|
||||||
|
self.assertAlmostEqual(food["score"], 0.5 * 1 + 0.5 * 1 / 4) # meat is short of a day's worth
|
||||||
|
self.city["starving"] = True
|
||||||
|
self.assertEqual(game.happiness(self.world, self.now)["needs"]["food"]["score"], 0)
|
||||||
|
|
||||||
|
def test_the_biggest_pile_is_eaten_first(self):
|
||||||
|
self.city["stockpile"] = {"bread": 10, "cooked_meat": 500}
|
||||||
|
game._city_hour(self.world, self.now)
|
||||||
|
self.assertEqual(self.city["stockpile"]["bread"], 10)
|
||||||
|
self.assertLess(self.city["stockpile"]["cooked_meat"], 500)
|
||||||
|
|
||||||
|
def test_hearths_burn_kindling_first_and_only_from_a_village(self):
|
||||||
|
self.city["stockpile"] = {"kindling": 10, "logs": 10}
|
||||||
|
self.city["weather"]["kind"] = "clear"
|
||||||
|
game._happenings_hour(self.world, self.now, random.Random(1))
|
||||||
|
self.assertEqual(self.city["stockpile"], {"kindling": 10, "logs": 10}) # a hamlet has no hearths
|
||||||
|
self.city["tier"] = 1
|
||||||
|
game._happenings_hour(self.world, self.now, random.Random(1))
|
||||||
|
self.assertLess(self.city["stockpile"]["kindling"], 10)
|
||||||
|
self.assertEqual(self.city["stockpile"]["logs"], 10)
|
||||||
|
|
||||||
|
def test_no_fuel_means_cold_homes(self):
|
||||||
|
self.city["tier"] = 1
|
||||||
|
self.city["weather"]["kind"] = "clear"
|
||||||
|
game._happenings_hour(self.world, self.now, random.Random(1))
|
||||||
|
self.assertTrue(self.city["unheated"])
|
||||||
|
self.assertFalse(self.city["cold"]) # no extra food unless it's snowing
|
||||||
|
self.assertEqual(game.happiness(self.world, self.now)["needs"]["warmth"]["score"], 0)
|
||||||
|
|
||||||
|
def test_clothes_for_the_season_wear_out(self):
|
||||||
|
self.city["tier"] = 2
|
||||||
|
garment = game.SEASON_CLOTHES[game.season(self.world, self.now)["index"]]
|
||||||
|
self.city["stockpile"] = {garment: 100}
|
||||||
|
self.assertEqual(game.happiness(self.world, self.now)["needs"]["clothes"]["score"], 1)
|
||||||
|
for _hour in range(24):
|
||||||
|
game._townsfolk_hour(self.world, self.now, random.Random(1))
|
||||||
|
# 100 people wear out 100 in a 7-day season: about 14 a day.
|
||||||
|
self.assertEqual(self.city["stockpile"][garment], 100 - int(100 / game.SEASON_DAYS))
|
||||||
|
|
||||||
|
def test_mood_drifts_slowly(self):
|
||||||
|
self.assertEqual(game.happiness(self.world, self.now)["target"], 0) # nothing to eat
|
||||||
|
game._townsfolk_hour(self.world, self.now, random.Random(1))
|
||||||
|
self.assertAlmostEqual(self.city["happiness"], 0.5 - game.MOOD_DRIFT)
|
||||||
|
|
||||||
|
def test_unsolved_problems_weigh_on_the_mood(self):
|
||||||
|
self.city["stockpile"] = {"bread": 1000, "cooked_meat": 100, "stew": 100, "cooked_shrimp": 100}
|
||||||
|
self.assertEqual(game.happiness(self.world, self.now)["target"], 1)
|
||||||
|
self.city["problems"].append({"kind": "goblins", "started": self.now, "progress": {}, "work": 0})
|
||||||
|
self.assertAlmostEqual(game.happiness(self.world, self.now)["target"], 1 - game.PROBLEM_MOOD)
|
||||||
|
|
||||||
|
def test_a_lost_raid_shocks_the_mood(self):
|
||||||
|
event = {"strength": 10_000, "sabotage": 0, "group": "the Red Hand"}
|
||||||
|
game._resolve_raid(self.world, event, self.now, random.Random(1))
|
||||||
|
self.assertAlmostEqual(self.city["happiness"], 0.5 + game.MOOD_SHOCKS["raid_lost"])
|
||||||
|
|
||||||
|
def test_misery_drives_people_away_and_weakens_the_guards(self):
|
||||||
|
self.city["buildings"]["barracks"]["level"] = 1
|
||||||
|
self.city["wages"] = 10_000
|
||||||
|
self.city["stockpile"] = {"bread": 1000}
|
||||||
|
content = game.defence(self.world)["parts"]["militia"]
|
||||||
|
self.city["happiness"] = 0.1
|
||||||
|
self.assertAlmostEqual(game.defence(self.world)["parts"]["militia"], round(content * 0.8, 1))
|
||||||
|
self.city["population"] = game.housing(self.world) # full: no growth to hide the leaving
|
||||||
|
before = self.city["population"]
|
||||||
|
game._city_hour(self.world, self.now)
|
||||||
|
self.assertAlmostEqual(self.city["population"], before * (1 - 0.03 / 24))
|
||||||
|
|
||||||
|
def test_unhappy_halts_growth(self):
|
||||||
|
self.city["stockpile"] = {"bread": 1000}
|
||||||
|
self.city["happiness"] = 0.3
|
||||||
|
self.city["population"] = game.housing(self.world) - 5.0 # room to grow
|
||||||
|
before = self.city["population"]
|
||||||
|
game._city_hour(self.world, self.now)
|
||||||
|
self.assertEqual(self.city["population"], before)
|
||||||
|
self.assertTrue(game.population_view(self.world)["halted"])
|
||||||
|
|
||||||
|
def test_misery_makes_trouble(self):
|
||||||
|
self.city["happiness"] = 0.05
|
||||||
|
rng = random.Random(1)
|
||||||
|
rng.random = lambda: 0.0
|
||||||
|
game._townsfolk_hour(self.world, self.now, rng)
|
||||||
|
self.assertIn(self.world["events"][-1]["kind"], ("strike", "theft", "riot"))
|
||||||
|
|
||||||
|
def test_kindling_is_split_under_firekeeping(self):
|
||||||
|
split = game.ACTION_BY_ID["split_willow_logs"]
|
||||||
|
self.assertEqual((split["skill"], split["inputs"], split["outputs"]),
|
||||||
|
("firekeeping", {"willow_logs": 1}, {"kindling": 4}))
|
||||||
|
self.assertIn("the hearths", game.city_uses("kindling"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue