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
12
README.md
12
README.md
|
|
@ -19,10 +19,11 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
|
||||||
or crit. Every blow has a damage type (slashing, blunt, piercing, magic);
|
or crit. Every blow has a damage type (slashing, blunt, piercing, magic);
|
||||||
each enemy is vulnerable to some (+25%) and resists others (-25%), shown
|
each enemy is vulnerable to some (+25%) and resists others (-25%), shown
|
||||||
as tinted icons. Armour stops physical blows, ward stops magic. Below half
|
as tinted icons. Armour stops physical blows, ward stops magic. Below half
|
||||||
health you eat from your bank; out of food you retreat, losing nothing but
|
health you eat from your bank, and each bite holds back your next swing;
|
||||||
the fight. HP recovers 25% an hour. The Combat page replays the fight
|
out of food you retreat, losing nothing but the fight. HP recovers 25%
|
||||||
live. Fights are split into the Frontier (bandits, sabotage, drilling, the
|
an hour. The Combat page replays the fight live. Fights are split into
|
||||||
city's troubles) and expeditions: Greenwood to start, then one area per
|
the Frontier (bandits, sabotage, drilling, the city's troubles) and
|
||||||
|
expeditions: Greenwood to start, then one area per
|
||||||
tier (Old Watchtower, Sunken Barrows, Iron Hills, Frostpeak, Dragon's
|
tier (Old Watchtower, Sunken Barrows, Iron Hills, Frostpeak, Dragon's
|
||||||
Spine), each with four races and a boss (the dragons at the top). Every
|
Spine), each with four races and a boss (the dragons at the top). Every
|
||||||
kill is one of the race's variants (a brute, a skirmisher, a caster, now
|
kill is one of the race's variants (a brute, a skirmisher, a caster, now
|
||||||
|
|
@ -69,7 +70,8 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
|
||||||
helmet, greaves, cuirass and five tools; every leather tier a cap,
|
helmet, greaves, cuirass and five tools; every leather tier a cap,
|
||||||
trousers and jerkin; every cloth tier a hood, leggings and robe. Higher
|
trousers and jerkin; every cloth tier a hood, leggings and robe. Higher
|
||||||
bars need coal; each metal past copper needs a Smithy level (rune needs
|
bars need coal; each metal past copper needs a Smithy level (rune needs
|
||||||
level 5, so a city). Gear needs a combat level to wear, tools a skill
|
level 5, so a city). Gear needs the combat level of its tier (15, 30, 45,
|
||||||
|
60, 75) to wear, and the areas open at the same levels; tools need a skill
|
||||||
level to use.
|
level to use.
|
||||||
- **Classes are trades,** from gathering to finished goods, so each player has
|
- **Classes are trades,** from gathering to finished goods, so each player has
|
||||||
a quiet gathering side and a busier crafting side: miner (mining, smithing,
|
a quiet gathering side and a busier crafting side: miner (mining, smithing,
|
||||||
|
|
|
||||||
61
game.py
61
game.py
|
|
@ -80,8 +80,8 @@ ITEMS = {
|
||||||
# ladder generates its items and recipes below; the copper and iron ids
|
# ladder generates its items and recipes below; the copper and iron ids
|
||||||
# predate the ladders and are kept as they were.
|
# predate the ladders and are kept as they were.
|
||||||
|
|
||||||
TIER_LEVELS = [1, 15, 30, 45, 60, 75] # gathering / smelting level of each tier
|
TIER_LEVELS = [1, 15, 30, 45, 60, 75] # gathering / smelting level of each tier, and the combat level to wear it
|
||||||
EQUIP_LEVELS = [1, 10, 20, 30, 40, 50] # combat level to wear, skill level to use a tool
|
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
|
# key, name, ore (item id, name, icon, value), coal per bar, bar icon, bar value, smithy level, handle wood
|
||||||
METALS = [
|
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]}",
|
_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],
|
"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,
|
"value": round(_bars * _bar_value * 1.2), "slot": _slot, "tier": _tier,
|
||||||
"req": ["combat", EQUIP_LEVELS[_tier]]}
|
"req": ["combat", TIER_LEVELS[_tier]]}
|
||||||
if _power:
|
if _power:
|
||||||
_item["power"] = _power[_tier]
|
_item["power"] = _power[_tier]
|
||||||
if _armour and _piece == "shield":
|
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():
|
for _skill, (_tool, _icon) in TOOLS.items():
|
||||||
ITEMS[f"{_key}_{_tool.replace(' ', '_')}"] = {
|
ITEMS[f"{_key}_{_tool.replace(' ', '_')}"] = {
|
||||||
"name": f"{_name} {_tool}", "icon": _icon, "value": round(2 * _bar_value * 1.4), "tier": _tier,
|
"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.
|
# 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
|
# 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],
|
"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),
|
"value": round((_bands * ITEMS[BANDS[_tier]]["value"] + _gems * _gem_value + _shards * 12 * _tier) * 1.3),
|
||||||
"slot": _slot, "tier": _tier, "jewel": _stat, _stat: _bonus[_tier],
|
"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
|
# Hardware: what the smith makes for builders, one kind per metal, used in
|
||||||
# fittings and in building levels in place of plain bars.
|
# fittings and in building levels in place of plain bars.
|
||||||
# id, singular, plural, bars used, pieces made
|
# 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):
|
for _tier, _metal in enumerate(METALS):
|
||||||
ITEMS[f"{_metal[0]}_sword"]["style"] = "sword"
|
ITEMS[f"{_metal[0]}_sword"]["style"] = "sword"
|
||||||
_weapon = {"slot": "weapon", "power": TWO_HANDED_POWER[_tier], "tier": _tier, "two_handed": True,
|
_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"{_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"),
|
ITEMS[f"{WOODS[_tier]}_staff"] = {"name": STAVES[_tier], "plural": STAVES[_tier].replace("staff", "staves"),
|
||||||
"icon": "🪄", "style": "staff", **_weapon}
|
"icon": "🪄", "style": "staff", **_weapon}
|
||||||
|
|
@ -321,7 +321,7 @@ for _tier, _wood in enumerate(WOODS):
|
||||||
for _skill, (_suffix, _noun, _glyph, _kind) in STATIONS.items():
|
for _skill, (_suffix, _noun, _glyph, _kind) in STATIONS.items():
|
||||||
ITEMS[f"{_wood}_{_suffix}"] = {
|
ITEMS[f"{_wood}_{_suffix}"] = {
|
||||||
"name": f"{WOOD_NAMES[_tier]} {_noun}", "icon": "🪚", "tier": _tier, "slot": f"station_{_skill}", "station": _skill,
|
"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",
|
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}
|
"icon": "🧰", "tier": _tier, "slot": "storage", "slots": STRONGBOX_SLOTS[_tier], "value": 0}
|
||||||
for _tier, (_fibre, _level, _time, _xp, _cloth, _prefix, _label) in enumerate(CLOTHS):
|
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:
|
for _suffix, _slot, _noun, _used, _offset, _icon in LEATHER_PIECES:
|
||||||
ITEMS[f"{_prefix}_{_suffix}"] = {
|
ITEMS[f"{_prefix}_{_suffix}"] = {
|
||||||
"name": f"{_label} {_noun}", "icon": _icon, "value": round(_used * _leather[2] * 1.3),
|
"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":
|
if _noun == "trousers":
|
||||||
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
|
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:
|
for _suffix, _slot, _noun, _used, _offset, _icon in CLOTH_PIECES:
|
||||||
ITEMS[f"{_prefix}_{_suffix}"] = {
|
ITEMS[f"{_prefix}_{_suffix}"] = {
|
||||||
"name": f"{_label} {_noun}", "icon": _icon, "value": round(_used * _cloth[2] * 1.3),
|
"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":
|
if _noun == "leggings":
|
||||||
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
|
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
|
RESPAWN = 2 # seconds after a kill before the next enemy turns up
|
||||||
MAX_FIGHT = 600 # a fight that drags on longer is broken off
|
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
|
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 = {
|
DAMAGE_TYPES = {
|
||||||
"slash": {"name": "Slashing", "glyph": "sword-slice", "tint": "#d4dae2"},
|
"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, interval, accuracy, evasion, hit, weight = ARCHETYPES[archetype]
|
||||||
hp = round((20 + 6 * level) * hp * (2.5 if boss else 1))
|
hp = round((20 + 6 * level) * hp * (2.5 if boss else 1))
|
||||||
return {"name": name, "archetype": archetype, "level": level, "boss": boss, "weight": weight,
|
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),
|
"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))),
|
"max_hit": max(1, round((2 + level / 3) * hit * (1.2 if boss else 1))),
|
||||||
"attack": _types(attack), "vulnerable": _types(vulnerable), "resists": _types(resists)}
|
"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.
|
# 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).
|
# Each race: (name, trophy, weapon line its trophy goes into, level offset, roster).
|
||||||
AREAS = [
|
AREAS = [
|
||||||
("Old Watchtower", 10, "Watchtower", [
|
("Old Watchtower", 15, "Watchtower", [
|
||||||
("Clay golems", "Clay core", "hammer", 0, [
|
("Clay golems", "Clay core", "hammer", 0, [
|
||||||
("Clay brute", "brute", "blunt", "blunt", "pierce"),
|
("Clay brute", "brute", "blunt", "blunt", "pierce"),
|
||||||
("Mud shaper", "caster", "magic", "pierce", "magic"),
|
("Mud shaper", "caster", "magic", "pierce", "magic"),
|
||||||
|
|
@ -705,7 +706,7 @@ AREAS = [
|
||||||
("Harpy windcaster", "caster", "magic", ("slash", "pierce"), "magic"),
|
("Harpy windcaster", "caster", "magic", ("slash", "pierce"), "magic"),
|
||||||
("Harpy matriarch", "elite", "slash", "magic", "pierce")]),
|
("Harpy matriarch", "elite", "slash", "magic", "pierce")]),
|
||||||
]),
|
]),
|
||||||
("Sunken Barrows", 20, "Barrow", [
|
("Sunken Barrows", 30, "Barrow", [
|
||||||
("Stone guardians", "Stone core", "hammer", 0, [
|
("Stone guardians", "Stone core", "hammer", 0, [
|
||||||
("Stone guardian", "brute", "blunt", "blunt", "slash"),
|
("Stone guardian", "brute", "blunt", "blunt", "slash"),
|
||||||
("Barrow sentry", "skirmisher", "pierce", "magic", "pierce"),
|
("Barrow sentry", "skirmisher", "pierce", "magic", "pierce"),
|
||||||
|
|
@ -723,7 +724,7 @@ AREAS = [
|
||||||
("Venom weaver", "caster", "magic", "blunt", "magic"),
|
("Venom weaver", "caster", "magic", "blunt", "magic"),
|
||||||
("Brood mother", "elite", "pierce", "magic", "blunt")]),
|
("Brood mother", "elite", "pierce", "magic", "blunt")]),
|
||||||
]),
|
]),
|
||||||
("Iron Hills", 30, "Ironhill", [
|
("Iron Hills", 45, "Ironhill", [
|
||||||
("Iron golems", "Iron core", "hammer", 0, [
|
("Iron golems", "Iron core", "hammer", 0, [
|
||||||
("Iron golem", "brute", "blunt", "blunt", "slash"),
|
("Iron golem", "brute", "blunt", "blunt", "slash"),
|
||||||
("Forge golem", "caster", "magic", "pierce", "magic"),
|
("Forge golem", "caster", "magic", "pierce", "magic"),
|
||||||
|
|
@ -741,7 +742,7 @@ AREAS = [
|
||||||
("Wyvern hatchling", "skirmisher", "pierce", "blunt", "magic"),
|
("Wyvern hatchling", "skirmisher", "pierce", "blunt", "magic"),
|
||||||
("Venomspitter", "caster", "magic", "slash", "pierce")]),
|
("Venomspitter", "caster", "magic", "slash", "pierce")]),
|
||||||
]),
|
]),
|
||||||
("Frostpeak", 40, "Frostpeak", [
|
("Frostpeak", 60, "Frostpeak", [
|
||||||
("Ice elementals", "Frozen core", "hammer", 0, [
|
("Ice elementals", "Frozen core", "hammer", 0, [
|
||||||
("Ice brute", "brute", "blunt", "blunt", "pierce"),
|
("Ice brute", "brute", "blunt", "blunt", "pierce"),
|
||||||
("Frost wisp", "skirmisher", "magic", "slash", "magic"),
|
("Frost wisp", "skirmisher", "magic", "slash", "magic"),
|
||||||
|
|
@ -759,7 +760,7 @@ AREAS = [
|
||||||
("Griffin rider", "skirmisher", "pierce", "slash", "pierce"),
|
("Griffin rider", "skirmisher", "pierce", "slash", "pierce"),
|
||||||
("Storm griffin", "caster", "magic", "blunt", "magic")]),
|
("Storm griffin", "caster", "magic", "blunt", "magic")]),
|
||||||
]),
|
]),
|
||||||
("Dragon's Spine", 50, "Dragonspine", [
|
("Dragon's Spine", 75, "Dragonspine", [
|
||||||
("Magma constructs", "Magma core", "hammer", 0, [
|
("Magma constructs", "Magma core", "hammer", 0, [
|
||||||
("Magma brute", "brute", "blunt", "blunt", "slash"),
|
("Magma brute", "brute", "blunt", "blunt", "slash"),
|
||||||
("Cinder golem", "skirmisher", "pierce", "magic", "pierce"),
|
("Cinder golem", "skirmisher", "pierce", "magic", "pierce"),
|
||||||
|
|
@ -905,23 +906,23 @@ COMBAT_ACTIONS = [
|
||||||
_encounter("fight_goblins", "Cave goblins", "Greenwood", 5, GOBLINS, gold=4,
|
_encounter("fight_goblins", "Cave goblins", "Greenwood", 5, GOBLINS, gold=4,
|
||||||
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
|
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
|
||||||
*_area_fights(),
|
*_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,
|
("blunt", "pierce"), gold=25,
|
||||||
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_drops(1)]),
|
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,
|
("pierce", "slash"), gold=40,
|
||||||
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_drops(2)]),
|
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.
|
# 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,
|
("slash", "blunt"), is_boss=False, gold=80,
|
||||||
drops=[("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(3, 0.6)]),
|
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,
|
("magic", "slash"), is_boss=False, gold=110,
|
||||||
drops=[("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(4, 0.6)]),
|
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,
|
("pierce", "blunt"), is_boss=False, gold=150,
|
||||||
drops=[("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_drops(5, 0.6)]),
|
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,
|
("slash", "pierce"), gold=220,
|
||||||
drops=[("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_drops(5, 0.8)]),
|
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
|
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)
|
me = fighter(player, blessed, brew, foe)
|
||||||
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
|
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"])
|
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"])
|
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
|
per_second = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * taken / foe["interval"]
|
||||||
hurt = max(0, hurt - blessed["mend"] * max_hp(player) * kill)
|
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"],
|
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"]))),
|
"their_max": max(1, round(foe["max_hit"] * max(_taken(me, kind) for kind in foe["attack"]))),
|
||||||
"kill_time": kill, "damage": hurt}
|
"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."""
|
"""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")
|
blessed, brew = blessing(player), potion_effect(player, "combat")
|
||||||
variants = action["enemy"]["variants"]
|
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)
|
total = sum(foe["weight"] for foe in variants)
|
||||||
mean = lambda key: sum(row[key] * foe["weight"] for row, foe in zip(rows, variants)) / total
|
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),
|
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
|
pantry[food] -= 1
|
||||||
eaten[food] = eaten.get(food, 0) + 1
|
eaten[food] = eaten.get(food, 0) + 1
|
||||||
hp = min(top, hp + ITEMS[food]["heal"])
|
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])
|
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())
|
beaten, starving = hp <= 0, hp < top * FLEE_BELOW and not any(pantry.values())
|
||||||
if (beaten or starving) and wind:
|
if (beaten or starving) and wind:
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,20 @@ class CombatTest(unittest.TestCase):
|
||||||
self.assertLess(p["hp"], game.max_hp(p) * game.TOO_HURT)
|
self.assertLess(p["hp"], game.max_hp(p) * game.TOO_HURT)
|
||||||
self.assertEqual(game.start_action(world, p, "fight_scouts", T0 + 6 * 3600), "Too hurt to fight. Rest, or bring food")
|
self.assertEqual(game.start_action(world, p, "fight_scouts", T0 + 6 * 3600), "Too hurt to fight. Rest, or bring food")
|
||||||
|
|
||||||
|
def test_eating_holds_back_your_swings(self):
|
||||||
|
world, p = self.fighter(level=15)
|
||||||
|
p["bank"] = {"cooked_shrimp": 100}
|
||||||
|
p["hp"] = 1
|
||||||
|
fight = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_clay_golems"], random.Random(3))
|
||||||
|
mine = [event for event in fight["events"] if event[1] == "you"]
|
||||||
|
bite = next(i for i, event in enumerate(mine) if event[2] == "eat")
|
||||||
|
swing = next(event for event in mine[bite:] if event[2] != "eat")
|
||||||
|
self.assertGreaterEqual(swing[0] - mine[bite][0], game.EAT_DELAY)
|
||||||
|
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||||||
|
eating = game.fight_odds(p, scouts)["kill_time"]
|
||||||
|
p["bank"] = {}
|
||||||
|
self.assertGreater(eating, game.fight_odds(p, scouts)["kill_time"])
|
||||||
|
|
||||||
def test_rest_heals(self):
|
def test_rest_heals(self):
|
||||||
world, p = self.fighter()
|
world, p = self.fighter()
|
||||||
p["hp"] = 1
|
p["hp"] = 1
|
||||||
|
|
@ -753,10 +767,10 @@ class TierTest(unittest.TestCase):
|
||||||
def test_gear_needs_levels_to_wear(self):
|
def test_gear_needs_levels_to_wear(self):
|
||||||
world = world_with(("wil", "commoner"))
|
world = world_with(("wil", "commoner"))
|
||||||
p = world["players"]["wil"]
|
p = world["players"]["wil"]
|
||||||
self.assertEqual(game.equip_problem(p, "rune_sword"), "Needs combat level 50")
|
self.assertEqual(game.equip_problem(p, "rune_sword"), "Needs combat level 75")
|
||||||
self.assertEqual(game.equip_problem(p, "steel_pickaxe"), "Needs mining level 20")
|
self.assertEqual(game.equip_problem(p, "steel_pickaxe"), "Needs mining level 20")
|
||||||
self.assertIsNone(game.equip_problem(p, "copper_sword"))
|
self.assertIsNone(game.equip_problem(p, "copper_sword"))
|
||||||
p["xp"]["combat"] = game.XP_TABLE[50]
|
p["xp"]["combat"] = game.XP_TABLE[75]
|
||||||
self.assertIsNone(game.equip_problem(p, "rune_sword"))
|
self.assertIsNone(game.equip_problem(p, "rune_sword"))
|
||||||
|
|
||||||
def test_militia_takes_the_best_weapons_first(self):
|
def test_militia_takes_the_best_weapons_first(self):
|
||||||
|
|
@ -946,9 +960,10 @@ class HerbloreTest(unittest.TestCase):
|
||||||
game.use_potion(p, "defence_potion_6")
|
game.use_potion(p, "defence_potion_6")
|
||||||
game.start_action(world, p, "fight_scouts", T0)
|
game.start_action(world, p, "fight_scouts", T0)
|
||||||
scouts = game.ACTION_BY_ID["fight_scouts"]
|
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||||||
brewed = game.fight_odds(p, scouts)["damage"]
|
per_second = lambda odds: odds["damage"] / odds["kill_time"] # less eating also shortens the fight
|
||||||
|
brewed = per_second(game.fight_odds(p, scouts))
|
||||||
active = p["potions"].pop("combat")
|
active = p["potions"].pop("combat")
|
||||||
self.assertAlmostEqual(brewed, game.fight_odds(p, scouts)["damage"] * 0.70, delta=0.2)
|
self.assertAlmostEqual(brewed, per_second(game.fight_odds(p, scouts)) * 0.70, delta=0.02)
|
||||||
p["potions"]["combat"] = active
|
p["potions"]["combat"] = active
|
||||||
summary = game.process_player(world, p, T0 + 75 * 10, random.Random(1))
|
summary = game.process_player(world, p, T0 + 75 * 10, random.Random(1))
|
||||||
self.assertEqual(p["potions"]["combat"]["charges"], game.POTION_CHARGES - summary["done"])
|
self.assertEqual(p["potions"]["combat"]["charges"], game.POTION_CHARGES - summary["done"])
|
||||||
|
|
@ -1364,7 +1379,7 @@ class TradeSkillsTest(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class WeaponStyleTest(unittest.TestCase):
|
class WeaponStyleTest(unittest.TestCase):
|
||||||
def armed(self, cls, weapon, shield=None, level=12):
|
def armed(self, cls, weapon, shield=None, level=15):
|
||||||
world = world_with(("pat", cls))
|
world = world_with(("pat", cls))
|
||||||
p = world["players"]["pat"]
|
p = world["players"]["pat"]
|
||||||
p["xp"]["combat"] = game.XP_TABLE[level]
|
p["xp"]["combat"] = game.XP_TABLE[level]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue