Combat engine: swing-by-swing fights, Frontier and expeditions, trophies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2ca90c43c0
commit
19ccab4ac3
7 changed files with 395 additions and 115 deletions
334
game.py
334
game.py
|
|
@ -397,7 +397,118 @@ def _tiered_actions() -> list[dict]:
|
|||
return actions
|
||||
|
||||
|
||||
# --- combat -----------------------------------------------------------------
|
||||
# A fight is played out swing by swing: you and the enemy each have HP, a swing
|
||||
# timer, accuracy and a max hit, and every swing hits, misses or crits. Combat
|
||||
# is split into the Frontier (bandits and the city's troubles) and expeditions:
|
||||
# one area per tier, each with a golem, a troll, a spirit and a flyer that drop
|
||||
# the trophies higher weapons need, and a boss.
|
||||
CRIT_CHANCE = 0.05
|
||||
CRIT_MULT = 1.5
|
||||
RESPAWN = 3 # seconds before the next enemy turns up
|
||||
PLAYER_INTERVAL = 2.4 # seconds between your swings
|
||||
MAX_FIGHT = 600 # a fight that drags on longer is broken off
|
||||
FLEE_BELOW = 0.2 # out of food and under this share of HP: get out
|
||||
|
||||
# kind: HP, seconds between swings, accuracy, evasion, max hit (multipliers), damage it shrugs off
|
||||
MONSTER_KINDS = {
|
||||
"human": (1.0, 2.6, 1.0, 1.0, 1.0, 0.0),
|
||||
"beast": (0.9, 2.2, 1.0, 1.0, 0.9, 0.0),
|
||||
"golem": (1.5, 3.2, 1.0, 0.7, 1.25, 0.2),
|
||||
"troll": (1.4, 3.0, 0.9, 1.0, 1.4, 0.0),
|
||||
"spirit": (0.8, 2.4, 1.0, 1.35, 0.9, 0.0),
|
||||
"flyer": (0.8, 2.0, 1.0, 1.2, 0.8, 0.0),
|
||||
"dragon": (2.5, 3.0, 1.1, 1.0, 1.5, 0.15),
|
||||
}
|
||||
|
||||
|
||||
def monster(name: str, kind: str, level: int, boss: bool = False) -> dict:
|
||||
hp, interval, accuracy, evasion, hit, shrug = MONSTER_KINDS[kind]
|
||||
return {"name": name, "kind": kind, "level": level, "boss": boss,
|
||||
"hp": round((20 + 6 * level) * hp * (2.5 if boss else 1)), "interval": interval,
|
||||
"accuracy": round((12 + 2.2 * level) * accuracy), "evasion": round((10 + 2 * level) * evasion),
|
||||
"max_hit": max(1, round((2 + level / 3) * hit * (1.2 if boss else 1))), "reduction": shrug}
|
||||
|
||||
|
||||
def _fight(id: str, enemy: dict, group: str, gold: int = 0, drops=(), name: str | None = None, **extra) -> dict:
|
||||
"""A combat action: XP per kill, needs the enemy's level in combat."""
|
||||
xp = round(enemy["hp"] * 0.4 + enemy["level"] * 1.5)
|
||||
return _action(id, "combat", name or enemy["name"], enemy["level"], RESPAWN, xp, gold=gold,
|
||||
drops=[list(drop) for drop in drops], enemy=enemy, group=group, **extra)
|
||||
|
||||
|
||||
# Expedition areas, tiers II to VI: name, level, and the trophy each kind of monster drops there.
|
||||
AREAS = ["Old Watchtower", "Sunken Barrows", "Iron Hills", "Frostpeak", "Dragon's Spine"]
|
||||
AREA_LEVELS = [10, 20, 30, 40, 50]
|
||||
AREA_MONSTERS = { # kind: (level offset, monsters, trophies)
|
||||
"golem": (0, ["Clay golem", "Stone golem", "Iron golem", "Obsidian golem", "Runic golem"],
|
||||
["Clay core", "Stone core", "Iron core", "Obsidian core", "Runic core"]),
|
||||
"troll": (2, ["Cave troll", "Hill troll", "Rock troll", "Frost troll", "Elder troll"],
|
||||
["Cave troll hide", "Hill troll hide", "Rock troll hide", "Frost troll hide", "Elder troll hide"]),
|
||||
"spirit": (1, ["Will-o'-wisp", "Bog wraith", "Mine ghost", "Banshee", "Shade"],
|
||||
["Wisp essence", "Wraith essence", "Ghost essence", "Banshee essence", "Shade essence"]),
|
||||
"flyer": (0, ["Giant bat", "Harpy", "Wyvern", "Griffin", "Roc"],
|
||||
["Bat sinew", "Harpy sinew", "Wyvern sinew", "Griffin sinew", "Roc sinew"]),
|
||||
}
|
||||
TROPHY_ODDS = 0.33
|
||||
AREA_BONES = ["bones", "big_bones", "big_bones", "dragon_bones", "dragon_bones"]
|
||||
|
||||
|
||||
def _slug(name: str) -> str:
|
||||
return "".join(c if c.isalnum() else "_" for c in name.lower().replace("'", "")).strip("_")
|
||||
|
||||
|
||||
TROPHIES = {} # id: (name, kind, tier)
|
||||
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
|
||||
for _tier, _trophy in enumerate(_trophies, start=1):
|
||||
TROPHIES[_slug(_trophy)] = (_trophy, _kind, _tier)
|
||||
ITEMS[_slug(_trophy)] = {"name": _trophy, "icon": "🏆", "value": 15 * _tier, "trophy": _kind, "tier": _tier}
|
||||
|
||||
|
||||
def _area_fights() -> list[dict]:
|
||||
fights = []
|
||||
for index, (area, level) in enumerate(zip(AREAS, AREA_LEVELS)):
|
||||
for kind, (offset, monsters, trophies) in AREA_MONSTERS.items():
|
||||
enemy = monster(monsters[index], kind, level + offset)
|
||||
fights.append(_fight(f"fight_{_slug(monsters[index])}", enemy, area, gold=level + offset,
|
||||
drops=[(AREA_BONES[index], 1, 1.0), (_slug(trophies[index]), 1, TROPHY_ODDS)]))
|
||||
return fights
|
||||
|
||||
|
||||
def _boss_trophies(tier: int) -> list[tuple]:
|
||||
return [(_slug(trophies[tier - 1]), 1, 0.25) for _offset, _monsters, trophies in AREA_MONSTERS.values()]
|
||||
|
||||
|
||||
COMBAT_ACTIONS = [
|
||||
# The Frontier: the city's own troubles.
|
||||
_fight("fight_scouts", monster("Bandit scouts", "human", 10), "Frontier", gold=10,
|
||||
drops=[("copper_bar", 1, 0.3), ("arrows", 5, 0.4), ("bones", 1, 1.0)]),
|
||||
_fight("sabotage", monster("Camp guards", "human", 12), "Frontier", name="Sabotage the bandit camp",
|
||||
special="sabotage"),
|
||||
_fight("fight_stronghold", monster("Bandit stronghold", "human", 40, boss=True), "Frontier", gold=60,
|
||||
drops=[("steel_bar", 1, 0.4), ("coal", 3, 0.5), ("iron_shield", 1, 0.04), ("big_bones", 1, 1.0)]),
|
||||
# Greenwood: where everyone starts.
|
||||
_fight("fight_wolves", monster("Wolves", "beast", 1), "Greenwood", gold=2, name="Wolves in the woods",
|
||||
drops=[("raw_meat", 1, 0.6), ("hide", 1, 0.5), ("bones", 1, 1.0)]),
|
||||
_fight("fight_goblins", monster("Cave goblins", "human", 5), "Greenwood", gold=4,
|
||||
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
|
||||
*_area_fights(),
|
||||
_fight("fight_ruins", monster("Watchtower ogre", "troll", 18, boss=True), "Old Watchtower", gold=25,
|
||||
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_trophies(1)]),
|
||||
_fight("fight_barrow_king", monster("Barrow king", "spirit", 28, boss=True), "Sunken Barrows", gold=40,
|
||||
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_trophies(2)]),
|
||||
_fight("fight_green_dragon", monster("Green dragon", "dragon", 38), "Iron Hills", gold=80,
|
||||
drops=[("green_dragonhide", 1, 0.9), ("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(3)]),
|
||||
_fight("fight_blue_dragon", monster("Blue dragon", "dragon", 48), "Frostpeak", gold=110,
|
||||
drops=[("blue_dragonhide", 1, 0.9), ("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(4)]),
|
||||
_fight("fight_red_dragon", monster("Red dragon", "dragon", 60), "Dragon's Spine", gold=150,
|
||||
drops=[("red_dragonhide", 1, 0.9), ("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_trophies(5)]),
|
||||
_fight("fight_black_dragon", monster("Black dragon", "dragon", 75, boss=True), "Dragon's Spine", gold=220,
|
||||
drops=[("red_dragonhide", 2, 0.9), ("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_trophies(5)]),
|
||||
]
|
||||
|
||||
ACTIONS = [
|
||||
*COMBAT_ACTIONS,
|
||||
_action("mine_stone", "mining", "Quarry stone", 1, 20, 5, outputs={"stone": 1}),
|
||||
_action("farm_wheat", "farming", "Grow wheat", 1, 25, 6, outputs={"wheat": 2}, group="Crops"),
|
||||
_action("farm_veg", "farming", "Grow vegetables", 12, 35, 13, outputs={"vegetables": 2}, group="Crops"),
|
||||
|
|
@ -435,25 +546,7 @@ ACTIONS = [
|
|||
_action("pack_bread", "cooking", "Pack bread rations", 1, 20, 6, {"bread": 2}, {"provisions": 1}, group="Rations"),
|
||||
_action("pack_meat", "cooking", "Pack meat rations", 5, 20, 7, {"cooked_meat": 2}, {"provisions": 1}, group="Rations"),
|
||||
_action("pack_fish", "cooking", "Pack fish rations", 1, 20, 6, {"cooked_shrimp": 2}, {"provisions": 1}, group="Rations"),
|
||||
_action("drill_militia", "combat", "Drill the militia", 1, 60, 12, special="drill"),
|
||||
_action("fight_wolves", "combat", "Wolves in the woods", 1, 60, 14, difficulty=2, damage=2, gold=2,
|
||||
drops=[["raw_meat", 1, 0.6], ["hide", 1, 0.5], ["bones", 1, 1.0]]),
|
||||
_action("fight_scouts", "combat", "Bandit scouts", 10, 75, 30, difficulty=14, damage=6, gold=10,
|
||||
drops=[["copper_bar", 1, 0.3], ["arrows", 5, 0.4], ["bones", 1, 1.0]]),
|
||||
_action("fight_ruins", "combat", "Watchtower ruins", 25, 90, 60, difficulty=30, damage=14, gold=25,
|
||||
drops=[["iron_bar", 1, 0.4], ["iron_sword", 1, 0.03], ["big_bones", 1, 0.8]]),
|
||||
_action("fight_stronghold", "combat", "Bandit stronghold", 40, 105, 95, difficulty=50, damage=25, gold=60,
|
||||
drops=[["steel_bar", 1, 0.4], ["coal", 3, 0.5], ["iron_shield", 1, 0.04], ["big_bones", 1, 1.0]]),
|
||||
_action("fight_green_dragon", "combat", "Green dragon", 50, 120, 135, difficulty=90, damage=30, gold=80,
|
||||
drops=[["green_dragonhide", 1, 0.9], ["mithril_ore", 2, 0.3], ["dragon_bones", 1, 1.0]]),
|
||||
_action("fight_blue_dragon", "combat", "Blue dragon", 62, 130, 175, difficulty=115, damage=40, gold=110,
|
||||
drops=[["blue_dragonhide", 1, 0.9], ["adamantite_ore", 2, 0.3], ["dragon_bones", 1, 1.0]]),
|
||||
_action("fight_red_dragon", "combat", "Red dragon", 75, 140, 220, difficulty=140, damage=52, gold=150,
|
||||
drops=[["red_dragonhide", 1, 0.9], ["runite_ore", 1, 0.25], ["dragon_bones", 1, 1.0]]),
|
||||
_action("fight_black_dragon", "combat", "Black dragon", 85, 150, 280, difficulty=165, damage=65, gold=220,
|
||||
drops=[["red_dragonhide", 2, 0.9], ["rune_bar", 1, 0.1], ["dragon_bones", 2, 1.0]]),
|
||||
_action("sabotage", "combat", "Sabotage the bandit camp", 10, 120, 35, difficulty=12, damage=10,
|
||||
special="sabotage"),
|
||||
_action("drill_militia", "combat", "Drill the militia", 1, 60, 12, special="drill", group="Frontier"),
|
||||
_action("bury_bones", "prayer", "Bury bones", 1, 10, 4, {"bones": 1}, special="bury", pp=2, group="Bury"),
|
||||
_action("bury_big_bones", "prayer", "Bury big bones", 15, 10, 15, {"big_bones": 1}, special="bury", pp=6, group="Bury"),
|
||||
_action("bury_dragon_bones", "prayer", "Bury dragon bones", 40, 12, 72, {"dragon_bones": 1}, special="bury", pp=20, group="Bury"),
|
||||
|
|
@ -980,12 +1073,87 @@ def regen(world: dict, player: dict, now: float, boost: float = 1) -> None:
|
|||
player["hp_at"] = now
|
||||
|
||||
|
||||
def fight_damage(player: dict, action: dict, blessing: dict | None = None) -> float:
|
||||
"""Average damage one fight deals: less when out-levelling the enemy, armoured or praying."""
|
||||
blessing = blessing or NO_BLESSING
|
||||
gap = combat_power(player) + blessing["power"] - action["difficulty"]
|
||||
scale = min(1.5, max(0.3, 1 - gap * 0.03))
|
||||
return action["damage"] * scale * (1 - damage_reduction(player)) * blessing["taken"]
|
||||
def fighter(player: dict, blessed: dict, brew: dict) -> dict:
|
||||
"""Your side of a fight: power from level and gear (plus prayers and potions) aims and hits; armour dodges and soaks."""
|
||||
power = combat_power(player) + blessed["power"] + brew.get("power", 0)
|
||||
return {"interval": PLAYER_INTERVAL, "accuracy": 10 + 2 * power, "max_hit": max(2, round(2 + power / 3)),
|
||||
"evasion": 10 + 2 * (skill_level(player, "combat") + armour(player) // 2),
|
||||
"taken": (1 - damage_reduction(player)) * blessed["taken"] * (1 - brew.get("reduce", 0))}
|
||||
|
||||
|
||||
def hit_chance(accuracy: float, evasion: float) -> float:
|
||||
return min(0.95, max(0.05, accuracy / (accuracy + evasion) + 0.15))
|
||||
|
||||
|
||||
def _average_hit(max_hit: float) -> float:
|
||||
return (1 - CRIT_CHANCE) * (max_hit + 1) / 2 + CRIT_CHANCE * max_hit * CRIT_MULT
|
||||
|
||||
|
||||
def fight_odds(player: dict, action: dict) -> dict:
|
||||
"""What a fight against this enemy looks like on average, for the cards."""
|
||||
me, foe = fighter(player, blessing(player), potion_effect(player, "combat")), action["enemy"]
|
||||
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
|
||||
dps = hit * _average_hit(me["max_hit"]) * (1 - foe["reduction"]) / me["interval"]
|
||||
kill = foe["hp"] / dps
|
||||
hurt = their_hit * _average_hit(foe["max_hit"]) * me["taken"] / foe["interval"] * kill
|
||||
return {"hit": round(hit, 2), "their_hit": round(their_hit, 2), "max_hit": me["max_hit"],
|
||||
"their_max": max(1, round(foe["max_hit"] * me["taken"])), "kill_time": round(kill, 1), "damage": round(hurt, 1)}
|
||||
|
||||
|
||||
def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) -> dict:
|
||||
"""Play out one fight swing by swing. Nothing changes until it's over: the result is applied then."""
|
||||
foe, blessed, brew = action["enemy"], blessing(player), potion_effect(player, "combat")
|
||||
me = fighter(player, blessed, brew)
|
||||
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
|
||||
top, hp, foe_hp = max_hp(player), player["hp"], foe["hp"]
|
||||
pantry = {item: player["bank"][item] for item in edible(player)}
|
||||
eaten, events, hurt = {}, [], 0
|
||||
wind = blessed["wind"] and not (player["action"] or {}).get("wind_used")
|
||||
pace = speed(world)
|
||||
mine, theirs, result, t = me["interval"], foe["interval"], None, 0.0
|
||||
while result is None:
|
||||
t = min(mine, theirs)
|
||||
if t > MAX_FIGHT:
|
||||
result = "fled"
|
||||
break
|
||||
crit = rng.random() < CRIT_CHANCE
|
||||
if mine <= theirs:
|
||||
mine += me["interval"]
|
||||
landed = rng.random() < hit
|
||||
dealt = max(1, round((me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))
|
||||
* (1 - foe["reduction"]))) if landed else 0
|
||||
foe_hp = max(0, foe_hp - dealt)
|
||||
events.append([round(t / pace, 2), "you", ("crit" if crit else "hit") if landed else "miss", dealt, round(hp), foe_hp])
|
||||
if not foe_hp:
|
||||
result = "won"
|
||||
continue
|
||||
theirs += foe["interval"]
|
||||
landed = rng.random() < their_hit
|
||||
dealt = max(1, round((foe["max_hit"] * CRIT_MULT if crit else rng.randint(1, foe["max_hit"])) * me["taken"])) if landed else 0
|
||||
hp -= dealt
|
||||
hurt += dealt
|
||||
events.append([round(t / pace, 2), "foe", ("crit" if crit else "hit") if landed else "miss", dealt, max(0, round(hp)), foe_hp])
|
||||
while hp < top * EAT_BELOW and any(pantry.values()):
|
||||
food = next(item for item, left in pantry.items() if left)
|
||||
pantry[food] -= 1
|
||||
eaten[food] = eaten.get(food, 0) + 1
|
||||
hp = min(top, hp + ITEMS[food]["heal"])
|
||||
events.append([round(t / pace, 2), "you", "eat", ITEMS[food]["heal"], round(hp), foe_hp, food])
|
||||
beaten, starving = hp <= 0, hp < top * FLEE_BELOW and not any(pantry.values())
|
||||
if (beaten or starving) and wind:
|
||||
# Second Wind: the retreat that was coming turns into half health, once per outing.
|
||||
wind = False
|
||||
hp = top / 2
|
||||
events.append([round(t / pace, 2), "you", "wind", 0, round(hp), foe_hp])
|
||||
elif beaten:
|
||||
hp, result = 1, "lost"
|
||||
elif starving:
|
||||
result = "fled"
|
||||
return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]),
|
||||
"hp": hp, "result": result, "end": round(t / pace, 2), "duration": (t + RESPAWN) / pace,
|
||||
"interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events,
|
||||
"eaten": eaten, "damage": hurt, "pp": blessed["cost"],
|
||||
"wind_used": blessed["wind"] and not wind and not (player["action"] or {}).get("wind_used")}
|
||||
|
||||
|
||||
# --- prayer -------------------------------------------------------------------
|
||||
|
|
@ -1067,17 +1235,6 @@ def edible(player: dict) -> list[str]:
|
|||
return foods
|
||||
|
||||
|
||||
def auto_eat(player: dict, summary: dict) -> None:
|
||||
top = max_hp(player)
|
||||
while player["hp"] < top * EAT_BELOW:
|
||||
foods = edible(player)
|
||||
if not foods:
|
||||
return
|
||||
bank_take(player, foods[0], 1)
|
||||
player["hp"] = min(top, player["hp"] + ITEMS[foods[0]]["heal"])
|
||||
summary["eaten"][foods[0]] = summary["eaten"].get(foods[0], 0) + 1
|
||||
|
||||
|
||||
def action_time(world: dict, player: dict, action: dict) -> float:
|
||||
seconds = action["time"]
|
||||
if action["skill"] in CLASSES[player["cls"]]["fast"]:
|
||||
|
|
@ -1140,12 +1297,6 @@ def plant_second_plot(world: dict, player: dict, action_id: str | None) -> str |
|
|||
return None
|
||||
|
||||
|
||||
def with_potion(blessed: dict, brew: dict) -> dict:
|
||||
"""A fight's prayer boosts with a combat potion's added on top."""
|
||||
return {**blessed, "power": blessed["power"] + brew.get("power", 0),
|
||||
"taken": blessed["taken"] * (1 - brew.get("reduce", 0))}
|
||||
|
||||
|
||||
def potion_effect(player: dict, skill: str) -> dict:
|
||||
active = player.get("potions", {}).get(skill)
|
||||
return ITEMS[active["item"]]["potion"] if active else {}
|
||||
|
|
@ -1227,7 +1378,7 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
|
|||
return "No raid to prepare for"
|
||||
if threat.get("drilled", 0) >= drill_needed(threat):
|
||||
return "The militia is as drilled as it can be"
|
||||
if "difficulty" in action and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
|
||||
if "enemy" in action and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
|
||||
return "Too hurt to fight. Rest, or bring food"
|
||||
return None
|
||||
|
||||
|
|
@ -1258,52 +1409,45 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
outputs[item] = int(exact) + (1 if rng.random() < exact - int(exact) else 0)
|
||||
xp *= mods["xp"]
|
||||
|
||||
if "difficulty" in action:
|
||||
blessed = blessing(player)
|
||||
if player["prayers"] and not blessed["cost"]:
|
||||
summary["prayer_out"] = True
|
||||
if blessed["cost"]:
|
||||
player["pp"] -= blessed["cost"]
|
||||
summary["pp_spent"] += blessed["cost"]
|
||||
gain_xp(world, player, "prayer", blessed["cost"] * PRAYER_XP_PER_POINT, now, summary)
|
||||
regen(world, player, now, blessed["regen"])
|
||||
blessed = with_potion(blessed, brew)
|
||||
auto_eat(player, summary)
|
||||
chance = 0.5 + (combat_power(player) + blessed["power"] - action["difficulty"]) * 0.04
|
||||
won = rng.random() < min(0.97, max(0.15, chance))
|
||||
hurt = fight_damage(player, action, blessed) * rng.uniform(0.6, 1.4) * (1 if won else 1.3)
|
||||
player["hp"] = min(max_hp(player), player["hp"] - hurt + brew.get("heal", 0))
|
||||
summary["damage"] += hurt
|
||||
auto_eat(player, summary)
|
||||
if "enemy" in action:
|
||||
outing = player["action"]
|
||||
if player["hp"] <= 0 and blessed["wind"] and outing and not outing.get("wind_used"):
|
||||
fight = outing.pop("fight", None) or simulate_fight(world, player, action, rng)
|
||||
if player["prayers"] and not fight["pp"]:
|
||||
summary["prayer_out"] = True
|
||||
paid = min(player["pp"], fight["pp"])
|
||||
if paid:
|
||||
player["pp"] -= paid
|
||||
summary["pp_spent"] += paid
|
||||
gain_xp(world, player, "prayer", paid * PRAYER_XP_PER_POINT, now, summary)
|
||||
for food, count in fight["eaten"].items():
|
||||
count = min(count, player["bank"].get(food, 0))
|
||||
if count:
|
||||
bank_take(player, food, count)
|
||||
summary["eaten"][food] = summary["eaten"].get(food, 0) + count
|
||||
player["hp"], player["hp_at"] = fight["hp"], now
|
||||
summary["damage"] += fight["damage"]
|
||||
if fight["wind_used"]:
|
||||
outing["wind_used"] = True
|
||||
player["hp"] = max_hp(player) / 2
|
||||
summary["second_wind"] += 1
|
||||
if player["hp"] <= 0:
|
||||
# Out of food and out of luck: fall back with nothing but your life.
|
||||
player["hp"] = 1
|
||||
if fight["result"] != "won":
|
||||
# Beaten or out of food: fall back with nothing but your life.
|
||||
summary["losses"] += 1
|
||||
reason = "Retreated: ran out of food" if not edible(player) else "Retreated"
|
||||
stop(player, reason)
|
||||
summary["stopped"] = reason
|
||||
gain_xp(world, player, "combat", xp // 3, now, summary)
|
||||
return
|
||||
if won:
|
||||
summary["wins"] += 1
|
||||
player["gold"] += action.get("gold", 0)
|
||||
summary["gold"] += action.get("gold", 0)
|
||||
for item, qty, odds in action.get("drops", []):
|
||||
if rng.random() < odds:
|
||||
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)
|
||||
if action.get("special") == "problem":
|
||||
_work_problem(world, player, action, now)
|
||||
else:
|
||||
summary["losses"] += 1
|
||||
xp = xp // 3
|
||||
summary["wins"] += 1
|
||||
player["gold"] += action.get("gold", 0)
|
||||
summary["gold"] += action.get("gold", 0)
|
||||
for item, qty, odds in action.get("drops", []):
|
||||
if rng.random() < odds:
|
||||
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)
|
||||
if action.get("special") == "problem":
|
||||
_work_problem(world, player, action, now)
|
||||
if action.get("special") == "recon":
|
||||
event = upcoming_raid(world)
|
||||
# Once a threat is fully scouted, scouting only trains the skill (and keeps the informant's leads).
|
||||
|
|
@ -1391,7 +1535,13 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
|
|||
# The second crop can't grow right now (a problem, too low a level): tend the first only.
|
||||
current["turn"] = 0
|
||||
action = growing(player)
|
||||
duration = action_time(world, player, action)
|
||||
if "enemy" in action and not can_start(world, player, action):
|
||||
if not current.get("fight"):
|
||||
regen(world, player, current["start"], blessing(player)["regen"])
|
||||
current["fight"] = simulate_fight(world, player, action, rng)
|
||||
duration = current["fight"]["duration"]
|
||||
else:
|
||||
duration = action_time(world, player, action)
|
||||
if current["start"] + duration > limit:
|
||||
break
|
||||
reason = can_start(world, player, action)
|
||||
|
|
@ -2266,10 +2416,10 @@ PROBLEMS = {
|
|||
}
|
||||
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
|
||||
PROBLEM_ACTIONS = [
|
||||
_action("clear_wolves", "combat", "Drive off the wolves", 1, 60, 16, difficulty=6, damage=3,
|
||||
special="problem", problem="wolves"),
|
||||
_action("catch_goblins", "combat", "Catch the goblin thieves", 1, 60, 16, difficulty=8, damage=3,
|
||||
special="problem", problem="goblins"),
|
||||
_fight("clear_wolves", monster("Wolf pack", "beast", 4), "Frontier", name="Drive off the wolves",
|
||||
special="problem", problem="wolves"),
|
||||
_fight("catch_goblins", monster("Goblin thieves", "human", 5), "Frontier", name="Catch the goblin thieves",
|
||||
special="problem", problem="goblins"),
|
||||
_action("drive_bear", "hunting", "Drive off the bear", 1, 45, 14, special="problem", problem="bear"),
|
||||
_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"),
|
||||
|
|
@ -2935,6 +3085,18 @@ PRAYER_GLYPHS = {"thick_skin": ("shield", "#a8a29e"), "burst_of_strength": ("bic
|
|||
"rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"),
|
||||
"superhuman_strength": ("biceps", "#ff8a3a"), "second_wind": ("wind-hole", "#8fd0e0"),
|
||||
"protection": ("aura", "#f0d060"), "piety": ("angel-wings", "#f0e6c0")}
|
||||
TROPHY_GLYPHS = {"golem": "crystal-growth", "troll": "animal-hide", "spirit": "magic-swirl", "flyer": "sewing-string"}
|
||||
TIER_TINTS = ["#9aa1ab", "#d4dae2", "#6f8fec", "#4cb878", "#3fd0e0"] # trophies of areas II to VI
|
||||
MONSTER_GLYPHS = {"golem": ["rock-golem"] * 5, "troll": ["troll"] * 5,
|
||||
"spirit": ["fairy", "spectre", "ghost", "spectre", "ghost"],
|
||||
"flyer": ["bat", "harpy", "wyvern", "griffin-symbol", "eagle-head"]}
|
||||
ACTION_GLYPHS.update({
|
||||
"fight_goblins": ("goblin-head", "#8fbf6a"), "fight_ruins": ("ogre", "#a08060"),
|
||||
"fight_barrow_king": ("crowned-skull", "#c8d0e0"), "drill_militia": ACTION_GLYPHS["drill_militia"],
|
||||
})
|
||||
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
|
||||
for _index, _name in enumerate(_monsters):
|
||||
ACTION_GLYPHS[f"fight_{_slug(_name)}"] = (MONSTER_GLYPHS[_kind][_index], TIER_TINTS[_index])
|
||||
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield"]
|
||||
|
||||
|
||||
|
|
@ -2959,6 +3121,8 @@ def _assign_glyphs() -> None:
|
|||
ITEMS[leather[0]].update(glyph="rolled-cloth", tint=tint)
|
||||
for suffix, glyph in LEATHER_GLYPHS.items():
|
||||
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
|
||||
for trophy, (_name, kind, tier) in TROPHIES.items():
|
||||
ITEMS[trophy].update(glyph=TROPHY_GLYPHS[kind], tint=TIER_TINTS[tier - 1])
|
||||
for herb, tint in HERB_TINTS.items():
|
||||
ITEMS[herb].update(glyph="herbs-bundle", tint=tint)
|
||||
flasks = ["round-bottom-flask", "round-bottom-flask", "standing-potion", "standing-potion",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue