Combat on the tier ladder, less combat XP, eating costs a swing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2816ca8606
commit
0f2b29e67e
3 changed files with 63 additions and 35 deletions
61
game.py
61
game.py
|
|
@ -80,8 +80,8 @@ ITEMS = {
|
|||
# ladder generates its items and recipes below; the copper and iron ids
|
||||
# predate the ladders and are kept as they were.
|
||||
|
||||
TIER_LEVELS = [1, 15, 30, 45, 60, 75] # gathering / smelting level of each tier
|
||||
EQUIP_LEVELS = [1, 10, 20, 30, 40, 50] # combat level to wear, skill level to use a tool
|
||||
TIER_LEVELS = [1, 15, 30, 45, 60, 75] # gathering / smelting level of each tier, and the combat level to wear it
|
||||
TOOL_LEVELS = [1, 10, 20, 30, 40, 50] # skill level to use a tool or station
|
||||
|
||||
# key, name, ore (item id, name, icon, value), coal per bar, bar icon, bar value, smithy level, handle wood
|
||||
METALS = [
|
||||
|
|
@ -203,7 +203,7 @@ for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in
|
|||
_item = {"name": f"{_name} {_noun}", "plural": f"{_name} {PLURALS[_noun]}",
|
||||
"icon": COPPER_ICONS.get(_piece, PIECE_ICONS[_piece]) if _key == "copper" else PIECE_ICONS[_piece],
|
||||
"value": round(_bars * _bar_value * 1.2), "slot": _slot, "tier": _tier,
|
||||
"req": ["combat", EQUIP_LEVELS[_tier]]}
|
||||
"req": ["combat", TIER_LEVELS[_tier]]}
|
||||
if _power:
|
||||
_item["power"] = _power[_tier]
|
||||
if _armour and _piece == "shield":
|
||||
|
|
@ -214,7 +214,7 @@ for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in
|
|||
for _skill, (_tool, _icon) in TOOLS.items():
|
||||
ITEMS[f"{_key}_{_tool.replace(' ', '_')}"] = {
|
||||
"name": f"{_name} {_tool}", "icon": _icon, "value": round(2 * _bar_value * 1.4), "tier": _tier,
|
||||
"slot": f"tool_{_skill}", "tool": _skill, "speed": TOOL_SPEED[_tier], "req": [_skill, EQUIP_LEVELS[_tier]],
|
||||
"slot": f"tool_{_skill}", "tool": _skill, "speed": TOOL_SPEED[_tier], "req": [_skill, TOOL_LEVELS[_tier]],
|
||||
}
|
||||
# Jewelcrafting: a gem from each tier's rock, set in a band of copper, silver or gold.
|
||||
# Silver and gold are soft metals the jeweller smelts, no smithy needed. Past the first
|
||||
|
|
@ -273,7 +273,7 @@ for _tier, (_gem, _gem_name, _gem_value, _tint, _glyph) in enumerate(GEMS):
|
|||
"name": f"{_gem_name} {_noun}", "icon": {"ring": "💍", "amulet": "📿", "trinket": "🔮"}[_slot],
|
||||
"value": round((_bands * ITEMS[BANDS[_tier]]["value"] + _gems * _gem_value + _shards * 12 * _tier) * 1.3),
|
||||
"slot": _slot, "tier": _tier, "jewel": _stat, _stat: _bonus[_tier],
|
||||
**({"req": ["combat", EQUIP_LEVELS[_tier]]} if _slot != "trinket" else {})}
|
||||
**({"req": ["combat", TIER_LEVELS[_tier]]} if _slot != "trinket" else {})}
|
||||
# Hardware: what the smith makes for builders, one kind per metal, used in
|
||||
# fittings and in building levels in place of plain bars.
|
||||
# id, singular, plural, bars used, pieces made
|
||||
|
|
@ -304,7 +304,7 @@ BOWS = ["Shortbow", "Oak bow", "Willow bow", "Maple bow", "Yew bow", "Enchanted
|
|||
for _tier, _metal in enumerate(METALS):
|
||||
ITEMS[f"{_metal[0]}_sword"]["style"] = "sword"
|
||||
_weapon = {"slot": "weapon", "power": TWO_HANDED_POWER[_tier], "tier": _tier, "two_handed": True,
|
||||
"req": ["combat", EQUIP_LEVELS[_tier]], "value": round(5 * _metal[5] * 1.2)}
|
||||
"req": ["combat", TIER_LEVELS[_tier]], "value": round(5 * _metal[5] * 1.2)}
|
||||
ITEMS[f"{_metal[0]}_warhammer"] = {"name": f"{_metal[1]} warhammer", "icon": "🔨", "style": "hammer", **_weapon}
|
||||
ITEMS[f"{WOODS[_tier]}_staff"] = {"name": STAVES[_tier], "plural": STAVES[_tier].replace("staff", "staves"),
|
||||
"icon": "🪄", "style": "staff", **_weapon}
|
||||
|
|
@ -321,7 +321,7 @@ for _tier, _wood in enumerate(WOODS):
|
|||
for _skill, (_suffix, _noun, _glyph, _kind) in STATIONS.items():
|
||||
ITEMS[f"{_wood}_{_suffix}"] = {
|
||||
"name": f"{WOOD_NAMES[_tier]} {_noun}", "icon": "🪚", "tier": _tier, "slot": f"station_{_skill}", "station": _skill,
|
||||
"speed": TOOL_SPEED[_tier], "req": [_skill, EQUIP_LEVELS[_tier]], "value": 0}
|
||||
"speed": TOOL_SPEED[_tier], "req": [_skill, TOOL_LEVELS[_tier]], "value": 0}
|
||||
ITEMS[f"{_wood}_strongbox"] = {"name": f"{WOOD_NAMES[_tier]} strongbox", "plural": f"{WOOD_NAMES[_tier]} strongboxes",
|
||||
"icon": "🧰", "tier": _tier, "slot": "storage", "slots": STRONGBOX_SLOTS[_tier], "value": 0}
|
||||
for _tier, (_fibre, _level, _time, _xp, _cloth, _prefix, _label) in enumerate(CLOTHS):
|
||||
|
|
@ -333,7 +333,7 @@ for _tier, (_raw, _leather, _prefix, _label, _level) in enumerate(LEATHERS):
|
|||
for _suffix, _slot, _noun, _used, _offset, _icon in LEATHER_PIECES:
|
||||
ITEMS[f"{_prefix}_{_suffix}"] = {
|
||||
"name": f"{_label} {_noun}", "icon": _icon, "value": round(_used * _leather[2] * 1.3),
|
||||
"slot": _slot, "tier": _tier, "req": ["combat", EQUIP_LEVELS[_tier]], **_armour_stats("medium", _slot, _tier),
|
||||
"slot": _slot, "tier": _tier, "req": ["combat", TIER_LEVELS[_tier]], **_armour_stats("medium", _slot, _tier),
|
||||
}
|
||||
if _noun == "trousers":
|
||||
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
|
||||
|
|
@ -343,7 +343,7 @@ for _tier, (_fibre, _level, _time, _xp, _cloth, _prefix, _label) in enumerate(CL
|
|||
for _suffix, _slot, _noun, _used, _offset, _icon in CLOTH_PIECES:
|
||||
ITEMS[f"{_prefix}_{_suffix}"] = {
|
||||
"name": f"{_label} {_noun}", "icon": _icon, "value": round(_used * _cloth[2] * 1.3),
|
||||
"slot": _slot, "tier": _tier, "req": ["combat", EQUIP_LEVELS[_tier]], **_armour_stats("light", _slot, _tier),
|
||||
"slot": _slot, "tier": _tier, "req": ["combat", TIER_LEVELS[_tier]], **_armour_stats("light", _slot, _tier),
|
||||
}
|
||||
if _noun == "leggings":
|
||||
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
|
||||
|
|
@ -594,6 +594,7 @@ CRIT_MULT = 1.5
|
|||
RESPAWN = 2 # seconds after a kill before the next enemy turns up
|
||||
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
|
||||
EAT_DELAY = 1.5 # seconds each bite holds back your next swing
|
||||
|
||||
DAMAGE_TYPES = {
|
||||
"slash": {"name": "Slashing", "glyph": "sword-slice", "tint": "#d4dae2"},
|
||||
|
|
@ -647,7 +648,7 @@ def monster(name: str, archetype: str, level: int, attack, vulnerable=(), resist
|
|||
hp, interval, accuracy, evasion, hit, weight = ARCHETYPES[archetype]
|
||||
hp = round((20 + 6 * level) * hp * (2.5 if boss else 1))
|
||||
return {"name": name, "archetype": archetype, "level": level, "boss": boss, "weight": weight,
|
||||
"hp": hp, "interval": interval, "xp": round(hp * 0.4 + level * 1.5),
|
||||
"hp": hp, "interval": interval, "xp": round(hp * 0.13 + level * 0.5),
|
||||
"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))),
|
||||
"attack": _types(attack), "vulnerable": _types(vulnerable), "resists": _types(resists)}
|
||||
|
|
@ -687,7 +688,7 @@ GOBLINS = [("Goblin brute", "brute", "blunt", "blunt", "slash"),
|
|||
# Expedition areas, tiers II to VI: name, level, the reagent prefix, and four races.
|
||||
# Each race: (name, trophy, weapon line its trophy goes into, level offset, roster).
|
||||
AREAS = [
|
||||
("Old Watchtower", 10, "Watchtower", [
|
||||
("Old Watchtower", 15, "Watchtower", [
|
||||
("Clay golems", "Clay core", "hammer", 0, [
|
||||
("Clay brute", "brute", "blunt", "blunt", "pierce"),
|
||||
("Mud shaper", "caster", "magic", "pierce", "magic"),
|
||||
|
|
@ -705,7 +706,7 @@ AREAS = [
|
|||
("Harpy windcaster", "caster", "magic", ("slash", "pierce"), "magic"),
|
||||
("Harpy matriarch", "elite", "slash", "magic", "pierce")]),
|
||||
]),
|
||||
("Sunken Barrows", 20, "Barrow", [
|
||||
("Sunken Barrows", 30, "Barrow", [
|
||||
("Stone guardians", "Stone core", "hammer", 0, [
|
||||
("Stone guardian", "brute", "blunt", "blunt", "slash"),
|
||||
("Barrow sentry", "skirmisher", "pierce", "magic", "pierce"),
|
||||
|
|
@ -723,7 +724,7 @@ AREAS = [
|
|||
("Venom weaver", "caster", "magic", "blunt", "magic"),
|
||||
("Brood mother", "elite", "pierce", "magic", "blunt")]),
|
||||
]),
|
||||
("Iron Hills", 30, "Ironhill", [
|
||||
("Iron Hills", 45, "Ironhill", [
|
||||
("Iron golems", "Iron core", "hammer", 0, [
|
||||
("Iron golem", "brute", "blunt", "blunt", "slash"),
|
||||
("Forge golem", "caster", "magic", "pierce", "magic"),
|
||||
|
|
@ -741,7 +742,7 @@ AREAS = [
|
|||
("Wyvern hatchling", "skirmisher", "pierce", "blunt", "magic"),
|
||||
("Venomspitter", "caster", "magic", "slash", "pierce")]),
|
||||
]),
|
||||
("Frostpeak", 40, "Frostpeak", [
|
||||
("Frostpeak", 60, "Frostpeak", [
|
||||
("Ice elementals", "Frozen core", "hammer", 0, [
|
||||
("Ice brute", "brute", "blunt", "blunt", "pierce"),
|
||||
("Frost wisp", "skirmisher", "magic", "slash", "magic"),
|
||||
|
|
@ -759,7 +760,7 @@ AREAS = [
|
|||
("Griffin rider", "skirmisher", "pierce", "slash", "pierce"),
|
||||
("Storm griffin", "caster", "magic", "blunt", "magic")]),
|
||||
]),
|
||||
("Dragon's Spine", 50, "Dragonspine", [
|
||||
("Dragon's Spine", 75, "Dragonspine", [
|
||||
("Magma constructs", "Magma core", "hammer", 0, [
|
||||
("Magma brute", "brute", "blunt", "blunt", "slash"),
|
||||
("Cinder golem", "skirmisher", "pierce", "magic", "pierce"),
|
||||
|
|
@ -905,23 +906,23 @@ COMBAT_ACTIONS = [
|
|||
_encounter("fight_goblins", "Cave goblins", "Greenwood", 5, GOBLINS, gold=4,
|
||||
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
|
||||
*_area_fights(),
|
||||
_boss("fight_ruins", "Watchtower ogre", "Old Watchtower", 18, "boss", ("blunt", "magic"), ("slash", "magic"),
|
||||
_boss("fight_ruins", "Watchtower ogre", "Old Watchtower", 26, "boss", ("blunt", "magic"), ("slash", "magic"),
|
||||
("blunt", "pierce"), gold=25,
|
||||
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_drops(1)]),
|
||||
_boss("fight_barrow_king", "Barrow king", "Sunken Barrows", 28, "boss", ("slash", "magic"), ("blunt", "magic"),
|
||||
_boss("fight_barrow_king", "Barrow king", "Sunken Barrows", 41, "boss", ("slash", "magic"), ("blunt", "magic"),
|
||||
("pierce", "slash"), gold=40,
|
||||
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_drops(2)]),
|
||||
# Dragons: the prize hunt, dropping their area's reagents far more often than anything else there.
|
||||
_boss("fight_green_dragon", "Green dragon", "Iron Hills", 38, "dragon", ("slash", "magic"), ("pierce", "magic"),
|
||||
_boss("fight_green_dragon", "Green dragon", "Iron Hills", 56, "dragon", ("slash", "magic"), ("pierce", "magic"),
|
||||
("slash", "blunt"), is_boss=False, gold=80,
|
||||
drops=[("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(3, 0.6)]),
|
||||
_boss("fight_blue_dragon", "Blue dragon", "Frostpeak", 48, "dragon", ("slash", "magic"), ("blunt", "pierce"),
|
||||
_boss("fight_blue_dragon", "Blue dragon", "Frostpeak", 71, "dragon", ("slash", "magic"), ("blunt", "pierce"),
|
||||
("magic", "slash"), is_boss=False, gold=110,
|
||||
drops=[("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(4, 0.6)]),
|
||||
_boss("fight_red_dragon", "Red dragon", "Dragon's Spine", 60, "dragon", ("slash", "magic"), ("slash", "magic"),
|
||||
_boss("fight_red_dragon", "Red dragon", "Dragon's Spine", 85, "dragon", ("slash", "magic"), ("slash", "magic"),
|
||||
("pierce", "blunt"), is_boss=False, gold=150,
|
||||
drops=[("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_drops(5, 0.6)]),
|
||||
_boss("fight_black_dragon", "Black dragon", "Dragon's Spine", 75, "dragon", ("slash", "magic"), ("blunt", "magic"),
|
||||
_boss("fight_black_dragon", "Black dragon", "Dragon's Spine", 95, "dragon", ("slash", "magic"), ("blunt", "magic"),
|
||||
("slash", "pierce"), gold=220,
|
||||
drops=[("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_drops(5, 0.8)]),
|
||||
]
|
||||
|
|
@ -1712,13 +1713,20 @@ def _average_hit(max_hit: float, crit: float = CRIT_CHANCE) -> float:
|
|||
return (1 - crit) * (max_hit + 1) / 2 + crit * max_hit * CRIT_MULT
|
||||
|
||||
|
||||
def _variant_odds(player: dict, foe: dict, blessed: dict, brew: dict) -> dict:
|
||||
def _variant_odds(player: dict, foe: dict, blessed: dict, brew: dict, bite: int = 0) -> dict:
|
||||
"""`bite` is what the next food heals: eating holds back your swings, so a fight you have
|
||||
to eat through runs longer."""
|
||||
me = fighter(player, blessed, brew, foe)
|
||||
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
|
||||
kill = foe["hp"] / (hit * _average_hit(me["max_hit"], me["crit"]) / me["interval"])
|
||||
taken = sum(_taken(me, kind) for kind in foe["attack"]) / len(foe["attack"])
|
||||
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * taken / foe["interval"] * kill
|
||||
hurt = max(0, hurt - blessed["mend"] * max_hp(player) * kill)
|
||||
per_second = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * taken / foe["interval"]
|
||||
per_second = max(0, per_second - blessed["mend"] * max_hp(player))
|
||||
if bite:
|
||||
eating = per_second * EAT_DELAY / bite # seconds spent eating per second of fighting
|
||||
kill = kill / (1 - eating) if eating < 1 else MAX_FIGHT
|
||||
kill = min(kill, MAX_FIGHT)
|
||||
hurt = per_second * kill
|
||||
return {"hit": hit, "their_hit": their_hit, "max_hit": me["max_hit"], "affinity": me["affinity"],
|
||||
"their_max": max(1, round(foe["max_hit"] * max(_taken(me, kind) for kind in foe["attack"]))),
|
||||
"kill_time": kill, "damage": hurt}
|
||||
|
|
@ -1728,7 +1736,9 @@ def fight_odds(player: dict, action: dict) -> dict:
|
|||
"""What a fight in this encounter looks like on average, for the cards, and against each variant."""
|
||||
blessed, brew = blessing(player), potion_effect(player, "combat")
|
||||
variants = action["enemy"]["variants"]
|
||||
rows = [_variant_odds(player, foe, blessed, brew) for foe in variants]
|
||||
pantry = edible(player)
|
||||
bite = ITEMS[pantry[0]]["heal"] if pantry else 0
|
||||
rows = [_variant_odds(player, foe, blessed, brew, bite) for foe in variants]
|
||||
total = sum(foe["weight"] for foe in variants)
|
||||
mean = lambda key: sum(row[key] * foe["weight"] for row, foe in zip(rows, variants)) / total
|
||||
return {"hit": round(mean("hit"), 2), "their_hit": round(mean("their_hit"), 2), "style": weapon_style(player),
|
||||
|
|
@ -1790,6 +1800,7 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
|
|||
pantry[food] -= 1
|
||||
eaten[food] = eaten.get(food, 0) + 1
|
||||
hp = min(top, hp + ITEMS[food]["heal"])
|
||||
mine += EAT_DELAY
|
||||
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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue