Combat rework: damage types, races with variants, armour weights, tailoring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a35bcd6512
commit
ba7556de4d
7 changed files with 715 additions and 254 deletions
72
README.md
72
README.md
|
|
@ -10,42 +10,56 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
|
||||||
## What's in it
|
## What's in it
|
||||||
|
|
||||||
- **Skills:** mining, woodcutting, farming, hunting, fishing, smithing,
|
- **Skills:** mining, woodcutting, farming, hunting, fishing, smithing,
|
||||||
construction (with masonry), cooking, leatherworking, herblore, combat,
|
construction (with masonry), cooking, leatherworking, tailoring, herblore,
|
||||||
prayer and reconnaissance. Smiths also make hardware for builders, one kind
|
combat, prayer and reconnaissance. Smiths also make hardware for builders,
|
||||||
per metal (copper rivets to rune locks), which fittings and buildings need.
|
one kind per metal (copper rivets to rune locks), which fittings and
|
||||||
|
buildings need.
|
||||||
- **Combat:** every fight is played out swing by swing: you and the enemy
|
- **Combat:** every fight is played out swing by swing: you and the enemy
|
||||||
each have HP, a swing timer, accuracy and a max hit, and swings hit, miss
|
each have HP, a swing timer, accuracy and a max hit, and swings hit, miss
|
||||||
or crit (power from level and gear aims and hits; armour dodges and soaks).
|
or crit. Every blow has a damage type (slashing, blunt, piercing, magic);
|
||||||
Below half health you eat from your bank; out of food you retreat, losing
|
each enemy is vulnerable to some (+25%) and resists others (-25%), shown
|
||||||
nothing but the fight. HP recovers 25% an hour. The Combat page replays
|
as tinted icons. Armour stops physical blows, ward stops magic. Below half
|
||||||
the fight live. Fights are split into the Frontier (bandits, sabotage,
|
health you eat from your bank; out of food you retreat, losing nothing but
|
||||||
drilling, the city's troubles) and expeditions: Greenwood to start, then
|
the fight. HP recovers 25% an hour. The Combat page replays the fight
|
||||||
one area per tier (Old Watchtower, Sunken Barrows, Iron Hills, Frostpeak,
|
live. Fights are split into the Frontier (bandits, sabotage, drilling, the
|
||||||
Dragon's Spine), each with a golem, a troll, a spirit and a flyer that drop
|
city's troubles) and expeditions: Greenwood to start, then one area per
|
||||||
rare trophies (3%, 6% from bosses) for stronger weapons, and a boss (the
|
tier (Old Watchtower, Sunken Barrows, Iron Hills, Frostpeak, Dragon's
|
||||||
dragons at the top). Each kill is followed by a 2-second pause.
|
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
|
||||||
|
and then a rare elite), so each encounter mixes damage types: spirit races
|
||||||
|
lean on magic, golems and trolls on blows. Races drop rare trophies (3%,
|
||||||
|
6% from bosses) for stronger weapons and the area's armour reagents (10%,
|
||||||
|
far more from dragons). Each kill is followed by a 2-second pause.
|
||||||
- **Weapons:** four styles, one per trade, and anyone can use any of them.
|
- **Weapons:** four styles, one per trade, and anyone can use any of them.
|
||||||
Sword and shield (2.4s swings, the shield blocks 15% of hits; swords from
|
Sword and shield (slashing, 2.4s swings, the shield blocks 15% of hits;
|
||||||
Smithing, shields from Construction), warhammer (Smithing; slow, heavy,
|
swords from Smithing, shields from Construction), warhammer (blunt,
|
||||||
twice the crits), staff (Herblore; accurate, ignores armour) and bow
|
Smithing; slow, heavy, twice the crits), staff (magic, Herblore;
|
||||||
(Leatherworking; fast, strikes first). The last three take both hands and
|
accurate) and bow (piercing, Leatherworking; fast, strikes first). The
|
||||||
carry a shield's power. Your trade's weapon does 15% more damage. Golems
|
last three take both hands and carry a shield's power. Your trade's weapon
|
||||||
break under hammers, trolls fall to swords, spirits only fear staves, and
|
does 15% more damage. Past copper, each weapon needs its tier's trophies.
|
||||||
flyers fall to bows. Past copper, each weapon needs its tier's trophies.
|
- **Armour:** three weights. Heavy plate (Smithing) is strong against blows
|
||||||
|
and weak to magic, light cloth (Tailoring) the other way round, medium
|
||||||
|
leather (Leatherworking) a bit of both. Every trade gathers its own base
|
||||||
|
material at every tier (ore, hunted hides, farmed fibres), and past copper
|
||||||
|
each piece also needs reagents from that tier's area: plating, lacing or
|
||||||
|
thread.
|
||||||
- **Tiers:** six RuneScape-style tiers (copper, iron, steel, mithril,
|
- **Tiers:** six RuneScape-style tiers (copper, iron, steel, mithril,
|
||||||
adamant, rune at levels 1–75) of ore, trees (up to enchanted) and hides (deer,
|
adamant, rune at levels 1–75) of ore, trees (up to enchanted), hides (deer,
|
||||||
boar and bear from hunting; green, blue and red dragonhide from fighting
|
boar, bear, elk, sabrecat, mammoth) and fibres (flax, cotton, silk,
|
||||||
dragons). Every metal tier has a sword, shield, helmet, greaves, cuirass and
|
moonpetal, starbloom, spiritvine). Every metal tier has a sword, shield,
|
||||||
five tools; every leather tier a cap, trousers and jerkin. Higher bars need
|
helmet, greaves, cuirass and five tools; every leather tier a cap,
|
||||||
coal; each metal past copper needs a Smithy level (rune needs level 5, so a
|
trousers and jerkin; every cloth tier a hood, leggings and robe. Higher
|
||||||
city). Gear needs a combat level to wear, tools a skill level to use.
|
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 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),
|
||||||
builder (woodcutting, construction), homesteader (farming, cooking,
|
builder (woodcutting, construction), homesteader (farming, cooking,
|
||||||
herblore) and ranger (hunting, fishing, leatherworking, cooking). A trade's
|
herblore, tailoring) and ranger (hunting, fishing, leatherworking,
|
||||||
skills are 20% faster, 1 in 10 of its crafts make double, and each has one
|
cooking). A trade's skills are 20% faster, 1 in 10 of its crafts make
|
||||||
bonus of its own. Homesteaders farm differently: two plots, either one crop
|
double, and each has one bonus of its own. Homesteaders farm differently:
|
||||||
30% faster or two crops at once sharing a 20% faster pace. Every class can train every skill, scout and drill.
|
two plots, either one crop 30% faster or two crops at once sharing a 20%
|
||||||
|
faster pace. Every class can train every skill, scout and drill.
|
||||||
Building costs draw on all four trades: thatch, flax for rope, hide glue,
|
Building costs draw on all four trades: thatch, flax for rope, hide glue,
|
||||||
leather and provisions as well as wood, stone and metal.
|
leather and provisions as well as wood, stone and metal.
|
||||||
- **Bank:** 12 slots of 1,000 each to start; more than a stack of one item
|
- **Bank:** 12 slots of 1,000 each to start; more than a stack of one item
|
||||||
|
|
|
||||||
3
app.py
3
app.py
|
|
@ -98,6 +98,8 @@ def player_view(world: dict, player: dict, now: float) -> dict:
|
||||||
view["max_hp"] = game.max_hp(player)
|
view["max_hp"] = game.max_hp(player)
|
||||||
view["armour"] = game.armour(player)
|
view["armour"] = game.armour(player)
|
||||||
view["reduction"] = round(game.damage_reduction(player), 3)
|
view["reduction"] = round(game.damage_reduction(player), 3)
|
||||||
|
view["ward"] = game.magic_ward(player)
|
||||||
|
view["ward_reduction"] = round(game.reduction(game.magic_ward(player)), 3)
|
||||||
view["edible"] = game.edible(player)
|
view["edible"] = game.edible(player)
|
||||||
view["blessing"] = game.blessing(player)
|
view["blessing"] = game.blessing(player)
|
||||||
view["fight_odds"] = {a["id"]: game.fight_odds(player, a) for a in game.ACTIONS if "enemy" in a}
|
view["fight_odds"] = {a["id"]: game.fight_odds(player, a) for a in game.ACTIONS if "enemy" in a}
|
||||||
|
|
@ -170,6 +172,7 @@ def roster(world: dict, now: float) -> list[dict]:
|
||||||
"xp": player["xp"], "equip": player["equip"], "joined": player["joined"],
|
"xp": player["xp"], "equip": player["equip"], "joined": player["joined"],
|
||||||
"hp": int(player["hp"]), "max_hp": game.max_hp(player), "power": game.combat_power(player),
|
"hp": int(player["hp"]), "max_hp": game.max_hp(player), "power": game.combat_power(player),
|
||||||
"armour": game.armour(player), "reduction": round(game.damage_reduction(player), 3),
|
"armour": game.armour(player), "reduction": round(game.damage_reduction(player), 3),
|
||||||
|
"ward": game.magic_ward(player), "ward_reduction": round(game.reduction(game.magic_ward(player)), 3),
|
||||||
"favour": player["favour"],
|
"favour": player["favour"],
|
||||||
"id": player["id"], "name": player["name"], "cls": player["cls"],
|
"id": player["id"], "name": player["name"], "cls": player["cls"],
|
||||||
"total": sum(game.skill_level(player, skill) for skill in game.SKILL_IDS),
|
"total": sum(game.skill_level(player, skill) for skill in game.SKILL_IDS),
|
||||||
|
|
|
||||||
691
game.py
691
game.py
|
|
@ -64,8 +64,8 @@ ITEMS = {
|
||||||
"bones": {"name": "Bones", "icon": "🦴", "value": 1},
|
"bones": {"name": "Bones", "icon": "🦴", "value": 1},
|
||||||
"big_bones": {"name": "Big bones", "icon": "🦴", "value": 4},
|
"big_bones": {"name": "Big bones", "icon": "🦴", "value": 4},
|
||||||
"dragon_bones": {"name": "Dragon bones", "icon": "🦴", "value": 15},
|
"dragon_bones": {"name": "Dragon bones", "icon": "🦴", "value": 15},
|
||||||
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 6,
|
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 5, "ward": 5,
|
||||||
"req": ["combat", 10]},
|
"weight": "medium", "req": ["combat", 10]},
|
||||||
# Building materials from every trade: thatch and flax from the fields, rope
|
# Building materials from every trade: thatch and flax from the fields, rope
|
||||||
# twisted from flax, glue boiled from hides, and provisions to feed the builders.
|
# twisted from flax, glue boiled from hides, and provisions to feed the builders.
|
||||||
"thatch": {"name": "Thatch", "icon": "🌾", "value": 2},
|
"thatch": {"name": "Thatch", "icon": "🌾", "value": 2},
|
||||||
|
|
@ -100,6 +100,23 @@ PIECES = [
|
||||||
("legs", "legs", "greaves", 4, 9, None, [3, 6, 9, 12, 16, 21]),
|
("legs", "legs", "greaves", 4, 9, None, [3, 6, 9, 12, 16, 21]),
|
||||||
("body", "body", "cuirass", 5, 11, None, [4, 8, 12, 16, 22, 28]),
|
("body", "body", "cuirass", 5, 11, None, [4, 8, 12, 16, 22, 28]),
|
||||||
]
|
]
|
||||||
|
# Armour weights: a piece's rating, split into armour (against physical blows) and
|
||||||
|
# ward (against magic). Heavy plate from Smithing, medium leather from
|
||||||
|
# Leatherworking, light cloth from Tailoring; each adds up to about the same.
|
||||||
|
WEIGHTS = {
|
||||||
|
"heavy": {"name": "Heavy", "armour": 1.0, "ward": 0.2},
|
||||||
|
"medium": {"name": "Medium", "armour": 0.6, "ward": 0.6},
|
||||||
|
"light": {"name": "Light", "armour": 0.2, "ward": 1.0},
|
||||||
|
}
|
||||||
|
ARMOUR_RATING = {slot: rating for piece, slot, _noun, _bars, _offset, _power, rating in PIECES if piece != "shield" and rating}
|
||||||
|
|
||||||
|
|
||||||
|
def _armour_stats(weight: str, slot: str, tier: int) -> dict:
|
||||||
|
rating = ARMOUR_RATING[slot][tier]
|
||||||
|
return {"armour": round(rating * WEIGHTS[weight]["armour"]), "ward": round(rating * WEIGHTS[weight]["ward"]),
|
||||||
|
"weight": weight}
|
||||||
|
|
||||||
|
|
||||||
PLURALS = {"sword": "swords", "shield": "shields", "helmet": "helmets", "greaves": "greaves", "cuirass": "cuirasses"}
|
PLURALS = {"sword": "swords", "shield": "shields", "helmet": "helmets", "greaves": "greaves", "cuirass": "cuirasses"}
|
||||||
PIECE_ICONS = {"sword": "⚔️", "shield": "🛡️", "helmet": "🪖", "legs": "🦿", "body": "🦾"}
|
PIECE_ICONS = {"sword": "⚔️", "shield": "🛡️", "helmet": "🪖", "legs": "🦿", "body": "🦾"}
|
||||||
COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body": "🥋"}
|
COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body": "🥋"}
|
||||||
|
|
@ -118,22 +135,34 @@ TREES = [
|
||||||
("magic_logs", "Enchanted logs", "🔮", 55, "cut_magic", "Cut enchanted trees"),
|
("magic_logs", "Enchanted logs", "🔮", 55, "cut_magic", "Cut enchanted trees"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# raw hide (id, name, icon, value), leather (id, name, value), armour id prefix, armour name prefix,
|
# Every tier's hide comes from Hunting. raw hide (id, name, icon, value), leather (id, name, value),
|
||||||
# leatherworking level, armour by piece
|
# armour id prefix, armour name prefix, leatherworking level
|
||||||
LEATHERS = [
|
LEATHERS = [
|
||||||
(("hide", "Hide", "🟫", 3), ("leather", "Leather", 5), "leather", "Leather", 1, (1, 2, 3)),
|
(("hide", "Hide", "🟫", 3), ("leather", "Leather", 5), "leather", "Leather", 1),
|
||||||
(("boar_hide", "Boar hide", "🐗", 8), ("hard_leather", "Hard leather", 12), "hard_leather", "Hard leather", 20, (2, 4, 6)),
|
(("boar_hide", "Boar hide", "🐗", 8), ("hard_leather", "Hard leather", 12), "hard_leather", "Hard leather", 20),
|
||||||
(("bear_pelt", "Bear pelt", "🐻", 16), ("bear_leather", "Bear leather", 24), "bear_leather", "Bear leather", 35, (4, 6, 9)),
|
(("bear_pelt", "Bear pelt", "🐻", 16), ("bear_leather", "Bear leather", 24), "bear_leather", "Bear leather", 35),
|
||||||
(("green_dragonhide", "Green dragonhide", "🐉", 35), ("green_dragon_leather", "Green dragon leather", 50),
|
(("elk_hide", "Elk hide", "🦌", 35), ("elk_leather", "Elk leather", 50), "elk_leather", "Elk leather", 50),
|
||||||
"green_dhide", "Green dragonhide", 50, (6, 9, 13)),
|
(("sabrecat_pelt", "Sabrecat pelt", "🐆", 60), ("sabrecat_leather", "Sabrecat leather", 85),
|
||||||
(("blue_dragonhide", "Blue dragonhide", "🐲", 60), ("blue_dragon_leather", "Blue dragon leather", 85),
|
"sabrecat_leather", "Sabrecat leather", 65),
|
||||||
"blue_dhide", "Blue dragonhide", 65, (8, 12, 17)),
|
(("mammoth_hide", "Mammoth hide", "🦣", 100), ("mammoth_leather", "Mammoth leather", 140),
|
||||||
(("red_dragonhide", "Red dragonhide", "🔥", 100), ("red_dragon_leather", "Red dragon leather", 140),
|
"mammoth_leather", "Mammoth leather", 80),
|
||||||
"red_dhide", "Red dragonhide", 80, (10, 15, 21)),
|
|
||||||
]
|
]
|
||||||
# piece suffix, slot, noun, leather used, level above the tier, icon
|
# piece suffix, slot, noun, leather used, level above the tier, icon
|
||||||
LEATHER_PIECES = [("cap", "helmet", "cap", 2, 2, "🧢"), ("legs", "legs", "trousers", 3, 7, "👖"),
|
LEATHER_PIECES = [("cap", "helmet", "cap", 2, 2, "🧢"), ("legs", "legs", "trousers", 3, 7, "👖"),
|
||||||
("body", "body", "jerkin", 4, 11, "🦺")]
|
("body", "body", "jerkin", 4, 11, "🦺")]
|
||||||
|
# Tailoring: a fibre from Farming, the cloth woven from it, and the light armour sewn from that.
|
||||||
|
# fibre (id, name, icon, value), farming level, seconds, xp, cloth (id, name, value), armour id prefix, armour name prefix
|
||||||
|
CLOTHS = [
|
||||||
|
(("flax", "Flax", "🌿", 2), 1, 35, 11, ("linen", "Linen", 5), "linen", "Linen"),
|
||||||
|
(("cotton", "Cotton", "☁️", 4), 15, 45, 20, ("cotton_cloth", "Cotton cloth", 12), "cotton", "Cotton"),
|
||||||
|
(("silk_cocoons", "Silk cocoons", "🐛", 8), 30, 55, 32, ("silk", "Silk", 24), "silk", "Silk"),
|
||||||
|
(("moonpetal", "Moonpetal", "🌙", 15), 45, 60, 45, ("moonweave", "Moonweave", 45), "moonweave", "Moonweave"),
|
||||||
|
(("starbloom", "Starbloom", "⭐", 28), 60, 70, 60, ("starweave", "Starweave", 80), "starweave", "Starweave"),
|
||||||
|
(("spiritvine", "Spiritvine", "🌀", 50), 75, 80, 80, ("spiritweave", "Spiritweave", 135), "spiritweave", "Spiritweave"),
|
||||||
|
]
|
||||||
|
FIBRE_VERBS = {"silk_cocoons": "Raise silkworms"}
|
||||||
|
CLOTH_PIECES = [("hood", "helmet", "hood", 2, 2, "🧢"), ("legs", "legs", "leggings", 3, 7, "👖"),
|
||||||
|
("body", "body", "robe", 4, 11, "👘")]
|
||||||
|
|
||||||
# raw (id, name, icon, value), cooked (id, name, icon, value, heal, food), fishing level, fish action, verb
|
# raw (id, name, icon, value), cooked (id, name, icon, value, heal, food), fishing level, fish action, verb
|
||||||
FISH = [
|
FISH = [
|
||||||
|
|
@ -159,8 +188,10 @@ for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in
|
||||||
"req": ["combat", EQUIP_LEVELS[_tier]]}
|
"req": ["combat", EQUIP_LEVELS[_tier]]}
|
||||||
if _power:
|
if _power:
|
||||||
_item["power"] = _power[_tier]
|
_item["power"] = _power[_tier]
|
||||||
if _armour:
|
if _armour and _piece == "shield":
|
||||||
_item["armour"] = _armour[_tier]
|
_item["armour"] = _armour[_tier]
|
||||||
|
elif _armour:
|
||||||
|
_item.update(_armour_stats("heavy", _slot, _tier))
|
||||||
ITEMS[f"{_key}_{_piece}"] = {k: v for k, v in _item.items() if v is not None}
|
ITEMS[f"{_key}_{_piece}"] = {k: v for k, v in _item.items() if v is not None}
|
||||||
for _skill, (_tool, _icon) in TOOLS.items():
|
for _skill, (_tool, _icon) in TOOLS.items():
|
||||||
ITEMS[f"{_key}_{_tool.replace(' ', '_')}"] = {
|
ITEMS[f"{_key}_{_tool.replace(' ', '_')}"] = {
|
||||||
|
|
@ -202,16 +233,26 @@ for _tier, _metal in enumerate(METALS):
|
||||||
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}
|
||||||
ITEMS[f"{WOODS[_tier]}_bow"] = {"name": BOWS[_tier], "icon": "🏹", "style": "bow", **_weapon}
|
ITEMS[f"{WOODS[_tier]}_bow"] = {"name": BOWS[_tier], "icon": "🏹", "style": "bow", **_weapon}
|
||||||
for _tier, (_raw, _leather, _prefix, _label, _level, _armour) in enumerate(LEATHERS):
|
for _tier, (_raw, _leather, _prefix, _label, _level) in enumerate(LEATHERS):
|
||||||
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
|
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
|
||||||
ITEMS[_leather[0]] = {"name": _leather[1], "icon": "🪢", "value": _leather[2]}
|
ITEMS[_leather[0]] = {"name": _leather[1], "icon": "🪢", "value": _leather[2]}
|
||||||
for (_suffix, _slot, _noun, _used, _offset, _icon), _points in zip(LEATHER_PIECES, _armour):
|
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, "armour": _points, "tier": _tier, "req": ["combat", EQUIP_LEVELS[_tier]],
|
"slot": _slot, "tier": _tier, "req": ["combat", EQUIP_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"]
|
||||||
|
for _tier, (_fibre, _level, _time, _xp, _cloth, _prefix, _label) in enumerate(CLOTHS):
|
||||||
|
ITEMS.setdefault(_fibre[0], {"name": _fibre[1], "icon": _fibre[2], "value": _fibre[3]})
|
||||||
|
ITEMS[_cloth[0]] = {"name": _cloth[1], "icon": "🧶", "value": _cloth[2]}
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
if _noun == "leggings":
|
||||||
|
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
|
||||||
for _raw, _cooked, _level, _action_id, _verb in FISH:
|
for _raw, _cooked, _level, _action_id, _verb in FISH:
|
||||||
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
|
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
|
||||||
ITEMS[_cooked[0]] = {"name": _cooked[1], "icon": _cooked[2], "value": _cooked[3], "heal": _cooked[4], "food": _cooked[5]}
|
ITEMS[_cooked[0]] = {"name": _cooked[1], "icon": _cooked[2], "value": _cooked[3], "heal": _cooked[4], "food": _cooked[5]}
|
||||||
|
|
@ -245,12 +286,14 @@ POTION_LINES = [
|
||||||
("fishing", "Fishing potion", "fishing", "speed", SPEEDS, [fish[0][0] for fish in FISH]),
|
("fishing", "Fishing potion", "fishing", "speed", SPEEDS, [fish[0][0] for fish in FISH]),
|
||||||
("farming", "Farming potion", "farming", "speed", SPEEDS,
|
("farming", "Farming potion", "farming", "speed", SPEEDS,
|
||||||
["wheat", "vegetables", "guam_leaf", "marrentill", "tarromin", "harralander"]),
|
["wheat", "vegetables", "guam_leaf", "marrentill", "tarromin", "harralander"]),
|
||||||
("hunting", "Hunting potion", "hunting", "speed", SPEEDS[:4], ["feathers", "raw_meat", "boar_hide", "bear_pelt"]),
|
("hunting", "Hunting potion", "hunting", "speed", SPEEDS,
|
||||||
|
["feathers", "raw_meat", "boar_hide", "bear_pelt", "elk_hide", "sabrecat_pelt"]),
|
||||||
("construction", "Construction potion", "construction", "speed", SPEEDS, [plank for plank, _name in PLANKS]),
|
("construction", "Construction potion", "construction", "speed", SPEEDS, [plank for plank, _name in PLANKS]),
|
||||||
("smithing", "Smithing potion", "smithing", "keep", SPEEDS, [f"{metal[0]}_bar" for metal in METALS]),
|
("smithing", "Smithing potion", "smithing", "keep", SPEEDS, [f"{metal[0]}_bar" for metal in METALS]),
|
||||||
("cooking", "Cooking potion", "cooking", "keep", SPEEDS, [fish[1][0] for fish in FISH]),
|
("cooking", "Cooking potion", "cooking", "keep", SPEEDS, [fish[1][0] for fish in FISH]),
|
||||||
("leatherworking", "Leatherworking potion", "leatherworking", "keep", SPEEDS,
|
("leatherworking", "Leatherworking potion", "leatherworking", "keep", SPEEDS,
|
||||||
[leather[1][0] for leather in LEATHERS]),
|
[leather[1][0] for leather in LEATHERS]),
|
||||||
|
("tailoring", "Tailoring potion", "tailoring", "keep", SPEEDS, [cloth[4][0] for cloth in CLOTHS]),
|
||||||
("prayer", "Prayer potion", "prayer", "bones", [0.25, 0.5, 0.75], ["bones", "big_bones", "dragon_bones"]),
|
("prayer", "Prayer potion", "prayer", "bones", [0.25, 0.5, 0.75], ["bones", "big_bones", "dragon_bones"]),
|
||||||
("strength", "Strength potion", "combat", "power", [4, 7, 11, 15, 20, 26], [leather[0][0] for leather in LEATHERS]),
|
("strength", "Strength potion", "combat", "power", [4, 7, 11, 15, 20, 26], [leather[0][0] for leather in LEATHERS]),
|
||||||
("defence", "Defence potion", "combat", "reduce", [0.10, 0.14, 0.18, 0.22, 0.26, 0.30],
|
("defence", "Defence potion", "combat", "reduce", [0.10, 0.14, 0.18, 0.22, 0.26, 0.30],
|
||||||
|
|
@ -312,6 +355,7 @@ SKILLS = [
|
||||||
{"id": "construction", "name": "Construction", "icon": "🏗️", "group": "Crafting"},
|
{"id": "construction", "name": "Construction", "icon": "🏗️", "group": "Crafting"},
|
||||||
{"id": "cooking", "name": "Cooking", "icon": "🍳", "group": "Crafting"},
|
{"id": "cooking", "name": "Cooking", "icon": "🍳", "group": "Crafting"},
|
||||||
{"id": "leatherworking", "name": "Leatherworking", "icon": "🪡", "group": "Crafting"},
|
{"id": "leatherworking", "name": "Leatherworking", "icon": "🪡", "group": "Crafting"},
|
||||||
|
{"id": "tailoring", "name": "Tailoring", "icon": "🧵", "group": "Crafting"},
|
||||||
{"id": "herblore", "name": "Herblore", "icon": "🧪", "group": "Crafting"},
|
{"id": "herblore", "name": "Herblore", "icon": "🧪", "group": "Crafting"},
|
||||||
{"id": "combat", "name": "Combat", "icon": "⚔️", "group": "Combat"},
|
{"id": "combat", "name": "Combat", "icon": "⚔️", "group": "Combat"},
|
||||||
{"id": "prayer", "name": "Prayer", "icon": "🙏", "group": "Combat"},
|
{"id": "prayer", "name": "Prayer", "icon": "🙏", "group": "Combat"},
|
||||||
|
|
@ -319,7 +363,7 @@ SKILLS = [
|
||||||
]
|
]
|
||||||
SKILL_IDS = [skill["id"] for skill in SKILLS]
|
SKILL_IDS = [skill["id"] for skill in SKILLS]
|
||||||
GATHERING = {"mining", "woodcutting", "farming", "hunting", "fishing"}
|
GATHERING = {"mining", "woodcutting", "farming", "hunting", "fishing"}
|
||||||
CRAFTING = {"smithing", "construction", "cooking", "leatherworking", "herblore"}
|
CRAFTING = {"smithing", "construction", "cooking", "leatherworking", "herblore", "tailoring"}
|
||||||
|
|
||||||
# A class is a whole trade, from gathering to the finished goods: its skills are
|
# A class is a whole trade, from gathering to the finished goods: its skills are
|
||||||
# 20% faster, 1 in 10 of its crafts make double, and it has one bonus of its own.
|
# 20% faster, 1 in 10 of its crafts make double, and it has one bonus of its own.
|
||||||
|
|
@ -340,9 +384,10 @@ CLASSES = {
|
||||||
"combat": "Sword: +15% damage",
|
"combat": "Sword: +15% damage",
|
||||||
},
|
},
|
||||||
"homesteader": {
|
"homesteader": {
|
||||||
"name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore"], "craft": ["cooking", "herblore"],
|
"name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore", "tailoring"],
|
||||||
"bonus": "Cooking and herblore 20% faster, two farm plots",
|
"craft": ["cooking", "herblore", "tailoring"],
|
||||||
"masterwork": "Masterwork: 10% double cooking and herblore crafts",
|
"bonus": "Cooking, herblore and tailoring 20% faster, two farm plots",
|
||||||
|
"masterwork": "Masterwork: 10% double cooking, herblore and tailoring crafts",
|
||||||
"signature": "Steady hand: potions last 25% longer",
|
"signature": "Steady hand: potions last 25% longer",
|
||||||
"combat": "Staff: +15% damage",
|
"combat": "Staff: +15% damage",
|
||||||
},
|
},
|
||||||
|
|
@ -391,13 +436,16 @@ def _tiered_actions() -> list[dict]:
|
||||||
for piece, _slot, noun, bars, offset, _power, _armour in PIECES:
|
for piece, _slot, noun, bars, offset, _power, _armour in PIECES:
|
||||||
if piece == "shield":
|
if piece == "shield":
|
||||||
continue # builders make shields now, in Construction
|
continue # builders make shields now, in Construction
|
||||||
inputs = {f"{key}_bar": bars, **({trophy("troll", tier): tier} if piece == "sword" and tier else {})}
|
inputs = {f"{key}_bar": bars}
|
||||||
|
if tier:
|
||||||
|
inputs.update({trophy("sword", tier): tier} if piece == "sword" else
|
||||||
|
{reagent("heavy", tier): REAGENT_COUNTS[_slot]})
|
||||||
actions.append(_action(f"smith_{key}_{piece}", "smithing", f"{name} {noun}", TIER_LEVELS[tier] + offset,
|
actions.append(_action(f"smith_{key}_{piece}", "smithing", f"{name} {noun}", TIER_LEVELS[tier] + offset,
|
||||||
25 + 5 * bars + 2 * tier, bars * piece_xp[tier], inputs,
|
25 + 5 * bars + 2 * tier, bars * piece_xp[tier], inputs,
|
||||||
{f"{key}_{piece}": 1}, group=name, city=city))
|
{f"{key}_{piece}": 1}, group=name, city=city))
|
||||||
actions.append(_action(f"smith_{key}_warhammer", "smithing", f"{name} warhammer", TIER_LEVELS[tier] + 6,
|
actions.append(_action(f"smith_{key}_warhammer", "smithing", f"{name} warhammer", TIER_LEVELS[tier] + 6,
|
||||||
45 + 2 * tier, 4 * piece_xp[tier],
|
45 + 2 * tier, 4 * piece_xp[tier],
|
||||||
{f"{key}_bar": 3, PLANKS[tier][0]: 1, **({trophy("golem", tier): 2 + tier} if tier else {})},
|
{f"{key}_bar": 3, PLANKS[tier][0]: 1, **({trophy("hammer", tier): 2 + tier} if tier else {})},
|
||||||
{f"{key}_warhammer": 1}, group=name, city=city))
|
{f"{key}_warhammer": 1}, group=name, city=city))
|
||||||
for tool, _icon in TOOLS.values():
|
for tool, _icon in TOOLS.values():
|
||||||
tool_id = f"{key}_{tool.replace(' ', '_')}"
|
tool_id = f"{key}_{tool.replace(' ', '_')}"
|
||||||
|
|
@ -407,14 +455,26 @@ def _tiered_actions() -> list[dict]:
|
||||||
for tier, (log, _name, _icon, _value, action_id, verb) in enumerate(TREES):
|
for tier, (log, _name, _icon, _value, action_id, verb) in enumerate(TREES):
|
||||||
actions.append(_action(action_id, "woodcutting", verb, TIER_LEVELS[tier], [20, 35, 40, 50, 60, 75][tier],
|
actions.append(_action(action_id, "woodcutting", verb, TIER_LEVELS[tier], [20, 35, 40, 50, 60, 75][tier],
|
||||||
[5, 15, 24, 36, 52, 75][tier], outputs={log: 1}))
|
[5, 15, 24, 36, 52, 75][tier], outputs={log: 1}))
|
||||||
for tier, (raw, leather, prefix, label, level, _armour) in enumerate(LEATHERS):
|
for tier, (raw, leather, prefix, label, level) in enumerate(LEATHERS):
|
||||||
actions.append(_action("tan_leather" if tier == 0 else f"tan_{leather[0]}", "leatherworking",
|
actions.append(_action("tan_leather" if tier == 0 else f"tan_{leather[0]}", "leatherworking",
|
||||||
f"Tan {leather[1].lower()}", level, 25 + 3 * tier, bar_xp[tier],
|
f"Tan {leather[1].lower()}", level, 25 + 3 * tier, bar_xp[tier],
|
||||||
{raw[0]: 1}, {leather[0]: 1}, group="Tanning"))
|
{raw[0]: 1}, {leather[0]: 1}, group="Tanning"))
|
||||||
for suffix, _slot, noun, used, offset, _icon in LEATHER_PIECES:
|
for suffix, slot, noun, used, offset, _icon in LEATHER_PIECES:
|
||||||
actions.append(_action(f"lw_{suffix}" if tier == 0 else f"lw_{prefix}_{suffix}", "leatherworking",
|
actions.append(_action(f"lw_{suffix}" if tier == 0 else f"lw_{prefix}_{suffix}", "leatherworking",
|
||||||
f"{label} {noun}", level + offset, 25 + 5 * used + 2 * tier, used * piece_xp[tier],
|
f"{label} {noun}", level + offset, 25 + 5 * used + 2 * tier, used * piece_xp[tier],
|
||||||
{leather[0]: used}, {f"{prefix}_{suffix}": 1}, group=label))
|
{leather[0]: used, **({reagent("medium", tier): REAGENT_COUNTS[slot]} if tier else {})},
|
||||||
|
{f"{prefix}_{suffix}": 1}, group=label))
|
||||||
|
for tier, (fibre, level, seconds, fibre_xp, cloth, prefix, label) in enumerate(CLOTHS):
|
||||||
|
if tier: # flax is grown with the other building materials
|
||||||
|
actions.append(_action(f"grow_{fibre[0]}", "farming", FIBRE_VERBS.get(fibre[0], f"Grow {fibre[1].lower()}"),
|
||||||
|
level, seconds, fibre_xp, outputs={fibre[0]: 2}, group="Fibres"))
|
||||||
|
actions.append(_action(f"weave_{cloth[0]}", "tailoring", f"Weave {cloth[1].lower()}", TIER_LEVELS[tier],
|
||||||
|
25 + 3 * tier, bar_xp[tier], {fibre[0]: 2}, {cloth[0]: 1}, group="Weaving"))
|
||||||
|
for suffix, slot, noun, used, offset, _icon in CLOTH_PIECES:
|
||||||
|
actions.append(_action(f"sew_{prefix}_{suffix}", "tailoring", f"{label} {noun}", TIER_LEVELS[tier] + offset,
|
||||||
|
25 + 5 * used + 2 * tier, used * piece_xp[tier],
|
||||||
|
{cloth[0]: used, **({reagent("light", tier): REAGENT_COUNTS[slot]} if tier else {})},
|
||||||
|
{f"{prefix}_{suffix}": 1}, group=label))
|
||||||
for raw, cooked, level, fish_id, verb in FISH:
|
for raw, cooked, level, fish_id, verb in FISH:
|
||||||
tier = TIER_LEVELS.index(level) if level in TIER_LEVELS else 3
|
tier = TIER_LEVELS.index(level) if level in TIER_LEVELS else 3
|
||||||
actions.append(_action(fish_id, "fishing", verb, level, [25, 35, 45, 55, 65, 80][tier],
|
actions.append(_action(fish_id, "fishing", verb, level, [25, 35, 45, 55, 65, 80][tier],
|
||||||
|
|
@ -426,38 +486,224 @@ def _tiered_actions() -> list[dict]:
|
||||||
|
|
||||||
# --- combat -----------------------------------------------------------------
|
# --- combat -----------------------------------------------------------------
|
||||||
# A fight is played out swing by swing: you and the enemy each have HP, a swing
|
# 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
|
# timer, accuracy and a max hit, and every swing hits, misses or crits. Every
|
||||||
# is split into the Frontier (bandits and the city's troubles) and expeditions:
|
# blow has a damage type: weapons deal one, and each enemy is vulnerable to some
|
||||||
# one area per tier, each with a golem, a troll, a spirit and a flyer that drop
|
# types and resists others. Armour stops physical blows, ward stops magic.
|
||||||
# the trophies higher weapons need, and a boss.
|
# Combat is split into the Frontier (bandits and the city's troubles) and
|
||||||
|
# expeditions: one area per tier, each with four races (each feeding one weapon
|
||||||
|
# line with its trophy, and every armour line with the area's reagents) and a boss.
|
||||||
CRIT_CHANCE = 0.05
|
CRIT_CHANCE = 0.05
|
||||||
CRIT_MULT = 1.5
|
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
|
||||||
|
|
||||||
|
DAMAGE_TYPES = {
|
||||||
|
"slash": {"name": "Slashing", "glyph": "sword-slice", "tint": "#d4dae2"},
|
||||||
|
"blunt": {"name": "Blunt", "glyph": "hammer-drop", "tint": "#d1a46a"},
|
||||||
|
"pierce": {"name": "Piercing", "glyph": "arrowhead", "tint": "#5fae8a"},
|
||||||
|
"magic": {"name": "Magic", "glyph": "magic-swirl", "tint": "#a77be8"},
|
||||||
|
}
|
||||||
|
VULNERABLE = 1.25 # damage an enemy takes from a type it's vulnerable to
|
||||||
|
RESISTANT = 0.75 # ...and from one it resists
|
||||||
|
|
||||||
# How each kind of weapon fights. Two-handed weapons carry the shield's power themselves.
|
# How each kind of weapon fights. Two-handed weapons carry the shield's power themselves.
|
||||||
STYLES = {
|
STYLES = {
|
||||||
"sword": {"name": "sword", "interval": 2.4, "hit": 1.0, "accuracy": 1.0, "crit": 0.05},
|
"sword": {"name": "sword", "damage": "slash", "interval": 2.4, "hit": 1.0, "accuracy": 1.0, "crit": 0.05},
|
||||||
"hammer": {"name": "warhammer", "interval": 3.6, "hit": 1.6, "accuracy": 0.95, "crit": 0.10},
|
"hammer": {"name": "warhammer", "damage": "blunt", "interval": 3.6, "hit": 1.6, "accuracy": 0.95, "crit": 0.10},
|
||||||
"staff": {"name": "staff", "interval": 3.0, "hit": 1.15, "accuracy": 1.25, "crit": 0.05, "pierce": True},
|
"staff": {"name": "staff", "damage": "magic", "interval": 3.0, "hit": 1.2, "accuracy": 1.25, "crit": 0.05},
|
||||||
"bow": {"name": "bow", "interval": 1.8, "hit": 0.75, "accuracy": 1.05, "crit": 0.05, "first": True},
|
"bow": {"name": "bow", "damage": "pierce", "interval": 1.8, "hit": 0.75, "accuracy": 1.05, "crit": 0.05, "first": True},
|
||||||
}
|
}
|
||||||
CLASS_STYLE = {"miner": "hammer", "builder": "sword", "homesteader": "staff", "ranger": "bow"}
|
CLASS_STYLE = {"miner": "hammer", "builder": "sword", "homesteader": "staff", "ranger": "bow"}
|
||||||
CLASS_STYLE_BONUS = 1.15 # damage with your own trade's weapon
|
CLASS_STYLE_BONUS = 1.15 # damage with your own trade's weapon
|
||||||
BLOCK_CHANCE = 0.15 # a shield turns aside this share of hits
|
BLOCK_CHANCE = 0.15 # a shield turns aside this share of hits
|
||||||
# Damage each style does to each kind of monster (1 when not listed).
|
|
||||||
WEAKNESS = {
|
# Each armour piece past copper needs reagents from its tier's expedition area.
|
||||||
"golem": {"hammer": 1.5, "bow": 0.5},
|
REAGENT_COUNTS = {"helmet": 1, "legs": 2, "body": 3}
|
||||||
"troll": {"sword": 1.5},
|
REAGENT_NOUNS = {"heavy": "plating", "medium": "lacing", "light": "thread"}
|
||||||
"spirit": {"hammer": 0.5, "sword": 0.5, "bow": 0.5, "staff": 1.5},
|
REAGENT_ODDS = 0.10 # each of the area's three reagents, from any of its races
|
||||||
"flyer": {"hammer": 0.5, "sword": 0.5, "bow": 1.5},
|
TROPHY_ODDS = 0.03 # rare: higher-tier weapons are hunted for
|
||||||
|
BOSS_TROPHY_ODDS = 0.06 # each of the four lines, so a boss is worth the longer fight
|
||||||
|
|
||||||
|
|
||||||
|
def _slug(name: str) -> str:
|
||||||
|
return "".join(c if c.isalnum() else "_" for c in name.lower().replace("'", "")).strip("_")
|
||||||
|
|
||||||
|
|
||||||
|
# Archetypes: HP, seconds between swings, accuracy, evasion, max hit (multipliers), how often it turns up.
|
||||||
|
ARCHETYPES = {
|
||||||
|
"brute": (1.3, 3.0, 1.0, 0.85, 1.2, 4),
|
||||||
|
"skirmisher": (0.85, 2.2, 1.1, 1.25, 0.85, 4),
|
||||||
|
"caster": (0.85, 2.6, 1.15, 1.0, 1.25, 3),
|
||||||
|
"elite": (1.7, 2.8, 1.1, 1.1, 1.3, 1),
|
||||||
|
"boss": (1.4, 3.0, 1.0, 1.0, 1.35, 1),
|
||||||
|
"dragon": (2.5, 3.0, 1.1, 1.0, 1.5, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def trophy(kind: str, tier: int) -> str:
|
def _types(value) -> list:
|
||||||
"""The trophy a tier's monster of this kind drops (tier 1 is the Old Watchtower)."""
|
return [value] if isinstance(value, str) else list(value)
|
||||||
return _slug(AREA_MONSTERS[kind][2][tier - 1])
|
|
||||||
|
|
||||||
|
def monster(name: str, archetype: str, level: int, attack, vulnerable=(), resists=(), boss: bool = False) -> dict:
|
||||||
|
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),
|
||||||
|
"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)}
|
||||||
|
|
||||||
|
|
||||||
|
def _encounter(id: str, name: str, group: str, level: int, roster: list, gold: int = 0, drops=(),
|
||||||
|
boss: bool = False, **extra) -> dict:
|
||||||
|
"""A combat action: each kill is one of the roster's variants, picked by how often each turns up.
|
||||||
|
An elite is a couple of levels above the rest."""
|
||||||
|
variants = [monster(vname, archetype, level + (2 if archetype == "elite" else 0), attack, vulnerable, resists, boss)
|
||||||
|
for vname, archetype, attack, vulnerable, resists in roster]
|
||||||
|
total = sum(v["weight"] for v in variants)
|
||||||
|
xp = round(sum(v["xp"] * v["weight"] for v in variants) / total)
|
||||||
|
enemy = {"name": name, "level": level, "boss": boss, "variants": variants}
|
||||||
|
return _action(id, "combat", extra.pop("label", None) or name, level, RESPAWN, xp, gold=gold,
|
||||||
|
drops=[list(drop) for drop in drops], enemy=enemy, group=group, **extra)
|
||||||
|
|
||||||
|
|
||||||
|
def _boss(id: str, name: str, group: str, level: int, archetype: str, attack, vulnerable, resists, **extra) -> dict:
|
||||||
|
return _encounter(id, name, group, level, [(name, archetype, attack, vulnerable, resists)],
|
||||||
|
boss=extra.pop("is_boss", True), **extra)
|
||||||
|
|
||||||
|
|
||||||
|
# Rosters: (variant, archetype, attacks with, vulnerable to, resists).
|
||||||
|
BANDITS = [("Bandit brute", "brute", "blunt", "slash", "blunt"),
|
||||||
|
("Bandit archer", "skirmisher", "pierce", "blunt", "pierce"),
|
||||||
|
("Hedge mage", "caster", "magic", "pierce", "magic")]
|
||||||
|
WOLVES = [("Grey wolf", "skirmisher", "slash", "pierce", "blunt"),
|
||||||
|
("Timber wolf", "brute", "slash", "slash", "pierce"),
|
||||||
|
("Wolf alpha", "elite", "slash", "blunt", "slash")]
|
||||||
|
GOBLINS = [("Goblin brute", "brute", "blunt", "blunt", "slash"),
|
||||||
|
("Goblin rogue", "skirmisher", "pierce", "magic", "blunt"),
|
||||||
|
("Goblin warlock", "caster", "magic", "slash", "magic")]
|
||||||
|
|
||||||
|
# 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", [
|
||||||
|
("Clay golems", "Clay core", "hammer", 0, [
|
||||||
|
("Clay brute", "brute", "blunt", "blunt", "pierce"),
|
||||||
|
("Mud shaper", "caster", "magic", "pierce", "magic"),
|
||||||
|
("Clay colossus", "elite", "blunt", "magic", "blunt")]),
|
||||||
|
("Cave trolls", "Cave troll hide", "sword", 2, [
|
||||||
|
("Troll bruiser", "brute", "blunt", "slash", "blunt"),
|
||||||
|
("Troll rockhurler", "skirmisher", "pierce", "blunt", "pierce"),
|
||||||
|
("Troll hexer", "caster", "magic", "pierce", "slash")]),
|
||||||
|
("Restless dead", "Grave dust", "staff", 1, [
|
||||||
|
("Skeleton warrior", "brute", "slash", "blunt", "pierce"),
|
||||||
|
("Wisp", "skirmisher", "magic", "magic", "slash"),
|
||||||
|
("Necromancer", "caster", "magic", "slash", "blunt")]),
|
||||||
|
("Harpies", "Harpy sinew", "bow", 0, [
|
||||||
|
("Harpy slasher", "skirmisher", "slash", "pierce", "blunt"),
|
||||||
|
("Harpy windcaster", "caster", "magic", ("slash", "pierce"), "magic"),
|
||||||
|
("Harpy matriarch", "elite", "slash", "magic", "pierce")]),
|
||||||
|
]),
|
||||||
|
("Sunken Barrows", 20, "Barrow", [
|
||||||
|
("Stone guardians", "Stone core", "hammer", 0, [
|
||||||
|
("Stone guardian", "brute", "blunt", "blunt", "slash"),
|
||||||
|
("Barrow sentry", "skirmisher", "pierce", "magic", "pierce"),
|
||||||
|
("Runebound guardian", "elite", "magic", "slash", "magic")]),
|
||||||
|
("Lizardfolk", "Lizard scale", "sword", 2, [
|
||||||
|
("Lizard warrior", "brute", "slash", "blunt", "slash"),
|
||||||
|
("Lizard spearman", "skirmisher", "pierce", "slash", "pierce"),
|
||||||
|
("Lizard shaman", "caster", "magic", "pierce", "magic")]),
|
||||||
|
("Bog wraiths", "Wraith essence", "staff", 1, [
|
||||||
|
("Bog wraith", "caster", "magic", "magic", "slash"),
|
||||||
|
("Drowned knight", "brute", "slash", "blunt", "pierce"),
|
||||||
|
("Marsh wisp", "skirmisher", "magic", "pierce", "blunt")]),
|
||||||
|
("Giant spiders", "Spider silk", "bow", 0, [
|
||||||
|
("Spider hunter", "skirmisher", "pierce", "slash", "pierce"),
|
||||||
|
("Venom weaver", "caster", "magic", "blunt", "magic"),
|
||||||
|
("Brood mother", "elite", "pierce", "magic", "blunt")]),
|
||||||
|
]),
|
||||||
|
("Iron Hills", 30, "Ironhill", [
|
||||||
|
("Iron golems", "Iron core", "hammer", 0, [
|
||||||
|
("Iron golem", "brute", "blunt", "blunt", "slash"),
|
||||||
|
("Forge golem", "caster", "magic", "pierce", "magic"),
|
||||||
|
("Iron colossus", "elite", "blunt", "magic", "pierce")]),
|
||||||
|
("Orcs", "Orc tusk", "sword", 2, [
|
||||||
|
("Orc berserker", "brute", "slash", "pierce", "blunt"),
|
||||||
|
("Orc archer", "skirmisher", "pierce", "slash", "pierce"),
|
||||||
|
("Orc shaman", "caster", "magic", "blunt", "magic")]),
|
||||||
|
("Mine ghosts", "Ghost essence", "staff", 1, [
|
||||||
|
("Lost miner", "brute", "blunt", "magic", "slash"),
|
||||||
|
("Lantern ghost", "caster", "magic", "blunt", "magic"),
|
||||||
|
("Shaft howler", "skirmisher", "magic", "pierce", "blunt")]),
|
||||||
|
("Wyverns", "Wyvern sinew", "bow", 0, [
|
||||||
|
("Wyvern", "brute", "slash", "pierce", "slash"),
|
||||||
|
("Wyvern hatchling", "skirmisher", "pierce", "blunt", "magic"),
|
||||||
|
("Venomspitter", "caster", "magic", "slash", "pierce")]),
|
||||||
|
]),
|
||||||
|
("Frostpeak", 40, "Frostpeak", [
|
||||||
|
("Ice elementals", "Frozen core", "hammer", 0, [
|
||||||
|
("Ice brute", "brute", "blunt", "blunt", "pierce"),
|
||||||
|
("Frost wisp", "skirmisher", "magic", "slash", "magic"),
|
||||||
|
("Glacier colossus", "elite", "blunt", "magic", "slash")]),
|
||||||
|
("Frost trolls", "Frost troll hide", "sword", 2, [
|
||||||
|
("Frost troll", "brute", "blunt", "slash", "blunt"),
|
||||||
|
("Troll icehurler", "skirmisher", "pierce", "magic", "pierce"),
|
||||||
|
("Troll rimecaller", "caster", "magic", "blunt", "magic")]),
|
||||||
|
("Banshees", "Banshee essence", "staff", 1, [
|
||||||
|
("Banshee", "caster", "magic", "magic", "slash"),
|
||||||
|
("Wailing knight", "brute", "slash", "blunt", "pierce"),
|
||||||
|
("Keening shade", "skirmisher", "magic", "pierce", "blunt")]),
|
||||||
|
("Griffins", "Griffin sinew", "bow", 0, [
|
||||||
|
("Griffin", "brute", "slash", "pierce", "blunt"),
|
||||||
|
("Griffin rider", "skirmisher", "pierce", "slash", "pierce"),
|
||||||
|
("Storm griffin", "caster", "magic", "blunt", "magic")]),
|
||||||
|
]),
|
||||||
|
("Dragon's Spine", 50, "Dragonspine", [
|
||||||
|
("Magma constructs", "Magma core", "hammer", 0, [
|
||||||
|
("Magma brute", "brute", "blunt", "blunt", "slash"),
|
||||||
|
("Cinder golem", "skirmisher", "pierce", "magic", "pierce"),
|
||||||
|
("Fire binder", "caster", "magic", "pierce", "magic")]),
|
||||||
|
("Dragonkin", "Dragonkin scale", "sword", 2, [
|
||||||
|
("Drakeguard", "brute", "slash", "blunt", "slash"),
|
||||||
|
("Drake lancer", "skirmisher", "pierce", "magic", "pierce"),
|
||||||
|
("Flame priest", "caster", "magic", "slash", "magic")]),
|
||||||
|
("Shades", "Shade essence", "staff", 1, [
|
||||||
|
("Shade", "caster", "magic", "magic", "pierce"),
|
||||||
|
("Shadow blade", "skirmisher", "slash", "blunt", "slash"),
|
||||||
|
("Void knight", "elite", "magic", "pierce", "blunt")]),
|
||||||
|
("Rocs", "Roc sinew", "bow", 0, [
|
||||||
|
("Roc", "brute", "slash", "pierce", "blunt"),
|
||||||
|
("Roc hatchling", "skirmisher", "pierce", "slash", "pierce"),
|
||||||
|
("Thunder roc", "caster", "magic", "blunt", "magic")]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
AREA_BONES = ["bones", "big_bones", "big_bones", "dragon_bones", "dragon_bones"]
|
||||||
|
|
||||||
|
TROPHIES = {} # id: (name, weapon line, tier)
|
||||||
|
REAGENTS = {} # id: (weight, tier)
|
||||||
|
for _tier, (_area, _level, _prefix, _races) in enumerate(AREAS, start=1):
|
||||||
|
for _race, _trophy, _line, _offset, _roster in _races:
|
||||||
|
TROPHIES[_slug(_trophy)] = (_trophy, _line, _tier)
|
||||||
|
ITEMS[_slug(_trophy)] = {"name": _trophy, "icon": "🏆", "value": 15 * _tier, "trophy": _line, "tier": _tier}
|
||||||
|
for _weight, _noun in REAGENT_NOUNS.items():
|
||||||
|
_id = f"{_slug(_prefix)}_{_noun}"
|
||||||
|
REAGENTS[_id] = (_weight, _tier)
|
||||||
|
ITEMS[_id] = {"name": f"{_prefix} {_noun}", "icon": "✨", "value": 12 * _tier, "reagent": _weight, "tier": _tier}
|
||||||
|
|
||||||
|
|
||||||
|
def trophy(line: str, tier: int) -> str:
|
||||||
|
"""The trophy for a weapon line from a tier's area (tier 1 is the Old Watchtower)."""
|
||||||
|
return next(item for item, (_name, kind, t) in TROPHIES.items() if kind == line and t == tier)
|
||||||
|
|
||||||
|
|
||||||
|
def reagent(weight: str, tier: int) -> str:
|
||||||
|
"""The reagent an armour weight needs at a tier, from that tier's area."""
|
||||||
|
return next(item for item, (kind, t) in REAGENTS.items() if kind == weight and t == tier)
|
||||||
|
|
||||||
|
|
||||||
|
def _area_drops(tier: int, odds: float = REAGENT_ODDS) -> list[tuple]:
|
||||||
|
return [(reagent(weight, tier), 1, odds) for weight in WEIGHTS]
|
||||||
|
|
||||||
|
|
||||||
def _weapon_actions() -> list[dict]:
|
def _weapon_actions() -> list[dict]:
|
||||||
|
|
@ -465,118 +711,69 @@ def _weapon_actions() -> list[dict]:
|
||||||
actions = []
|
actions = []
|
||||||
for tier, (key, name, *_rest) in enumerate(METALS):
|
for tier, (key, name, *_rest) in enumerate(METALS):
|
||||||
plank, leather, herb = PLANKS[tier][0], LEATHERS[tier][1][0], HERBS[tier][0]
|
plank, leather, herb = PLANKS[tier][0], LEATHERS[tier][1][0], HERBS[tier][0]
|
||||||
extra = lambda kind, count: {trophy(kind, tier): count} if tier else {}
|
extra = lambda line, count: {trophy(line, tier): count} if tier else {}
|
||||||
actions.append(_action(f"make_{key}_shield", "construction", f"{name} shield", TIER_LEVELS[tier] + 5,
|
actions.append(_action(f"make_{key}_shield", "construction", f"{name} shield", TIER_LEVELS[tier] + 5,
|
||||||
40 + 2 * tier, [10, 22, 36, 52, 70, 92][tier],
|
40 + 2 * tier, [10, 22, 36, 52, 70, 92][tier],
|
||||||
{plank: 3, leather: 2, f"{key}_bar": 1, **extra("troll", 1 + tier)},
|
{plank: 3, leather: 2, f"{key}_bar": 1, **extra("sword", 1 + tier)},
|
||||||
{f"{key}_shield": 1}, group="Shields"))
|
{f"{key}_shield": 1}, group="Shields"))
|
||||||
actions.append(_action(f"brew_{WOODS[tier]}_staff", "herblore", STAVES[tier], TIER_LEVELS[tier] + 5,
|
actions.append(_action(f"brew_{WOODS[tier]}_staff", "herblore", STAVES[tier], TIER_LEVELS[tier] + 5,
|
||||||
45 + 2 * tier, [12, 26, 42, 60, 80, 105][tier],
|
45 + 2 * tier, [12, 26, 42, 60, 80, 105][tier],
|
||||||
{plank: 2, herb: 3, **extra("spirit", 2 + tier)}, {f"{WOODS[tier]}_staff": 1},
|
{plank: 2, herb: 3, **extra("staff", 2 + tier)}, {f"{WOODS[tier]}_staff": 1},
|
||||||
group="Staves"))
|
group="Staves"))
|
||||||
actions.append(_action(f"lw_{WOODS[tier]}_bow", "leatherworking", BOWS[tier], LEATHERS[tier][4] + 4,
|
actions.append(_action(f"lw_{WOODS[tier]}_bow", "leatherworking", BOWS[tier], LEATHERS[tier][4] + 4,
|
||||||
40 + 2 * tier, [12, 26, 42, 60, 80, 105][tier],
|
40 + 2 * tier, [12, 26, 42, 60, 80, 105][tier],
|
||||||
{plank: 2, leather: 2, **extra("flyer", 2 + tier)}, {f"{WOODS[tier]}_bow": 1},
|
{plank: 2, leather: 2, **extra("bow", 2 + tier)}, {f"{WOODS[tier]}_bow": 1},
|
||||||
group="Bows"))
|
group="Bows"))
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
# 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.03 # rare: higher-tier weapons are hunted for
|
|
||||||
BOSS_TROPHY_ODDS = 0.06 # each of the four kinds, so a boss is worth the longer fight
|
|
||||||
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]:
|
def _area_fights() -> list[dict]:
|
||||||
fights = []
|
fights = []
|
||||||
for index, (area, level) in enumerate(zip(AREAS, AREA_LEVELS)):
|
for tier, (area, level, _prefix, races) in enumerate(AREAS, start=1):
|
||||||
for kind, (offset, monsters, trophies) in AREA_MONSTERS.items():
|
for race, trophy_name, _line, offset, roster in races:
|
||||||
enemy = monster(monsters[index], kind, level + offset)
|
fights.append(_encounter(f"fight_{_slug(race)}", race, area, level + offset, roster, gold=level + offset,
|
||||||
fights.append(_fight(f"fight_{_slug(monsters[index])}", enemy, area, gold=level + offset,
|
drops=[(AREA_BONES[tier - 1], 1, 1.0), (_slug(trophy_name), 1, TROPHY_ODDS),
|
||||||
drops=[(AREA_BONES[index], 1, 1.0), (_slug(trophies[index]), 1, TROPHY_ODDS)]))
|
*_area_drops(tier)]))
|
||||||
return fights
|
return fights
|
||||||
|
|
||||||
|
|
||||||
def _boss_trophies(tier: int) -> list[tuple]:
|
def _boss_drops(tier: int, reagents: float = 0.3) -> list[tuple]:
|
||||||
return [(_slug(trophies[tier - 1]), 1, BOSS_TROPHY_ODDS) for _offset, _monsters, trophies in AREA_MONSTERS.values()]
|
return [*[(trophy(line, tier), 1, BOSS_TROPHY_ODDS) for line in STYLES], *_area_drops(tier, reagents)]
|
||||||
|
|
||||||
|
|
||||||
COMBAT_ACTIONS = [
|
COMBAT_ACTIONS = [
|
||||||
# The Frontier: the city's own troubles.
|
# The Frontier: the city's own troubles.
|
||||||
_fight("fight_scouts", monster("Bandit scouts", "human", 10), "Frontier", gold=10,
|
_encounter("fight_scouts", "Bandit scouts", "Frontier", 10, BANDITS, gold=10,
|
||||||
drops=[("copper_bar", 1, 0.3), ("arrows", 5, 0.4), ("bones", 1, 1.0)]),
|
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",
|
_encounter("sabotage", "Camp guards", "Frontier", 12, BANDITS, label="Sabotage the bandit camp", special="sabotage"),
|
||||||
special="sabotage"),
|
_boss("fight_stronghold", "Bandit warlord", "Frontier", 40, "boss", ("slash", "magic"), ("blunt", "magic"),
|
||||||
_fight("fight_stronghold", monster("Bandit stronghold", "human", 40, boss=True), "Frontier", gold=60,
|
("slash", "pierce"), label="Bandit stronghold", gold=60,
|
||||||
drops=[("steel_bar", 1, 0.4), ("coal", 3, 0.5), ("iron_shield", 1, 0.04), ("big_bones", 1, 1.0)]),
|
drops=[("steel_bar", 1, 0.4), ("coal", 3, 0.5), ("iron_shield", 1, 0.04), ("big_bones", 1, 1.0)]),
|
||||||
# Greenwood: where everyone starts.
|
# Greenwood: where everyone starts.
|
||||||
_fight("fight_wolves", monster("Wolves", "beast", 1), "Greenwood", gold=2, name="Wolves in the woods",
|
_encounter("fight_wolves", "Wolves", "Greenwood", 1, WOLVES, gold=2, label="Wolves in the woods",
|
||||||
drops=[("raw_meat", 1, 0.6), ("hide", 1, 0.5), ("bones", 1, 1.0)]),
|
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,
|
_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(),
|
||||||
_fight("fight_ruins", monster("Watchtower ogre", "troll", 18, boss=True), "Old Watchtower", gold=25,
|
_boss("fight_ruins", "Watchtower ogre", "Old Watchtower", 18, "boss", ("blunt", "magic"), ("slash", "magic"),
|
||||||
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_trophies(1)]),
|
("blunt", "pierce"), gold=25,
|
||||||
_fight("fight_barrow_king", monster("Barrow king", "spirit", 28, boss=True), "Sunken Barrows", gold=40,
|
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_drops(1)]),
|
||||||
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_trophies(2)]),
|
_boss("fight_barrow_king", "Barrow king", "Sunken Barrows", 28, "boss", ("slash", "magic"), ("blunt", "magic"),
|
||||||
_fight("fight_green_dragon", monster("Green dragon", "dragon", 38), "Iron Hills", gold=80,
|
("pierce", "slash"), gold=40,
|
||||||
drops=[("green_dragonhide", 1, 0.9), ("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(3)]),
|
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_drops(2)]),
|
||||||
_fight("fight_blue_dragon", monster("Blue dragon", "dragon", 48), "Frostpeak", gold=110,
|
# Dragons: the prize hunt, dropping their area's reagents far more often than anything else there.
|
||||||
drops=[("blue_dragonhide", 1, 0.9), ("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(4)]),
|
_boss("fight_green_dragon", "Green dragon", "Iron Hills", 38, "dragon", ("slash", "magic"), ("pierce", "magic"),
|
||||||
_fight("fight_red_dragon", monster("Red dragon", "dragon", 60), "Dragon's Spine", gold=150,
|
("slash", "blunt"), is_boss=False, gold=80,
|
||||||
drops=[("red_dragonhide", 1, 0.9), ("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_trophies(5)]),
|
drops=[("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(3, 0.6)]),
|
||||||
_fight("fight_black_dragon", monster("Black dragon", "dragon", 75, boss=True), "Dragon's Spine", gold=220,
|
_boss("fight_blue_dragon", "Blue dragon", "Frostpeak", 48, "dragon", ("slash", "magic"), ("blunt", "pierce"),
|
||||||
drops=[("red_dragonhide", 2, 0.9), ("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_trophies(5)]),
|
("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"),
|
||||||
|
("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"),
|
||||||
|
("slash", "pierce"), gold=220,
|
||||||
|
drops=[("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_drops(5, 0.8)]),
|
||||||
]
|
]
|
||||||
|
|
||||||
ACTIONS = [
|
ACTIONS = [
|
||||||
|
|
@ -593,6 +790,9 @@ ACTIONS = [
|
||||||
_action("hunt_deer", "hunting", "Hunt deer", 8, 40, 13, outputs={"raw_meat": 2, "hide": 1}),
|
_action("hunt_deer", "hunting", "Hunt deer", 8, 40, 13, outputs={"raw_meat": 2, "hide": 1}),
|
||||||
_action("hunt_boar", "hunting", "Hunt boar", 25, 50, 30, outputs={"raw_meat": 2, "boar_hide": 1}),
|
_action("hunt_boar", "hunting", "Hunt boar", 25, 50, 30, outputs={"raw_meat": 2, "boar_hide": 1}),
|
||||||
_action("hunt_bear", "hunting", "Hunt bears", 40, 60, 48, outputs={"raw_meat": 3, "bear_pelt": 1}),
|
_action("hunt_bear", "hunting", "Hunt bears", 40, 60, 48, outputs={"raw_meat": 3, "bear_pelt": 1}),
|
||||||
|
_action("hunt_elk", "hunting", "Hunt elk", 50, 70, 64, outputs={"raw_meat": 3, "elk_hide": 1}),
|
||||||
|
_action("hunt_sabrecat", "hunting", "Track sabrecats", 65, 80, 84, outputs={"raw_meat": 3, "sabrecat_pelt": 1}),
|
||||||
|
_action("hunt_mammoth", "hunting", "Hunt mammoths", 80, 90, 108, outputs={"raw_meat": 5, "mammoth_hide": 1}),
|
||||||
*_tiered_actions(),
|
*_tiered_actions(),
|
||||||
_action("smith_arrows", "smithing", "Arrows", 5, 30, 10,
|
_action("smith_arrows", "smithing", "Arrows", 5, 30, 10,
|
||||||
{"copper_bar": 1, "logs": 1, "feathers": 2}, {"arrows": 15}, group="Copper"),
|
{"copper_bar": 1, "logs": 1, "feathers": 2}, {"arrows": 15}, group="Copper"),
|
||||||
|
|
@ -601,7 +801,7 @@ ACTIONS = [
|
||||||
_action("labour", "construction", "Work on the building site", 1, 30, 10, special="labour"),
|
_action("labour", "construction", "Work on the building site", 1, 30, 10, special="labour"),
|
||||||
_action("twist_rope", "construction", "Twist rope", 5, 30, 10, {"flax": 3}, {"rope": 1}, group="Supplies"),
|
_action("twist_rope", "construction", "Twist rope", 5, 30, 10, {"flax": 3}, {"rope": 1}, group="Supplies"),
|
||||||
_action("grow_thatch", "farming", "Cut thatch reeds", 3, 30, 7, outputs={"thatch": 3}, group="Materials"),
|
_action("grow_thatch", "farming", "Cut thatch reeds", 3, 30, 7, outputs={"thatch": 3}, group="Materials"),
|
||||||
_action("grow_flax", "farming", "Grow flax", 10, 35, 11, outputs={"flax": 3}, group="Materials"),
|
_action("grow_flax", "farming", "Grow flax", 1, 35, 11, outputs={"flax": 3}, group="Fibres"),
|
||||||
_action("boil_glue", "leatherworking", "Boil hide glue", 5, 30, 10, {"hide": 1, "logs": 1}, {"hide_glue": 2},
|
_action("boil_glue", "leatherworking", "Boil hide glue", 5, 30, 10, {"hide": 1, "logs": 1}, {"hide_glue": 2},
|
||||||
group="Tanning"),
|
group="Tanning"),
|
||||||
*[_action(f"saw_{plank}", "construction", f"Saw {name.lower()}s", TIER_LEVELS[tier], 25 + 3 * tier,
|
*[_action(f"saw_{plank}", "construction", f"Saw {name.lower()}s", TIER_LEVELS[tier], 25 + 3 * tier,
|
||||||
|
|
@ -743,7 +943,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
|
||||||
|
|
||||||
# --- world ---------------------------------------------------------------
|
# --- world ---------------------------------------------------------------
|
||||||
|
|
||||||
WORLD_VERSION = 15
|
WORLD_VERSION = 16
|
||||||
|
|
||||||
|
|
||||||
def new_world(now: float, speed: float = 1.0) -> dict:
|
def new_world(now: float, speed: float = 1.0) -> dict:
|
||||||
|
|
@ -878,9 +1078,67 @@ def migrate(world: dict, now: float | None = None) -> dict:
|
||||||
for player in world["players"].values():
|
for player in world["players"].values():
|
||||||
player["seen_threat"] = latest["id"] if latest else 0
|
player["seen_threat"] = latest["id"] if latest else 0
|
||||||
world["version"] = 15
|
world["version"] = 15
|
||||||
|
if world["version"] < 16:
|
||||||
|
_migrate_combat_rework(world)
|
||||||
|
world["version"] = 16
|
||||||
return world
|
return world
|
||||||
|
|
||||||
|
|
||||||
|
# The combat rework: the old area monsters became races, their trophies carry over by
|
||||||
|
# weapon line and tier, dragonhide became the dragons' reagents, and the leathers past
|
||||||
|
# bear are hunted now.
|
||||||
|
RENAMED_ITEMS = {
|
||||||
|
"bat_sinew": "harpy_sinew", "harpy_sinew": "spider_silk", "wisp_essence": "grave_dust",
|
||||||
|
"hill_troll_hide": "lizard_scale", "rock_troll_hide": "orc_tusk", "elder_troll_hide": "dragonkin_scale",
|
||||||
|
"obsidian_core": "frozen_core", "runic_core": "magma_core",
|
||||||
|
"green_dragonhide": "ironhill_lacing", "blue_dragonhide": "frostpeak_lacing", "red_dragonhide": "dragonspine_lacing",
|
||||||
|
"green_dragon_leather": "elk_leather", "blue_dragon_leather": "sabrecat_leather", "red_dragon_leather": "mammoth_leather",
|
||||||
|
**{f"{old}_{suffix}": f"{new}_{suffix}" for old, new in (("green_dhide", "elk_leather"), ("blue_dhide", "sabrecat_leather"),
|
||||||
|
("red_dhide", "mammoth_leather")) for suffix in ("cap", "legs", "body")},
|
||||||
|
}
|
||||||
|
RENAMED_ACTIONS = {
|
||||||
|
**{f"fight_{_slug(old)}": f"fight_{_slug(new)}" for old, new in (
|
||||||
|
("Clay golem", "Clay golems"), ("Cave troll", "Cave trolls"), ("Will-o'-wisp", "Restless dead"),
|
||||||
|
("Giant bat", "Harpies"), ("Stone golem", "Stone guardians"), ("Hill troll", "Lizardfolk"),
|
||||||
|
("Bog wraith", "Bog wraiths"), ("Harpy", "Giant spiders"), ("Iron golem", "Iron golems"), ("Rock troll", "Orcs"),
|
||||||
|
("Mine ghost", "Mine ghosts"), ("Wyvern", "Wyverns"), ("Obsidian golem", "Ice elementals"),
|
||||||
|
("Frost troll", "Frost trolls"), ("Banshee", "Banshees"), ("Griffin", "Griffins"),
|
||||||
|
("Runic golem", "Magma constructs"), ("Elder troll", "Dragonkin"), ("Shade", "Shades"), ("Roc", "Rocs"))},
|
||||||
|
**{f"tan_{old}_dragon_leather": f"tan_{new}_leather" for old, new in
|
||||||
|
(("green", "elk"), ("blue", "sabrecat"), ("red", "mammoth"))},
|
||||||
|
**{f"lw_{old}_{suffix}": f"lw_{new}_{suffix}" for old, new in (("green_dhide", "elk_leather"),
|
||||||
|
("blue_dhide", "sabrecat_leather"), ("red_dhide", "mammoth_leather")) for suffix in ("cap", "legs", "body")},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _renamed(stock: dict) -> dict:
|
||||||
|
out = {}
|
||||||
|
for item, qty in stock.items():
|
||||||
|
new = RENAMED_ITEMS.get(item, item)
|
||||||
|
out[new] = out.get(new, 0) + qty
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_combat_rework(world: dict) -> None:
|
||||||
|
city = world["city"]
|
||||||
|
city["stockpile"] = _renamed(city["stockpile"])
|
||||||
|
for building in city["buildings"].values():
|
||||||
|
building["fund"] = _renamed(building["fund"])
|
||||||
|
for player in world["players"].values():
|
||||||
|
player["xp"].setdefault("tailoring", 0)
|
||||||
|
player["bank"] = _renamed(player["bank"])
|
||||||
|
for slot, item in list(player["equip"].items()):
|
||||||
|
if item:
|
||||||
|
player["equip"][slot] = RENAMED_ITEMS.get(item, item)
|
||||||
|
action = player["action"]
|
||||||
|
if action:
|
||||||
|
for key in ("id", "resume"):
|
||||||
|
if key in action:
|
||||||
|
action[key] = RENAMED_ACTIONS.get(action[key], action[key])
|
||||||
|
if action["id"] not in ACTION_BY_ID or action.get("resume", "") and action["resume"] not in ACTION_BY_ID:
|
||||||
|
stop(player, "Combat has changed: pick your fight again")
|
||||||
|
|
||||||
|
|
||||||
def last_threat(world: dict) -> dict | None:
|
def last_threat(world: dict) -> dict | None:
|
||||||
"""The most recent raid or dragon attack that has played out."""
|
"""The most recent raid or dragon attack that has played out."""
|
||||||
return next((e for e in reversed(world["events"]) if e["status"] == "done" and e["kind"] in THREATS
|
return next((e for e in reversed(world["events"]) if e["status"] == "done" and e["kind"] in THREATS
|
||||||
|
|
@ -1143,16 +1401,30 @@ def bank_take(player: dict, item: str, qty: int) -> None:
|
||||||
|
|
||||||
|
|
||||||
def armour(player: dict) -> int:
|
def armour(player: dict) -> int:
|
||||||
|
"""Protection from physical blows: slashing, blunt and piercing."""
|
||||||
return sum(ITEMS[item].get("armour", 0) for item in player["equip"].values() if item)
|
return sum(ITEMS[item].get("armour", 0) for item in player["equip"].values() if item)
|
||||||
|
|
||||||
|
|
||||||
def damage_reduction(player: dict) -> float:
|
def magic_ward(player: dict) -> int:
|
||||||
points = armour(player)
|
"""Protection from magic."""
|
||||||
|
return sum(ITEMS[item].get("ward", 0) for item in player["equip"].values() if item)
|
||||||
|
|
||||||
|
|
||||||
|
def reduction(points: float) -> float:
|
||||||
return points / (points + 25)
|
return points / (points + 25)
|
||||||
|
|
||||||
|
|
||||||
|
def damage_reduction(player: dict) -> float:
|
||||||
|
return reduction(armour(player))
|
||||||
|
|
||||||
|
|
||||||
|
def defence_bonus(player: dict) -> int:
|
||||||
|
"""What all your armour adds to power and evasion, whatever its weight."""
|
||||||
|
return round((armour(player) + magic_ward(player)) * 0.4)
|
||||||
|
|
||||||
|
|
||||||
def combat_power(player: dict) -> int:
|
def combat_power(player: dict) -> int:
|
||||||
power = skill_level(player, "combat") + armour(player) // 2
|
power = skill_level(player, "combat") + defence_bonus(player)
|
||||||
for item in player["equip"].values():
|
for item in player["equip"].values():
|
||||||
if item:
|
if item:
|
||||||
power += ITEMS[item].get("power", 0)
|
power += ITEMS[item].get("power", 0)
|
||||||
|
|
@ -1178,20 +1450,32 @@ def weapon_style(player: dict) -> str:
|
||||||
return ITEMS[weapon].get("style", "sword") if weapon else "sword"
|
return ITEMS[weapon].get("style", "sword") if weapon else "sword"
|
||||||
|
|
||||||
|
|
||||||
|
def affinity(enemy: dict, damage: str) -> float:
|
||||||
|
"""How hard a damage type lands on this enemy."""
|
||||||
|
return VULNERABLE if damage in enemy["vulnerable"] else RESISTANT if damage in enemy["resists"] else 1
|
||||||
|
|
||||||
|
|
||||||
def fighter(player: dict, blessed: dict, brew: dict, enemy: dict | None = None) -> dict:
|
def fighter(player: dict, blessed: dict, brew: dict, enemy: dict | None = None) -> dict:
|
||||||
"""Your side of a fight: power from level and gear (plus prayers and potions) aims and hits, shaped by
|
"""Your side of a fight: power from level and gear (plus prayers and potions) aims and hits, shaped by
|
||||||
the weapon's style, your trade and the enemy's weakness; armour dodges and soaks, a shield blocks."""
|
the weapon's style, your trade and how the enemy takes its damage type. Armour soaks physical blows,
|
||||||
|
ward soaks magic, both help you dodge, and a shield blocks."""
|
||||||
style_id = weapon_style(player)
|
style_id = weapon_style(player)
|
||||||
style = STYLES[style_id]
|
style = STYLES[style_id]
|
||||||
power = combat_power(player) + blessed["power"] + brew.get("power", 0)
|
power = combat_power(player) + blessed["power"] + brew.get("power", 0)
|
||||||
damage = style["hit"] * (CLASS_STYLE_BONUS if CLASS_STYLE.get(player["cls"]) == style_id else 1)
|
damage = style["hit"] * (CLASS_STYLE_BONUS if CLASS_STYLE.get(player["cls"]) == style_id else 1)
|
||||||
weakness = WEAKNESS.get(enemy["kind"], {}).get(style_id, 1) if enemy else 1
|
lands = affinity(enemy, style["damage"]) if enemy else 1
|
||||||
return {"style": style_id, "interval": style["interval"], "accuracy": (10 + 2 * power) * style["accuracy"],
|
taken = blessed["taken"] * (1 - brew.get("reduce", 0))
|
||||||
"max_hit": max(2, round((2 + power / 3) * damage * weakness)), "crit": style["crit"],
|
return {"style": style_id, "damage": style["damage"], "interval": style["interval"],
|
||||||
"pierce": style.get("pierce", False), "first": style.get("first", False), "weakness": weakness,
|
"accuracy": (10 + 2 * power) * style["accuracy"],
|
||||||
|
"max_hit": max(2, round((2 + power / 3) * damage * lands)), "crit": style["crit"],
|
||||||
|
"first": style.get("first", False), "affinity": lands,
|
||||||
"block": BLOCK_CHANCE if player["equip"].get("shield") else 0,
|
"block": BLOCK_CHANCE if player["equip"].get("shield") else 0,
|
||||||
"evasion": 10 + 2 * (skill_level(player, "combat") + armour(player) // 2),
|
"evasion": 10 + 2 * (skill_level(player, "combat") + defence_bonus(player)),
|
||||||
"taken": (1 - damage_reduction(player)) * blessed["taken"] * (1 - brew.get("reduce", 0))}
|
"taken": {"physical": (1 - damage_reduction(player)) * taken, "magic": (1 - reduction(magic_ward(player))) * taken}}
|
||||||
|
|
||||||
|
|
||||||
|
def _taken(me: dict, damage: str) -> float:
|
||||||
|
return me["taken"]["magic" if damage == "magic" else "physical"]
|
||||||
|
|
||||||
|
|
||||||
def hit_chance(accuracy: float, evasion: float) -> float:
|
def hit_chance(accuracy: float, evasion: float) -> float:
|
||||||
|
|
@ -1202,26 +1486,38 @@ 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 fight_odds(player: dict, action: dict) -> dict:
|
def _variant_odds(player: dict, foe: dict, blessed: dict, brew: dict) -> dict:
|
||||||
"""What a fight against this enemy looks like on average, for the cards."""
|
me = fighter(player, blessed, brew, foe)
|
||||||
foe = action["enemy"]
|
|
||||||
me = fighter(player, blessing(player), potion_effect(player, "combat"), 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"])
|
||||||
shrug = 0 if me["pierce"] else foe["reduction"]
|
kill = foe["hp"] / (hit * _average_hit(me["max_hit"], me["crit"]) / me["interval"])
|
||||||
dps = hit * _average_hit(me["max_hit"], me["crit"]) * (1 - shrug) / me["interval"]
|
taken = sum(_taken(me, kind) for kind in foe["attack"]) / len(foe["attack"])
|
||||||
kill = foe["hp"] / dps
|
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * taken / foe["interval"] * kill
|
||||||
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * me["taken"] / foe["interval"] * kill
|
return {"hit": hit, "their_hit": their_hit, "max_hit": me["max_hit"], "affinity": me["affinity"],
|
||||||
return {"hit": round(hit, 2), "their_hit": round(their_hit, 2), "max_hit": me["max_hit"], "style": me["style"],
|
"their_max": max(1, round(foe["max_hit"] * max(_taken(me, kind) for kind in foe["attack"]))),
|
||||||
"weakness": me["weakness"], "their_max": max(1, round(foe["max_hit"] * me["taken"])),
|
"kill_time": kill, "damage": hurt}
|
||||||
"kill_time": round(kill, 1), "damage": round(hurt, 1)}
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
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),
|
||||||
|
"max_hit": max(row["max_hit"] for row in rows), "their_max": max(row["their_max"] for row in rows),
|
||||||
|
"kill_time": round(mean("kill_time"), 1), "damage": round(mean("damage"), 1),
|
||||||
|
"variants": [{"affinity": row["affinity"], "kill_time": round(row["kill_time"], 1),
|
||||||
|
"damage": round(row["damage"], 1)} for row in rows]}
|
||||||
|
|
||||||
|
|
||||||
def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) -> dict:
|
def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) -> dict:
|
||||||
"""Play out one fight swing by swing, both swing timers starting empty. Nothing changes
|
"""Play out one fight swing by swing, both swing timers starting empty. Nothing changes
|
||||||
until it's over: the result is applied then."""
|
until it's over: the result is applied then."""
|
||||||
foe, blessed, brew = action["enemy"], blessing(player), potion_effect(player, "combat")
|
variants = action["enemy"]["variants"]
|
||||||
|
foe = rng.choices(variants, [v["weight"] for v in variants])[0]
|
||||||
|
blessed, brew = blessing(player), potion_effect(player, "combat")
|
||||||
me = fighter(player, blessed, brew, foe)
|
me = fighter(player, blessed, brew, foe)
|
||||||
shrug = 0 if me["pierce"] else foe["reduction"]
|
|
||||||
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"])
|
||||||
top, hp, foe_hp = max_hp(player), player["hp"], foe["hp"]
|
top, hp, foe_hp = max_hp(player), player["hp"], foe["hp"]
|
||||||
pantry = {item: player["bank"][item] for item in edible(player)}
|
pantry = {item: player["bank"][item] for item in edible(player)}
|
||||||
|
|
@ -1230,6 +1526,7 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
|
||||||
pace = speed(world)
|
pace = speed(world)
|
||||||
# A bow looses its first arrow before the enemy closes in.
|
# A bow looses its first arrow before the enemy closes in.
|
||||||
mine, theirs, result, t = (0.5 if me["first"] else me["interval"]), foe["interval"], None, 0.0
|
mine, theirs, result, t = (0.5 if me["first"] else me["interval"]), foe["interval"], None, 0.0
|
||||||
|
swings = 0 # an enemy with more than one attack takes turns with them
|
||||||
while result is None:
|
while result is None:
|
||||||
t = min(mine, theirs)
|
t = min(mine, theirs)
|
||||||
if t > MAX_FIGHT:
|
if t > MAX_FIGHT:
|
||||||
|
|
@ -1239,23 +1536,27 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
|
||||||
mine += me["interval"]
|
mine += me["interval"]
|
||||||
crit = rng.random() < me["crit"]
|
crit = rng.random() < me["crit"]
|
||||||
landed = rng.random() < hit
|
landed = rng.random() < hit
|
||||||
dealt = max(1, round((me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))
|
dealt = max(1, round(me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))) if landed else 0
|
||||||
* (1 - shrug))) if landed else 0
|
|
||||||
foe_hp = max(0, foe_hp - dealt)
|
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])
|
events.append([round(t / pace, 2), "you", ("crit" if crit else "hit") if landed else "miss", dealt, round(hp),
|
||||||
|
foe_hp, me["damage"]])
|
||||||
if not foe_hp:
|
if not foe_hp:
|
||||||
result = "won"
|
result = "won"
|
||||||
continue
|
continue
|
||||||
theirs += foe["interval"]
|
theirs += foe["interval"]
|
||||||
|
kind = foe["attack"][swings % len(foe["attack"])]
|
||||||
|
swings += 1
|
||||||
crit = rng.random() < CRIT_CHANCE
|
crit = rng.random() < CRIT_CHANCE
|
||||||
landed = rng.random() < their_hit
|
landed = rng.random() < their_hit
|
||||||
if landed and rng.random() < me["block"]:
|
if landed and rng.random() < me["block"]:
|
||||||
events.append([round(t / pace, 2), "foe", "block", 0, max(0, round(hp)), foe_hp])
|
events.append([round(t / pace, 2), "foe", "block", 0, max(0, round(hp)), foe_hp])
|
||||||
continue
|
continue
|
||||||
dealt = max(1, round((foe["max_hit"] * CRIT_MULT if crit else rng.randint(1, foe["max_hit"])) * me["taken"])) if landed else 0
|
dealt = max(1, round((foe["max_hit"] * CRIT_MULT if crit else rng.randint(1, foe["max_hit"])) * _taken(me, kind))) \
|
||||||
|
if landed else 0
|
||||||
hp -= dealt
|
hp -= dealt
|
||||||
hurt += 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])
|
events.append([round(t / pace, 2), "foe", ("crit" if crit else "hit") if landed else "miss", dealt, max(0, round(hp)),
|
||||||
|
foe_hp, kind])
|
||||||
while hp < top * EAT_BELOW and any(pantry.values()):
|
while hp < top * EAT_BELOW and any(pantry.values()):
|
||||||
food = next(item for item, left in pantry.items() if left)
|
food = next(item for item, left in pantry.items() if left)
|
||||||
pantry[food] -= 1
|
pantry[food] -= 1
|
||||||
|
|
@ -1272,7 +1573,8 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
|
||||||
hp, result = 1, "lost"
|
hp, result = 1, "lost"
|
||||||
elif starving:
|
elif starving:
|
||||||
result = "fled"
|
result = "fled"
|
||||||
return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]),
|
return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]), "xp": foe["xp"],
|
||||||
|
"foe": {key: foe[key] for key in ("archetype", "attack", "vulnerable", "resists")},
|
||||||
"hp": hp, "result": result, "end": round(t / pace, 2), "duration": t / pace,
|
"hp": hp, "result": result, "end": round(t / pace, 2), "duration": t / pace,
|
||||||
"interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events, "style": me["style"],
|
"interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events, "style": me["style"],
|
||||||
"eaten": eaten, "damage": hurt, "pp": blessed["cost"],
|
"eaten": eaten, "damage": hurt, "pp": blessed["cost"],
|
||||||
|
|
@ -1536,6 +1838,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
||||||
outing = player["action"]
|
outing = player["action"]
|
||||||
fight = outing.pop("fight", None) or simulate_fight(world, player, action, rng)
|
fight = outing.pop("fight", None) or simulate_fight(world, player, action, rng)
|
||||||
outing.pop("carry", None) # from a short-lived version that ran the swing timer on
|
outing.pop("carry", None) # from a short-lived version that ran the swing timer on
|
||||||
|
xp = fight.get("xp", action["xp"]) * mods["xp"] # the variant you met
|
||||||
if player["prayers"] and not fight["pp"]:
|
if player["prayers"] and not fight["pp"]:
|
||||||
summary["prayer_out"] = True
|
summary["prayer_out"] = True
|
||||||
paid = min(player["pp"], fight["pp"])
|
paid = min(player["pp"], fight["pp"])
|
||||||
|
|
@ -2453,9 +2756,10 @@ def _resolve_dragon(world: dict, event: dict, now: float, rng: random.Random) ->
|
||||||
player["favour"] += 40
|
player["favour"] += 40
|
||||||
gain_xp(world, player, "combat", round(attack * 6), now)
|
gain_xp(world, player, "combat", round(attack * 6), now)
|
||||||
bank_add(world, player, "dragon_bones", 2, now, empty_summary())
|
bank_add(world, player, "dragon_bones", 2, now, empty_summary())
|
||||||
bank_add(world, player, "green_dragonhide", 1, now, empty_summary())
|
for weight in WEIGHTS:
|
||||||
|
bank_add(world, player, reagent(weight, 3), 1, now, empty_summary())
|
||||||
if garrison:
|
if garrison:
|
||||||
lines.append("Everyone in the garrison earned 40 favour, combat experience, dragon bones and a hide.")
|
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"])
|
||||||
else:
|
else:
|
||||||
severity = min(1.0, 1 - guard["total"] / attack)
|
severity = min(1.0, 1 - guard["total"] / attack)
|
||||||
|
|
@ -2556,10 +2860,10 @@ PROBLEMS = {
|
||||||
}
|
}
|
||||||
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
|
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
|
||||||
PROBLEM_ACTIONS = [
|
PROBLEM_ACTIONS = [
|
||||||
_fight("clear_wolves", monster("Wolf pack", "beast", 4), "Frontier", name="Drive off the wolves",
|
_encounter("clear_wolves", "Wolf pack", "Frontier", 4, WOLVES, label="Drive off the wolves",
|
||||||
special="problem", problem="wolves"),
|
special="problem", problem="wolves"),
|
||||||
_fight("catch_goblins", monster("Goblin thieves", "human", 5), "Frontier", name="Catch the goblin thieves",
|
_encounter("catch_goblins", "Goblin thieves", "Frontier", 5, GOBLINS, label="Catch the goblin thieves",
|
||||||
special="problem", problem="goblins"),
|
special="problem", problem="goblins"),
|
||||||
_action("drive_bear", "hunting", "Drive off the bear", 1, 45, 14, special="problem", problem="bear"),
|
_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("cull_boars", "hunting", "Cull the boar herd", 1, 45, 14, special="problem", problem="boars"),
|
||||||
_action("catch_rats", "hunting", "Catch the rats", 1, 30, 8, special="problem", problem="rats"),
|
_action("catch_rats", "hunting", "Catch the rats", 1, 30, 8, special="problem", problem="rats"),
|
||||||
|
|
@ -2600,7 +2904,7 @@ FITTINGS = [
|
||||||
"+1 prayer point per bone, ward +15", "star-altar", "#6f9a4a"),
|
"+1 prayer point per bone, ward +15", "star-altar", "#6f9a4a"),
|
||||||
("enchanted_chests", "Enchanted chests", "storehouse", 4, 75, {"magic_plank": 40, "rune_lock": 6},
|
("enchanted_chests", "Enchanted chests", "storehouse", 4, 75, {"magic_plank": 40, "rune_lock": 6},
|
||||||
"+500 stack size", "open-treasure-chest", "#a77be8"),
|
"+500 stack size", "open-treasure-chest", "#a77be8"),
|
||||||
("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "red_dragon_leather": 5, "adamant_chain": 6},
|
("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "mammoth_leather": 5, "adamant_chain": 6},
|
||||||
"HP recovers twice as fast", "fireplace", "#a77be8"),
|
"HP recovers twice as fast", "fireplace", "#a77be8"),
|
||||||
]
|
]
|
||||||
# Builders' shields, homesteaders' staves and rangers' bows, after each skill's other tabs.
|
# Builders' shields, homesteaders' staves and rangers' bows, after each skill's other tabs.
|
||||||
|
|
@ -3182,6 +3486,11 @@ PIECE_GLYPHS = {"sword": "pointy-sword", "shield": "round-shield", "helmet": "vi
|
||||||
TOOL_GLYPHS = {"pickaxe": "war-pick", "axe": "wood-axe", "hoe": "sickle", "fishing rod": "fishing-pole",
|
TOOL_GLYPHS = {"pickaxe": "war-pick", "axe": "wood-axe", "hoe": "sickle", "fishing rod": "fishing-pole",
|
||||||
"hunting knife": "bowie-knife", "hammer": "claw-hammer"}
|
"hunting knife": "bowie-knife", "hammer": "claw-hammer"}
|
||||||
LEATHER_GLYPHS = {"cap": "hood", "legs": "trousers", "body": "leather-vest"}
|
LEATHER_GLYPHS = {"cap": "hood", "legs": "trousers", "body": "leather-vest"}
|
||||||
|
CLOTH_GLYPHS = {"hood": "pointy-hat", "legs": "trousers", "body": "robe"}
|
||||||
|
FIBRE_GLYPHS = {"cotton": "cotton-flower", "silk_cocoons": "butterfly", "moonpetal": "lotus-flower",
|
||||||
|
"starbloom": "spiral-bloom", "spiritvine": "vine-flower"}
|
||||||
|
CLOTH_TINTS = ["#d8d0b8", "#f0f0f0", "#e8c8e0", "#a8c8f0", "#f0e080", "#9be0c8"]
|
||||||
|
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 = {
|
||||||
|
|
@ -3206,7 +3515,7 @@ SKILL_GLYPHS = {
|
||||||
"construction": ("hammer-nails", "#d1a46a"),
|
"construction": ("hammer-nails", "#d1a46a"),
|
||||||
"cooking": ("cooking-pot", "#e0703a"), "leatherworking": ("sewing-needle", "#d6a26e"),
|
"cooking": ("cooking-pot", "#e0703a"), "leatherworking": ("sewing-needle", "#d6a26e"),
|
||||||
"herblore": ("cauldron", "#7fbf5a"), "combat": ("crossed-swords", "#e05a5a"), "prayer": ("prayer", "#e9dfc6"),
|
"herblore": ("cauldron", "#7fbf5a"), "combat": ("crossed-swords", "#e05a5a"), "prayer": ("prayer", "#e9dfc6"),
|
||||||
"reconnaissance": ("spyglass", "#6f8fec"),
|
"reconnaissance": ("spyglass", "#6f8fec"), "tailoring": ("sewing-machine", "#c8a0e0"),
|
||||||
}
|
}
|
||||||
CLASS_GLYPHS = {"miner": ("miner", "#d07a3c"), "builder": ("crane", "#d1a46a"),
|
CLASS_GLYPHS = {"miner": ("miner", "#d07a3c"), "builder": ("crane", "#d1a46a"),
|
||||||
"homesteader": ("farmer", "#8fbf6a"), "ranger": ("bow-arrow", "#5fae8a")}
|
"homesteader": ("farmer", "#8fbf6a"), "ranger": ("bow-arrow", "#5fae8a")}
|
||||||
|
|
@ -3225,24 +3534,31 @@ ACTION_GLYPHS = {
|
||||||
"labour": ("hammer-nails", "#d1a46a"), "clear_wolves": ("wolf-head", "#9aa1ab"),
|
"labour": ("hammer-nails", "#d1a46a"), "clear_wolves": ("wolf-head", "#9aa1ab"),
|
||||||
"catch_goblins": ("goblin-head", "#8fbf6a"), "drive_bear": ("bear-head", "#8a5a32"), "cull_boars": ("boar", "#a06a4a"),
|
"catch_goblins": ("goblin-head", "#8fbf6a"), "drive_bear": ("bear-head", "#8a5a32"), "cull_boars": ("boar", "#a06a4a"),
|
||||||
"catch_rats": ("rat", "#9aa1ab"), "clear_rockslide": ("falling-rocks", "#a8a29e"),
|
"catch_rats": ("rat", "#9aa1ab"), "clear_rockslide": ("falling-rocks", "#a8a29e"),
|
||||||
|
"hunt_elk": ("deer", "#a07a50"), "hunt_sabrecat": ("saber-toothed-cat-head", "#e0a050"),
|
||||||
|
"hunt_mammoth": ("mammoth", "#8a6a50"),
|
||||||
}
|
}
|
||||||
PRAYER_GLYPHS = {"thick_skin": ("shield", "#a8a29e"), "burst_of_strength": ("biceps", "#e06a6a"),
|
PRAYER_GLYPHS = {"thick_skin": ("shield", "#a8a29e"), "burst_of_strength": ("biceps", "#e06a6a"),
|
||||||
"rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"),
|
"rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"),
|
||||||
"superhuman_strength": ("biceps", "#ff8a3a"), "second_wind": ("wind-hole", "#8fd0e0"),
|
"superhuman_strength": ("biceps", "#ff8a3a"), "second_wind": ("wind-hole", "#8fd0e0"),
|
||||||
"protection": ("aura", "#f0d060"), "piety": ("angel-wings", "#f0e6c0")}
|
"protection": ("aura", "#f0d060"), "piety": ("angel-wings", "#f0e6c0")}
|
||||||
TROPHY_GLYPHS = {"golem": "crystal-growth", "troll": "animal-hide", "spirit": "magic-swirl", "flyer": "sewing-string"}
|
TROPHY_GLYPHS = {"hammer": "crystal-growth", "sword": "animal-hide", "staff": "spiral-bloom", "bow": "sewing-string"}
|
||||||
TIER_TINTS = ["#9aa1ab", "#d4dae2", "#6f8fec", "#4cb878", "#3fd0e0"] # trophies of areas II to VI
|
TIER_TINTS = ["#9aa1ab", "#d4dae2", "#6f8fec", "#4cb878", "#3fd0e0"] # trophies of areas II to VI
|
||||||
MONSTER_GLYPHS = {"golem": ["rock-golem"] * 5, "troll": ["troll"] * 5,
|
RACE_GLYPHS = {
|
||||||
"spirit": ["fairy", "spectre", "ghost", "spectre", "ghost"],
|
"Clay golems": "rock-golem", "Cave trolls": "troll", "Restless dead": "skeleton", "Harpies": "harpy",
|
||||||
"flyer": ["bat", "harpy", "wyvern", "griffin-symbol", "eagle-head"]}
|
"Stone guardians": "golem-head", "Lizardfolk": "lizardman", "Bog wraiths": "spectre", "Giant spiders": "spider-face",
|
||||||
|
"Iron golems": "robot-golem", "Orcs": "orc-head", "Mine ghosts": "ghost", "Wyverns": "wyvern",
|
||||||
|
"Ice elementals": "ice-golem", "Frost trolls": "troll", "Banshees": "screaming", "Griffins": "griffin-symbol",
|
||||||
|
"Magma constructs": "lava", "Dragonkin": "dragon-spiral", "Shades": "ghost-ally", "Rocs": "eagle-head",
|
||||||
|
}
|
||||||
ACTION_GLYPHS.update({
|
ACTION_GLYPHS.update({
|
||||||
"fight_goblins": ("goblin-head", "#8fbf6a"), "fight_ruins": ("ogre", "#a08060"),
|
"fight_goblins": ("goblin-head", "#8fbf6a"), "fight_ruins": ("ogre", "#a08060"),
|
||||||
"fight_barrow_king": ("crowned-skull", "#c8d0e0"), "drill_militia": ACTION_GLYPHS["drill_militia"],
|
"fight_barrow_king": ("crowned-skull", "#c8d0e0"), "drill_militia": ACTION_GLYPHS["drill_militia"],
|
||||||
})
|
})
|
||||||
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
|
for _index, (_area, _level, _prefix, _races) in enumerate(AREAS):
|
||||||
for _index, _name in enumerate(_monsters):
|
for _race, *_rest in _races:
|
||||||
ACTION_GLYPHS[f"fight_{_slug(_name)}"] = (MONSTER_GLYPHS[_kind][_index], TIER_TINTS[_index])
|
ACTION_GLYPHS[f"fight_{_slug(_race)}"] = (RACE_GLYPHS[_race], TIER_TINTS[_index])
|
||||||
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield"]
|
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield",
|
||||||
|
*(kind["glyph"] for kind in DAMAGE_TYPES.values())]
|
||||||
|
|
||||||
|
|
||||||
def _assign_glyphs() -> None:
|
def _assign_glyphs() -> None:
|
||||||
|
|
@ -3269,8 +3585,16 @@ def _assign_glyphs() -> None:
|
||||||
ITEMS[leather[0]].update(glyph="rolled-cloth", tint=tint)
|
ITEMS[leather[0]].update(glyph="rolled-cloth", tint=tint)
|
||||||
for suffix, glyph in LEATHER_GLYPHS.items():
|
for suffix, glyph in LEATHER_GLYPHS.items():
|
||||||
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
|
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
|
||||||
for trophy, (_name, kind, tier) in TROPHIES.items():
|
for trophy, (_name, line, tier) in TROPHIES.items():
|
||||||
ITEMS[trophy].update(glyph=TROPHY_GLYPHS[kind], tint=TIER_TINTS[tier - 1])
|
ITEMS[trophy].update(glyph=TROPHY_GLYPHS[line], tint=TIER_TINTS[tier - 1])
|
||||||
|
for item, (weight, tier) in REAGENTS.items():
|
||||||
|
ITEMS[item].update(glyph=REAGENT_GLYPHS[weight], tint=TIER_TINTS[tier - 1])
|
||||||
|
for (fibre, _level, _time, _xp, cloth, prefix, _label), tint in zip(CLOTHS, CLOTH_TINTS):
|
||||||
|
if fibre[0] in FIBRE_GLYPHS:
|
||||||
|
ITEMS[fibre[0]].update(glyph=FIBRE_GLYPHS[fibre[0]], tint=tint)
|
||||||
|
ITEMS[cloth[0]].update(glyph="rolled-cloth", tint=tint)
|
||||||
|
for suffix, glyph in CLOTH_GLYPHS.items():
|
||||||
|
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
|
||||||
for herb, tint in HERB_TINTS.items():
|
for herb, tint in HERB_TINTS.items():
|
||||||
ITEMS[herb].update(glyph="herbs-bundle", tint=tint)
|
ITEMS[herb].update(glyph="herbs-bundle", tint=tint)
|
||||||
flasks = ["round-bottom-flask", "round-bottom-flask", "standing-potion", "standing-potion",
|
flasks = ["round-bottom-flask", "round-bottom-flask", "standing-potion", "standing-potion",
|
||||||
|
|
@ -3324,6 +3648,7 @@ def content() -> dict:
|
||||||
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
|
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
|
||||||
"potion_charges": POTION_CHARGES, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
|
"potion_charges": POTION_CHARGES, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
|
||||||
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
|
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
|
||||||
"styles": STYLES, "weakness": WEAKNESS, "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
|
"styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE,
|
||||||
|
"resistant": RESISTANT, "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
|
||||||
"regen_per_hour": REGEN_PER_HOUR,
|
"regen_per_hour": REGEN_PER_HOUR,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -439,7 +439,7 @@ function skillPage(id) {
|
||||||
const open = (g) => actions.some((a) => a.group === g && !isLocked(a));
|
const open = (g) => actions.some((a) => a.group === g && !isLocked(a));
|
||||||
let current = tabs[id] || remember(`tab.${id}`);
|
let current = tabs[id] || remember(`tab.${id}`);
|
||||||
// Tier ladders open on the highest tier you can work; categories open on the first.
|
// Tier ladders open on the highest tier you can work; categories open on the first.
|
||||||
const ladder = ["smithing", "leatherworking", "herblore", "combat"].includes(id);
|
const ladder = ["smithing", "leatherworking", "tailoring", "herblore", "combat"].includes(id);
|
||||||
if (!groups.includes(current)) current = (ladder && [...groups].reverse().find(open)) || groups[0];
|
if (!groups.includes(current)) current = (ladder && [...groups].reverse().find(open)) || groups[0];
|
||||||
const bar = groups.map((g) => `<button class="tab ${g === current ? "on" : ""} ${open(g) ? "" : "dim"}" data-tab="${id}" data-group="${esc(g)}">${esc(g)}</button>`).join("");
|
const bar = groups.map((g) => `<button class="tab ${g === current ? "on" : ""} ${open(g) ? "" : "dim"}" data-tab="${id}" data-group="${esc(g)}">${esc(g)}</button>`).join("");
|
||||||
// Ungrouped actions (the building site) stay above the tabs.
|
// Ungrouped actions (the building site) stay above the tabs.
|
||||||
|
|
@ -522,40 +522,57 @@ function toolPanel(skill) {
|
||||||
: `<span class="muted">No tool.</span>`}</div>`;
|
: `<span class="muted">No tool.</span>`}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// What a monster is weak to and shrugs off, and how your weapon fares against it.
|
// A damage type's icon: what an enemy attacks with, or (tinted) what it's vulnerable to or resists.
|
||||||
function weaknessLine(kind, odds) {
|
function dmgIcon(type, how = "") {
|
||||||
const table = C.weakness[kind];
|
const t = C.damage_types[type], name = t.name.toLowerCase();
|
||||||
if (!table) return "";
|
const tip = how === "vuln" ? `Vulnerable to ${name} (+${Math.round((C.vulnerable - 1) * 100)}%)`
|
||||||
const names = (pick) => Object.entries(table).filter(([, m]) => pick(m)).map(([s]) => `${C.styles[s].name}s`).join(", ");
|
: how === "resist" ? `Resists ${name} (-${Math.round((1 - C.resistant) * 100)}%)`
|
||||||
const weak = names((m) => m > 1), tough = names((m) => m < 1);
|
: how === "attack" ? `Attacks with ${name}` : `${t.name} damage`;
|
||||||
const yours = odds.weakness > 1 ? ` · <b class="good">your ${C.styles[odds.style].name} hits it hard</b>`
|
return `<span class="dmg ${how}" title="${esc(tip)}">${glyph(t.glyph, t.tint)}</span>`;
|
||||||
: odds.weakness < 1 ? ` · <b class="bad">your ${C.styles[odds.style].name} does half damage</b>` : "";
|
}
|
||||||
return `<div class="small muted">${weak ? `Weak to ${weak}` : ""}${weak && tough ? " · " : ""}${tough ? `shrugs off ${tough}` : ""}${yours}</div>`;
|
|
||||||
|
function resistIcons(v) {
|
||||||
|
return v.vulnerable.map((k) => dmgIcon(k, "vuln")).join("") + v.resists.map((k) => dmgIcon(k, "resist")).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Who turns up in an encounter: each variant, what it attacks with, and how it takes each damage type.
|
||||||
|
// A variant your weapon is good against is green, one that resists it red.
|
||||||
|
function variantRows(e, o) {
|
||||||
|
return `<div class="variants">${e.variants.map((v, i) => {
|
||||||
|
const fit = o.variants[i].affinity;
|
||||||
|
return `<div class="variant ${fit > 1 ? "good" : fit < 1 ? "bad" : ""}">
|
||||||
|
<span class="v-attack">${v.attack.map((k) => dmgIcon(k, "attack")).join("")}</span>
|
||||||
|
<span class="v-name">${esc(v.name)}${v.archetype === "elite" ? ` <span class="muted">· rare</span>` : ""}</span>
|
||||||
|
<span class="v-hp muted">${fmt(v.hp)} HP</span>
|
||||||
|
<span class="v-res">${resistIcons(v)}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join("")}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fight in progress, replayed from the server's swing-by-swing record.
|
// The fight in progress, replayed from the server's swing-by-swing record.
|
||||||
function fightPanel() {
|
function fightPanel() {
|
||||||
const f = S.me.action?.fight;
|
const f = S.me.action?.fight;
|
||||||
if (!f) return "";
|
if (!f) return "";
|
||||||
const side = (who, name) => `<div class="fighter">
|
const side = (who, name, foe) => `<div class="fighter">
|
||||||
<div class="fighter-name"><b>${esc(name)}</b><span class="small muted" data-f="${who}-hp"></span></div>
|
<div class="fighter-name"><b>${esc(name)}</b>${foe ? `<span class="v-res">${resistIcons(foe)}</span>` : ""}<span class="small muted" data-f="${who}-hp"></span></div>
|
||||||
<div class="bar hp ${who}"><span data-f="${who}-bar"></span></div>
|
<div class="bar hp ${who}"><span data-f="${who}-bar"></span></div>
|
||||||
<div class="bar swing"><span data-f="${who}-swing"></span></div>
|
<div class="bar swing"><span data-f="${who}-swing"></span></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
return `<div class="panel fight" data-fight="${S.me.action.start}">
|
return `<div class="panel fight" data-fight="${S.me.action.start}">
|
||||||
<div class="fighters">${side("you", S.me.name)}<span class="vs">⚔️</span>${side("foe", f.enemy)}</div>
|
<div class="fighters">${side("you", S.me.name)}<span class="vs">⚔️</span>${side("foe", f.enemy, f.foe)}</div>
|
||||||
<ul class="fight-log" data-f="log"></ul>
|
<ul class="fight-log" data-f="log"></ul>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fightLine(e, enemy) {
|
function fightLine(e, enemy) {
|
||||||
const [, who, kind, amount, , , food] = e;
|
const [, who, kind, amount, , , food] = e;
|
||||||
|
const type = kind === "hit" || kind === "crit" ? (C.damage_types[food] ? ` ${dmgIcon(food)}` : "") : "";
|
||||||
const foe = esc(enemy);
|
const foe = esc(enemy);
|
||||||
if (kind === "eat") return `<li class="good">You eat ${esc(item(food).name.toLowerCase())} (+${amount})</li>`;
|
if (kind === "eat") return `<li class="good">You eat ${esc(item(food).name.toLowerCase())} (+${amount})</li>`;
|
||||||
if (kind === "wind") return `<li class="good">Second wind! Back on your feet</li>`;
|
if (kind === "wind") return `<li class="good">Second wind! Back on your feet</li>`;
|
||||||
if (kind === "block") return `<li class="good">You block ${foe}'s blow</li>`;
|
if (kind === "block") return `<li class="good">You block ${foe}'s blow</li>`;
|
||||||
if (who === "you") return kind === "miss" ? `<li class="muted">You miss</li>` : `<li>You ${kind === "crit" ? "<b>crit</b>" : "hit"} for <b>${amount}</b></li>`;
|
if (who === "you") return kind === "miss" ? `<li class="muted">You miss</li>` : `<li>You ${kind === "crit" ? "<b>crit</b>" : "hit"} for <b>${amount}</b>${type}</li>`;
|
||||||
return kind === "miss" ? `<li class="muted">${foe} misses</li>` : `<li class="bad">${foe} ${kind === "crit" ? "<b>crits</b>" : "hits"} you for <b>${amount}</b></li>`;
|
return kind === "miss" ? `<li class="muted">${foe} misses</li>` : `<li class="bad">${foe} ${kind === "crit" ? "<b>crits</b>" : "hits"} you for <b>${amount}</b>${type}</li>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateFight(now) {
|
function animateFight(now) {
|
||||||
|
|
@ -605,16 +622,16 @@ function styleNote(me) {
|
||||||
const own = C.class_style[me.cls], traits = {
|
const own = C.class_style[me.cls], traits = {
|
||||||
sword: `${st.interval}s${me.equip.shield ? `, shield blocks ${Math.round(C.block_chance * 100)}%` : ""}`,
|
sword: `${st.interval}s${me.equip.shield ? `, shield blocks ${Math.round(C.block_chance * 100)}%` : ""}`,
|
||||||
hammer: `${st.interval}s, double crits`,
|
hammer: `${st.interval}s, double crits`,
|
||||||
staff: `${st.interval}s, accurate, ignores armour`,
|
staff: `${st.interval}s, accurate`,
|
||||||
bow: `${st.interval}s, shoots first`,
|
bow: `${st.interval}s, shoots first`,
|
||||||
};
|
};
|
||||||
return `<p class="small">${weapon ? st.name[0].toUpperCase() + st.name.slice(1) : "Fists"}: ${traits[styleId]}.
|
return `<p class="small">${dmgIcon(st.damage)} ${weapon ? st.name[0].toUpperCase() + st.name.slice(1) : "Fists"}: ${C.damage_types[st.damage].name.toLowerCase()}, ${traits[styleId]}.
|
||||||
${own === styleId ? `<b class="good">Trade weapon: +15% damage.</b>` : `Trade weapon: ${esc(C.styles[own].name)} (+15%).`}</p>`;
|
${own === styleId ? `<b class="good">Trade weapon: +15% damage.</b>` : `Trade weapon: ${esc(C.styles[own].name)} (+15%).`}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function combatPanel() {
|
function combatPanel() {
|
||||||
const me = S.me;
|
const me = S.me;
|
||||||
const stat = (id) => [item(id).power && `+${item(id).power} power`, item(id).armour && `+${item(id).armour} armour`].filter(Boolean).join(", ");
|
const stat = (id) => gearStats(item(id));
|
||||||
const slot = (name, label) => {
|
const slot = (name, label) => {
|
||||||
const id = me.equip[name];
|
const id = me.equip[name];
|
||||||
return `<div class="gear-slot">${id
|
return `<div class="gear-slot">${id
|
||||||
|
|
@ -636,11 +653,17 @@ function combatPanel() {
|
||||||
</div>
|
</div>
|
||||||
<div class="panel"><h2>Equipment</h2>
|
<div class="panel"><h2>Equipment</h2>
|
||||||
<div class="gear">${slot("weapon", "weapon")}${slot("shield", "shield")}${slot("helmet", "helmet")}${slot("body", "body armour")}${slot("legs", "leg armour")}</div>
|
<div class="gear">${slot("weapon", "weapon")}${slot("shield", "shield")}${slot("helmet", "helmet")}${slot("body", "body armour")}${slot("legs", "leg armour")}</div>
|
||||||
<p class="small muted">Power ${me.power} · armour ${me.armour} (-${Math.round(me.reduction * 100)}% damage)</p>
|
<p class="small muted">Power ${me.power} · armour ${me.armour} (-${Math.round(me.reduction * 100)}% physical) · ward ${me.ward} (-${Math.round(me.ward_reduction * 100)}% magic)</p>
|
||||||
${styleNote(me)}
|
${styleNote(me)}
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Power, armour and ward, and the armour's weight.
|
||||||
|
function gearStats(it) {
|
||||||
|
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`, it.ward && `+${it.ward} ward`];
|
||||||
|
return stats.filter(Boolean).join(", ") + (it.weight ? ` (${C.weights[it.weight].name.toLowerCase()})` : "");
|
||||||
|
}
|
||||||
|
|
||||||
// What an item does once made: healing, defence, tool speed, and who can use it.
|
// What an item does once made: healing, defence, tool speed, and who can use it.
|
||||||
function itemStats(id) {
|
function itemStats(id) {
|
||||||
const it = item(id);
|
const it = item(id);
|
||||||
|
|
@ -652,7 +675,8 @@ function itemStats(id) {
|
||||||
const st = C.styles[it.style], trade = Object.entries(C.class_style).find(([, s]) => s === it.style)?.[0];
|
const st = C.styles[it.style], trade = Object.entries(C.class_style).find(([, s]) => s === it.style)?.[0];
|
||||||
parts.push(`${it.two_handed ? "two-handed " : ""}${st.name}, ${st.interval}s${trade ? ` (${C.classes[trade].name.toLowerCase()})` : ""}`);
|
parts.push(`${it.two_handed ? "two-handed " : ""}${st.name}, ${st.interval}s${trade ? ` (${C.classes[trade].name.toLowerCase()})` : ""}`);
|
||||||
}
|
}
|
||||||
if (it.armour) parts.push(`+${it.armour} armour`);
|
if (it.armour || it.ward) parts.push(gearStats({ ...it, power: 0 }));
|
||||||
|
if (it.reagent) parts.push(`Reagent for ${C.weights[it.reagent].name.toLowerCase()} armour`);
|
||||||
if (it.tool) parts.push(`${Math.round(it.speed * 100)}% faster ${skillOf(it.tool).name.toLowerCase()}`);
|
if (it.tool) parts.push(`${Math.round(it.speed * 100)}% faster ${skillOf(it.tool).name.toLowerCase()}`);
|
||||||
if (it.req && it.req[1] > 1) parts.push(`needs ${skillOf(it.req[0]).name.toLowerCase()} ${it.req[1]}`);
|
if (it.req && it.req[1] > 1) parts.push(`needs ${skillOf(it.req[0]).name.toLowerCase()} ${it.req[1]}`);
|
||||||
if (it.potion) parts.push(`${it.potion.text}, ${C.potion_charges} uses`);
|
if (it.potion) parts.push(`${it.potion.text}, ${C.potion_charges} uses`);
|
||||||
|
|
@ -685,10 +709,10 @@ function actionCard(a) {
|
||||||
let extra = "";
|
let extra = "";
|
||||||
if (a.enemy) {
|
if (a.enemy) {
|
||||||
const o = me.fight_odds[a.id], e = a.enemy, pct = (x) => `${Math.round(x * 100)}%`;
|
const o = me.fight_odds[a.id], e = a.enemy, pct = (x) => `${Math.round(x * 100)}%`;
|
||||||
extra = `<div class="small">${HEART} ${fmt(e.hp)} HP · hits up to <b>${o.their_max}</b> every ${e.interval}s${e.boss ? ` · <span class="tag warn">Boss</span>` : ""}</div>
|
extra = `${variantRows(e, o)}
|
||||||
|
<div class="small">Hits up to <b>${o.their_max}</b>${e.boss ? ` · <span class="tag warn">Boss</span>` : ""}</div>
|
||||||
<div class="small">You hit <b>${pct(o.hit)}</b> (max ${o.max_hit}) · it hits <b>${pct(o.their_hit)}</b></div>
|
<div class="small">You hit <b>${pct(o.hit)}</b> (max ${o.max_hit}) · it hits <b>${pct(o.their_hit)}</b></div>
|
||||||
<div class="small">~<b>${dur(o.kill_time)}</b> a kill · <b>${Math.round(o.damage)}</b> damage taken${a.gold ? ` · ${a.gold} gold` : ""}</div>
|
<div class="small">~<b>${dur(o.kill_time)}</b> a kill · <b>${Math.round(o.damage)}</b> damage taken${a.gold ? ` · ${a.gold} gold` : ""}</div>`;
|
||||||
${weaknessLine(e.kind, o)}`;
|
|
||||||
if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${esc(item(id).name)}${q > 1 ? ` ×${q}` : ""}: ${Math.round(odds * 100)}% chance">${item(id).icon} ${Math.round(odds * 100)}%</span>`).join("");
|
if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${esc(item(id).name)}${q > 1 ? ` ×${q}` : ""}: ${Math.round(odds * 100)}% chance">${item(id).icon} ${Math.round(odds * 100)}%</span>`).join("");
|
||||||
if (a.special === "sabotage") extra += `<div class="small muted">-3% raid strength per win (max 30%)</div>`;
|
if (a.special === "sabotage") extra += `<div class="small muted">-3% raid strength per win (max 30%)</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -811,7 +835,7 @@ function equipRow(id, it) {
|
||||||
|
|
||||||
function equipLabel(it) {
|
function equipLabel(it) {
|
||||||
if (it.tool) return `Use as ${skillOf(it.tool).name.toLowerCase()} tool (${Math.round(it.speed * 100)}% faster)`;
|
if (it.tool) return `Use as ${skillOf(it.tool).name.toLowerCase()} tool (${Math.round(it.speed * 100)}% faster)`;
|
||||||
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`].filter(Boolean).join(", ");
|
const stats = gearStats(it);
|
||||||
return `Equip ${it.slot === "body" ? "body armour" : it.slot === "legs" ? "leg armour" : it.slot} (${stats})`;
|
return `Equip ${it.slot === "body" ? "body armour" : it.slot === "legs" ? "leg armour" : it.slot} (${stats})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1147,7 +1171,7 @@ function characterSheet(uid) {
|
||||||
const id = p.equip[slot];
|
const id = p.equip[slot];
|
||||||
const it = id && item(id);
|
const it = id && item(id);
|
||||||
const stat = !it ? "" : it.tool ? `${Math.round(it.speed * 100)}% faster`
|
const stat = !it ? "" : it.tool ? `${Math.round(it.speed * 100)}% faster`
|
||||||
: [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`].filter(Boolean).join(", ");
|
: gearStats(it);
|
||||||
return `<div class="sheet-slot ${id ? "" : "empty"}">
|
return `<div class="sheet-slot ${id ? "" : "empty"}">
|
||||||
<div class="k">${label}</div>
|
<div class="k">${label}</div>
|
||||||
${id ? `<div class="sheet-item"><span class="ico">${it.icon}</span><span><b>${esc(it.name)}</b><span class="muted small">${stat}</span></span>
|
${id ? `<div class="sheet-item"><span class="ico">${it.icon}</span><span><b>${esc(it.name)}</b><span class="muted small">${stat}</span></span>
|
||||||
|
|
@ -1183,7 +1207,8 @@ function characterSheet(uid) {
|
||||||
<div class="stat"><div class="k">Health</div><div class="v">${fmt(p.hp)} <small>/ ${p.max_hp}</small></div><div class="bar hp" style="margin-top:6px"><span style="width:${p.hp / p.max_hp * 100}%"></span></div></div>
|
<div class="stat"><div class="k">Health</div><div class="v">${fmt(p.hp)} <small>/ ${p.max_hp}</small></div><div class="bar hp" style="margin-top:6px"><span style="width:${p.hp / p.max_hp * 100}%"></span></div></div>
|
||||||
<div class="stat"><div class="k">Combat power</div><div class="v">${p.power}</div><div class="note">${guard} in the garrison</div></div>
|
<div class="stat"><div class="k">Combat power</div><div class="v">${p.power}</div><div class="note">${guard} in the garrison</div></div>
|
||||||
${mine ? `<div class="stat"><div class="k">Prayer points</div><div class="v">${fmt(p.pp)}</div><div class="note">${p.prayers.length ? p.prayers.map((id) => esc(C.prayers.find((x) => x.id === id).name)).join(", ") : "No prayers active"}</div></div>` : ""}
|
${mine ? `<div class="stat"><div class="k">Prayer points</div><div class="v">${fmt(p.pp)}</div><div class="note">${p.prayers.length ? p.prayers.map((id) => esc(C.prayers.find((x) => x.id === id).name)).join(", ") : "No prayers active"}</div></div>` : ""}
|
||||||
<div class="stat"><div class="k">Armour</div><div class="v">${p.armour}</div><div class="note">${Math.round(p.reduction * 100)}% less damage</div></div>
|
<div class="stat"><div class="k">Armour</div><div class="v">${p.armour}</div><div class="note">${Math.round(p.reduction * 100)}% less physical</div></div>
|
||||||
|
<div class="stat"><div class="k">Ward</div><div class="v">${p.ward}</div><div class="note">${Math.round(p.ward_reduction * 100)}% less magic</div></div>
|
||||||
<div class="stat"><div class="k">Given to the city</div><div class="v favour">${fmt(p.donated)}</div><div class="note">${fmt(p.favour)} favour to spend</div></div>
|
<div class="stat"><div class="k">Given to the city</div><div class="v favour">${fmt(p.donated)}</div><div class="note">${fmt(p.favour)} favour to spend</div></div>
|
||||||
${mine ? `<div class="stat"><div class="k">Purse</div><div class="v gold">${fmt(p.gold)}</div><div class="note">${p.slots_used} / ${p.slots} bank slots</div></div>` : ""}
|
${mine ? `<div class="stat"><div class="k">Purse</div><div class="v gold">${fmt(p.gold)}</div><div class="note">${p.slots_used} / ${p.slots} bank slots</div></div>` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 266 KiB |
|
|
@ -443,3 +443,16 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
||||||
.battle-result { font-weight: 700; font-size: 17px; margin: 6px 0 8px; }
|
.battle-result { font-weight: 700; font-size: 17px; margin: 6px 0 8px; }
|
||||||
.battle-result.win { color: var(--good); }
|
.battle-result.win { color: var(--good); }
|
||||||
.battle-result.loss { color: var(--bad); }
|
.battle-result.loss { color: var(--bad); }
|
||||||
|
|
||||||
|
/* Damage types and encounter rosters */
|
||||||
|
.dmg { display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 5px; vertical-align: -4px; }
|
||||||
|
.dmg .gi { width: 13px; height: 13px; }
|
||||||
|
.dmg.vuln { background: rgba(95, 174, 90, .22); box-shadow: inset 0 0 0 1px rgba(95, 174, 90, .6); }
|
||||||
|
.dmg.resist { background: rgba(226, 100, 92, .2); box-shadow: inset 0 0 0 1px rgba(226, 100, 92, .55); }
|
||||||
|
.variants { display: grid; gap: 3px; margin: 2px 0 4px; font-size: 13px; }
|
||||||
|
.variant { display: grid; grid-template-columns: auto 1fr auto auto; gap: 6px; align-items: center; }
|
||||||
|
.variant.good .v-name { color: var(--good); }
|
||||||
|
.variant.bad .v-name { color: var(--bad); }
|
||||||
|
.v-attack, .v-res { display: inline-flex; gap: 2px; }
|
||||||
|
.v-hp { font-size: 12px; }
|
||||||
|
.fighter-name .v-res { margin-left: 6px; }
|
||||||
|
|
|
||||||
|
|
@ -730,9 +730,13 @@ class TierTest(unittest.TestCase):
|
||||||
self.assertIn(f"{key}_{piece}", game.ITEMS)
|
self.assertIn(f"{key}_{piece}", game.ITEMS)
|
||||||
self.assertIn(f"smith_{key}_{piece}", game.ACTION_BY_ID)
|
self.assertIn(f"smith_{key}_{piece}", game.ACTION_BY_ID)
|
||||||
self.assertIn(f"make_{key}_shield", game.ACTION_BY_ID) # builders make shields
|
self.assertIn(f"make_{key}_shield", game.ACTION_BY_ID) # builders make shields
|
||||||
for *_, prefix, _label, _level, _armour in game.LEATHERS:
|
for *_, prefix, _label, _level in game.LEATHERS:
|
||||||
for piece in ("cap", "legs", "body"):
|
for piece in ("cap", "legs", "body"):
|
||||||
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
|
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
|
||||||
|
for *_, prefix, _label in game.CLOTHS:
|
||||||
|
for piece in ("hood", "legs", "body"):
|
||||||
|
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
|
||||||
|
self.assertIn(f"sew_{prefix}_{piece}", game.ACTION_BY_ID)
|
||||||
self.assertEqual(game.ITEMS["rune_body"]["plural"], "Rune cuirasses")
|
self.assertEqual(game.ITEMS["rune_body"]["plural"], "Rune cuirasses")
|
||||||
|
|
||||||
def test_gear_needs_levels_to_wear(self):
|
def test_gear_needs_levels_to_wear(self):
|
||||||
|
|
@ -758,7 +762,7 @@ class TierTest(unittest.TestCase):
|
||||||
self.assertEqual(game.build_cost("walls", 5)["mithril_brackets"], 60)
|
self.assertEqual(game.build_cost("walls", 5)["mithril_brackets"], 60)
|
||||||
self.assertEqual(game.build_cost("walls", 5)["steel_bar"], 180)
|
self.assertEqual(game.build_cost("walls", 5)["steel_bar"], 180)
|
||||||
|
|
||||||
def test_dragons_drop_dragonhide(self):
|
def test_dragons_drop_their_areas_reagents(self):
|
||||||
world = world_with(("wil", "commoner"))
|
world = world_with(("wil", "commoner"))
|
||||||
p = world["players"]["wil"]
|
p = world["players"]["wil"]
|
||||||
p["xp"]["combat"] = game.XP_TABLE[80]
|
p["xp"]["combat"] = game.XP_TABLE[80]
|
||||||
|
|
@ -767,7 +771,7 @@ class TierTest(unittest.TestCase):
|
||||||
p["bank"] = {"cooked_swordfish": 200}
|
p["bank"] = {"cooked_swordfish": 200}
|
||||||
game.start_action(world, p, "fight_green_dragon", T0)
|
game.start_action(world, p, "fight_green_dragon", T0)
|
||||||
game.process_player(world, p, T0 + 3600 * 2, random.Random(3))
|
game.process_player(world, p, T0 + 3600 * 2, random.Random(3))
|
||||||
self.assertGreater(p["bank"].get("green_dragonhide", 0), 30)
|
self.assertGreater(sum(p["bank"].get(game.reagent(weight, 3), 0) for weight in game.WEIGHTS), 30)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1345,28 +1349,30 @@ class WeaponStyleTest(unittest.TestCase):
|
||||||
p["equip"].update(weapon=weapon, shield=shield, body="iron_body", helmet="iron_helmet", legs="iron_legs")
|
p["equip"].update(weapon=weapon, shield=shield, body="iron_body", helmet="iron_helmet", legs="iron_legs")
|
||||||
return world, p
|
return world, p
|
||||||
|
|
||||||
def test_each_trade_fights_its_own_monster_best(self):
|
def test_no_trade_is_funnelled_into_one_race(self):
|
||||||
|
# Every race mixes its weaknesses across its variants, so no weapon has one easy target.
|
||||||
styles = {"miner": "iron_warhammer", "builder": "iron_sword", "homesteader": "oak_staff", "ranger": "oak_bow"}
|
styles = {"miner": "iron_warhammer", "builder": "iron_sword", "homesteader": "oak_staff", "ranger": "oak_bow"}
|
||||||
prey = {"miner": "fight_clay_golem", "builder": "fight_cave_troll", "homesteader": "fight_will_o_wisp",
|
races = ["fight_clay_golems", "fight_cave_trolls", "fight_restless_dead", "fight_harpies"]
|
||||||
"ranger": "fight_giant_bat"}
|
|
||||||
for cls, weapon in styles.items():
|
for cls, weapon in styles.items():
|
||||||
_world, p = self.armed(cls, weapon, "iron_shield" if cls == "builder" else None)
|
_world, p = self.armed(cls, weapon, "iron_shield" if cls == "builder" else None)
|
||||||
times = {target: game.fight_odds(p, game.ACTION_BY_ID[target])["kill_time"] for target in prey.values()}
|
times = [game.fight_odds(p, game.ACTION_BY_ID[race])["kill_time"] for race in races]
|
||||||
self.assertEqual(min(times, key=times.get), prey[cls], cls)
|
self.assertLess(max(times) / min(times), 1.45, cls)
|
||||||
|
|
||||||
def test_style_traits(self):
|
def test_style_traits(self):
|
||||||
_w, swordsman = self.armed("builder", "iron_sword", "iron_shield")
|
_w, swordsman = self.armed("builder", "iron_sword", "iron_shield")
|
||||||
_w, smith = self.armed("miner", "iron_warhammer")
|
_w, smith = self.armed("miner", "iron_warhammer")
|
||||||
_w, archer = self.armed("ranger", "oak_bow")
|
_w, archer = self.armed("ranger", "oak_bow")
|
||||||
spec = lambda p: game.fighter(p, game.NO_BLESSING, {}, game.ACTION_BY_ID["fight_clay_golem"]["enemy"])
|
brute = game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"][0] # vulnerable to blunt, resists piercing
|
||||||
|
spec = lambda p: game.fighter(p, game.NO_BLESSING, {}, brute)
|
||||||
self.assertEqual(spec(swordsman)["block"], game.BLOCK_CHANCE)
|
self.assertEqual(spec(swordsman)["block"], game.BLOCK_CHANCE)
|
||||||
self.assertEqual(spec(smith)["crit"], 0.10)
|
self.assertEqual(spec(smith)["crit"], 0.10)
|
||||||
self.assertEqual(spec(smith)["weakness"], 1.5)
|
self.assertEqual(spec(smith)["affinity"], game.VULNERABLE)
|
||||||
self.assertEqual(spec(archer)["weakness"], 0.5)
|
self.assertEqual(spec(archer)["affinity"], game.RESISTANT)
|
||||||
|
self.assertEqual(spec(swordsman)["affinity"], 1)
|
||||||
_w, other = self.armed("builder", "iron_warhammer")
|
_w, other = self.armed("builder", "iron_warhammer")
|
||||||
self.assertGreater(spec(smith)["max_hit"], spec(other)["max_hit"]) # the trade's own weapon hits harder
|
self.assertGreater(spec(smith)["max_hit"], spec(other)["max_hit"]) # the trade's own weapon hits harder
|
||||||
world, archer = self.armed("ranger", "oak_bow")
|
world, archer = self.armed("ranger", "oak_bow")
|
||||||
fight = game.simulate_fight(world, archer, game.ACTION_BY_ID["fight_giant_bat"], random.Random(1))
|
fight = game.simulate_fight(world, archer, game.ACTION_BY_ID["fight_harpies"], random.Random(1))
|
||||||
self.assertEqual(fight["events"][0][1], "you") # the bow strikes first
|
self.assertEqual(fight["events"][0][1], "you") # the bow strikes first
|
||||||
|
|
||||||
def test_two_handed_weapons_leave_no_room_for_a_shield(self):
|
def test_two_handed_weapons_leave_no_room_for_a_shield(self):
|
||||||
|
|
@ -1377,13 +1383,40 @@ class WeaponStyleTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_weapons_past_copper_need_trophies(self):
|
def test_weapons_past_copper_need_trophies(self):
|
||||||
for action_id, trophy in (("smith_iron_warhammer", "clay_core"), ("smith_iron_sword", "cave_troll_hide"),
|
for action_id, trophy in (("smith_iron_warhammer", "clay_core"), ("smith_iron_sword", "cave_troll_hide"),
|
||||||
("make_iron_shield", "cave_troll_hide"), ("brew_oak_staff", "wisp_essence"),
|
("make_iron_shield", "cave_troll_hide"), ("brew_oak_staff", "grave_dust"),
|
||||||
("lw_oak_bow", "bat_sinew"), ("smith_rune_warhammer", "runic_core")):
|
("lw_oak_bow", "harpy_sinew"), ("smith_rune_warhammer", "magma_core")):
|
||||||
self.assertIn(trophy, game.ACTION_BY_ID[action_id]["inputs"], action_id)
|
self.assertIn(trophy, game.ACTION_BY_ID[action_id]["inputs"], action_id)
|
||||||
for action_id in ("smith_copper_warhammer", "smith_copper_sword", "make_copper_shield",
|
for action_id in ("smith_copper_warhammer", "smith_copper_sword", "make_copper_shield",
|
||||||
"brew_wooden_staff", "lw_wooden_bow"):
|
"brew_wooden_staff", "lw_wooden_bow"):
|
||||||
self.assertFalse(any(item in game.TROPHIES for item in game.ACTION_BY_ID[action_id]["inputs"]), action_id)
|
self.assertFalse(any(item in game.TROPHIES for item in game.ACTION_BY_ID[action_id]["inputs"]), action_id)
|
||||||
|
|
||||||
|
def test_armour_past_copper_needs_its_areas_reagent(self):
|
||||||
|
self.assertEqual(game.ACTION_BY_ID["smith_iron_body"]["inputs"], {"iron_bar": 5, "watchtower_plating": 3})
|
||||||
|
self.assertEqual(game.ACTION_BY_ID["lw_hard_leather_cap"]["inputs"], {"hard_leather": 2, "watchtower_lacing": 1})
|
||||||
|
self.assertEqual(game.ACTION_BY_ID["sew_moonweave_legs"]["inputs"], {"moonweave": 3, "ironhill_thread": 2})
|
||||||
|
for action_id in ("smith_copper_body", "lw_cap", "sew_linen_body"):
|
||||||
|
self.assertFalse(any(item in game.REAGENTS for item in game.ACTION_BY_ID[action_id]["inputs"]), action_id)
|
||||||
|
harpies = game.ACTION_BY_ID["fight_harpies"]["drops"]
|
||||||
|
self.assertIn(["watchtower_thread", 1, game.REAGENT_ODDS], harpies)
|
||||||
|
|
||||||
|
def test_heavy_stops_blows_light_stops_magic(self):
|
||||||
|
_w, plate = self.armed("miner", "iron_warhammer")
|
||||||
|
_w, cloth = self.armed("miner", "iron_warhammer")
|
||||||
|
cloth["equip"].update(helmet="cotton_hood", body="cotton_body", legs="cotton_legs")
|
||||||
|
self.assertGreater(game.armour(plate), game.armour(cloth))
|
||||||
|
self.assertGreater(game.magic_ward(cloth), game.magic_ward(plate))
|
||||||
|
shaper = next(v for v in game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"] if v["name"] == "Mud shaper")
|
||||||
|
brute = game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"][0]
|
||||||
|
hurt = lambda p, foe: game._variant_odds(p, foe, game.NO_BLESSING, {})["damage"]
|
||||||
|
self.assertLess(hurt(cloth, shaper), hurt(plate, shaper))
|
||||||
|
self.assertLess(hurt(plate, brute), hurt(cloth, brute))
|
||||||
|
|
||||||
|
def test_each_kill_is_one_of_the_variants(self):
|
||||||
|
world, p = self.armed("miner", "iron_warhammer", level=30)
|
||||||
|
seen = {game.simulate_fight(world, p, game.ACTION_BY_ID["fight_goblins"], random.Random(seed))["enemy"]
|
||||||
|
for seed in range(60)}
|
||||||
|
self.assertEqual(seen, {"Goblin brute", "Goblin rogue", "Goblin warlock"})
|
||||||
|
|
||||||
def test_shield_smiths_stop_after_the_move(self):
|
def test_shield_smiths_stop_after_the_move(self):
|
||||||
world = world_with(("kay", "miner"))
|
world = world_with(("kay", "miner"))
|
||||||
world["version"] = 13
|
world["version"] = 13
|
||||||
|
|
@ -1463,3 +1496,24 @@ class IconTest(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
class CombatReworkMigrationTest(unittest.TestCase):
|
||||||
|
def test_old_monsters_trophies_and_dragonhide_carry_over(self):
|
||||||
|
world = world_with(("kay", "ranger"))
|
||||||
|
world["version"] = 15
|
||||||
|
p = world["players"]["kay"]
|
||||||
|
del p["xp"]["tailoring"]
|
||||||
|
p["bank"] = {"bat_sinew": 3, "harpy_sinew": 2, "green_dragonhide": 4, "red_dragon_leather": 1}
|
||||||
|
p["equip"]["body"] = "blue_dhide_body"
|
||||||
|
p["action"] = {"id": "fight_giant_bat", "start": T0}
|
||||||
|
world["players"]["kay"] = p
|
||||||
|
game.migrate(world, T0)
|
||||||
|
self.assertEqual(p["bank"], {"harpy_sinew": 3, "spider_silk": 2, "ironhill_lacing": 4, "mammoth_leather": 1})
|
||||||
|
self.assertEqual(p["equip"]["body"], "sabrecat_leather_body")
|
||||||
|
self.assertEqual(p["action"]["id"], "fight_harpies")
|
||||||
|
self.assertEqual(p["xp"]["tailoring"], 0)
|
||||||
|
|
||||||
|
def test_homesteaders_tailor(self):
|
||||||
|
self.assertIn("tailoring", game.CLASSES["homesteader"]["fast"])
|
||||||
|
self.assertIn("tailoring", game.CLASSES["homesteader"]["craft"])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue