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
691
game.py
691
game.py
|
|
@ -64,8 +64,8 @@ ITEMS = {
|
|||
"bones": {"name": "Bones", "icon": "🦴", "value": 1},
|
||||
"big_bones": {"name": "Big bones", "icon": "🦴", "value": 4},
|
||||
"dragon_bones": {"name": "Dragon bones", "icon": "🦴", "value": 15},
|
||||
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 6,
|
||||
"req": ["combat", 10]},
|
||||
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 5, "ward": 5,
|
||||
"weight": "medium", "req": ["combat", 10]},
|
||||
# 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.
|
||||
"thatch": {"name": "Thatch", "icon": "🌾", "value": 2},
|
||||
|
|
@ -100,6 +100,23 @@ PIECES = [
|
|||
("legs", "legs", "greaves", 4, 9, None, [3, 6, 9, 12, 16, 21]),
|
||||
("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"}
|
||||
PIECE_ICONS = {"sword": "⚔️", "shield": "🛡️", "helmet": "🪖", "legs": "🦿", "body": "🦾"}
|
||||
COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body": "🥋"}
|
||||
|
|
@ -118,22 +135,34 @@ 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,
|
||||
# leatherworking level, armour by piece
|
||||
# Every tier's hide comes from Hunting. raw hide (id, name, icon, value), leather (id, name, value),
|
||||
# armour id prefix, armour name prefix, leatherworking level
|
||||
LEATHERS = [
|
||||
(("hide", "Hide", "🟫", 3), ("leather", "Leather", 5), "leather", "Leather", 1, (1, 2, 3)),
|
||||
(("boar_hide", "Boar hide", "🐗", 8), ("hard_leather", "Hard leather", 12), "hard_leather", "Hard leather", 20, (2, 4, 6)),
|
||||
(("bear_pelt", "Bear pelt", "🐻", 16), ("bear_leather", "Bear leather", 24), "bear_leather", "Bear leather", 35, (4, 6, 9)),
|
||||
(("green_dragonhide", "Green dragonhide", "🐉", 35), ("green_dragon_leather", "Green dragon leather", 50),
|
||||
"green_dhide", "Green dragonhide", 50, (6, 9, 13)),
|
||||
(("blue_dragonhide", "Blue dragonhide", "🐲", 60), ("blue_dragon_leather", "Blue dragon leather", 85),
|
||||
"blue_dhide", "Blue dragonhide", 65, (8, 12, 17)),
|
||||
(("red_dragonhide", "Red dragonhide", "🔥", 100), ("red_dragon_leather", "Red dragon leather", 140),
|
||||
"red_dhide", "Red dragonhide", 80, (10, 15, 21)),
|
||||
(("hide", "Hide", "🟫", 3), ("leather", "Leather", 5), "leather", "Leather", 1),
|
||||
(("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),
|
||||
(("elk_hide", "Elk hide", "🦌", 35), ("elk_leather", "Elk leather", 50), "elk_leather", "Elk leather", 50),
|
||||
(("sabrecat_pelt", "Sabrecat pelt", "🐆", 60), ("sabrecat_leather", "Sabrecat leather", 85),
|
||||
"sabrecat_leather", "Sabrecat leather", 65),
|
||||
(("mammoth_hide", "Mammoth hide", "🦣", 100), ("mammoth_leather", "Mammoth leather", 140),
|
||||
"mammoth_leather", "Mammoth leather", 80),
|
||||
]
|
||||
# piece suffix, slot, noun, leather used, level above the tier, icon
|
||||
LEATHER_PIECES = [("cap", "helmet", "cap", 2, 2, "🧢"), ("legs", "legs", "trousers", 3, 7, "👖"),
|
||||
("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
|
||||
FISH = [
|
||||
|
|
@ -159,8 +188,10 @@ for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in
|
|||
"req": ["combat", EQUIP_LEVELS[_tier]]}
|
||||
if _power:
|
||||
_item["power"] = _power[_tier]
|
||||
if _armour:
|
||||
if _armour and _piece == "shield":
|
||||
_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}
|
||||
for _skill, (_tool, _icon) in TOOLS.items():
|
||||
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"),
|
||||
"icon": "🪄", "style": "staff", **_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[_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}"] = {
|
||||
"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":
|
||||
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:
|
||||
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]}
|
||||
|
|
@ -245,12 +286,14 @@ POTION_LINES = [
|
|||
("fishing", "Fishing potion", "fishing", "speed", SPEEDS, [fish[0][0] for fish in FISH]),
|
||||
("farming", "Farming potion", "farming", "speed", SPEEDS,
|
||||
["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]),
|
||||
("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]),
|
||||
("leatherworking", "Leatherworking potion", "leatherworking", "keep", SPEEDS,
|
||||
[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"]),
|
||||
("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],
|
||||
|
|
@ -312,6 +355,7 @@ SKILLS = [
|
|||
{"id": "construction", "name": "Construction", "icon": "🏗️", "group": "Crafting"},
|
||||
{"id": "cooking", "name": "Cooking", "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": "combat", "name": "Combat", "icon": "⚔️", "group": "Combat"},
|
||||
{"id": "prayer", "name": "Prayer", "icon": "🙏", "group": "Combat"},
|
||||
|
|
@ -319,7 +363,7 @@ SKILLS = [
|
|||
]
|
||||
SKILL_IDS = [skill["id"] for skill in SKILLS]
|
||||
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
|
||||
# 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",
|
||||
},
|
||||
"homesteader": {
|
||||
"name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore"], "craft": ["cooking", "herblore"],
|
||||
"bonus": "Cooking and herblore 20% faster, two farm plots",
|
||||
"masterwork": "Masterwork: 10% double cooking and herblore crafts",
|
||||
"name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore", "tailoring"],
|
||||
"craft": ["cooking", "herblore", "tailoring"],
|
||||
"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",
|
||||
"combat": "Staff: +15% damage",
|
||||
},
|
||||
|
|
@ -391,13 +436,16 @@ def _tiered_actions() -> list[dict]:
|
|||
for piece, _slot, noun, bars, offset, _power, _armour in PIECES:
|
||||
if piece == "shield":
|
||||
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,
|
||||
25 + 5 * bars + 2 * tier, bars * piece_xp[tier], inputs,
|
||||
{f"{key}_{piece}": 1}, group=name, city=city))
|
||||
actions.append(_action(f"smith_{key}_warhammer", "smithing", f"{name} warhammer", TIER_LEVELS[tier] + 6,
|
||||
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))
|
||||
for tool, _icon in TOOLS.values():
|
||||
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):
|
||||
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}))
|
||||
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",
|
||||
f"Tan {leather[1].lower()}", level, 25 + 3 * tier, bar_xp[tier],
|
||||
{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",
|
||||
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:
|
||||
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],
|
||||
|
|
@ -426,38 +486,224 @@ def _tiered_actions() -> list[dict]:
|
|||
|
||||
# --- combat -----------------------------------------------------------------
|
||||
# A fight is played out swing by swing: you and the enemy each have HP, a swing
|
||||
# timer, accuracy and a max hit, and every swing hits, misses or crits. Combat
|
||||
# is split into the Frontier (bandits and the city's troubles) and expeditions:
|
||||
# one area per tier, each with a golem, a troll, a spirit and a flyer that drop
|
||||
# the trophies higher weapons need, and a boss.
|
||||
# timer, accuracy and a max hit, and every swing hits, misses or crits. Every
|
||||
# blow has a damage type: weapons deal one, and each enemy is vulnerable to some
|
||||
# types and resists others. Armour stops physical blows, ward stops magic.
|
||||
# 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_MULT = 1.5
|
||||
RESPAWN = 2 # seconds after a kill before the next enemy turns up
|
||||
MAX_FIGHT = 600 # a fight that drags on longer is broken off
|
||||
FLEE_BELOW = 0.2 # out of food and under this share of HP: get out
|
||||
|
||||
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.
|
||||
STYLES = {
|
||||
"sword": {"name": "sword", "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},
|
||||
"staff": {"name": "staff", "interval": 3.0, "hit": 1.15, "accuracy": 1.25, "crit": 0.05, "pierce": True},
|
||||
"bow": {"name": "bow", "interval": 1.8, "hit": 0.75, "accuracy": 1.05, "crit": 0.05, "first": True},
|
||||
"sword": {"name": "sword", "damage": "slash", "interval": 2.4, "hit": 1.0, "accuracy": 1.0, "crit": 0.05},
|
||||
"hammer": {"name": "warhammer", "damage": "blunt", "interval": 3.6, "hit": 1.6, "accuracy": 0.95, "crit": 0.10},
|
||||
"staff": {"name": "staff", "damage": "magic", "interval": 3.0, "hit": 1.2, "accuracy": 1.25, "crit": 0.05},
|
||||
"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_BONUS = 1.15 # damage with your own trade's weapon
|
||||
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 = {
|
||||
"golem": {"hammer": 1.5, "bow": 0.5},
|
||||
"troll": {"sword": 1.5},
|
||||
"spirit": {"hammer": 0.5, "sword": 0.5, "bow": 0.5, "staff": 1.5},
|
||||
"flyer": {"hammer": 0.5, "sword": 0.5, "bow": 1.5},
|
||||
|
||||
# Each armour piece past copper needs reagents from its tier's expedition area.
|
||||
REAGENT_COUNTS = {"helmet": 1, "legs": 2, "body": 3}
|
||||
REAGENT_NOUNS = {"heavy": "plating", "medium": "lacing", "light": "thread"}
|
||||
REAGENT_ODDS = 0.10 # each of the area's three reagents, from any of its races
|
||||
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:
|
||||
"""The trophy a tier's monster of this kind drops (tier 1 is the Old Watchtower)."""
|
||||
return _slug(AREA_MONSTERS[kind][2][tier - 1])
|
||||
def _types(value) -> list:
|
||||
return [value] if isinstance(value, str) else list(value)
|
||||
|
||||
|
||||
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]:
|
||||
|
|
@ -465,118 +711,69 @@ def _weapon_actions() -> list[dict]:
|
|||
actions = []
|
||||
for tier, (key, name, *_rest) in enumerate(METALS):
|
||||
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,
|
||||
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"))
|
||||
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],
|
||||
{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"))
|
||||
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],
|
||||
{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"))
|
||||
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]:
|
||||
fights = []
|
||||
for index, (area, level) in enumerate(zip(AREAS, AREA_LEVELS)):
|
||||
for kind, (offset, monsters, trophies) in AREA_MONSTERS.items():
|
||||
enemy = monster(monsters[index], kind, level + offset)
|
||||
fights.append(_fight(f"fight_{_slug(monsters[index])}", enemy, area, gold=level + offset,
|
||||
drops=[(AREA_BONES[index], 1, 1.0), (_slug(trophies[index]), 1, TROPHY_ODDS)]))
|
||||
for tier, (area, level, _prefix, races) in enumerate(AREAS, start=1):
|
||||
for race, trophy_name, _line, offset, roster in races:
|
||||
fights.append(_encounter(f"fight_{_slug(race)}", race, area, level + offset, roster, gold=level + offset,
|
||||
drops=[(AREA_BONES[tier - 1], 1, 1.0), (_slug(trophy_name), 1, TROPHY_ODDS),
|
||||
*_area_drops(tier)]))
|
||||
return fights
|
||||
|
||||
|
||||
def _boss_trophies(tier: int) -> list[tuple]:
|
||||
return [(_slug(trophies[tier - 1]), 1, BOSS_TROPHY_ODDS) for _offset, _monsters, trophies in AREA_MONSTERS.values()]
|
||||
def _boss_drops(tier: int, reagents: float = 0.3) -> list[tuple]:
|
||||
return [*[(trophy(line, tier), 1, BOSS_TROPHY_ODDS) for line in STYLES], *_area_drops(tier, reagents)]
|
||||
|
||||
|
||||
COMBAT_ACTIONS = [
|
||||
# The Frontier: the city's own troubles.
|
||||
_fight("fight_scouts", monster("Bandit scouts", "human", 10), "Frontier", gold=10,
|
||||
drops=[("copper_bar", 1, 0.3), ("arrows", 5, 0.4), ("bones", 1, 1.0)]),
|
||||
_fight("sabotage", monster("Camp guards", "human", 12), "Frontier", name="Sabotage the bandit camp",
|
||||
special="sabotage"),
|
||||
_fight("fight_stronghold", monster("Bandit stronghold", "human", 40, boss=True), "Frontier", gold=60,
|
||||
drops=[("steel_bar", 1, 0.4), ("coal", 3, 0.5), ("iron_shield", 1, 0.04), ("big_bones", 1, 1.0)]),
|
||||
_encounter("fight_scouts", "Bandit scouts", "Frontier", 10, BANDITS, gold=10,
|
||||
drops=[("copper_bar", 1, 0.3), ("arrows", 5, 0.4), ("bones", 1, 1.0)]),
|
||||
_encounter("sabotage", "Camp guards", "Frontier", 12, BANDITS, label="Sabotage the bandit camp", special="sabotage"),
|
||||
_boss("fight_stronghold", "Bandit warlord", "Frontier", 40, "boss", ("slash", "magic"), ("blunt", "magic"),
|
||||
("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)]),
|
||||
# Greenwood: where everyone starts.
|
||||
_fight("fight_wolves", monster("Wolves", "beast", 1), "Greenwood", gold=2, name="Wolves in the woods",
|
||||
drops=[("raw_meat", 1, 0.6), ("hide", 1, 0.5), ("bones", 1, 1.0)]),
|
||||
_fight("fight_goblins", monster("Cave goblins", "human", 5), "Greenwood", gold=4,
|
||||
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
|
||||
_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)]),
|
||||
_encounter("fight_goblins", "Cave goblins", "Greenwood", 5, GOBLINS, gold=4,
|
||||
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
|
||||
*_area_fights(),
|
||||
_fight("fight_ruins", monster("Watchtower ogre", "troll", 18, boss=True), "Old Watchtower", gold=25,
|
||||
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_trophies(1)]),
|
||||
_fight("fight_barrow_king", monster("Barrow king", "spirit", 28, boss=True), "Sunken Barrows", gold=40,
|
||||
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_trophies(2)]),
|
||||
_fight("fight_green_dragon", monster("Green dragon", "dragon", 38), "Iron Hills", gold=80,
|
||||
drops=[("green_dragonhide", 1, 0.9), ("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(3)]),
|
||||
_fight("fight_blue_dragon", monster("Blue dragon", "dragon", 48), "Frostpeak", gold=110,
|
||||
drops=[("blue_dragonhide", 1, 0.9), ("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(4)]),
|
||||
_fight("fight_red_dragon", monster("Red dragon", "dragon", 60), "Dragon's Spine", gold=150,
|
||||
drops=[("red_dragonhide", 1, 0.9), ("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_trophies(5)]),
|
||||
_fight("fight_black_dragon", monster("Black dragon", "dragon", 75, boss=True), "Dragon's Spine", gold=220,
|
||||
drops=[("red_dragonhide", 2, 0.9), ("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_trophies(5)]),
|
||||
_boss("fight_ruins", "Watchtower ogre", "Old Watchtower", 18, "boss", ("blunt", "magic"), ("slash", "magic"),
|
||||
("blunt", "pierce"), gold=25,
|
||||
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_drops(1)]),
|
||||
_boss("fight_barrow_king", "Barrow king", "Sunken Barrows", 28, "boss", ("slash", "magic"), ("blunt", "magic"),
|
||||
("pierce", "slash"), gold=40,
|
||||
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_drops(2)]),
|
||||
# Dragons: the prize hunt, dropping their area's reagents far more often than anything else there.
|
||||
_boss("fight_green_dragon", "Green dragon", "Iron Hills", 38, "dragon", ("slash", "magic"), ("pierce", "magic"),
|
||||
("slash", "blunt"), is_boss=False, gold=80,
|
||||
drops=[("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(3, 0.6)]),
|
||||
_boss("fight_blue_dragon", "Blue dragon", "Frostpeak", 48, "dragon", ("slash", "magic"), ("blunt", "pierce"),
|
||||
("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 = [
|
||||
|
|
@ -593,6 +790,9 @@ ACTIONS = [
|
|||
_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_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(),
|
||||
_action("smith_arrows", "smithing", "Arrows", 5, 30, 10,
|
||||
{"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("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_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},
|
||||
group="Tanning"),
|
||||
*[_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_VERSION = 15
|
||||
WORLD_VERSION = 16
|
||||
|
||||
|
||||
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():
|
||||
player["seen_threat"] = latest["id"] if latest else 0
|
||||
world["version"] = 15
|
||||
if world["version"] < 16:
|
||||
_migrate_combat_rework(world)
|
||||
world["version"] = 16
|
||||
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:
|
||||
"""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
|
||||
|
|
@ -1143,16 +1401,30 @@ def bank_take(player: dict, item: str, qty: int) -> None:
|
|||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def damage_reduction(player: dict) -> float:
|
||||
points = armour(player)
|
||||
def magic_ward(player: dict) -> int:
|
||||
"""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)
|
||||
|
||||
|
||||
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:
|
||||
power = skill_level(player, "combat") + armour(player) // 2
|
||||
power = skill_level(player, "combat") + defence_bonus(player)
|
||||
for item in player["equip"].values():
|
||||
if item:
|
||||
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"
|
||||
|
||||
|
||||
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:
|
||||
"""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 = STYLES[style_id]
|
||||
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)
|
||||
weakness = WEAKNESS.get(enemy["kind"], {}).get(style_id, 1) if enemy else 1
|
||||
return {"style": style_id, "interval": style["interval"], "accuracy": (10 + 2 * power) * style["accuracy"],
|
||||
"max_hit": max(2, round((2 + power / 3) * damage * weakness)), "crit": style["crit"],
|
||||
"pierce": style.get("pierce", False), "first": style.get("first", False), "weakness": weakness,
|
||||
lands = affinity(enemy, style["damage"]) if enemy else 1
|
||||
taken = blessed["taken"] * (1 - brew.get("reduce", 0))
|
||||
return {"style": style_id, "damage": style["damage"], "interval": style["interval"],
|
||||
"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,
|
||||
"evasion": 10 + 2 * (skill_level(player, "combat") + armour(player) // 2),
|
||||
"taken": (1 - damage_reduction(player)) * blessed["taken"] * (1 - brew.get("reduce", 0))}
|
||||
"evasion": 10 + 2 * (skill_level(player, "combat") + defence_bonus(player)),
|
||||
"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:
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
def fight_odds(player: dict, action: dict) -> dict:
|
||||
"""What a fight against this enemy looks like on average, for the cards."""
|
||||
foe = action["enemy"]
|
||||
me = fighter(player, blessing(player), potion_effect(player, "combat"), foe)
|
||||
def _variant_odds(player: dict, foe: dict, blessed: dict, brew: dict) -> dict:
|
||||
me = fighter(player, blessed, brew, foe)
|
||||
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
|
||||
shrug = 0 if me["pierce"] else foe["reduction"]
|
||||
dps = hit * _average_hit(me["max_hit"], me["crit"]) * (1 - shrug) / me["interval"]
|
||||
kill = foe["hp"] / dps
|
||||
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * me["taken"] / foe["interval"] * kill
|
||||
return {"hit": round(hit, 2), "their_hit": round(their_hit, 2), "max_hit": me["max_hit"], "style": me["style"],
|
||||
"weakness": me["weakness"], "their_max": max(1, round(foe["max_hit"] * me["taken"])),
|
||||
"kill_time": round(kill, 1), "damage": round(hurt, 1)}
|
||||
kill = foe["hp"] / (hit * _average_hit(me["max_hit"], me["crit"]) / me["interval"])
|
||||
taken = sum(_taken(me, kind) for kind in foe["attack"]) / len(foe["attack"])
|
||||
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * taken / foe["interval"] * kill
|
||||
return {"hit": hit, "their_hit": their_hit, "max_hit": me["max_hit"], "affinity": me["affinity"],
|
||||
"their_max": max(1, round(foe["max_hit"] * max(_taken(me, kind) for kind in foe["attack"]))),
|
||||
"kill_time": kill, "damage": hurt}
|
||||
|
||||
|
||||
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:
|
||||
"""Play out one fight swing by swing, both swing timers starting empty. Nothing changes
|
||||
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)
|
||||
shrug = 0 if me["pierce"] else foe["reduction"]
|
||||
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
|
||||
top, hp, foe_hp = max_hp(player), player["hp"], foe["hp"]
|
||||
pantry = {item: player["bank"][item] for item in edible(player)}
|
||||
|
|
@ -1230,6 +1526,7 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
|
|||
pace = speed(world)
|
||||
# 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
|
||||
swings = 0 # an enemy with more than one attack takes turns with them
|
||||
while result is None:
|
||||
t = min(mine, theirs)
|
||||
if t > MAX_FIGHT:
|
||||
|
|
@ -1239,23 +1536,27 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
|
|||
mine += me["interval"]
|
||||
crit = rng.random() < me["crit"]
|
||||
landed = rng.random() < hit
|
||||
dealt = max(1, round((me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))
|
||||
* (1 - shrug))) if landed else 0
|
||||
dealt = max(1, round(me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))) if landed else 0
|
||||
foe_hp = max(0, foe_hp - dealt)
|
||||
events.append([round(t / pace, 2), "you", ("crit" if crit else "hit") if landed else "miss", dealt, round(hp), foe_hp])
|
||||
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:
|
||||
result = "won"
|
||||
continue
|
||||
theirs += foe["interval"]
|
||||
kind = foe["attack"][swings % len(foe["attack"])]
|
||||
swings += 1
|
||||
crit = rng.random() < CRIT_CHANCE
|
||||
landed = rng.random() < their_hit
|
||||
if landed and rng.random() < me["block"]:
|
||||
events.append([round(t / pace, 2), "foe", "block", 0, max(0, round(hp)), foe_hp])
|
||||
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
|
||||
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()):
|
||||
food = next(item for item, left in pantry.items() if left)
|
||||
pantry[food] -= 1
|
||||
|
|
@ -1272,7 +1573,8 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
|
|||
hp, result = 1, "lost"
|
||||
elif starving:
|
||||
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,
|
||||
"interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events, "style": me["style"],
|
||||
"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"]
|
||||
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
|
||||
xp = fight.get("xp", action["xp"]) * mods["xp"] # the variant you met
|
||||
if player["prayers"] and not fight["pp"]:
|
||||
summary["prayer_out"] = True
|
||||
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
|
||||
gain_xp(world, player, "combat", round(attack * 6), now)
|
||||
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:
|
||||
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"])
|
||||
else:
|
||||
severity = min(1.0, 1 - guard["total"] / attack)
|
||||
|
|
@ -2556,10 +2860,10 @@ PROBLEMS = {
|
|||
}
|
||||
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
|
||||
PROBLEM_ACTIONS = [
|
||||
_fight("clear_wolves", monster("Wolf pack", "beast", 4), "Frontier", name="Drive off the wolves",
|
||||
special="problem", problem="wolves"),
|
||||
_fight("catch_goblins", monster("Goblin thieves", "human", 5), "Frontier", name="Catch the goblin thieves",
|
||||
special="problem", problem="goblins"),
|
||||
_encounter("clear_wolves", "Wolf pack", "Frontier", 4, WOLVES, label="Drive off the wolves",
|
||||
special="problem", problem="wolves"),
|
||||
_encounter("catch_goblins", "Goblin thieves", "Frontier", 5, GOBLINS, label="Catch the goblin thieves",
|
||||
special="problem", problem="goblins"),
|
||||
_action("drive_bear", "hunting", "Drive off the bear", 1, 45, 14, special="problem", problem="bear"),
|
||||
_action("cull_boars", "hunting", "Cull the boar herd", 1, 45, 14, special="problem", problem="boars"),
|
||||
_action("catch_rats", "hunting", "Catch the rats", 1, 30, 8, special="problem", problem="rats"),
|
||||
|
|
@ -2600,7 +2904,7 @@ FITTINGS = [
|
|||
"+1 prayer point per bone, ward +15", "star-altar", "#6f9a4a"),
|
||||
("enchanted_chests", "Enchanted chests", "storehouse", 4, 75, {"magic_plank": 40, "rune_lock": 6},
|
||||
"+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"),
|
||||
]
|
||||
# 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",
|
||||
"hunting knife": "bowie-knife", "hammer": "claw-hammer"}
|
||||
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"}
|
||||
HARDWARE_GLYPHS = ["screw", "nails", "door-handle", "metal-plate", "crossed-chains", "padlock"]
|
||||
GLYPHS = {
|
||||
|
|
@ -3206,7 +3515,7 @@ SKILL_GLYPHS = {
|
|||
"construction": ("hammer-nails", "#d1a46a"),
|
||||
"cooking": ("cooking-pot", "#e0703a"), "leatherworking": ("sewing-needle", "#d6a26e"),
|
||||
"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"),
|
||||
"homesteader": ("farmer", "#8fbf6a"), "ranger": ("bow-arrow", "#5fae8a")}
|
||||
|
|
@ -3225,24 +3534,31 @@ ACTION_GLYPHS = {
|
|||
"labour": ("hammer-nails", "#d1a46a"), "clear_wolves": ("wolf-head", "#9aa1ab"),
|
||||
"catch_goblins": ("goblin-head", "#8fbf6a"), "drive_bear": ("bear-head", "#8a5a32"), "cull_boars": ("boar", "#a06a4a"),
|
||||
"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"),
|
||||
"rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"),
|
||||
"superhuman_strength": ("biceps", "#ff8a3a"), "second_wind": ("wind-hole", "#8fd0e0"),
|
||||
"protection": ("aura", "#f0d060"), "piety": ("angel-wings", "#f0e6c0")}
|
||||
TROPHY_GLYPHS = {"golem": "crystal-growth", "troll": "animal-hide", "spirit": "magic-swirl", "flyer": "sewing-string"}
|
||||
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
|
||||
MONSTER_GLYPHS = {"golem": ["rock-golem"] * 5, "troll": ["troll"] * 5,
|
||||
"spirit": ["fairy", "spectre", "ghost", "spectre", "ghost"],
|
||||
"flyer": ["bat", "harpy", "wyvern", "griffin-symbol", "eagle-head"]}
|
||||
RACE_GLYPHS = {
|
||||
"Clay golems": "rock-golem", "Cave trolls": "troll", "Restless dead": "skeleton", "Harpies": "harpy",
|
||||
"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({
|
||||
"fight_goblins": ("goblin-head", "#8fbf6a"), "fight_ruins": ("ogre", "#a08060"),
|
||||
"fight_barrow_king": ("crowned-skull", "#c8d0e0"), "drill_militia": ACTION_GLYPHS["drill_militia"],
|
||||
})
|
||||
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
|
||||
for _index, _name in enumerate(_monsters):
|
||||
ACTION_GLYPHS[f"fight_{_slug(_name)}"] = (MONSTER_GLYPHS[_kind][_index], TIER_TINTS[_index])
|
||||
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield"]
|
||||
for _index, (_area, _level, _prefix, _races) in enumerate(AREAS):
|
||||
for _race, *_rest in _races:
|
||||
ACTION_GLYPHS[f"fight_{_slug(_race)}"] = (RACE_GLYPHS[_race], TIER_TINTS[_index])
|
||||
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield",
|
||||
*(kind["glyph"] for kind in DAMAGE_TYPES.values())]
|
||||
|
||||
|
||||
def _assign_glyphs() -> None:
|
||||
|
|
@ -3269,8 +3585,16 @@ def _assign_glyphs() -> None:
|
|||
ITEMS[leather[0]].update(glyph="rolled-cloth", tint=tint)
|
||||
for suffix, glyph in LEATHER_GLYPHS.items():
|
||||
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
|
||||
for trophy, (_name, kind, tier) in TROPHIES.items():
|
||||
ITEMS[trophy].update(glyph=TROPHY_GLYPHS[kind], tint=TIER_TINTS[tier - 1])
|
||||
for trophy, (_name, line, tier) in TROPHIES.items():
|
||||
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():
|
||||
ITEMS[herb].update(glyph="herbs-bundle", tint=tint)
|
||||
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,
|
||||
"potion_charges": POTION_CHARGES, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
|
||||
"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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue