diff --git a/README.md b/README.md index b691596..c05adba 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Alpha. One season, bandit raids plus two friendly events, six buildings. ## What's in it - **Skills:** mining, woodcutting, farming, hunting, fishing, smithing, - construction (with masonry), cooking, leatherworking, tailoring, herblore, + jewelcrafting, construction (with masonry), cooking, leatherworking, tailoring, herblore, combat, prayer and reconnaissance. Smiths also make hardware for builders, one kind per metal (copper rivets to rune locks), which fittings and buildings need. @@ -43,6 +43,14 @@ Alpha. One season, bandit raids plus two friendly events, six buildings. 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. +- **Jewellery:** Jewelcrafting sets a gem in a band. Each tier's rock turns + up its gem 2% of the time (quartz, garnet, amethyst, sapphire, emerald, + diamond), and jewellers prospect spare ore for more (5 ore, 15%). Bands + are copper, then silver and gold, which the jeweller mines and smelts + without a smithy. Past quartz each piece needs a shard from that tier's + area. Rings for offence (accuracy, crit, power), amulets for defence (max + HP, ward, regen), trinkets for the rest (XP, double gathering, + masterwork). - **Tiers:** six RuneScape-style tiers (copper, iron, steel, mithril, adamant, rune at levels 1–75) of ore, trees (up to enchanted), hides (deer, boar, bear, elk, sabrecat, mammoth) and fibres (flax, cotton, silk, @@ -53,7 +61,8 @@ Alpha. One season, bandit raids plus two friendly events, six buildings. 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 - a quiet gathering side and a busier crafting side: miner (mining, smithing), + a quiet gathering side and a busier crafting side: miner (mining, smithing, + jewelcrafting), builder (woodcutting, construction), homesteader (farming, cooking, herblore, tailoring) and ranger (hunting, fishing, leatherworking, cooking). A trade's skills are 20% faster, 1 in 10 of its crafts make diff --git a/game.py b/game.py index ded8f53..9f6ff7e 100644 --- a/game.py +++ b/game.py @@ -198,6 +198,63 @@ for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in "name": f"{_name} {_tool}", "icon": _icon, "value": round(2 * _bar_value * 1.4), "tier": _tier, "slot": f"tool_{_skill}", "tool": _skill, "speed": TOOL_SPEED[_tier], "req": [_skill, EQUIP_LEVELS[_tier]], } +# Jewelcrafting: a gem from each tier's rock, set in a band of copper, silver or gold. +# Silver and gold are soft metals the jeweller smelts, no smithy needed. Past the first +# tier, every piece also needs a shard from that tier's expedition area. +# gem id, name, value, tint, glyph +GEMS = [ + ("quartz", "Quartz", 8, "#e8e4f0", "crystal-bars"), + ("garnet", "Garnet", 20, "#c8405a", "rupee"), + ("amethyst", "Amethyst", 40, "#a070e0", "amethyst"), + ("sapphire", "Sapphire", 70, "#4f7ff0", "saphir"), + ("emerald", "Emerald", 110, "#3fc070", "emerald"), + ("diamond", "Diamond", 160, "#d8f4ff", "diamond-hard"), +] +BANDS = ["copper_bar", "silver_bar", "silver_bar", "gold_bar", "gold_bar", "gold_bar"] +GEM_ODDS = 0.02 # a gem with each rock mined +PROSPECT_ORE = 5 # ore cracked open per prospecting action... +PROSPECT_ODDS = 0.15 # ...for this chance at its gem +# The gem each rock turns up when mined, and what's cracked open to prospect for each gem. +GEM_ROCKS = {"mine_stone": "quartz", "mine_copper": "quartz", "mine_iron": "garnet", "mine_coal": "amethyst", + "mine_mithril": "sapphire", "mine_adamantite": "emerald", "mine_runite": "diamond"} +PROSPECT = ["stone", "iron_ore", "coal", "mithril_ore", "adamantite_ore", "runite_ore"] +# Rings for offence, amulets for defence, trinkets for everything else. +# key, slot, noun, stat, bonus by tier, bands, gems, shards, level above the tier, glyph +JEWELLERY = [ + ("precision", "ring", "ring of precision", "accuracy", [4, 8, 12, 16, 20, 25], 1, 1, 1, 2, "diamond-ring"), + ("fury", "ring", "ring of fury", "crit", [2, 3, 5, 6, 8, 10], 1, 1, 1, 2, "big-diamond-ring"), + ("might", "ring", "ring of might", "power", [2, 4, 6, 9, 12, 15], 1, 1, 1, 2, "swirl-ring"), + ("vigour", "amulet", "amulet of vigour", "hp", [5, 10, 16, 23, 31, 40], 2, 1, 2, 8, "heart-necklace"), + ("warding", "amulet", "amulet of warding", "ward", [3, 5, 7, 10, 12, 15], 2, 1, 2, 8, "gem-pendant"), + ("mending", "amulet", "amulet of mending", "regen", [10, 20, 30, 40, 50, 60], 2, 1, 2, 8, "tribal-pendant"), + ("hourglass", "trinket", "hourglass", "xp", [2, 3, 5, 6, 8, 10], 1, 2, 1, 5, "hourglass"), + ("charm", "trinket", "lucky charm", "double", [2, 3, 4, 5, 6, 8], 1, 2, 1, 5, "clover"), + ("loupe", "trinket", "loupe", "masterwork", [2, 3, 4, 4, 5, 6], 1, 2, 1, 5, "magnifying-glass"), +] +# How each jewellery bonus reads: name, unit, glyph, tint. +JEWEL_STATS = { + "accuracy": {"name": "Accuracy", "unit": "%", "glyph": "archery-target", "tint": "#e0b04a"}, + "crit": {"name": "Crit chance", "unit": "%", "glyph": "striking-diamonds", "tint": "#e05a5a"}, + "power": {"name": "Power", "unit": "", "glyph": "biceps", "tint": "#e06a6a"}, + "hp": {"name": "Max HP", "unit": "", "glyph": "hearts", "tint": "#e06a8a"}, + "ward": {"name": "Ward", "unit": "", "glyph": "magic-shield", "tint": "#a77be8"}, + "regen": {"name": "HP regen", "unit": "%", "glyph": "heart-plus", "tint": "#e06a8a"}, + "xp": {"name": "XP", "unit": "%", "glyph": "sparkles", "tint": "#f0d060"}, + "double": {"name": "Double gathering", "unit": "%", "glyph": "clover", "tint": "#5fae5a"}, + "masterwork": {"name": "Masterwork", "unit": "%", "glyph": "anvil", "tint": "#d07a3c"}, +} +ITEMS["silver_ore"] = {"name": "Silver ore", "icon": "⚪", "value": 6} +ITEMS["silver_bar"] = {"name": "Silver bar", "icon": "🥈", "value": 15, "tier": 1} +ITEMS["gold_ore"] = {"name": "Gold ore", "icon": "🟡", "value": 20} +ITEMS["gold_bar"] = {"name": "Gold bar", "icon": "🥇", "value": 45, "tier": 3} +for _tier, (_gem, _gem_name, _gem_value, _tint, _glyph) in enumerate(GEMS): + ITEMS[_gem] = {"name": _gem_name, "icon": "💎", "value": _gem_value, "tier": _tier} + for _key, _slot, _noun, _stat, _bonus, _bands, _gems, _shards, _offset, _piece_glyph in JEWELLERY: + ITEMS[f"{_gem}_{_key}"] = { + "name": f"{_gem_name} {_noun}", "icon": {"ring": "💍", "amulet": "📿", "trinket": "🔮"}[_slot], + "value": round((_bands * ITEMS[BANDS[_tier]]["value"] + _gems * _gem_value + _shards * 12 * _tier) * 1.3), + "slot": _slot, "tier": _tier, "jewel": _stat, _stat: _bonus[_tier], + **({"req": ["combat", EQUIP_LEVELS[_tier]]} if _slot != "trinket" else {})} # Hardware: what the smith makes for builders, one kind per metal, used in # fittings and in building levels in place of plain bars. # id, singular, plural, bars used, pieces made @@ -343,7 +400,7 @@ for _id, _name, _level, _herb, _second, _skill, _effect in POTIONS: # What the city eats first: plain food before the good stuff. FOOD_ORDER = tuple(sorted((item for item, spec in ITEMS.items() if "food" in spec), key=lambda i: ITEMS[i]["heal"])) -EQUIP_SLOTS = ["weapon", "shield", "helmet", "body", "legs"] + [f"tool_{skill}" for skill in TOOLS] +EQUIP_SLOTS = ["weapon", "shield", "helmet", "body", "legs", "ring", "amulet", "trinket"] + [f"tool_{skill}" for skill in TOOLS] SKILLS = [ {"id": "mining", "name": "Mining", "icon": "⛏️", "group": "Gathering"}, @@ -352,6 +409,7 @@ SKILLS = [ {"id": "hunting", "name": "Hunting", "icon": "🦌", "group": "Gathering"}, {"id": "fishing", "name": "Fishing", "icon": "🎣", "group": "Gathering"}, {"id": "smithing", "name": "Smithing", "icon": "🔨", "group": "Crafting"}, + {"id": "jewelcrafting", "name": "Jewelcrafting", "icon": "💍", "group": "Crafting"}, {"id": "construction", "name": "Construction", "icon": "🏗️", "group": "Crafting"}, {"id": "cooking", "name": "Cooking", "icon": "🍳", "group": "Crafting"}, {"id": "leatherworking", "name": "Leatherworking", "icon": "🪡", "group": "Crafting"}, @@ -363,16 +421,17 @@ SKILLS = [ ] SKILL_IDS = [skill["id"] for skill in SKILLS] GATHERING = {"mining", "woodcutting", "farming", "hunting", "fishing"} -CRAFTING = {"smithing", "construction", "cooking", "leatherworking", "herblore", "tailoring"} +CRAFTING = {"smithing", "jewelcrafting", "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. # Nothing is locked: every class can train every skill. CLASSES = { "miner": { - "name": "Miner", "icon": "⛏️", "fast": ["mining", "smithing"], "craft": ["smithing"], - "bonus": "Mining and smithing 20% faster", - "masterwork": "Masterwork: 10% double smithing crafts", + "name": "Miner", "icon": "⛏️", "fast": ["mining", "smithing", "jewelcrafting"], + "craft": ["smithing", "jewelcrafting"], + "bonus": "Mining, smithing and jewelcrafting 20% faster", + "masterwork": "Masterwork: 10% double smithing and jewelcrafting crafts", "signature": "Prospector: 10% double ore", "combat": "Warhammer: +15% damage", }, @@ -518,10 +577,11 @@ CLASS_STYLE = {"miner": "hammer", "builder": "sword", "homesteader": "staff", "r CLASS_STYLE_BONUS = 1.15 # damage with your own trade's weapon BLOCK_CHANCE = 0.15 # a shield turns aside this share of hits -# Each armour piece past copper needs reagents from its tier's expedition area. +# Each armour piece past copper needs reagents from its tier's expedition area, and so +# does each piece of jewellery past quartz (its shards). 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 +REAGENT_NOUNS = {"heavy": "plating", "medium": "lacing", "light": "thread", "jewel": "shard"} +REAGENT_ODDS = 0.10 # each of the area's four 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 @@ -705,7 +765,7 @@ def reagent(weight: str, tier: int) -> str: def _area_drops(tier: int, odds: float = REAGENT_ODDS) -> list[tuple]: - return [(reagent(weight, tier), 1, odds) for weight in WEIGHTS] + return [(reagent(kind, tier), 1, odds) for kind in REAGENT_NOUNS] def _weapon_actions() -> list[dict]: @@ -729,6 +789,32 @@ def _weapon_actions() -> list[dict]: return actions +def _jewel_actions() -> list[dict]: + """Silver and gold, prospecting ore for gems, and every tier's jewellery.""" + actions = [ + _action("mine_silver", "mining", "Mine silver", 10, 35, 12, outputs={"silver_ore": 1}), + _action("mine_gold", "mining", "Mine gold", 40, 55, 36, outputs={"gold_ore": 1}), + ] + for tier, (source, (gem, *_rest)) in enumerate(zip(PROSPECT, GEMS)): + actions.append(_action(f"prospect_{gem}", "jewelcrafting", f"Prospect {ITEMS[source]['name'].lower()}", + TIER_LEVELS[tier], 20 + 3 * tier, [8, 18, 28, 40, 54, 70][tier], + {source: PROSPECT_ORE}, finds=[(gem, 1, PROSPECT_ODDS)], group="Prospecting")) + actions += [ + _action("jc_silver_bar", "jewelcrafting", "Silver bar", 10, 25, 12, {"silver_ore": 1, "logs": 1}, + {"silver_bar": 1}, group="Bars"), + _action("jc_gold_bar", "jewelcrafting", "Gold bar", 40, 30, 32, {"gold_ore": 1, "coal": 1}, + {"gold_bar": 1}, group="Bars"), + ] + for tier, (gem, gem_name, *_rest) in enumerate(GEMS): + for key, _slot, noun, _stat, _bonus, bands, gems, shards, offset, _glyph in JEWELLERY: + inputs = {BANDS[tier]: bands, gem: gems, **({reagent("jewel", tier): shards} if tier else {})} + actions.append(_action(f"jc_{gem}_{key}", "jewelcrafting", f"{gem_name} {noun}", TIER_LEVELS[tier] + offset, + 35 + 5 * (bands + gems) + 2 * tier, + round([15, 32, 52, 75, 100, 130][tier] * (bands + gems) / 2), + inputs, {f"{gem}_{key}": 1}, group=gem_name)) + return actions + + def _area_fights() -> list[dict]: fights = [] for tier, (area, level, _prefix, races) in enumerate(AREAS, start=1): @@ -834,6 +920,8 @@ ACTIONS = [ _action("recon_camps", "reconnaissance", "Shadow the war bands", 15, 90, 28, special="recon", recon=2), ] ACTION_BY_ID = {action["id"]: action for action in ACTIONS} +for _id, _gem in GEM_ROCKS.items(): + ACTION_BY_ID[_id]["finds"] = [(_gem, 1, GEM_ODDS)] BUILDINGS = { "houses": { @@ -945,7 +1033,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows # --- world --------------------------------------------------------------- -WORLD_VERSION = 17 +WORLD_VERSION = 18 def new_world(now: float, speed: float = 1.0) -> dict: @@ -1088,6 +1176,13 @@ def migrate(world: dict, now: float | None = None) -> dict: world.setdefault("orders", []) world.setdefault("next_order_id", 1) world["version"] = 17 + if world["version"] < 18: + # Jewelcrafting: a new skill and three new slots. + for player in world["players"].values(): + player["xp"].setdefault("jewelcrafting", 0) + for slot in ("ring", "amulet", "trinket"): + player["equip"].setdefault(slot, None) + world["version"] = 18 return world @@ -1430,6 +1525,11 @@ def defence_bonus(player: dict) -> int: return round((armour(player) + magic_ward(player)) * 0.4) +def gear_bonus(player: dict, stat: str) -> float: + """A bonus that comes from what you're wearing: jewellery's accuracy, crit, HP and so on.""" + return sum(ITEMS[item].get(stat, 0) for item in player["equip"].values() if item) + + def combat_power(player: dict) -> int: power = skill_level(player, "combat") + defence_bonus(player) for item in player["equip"].values(): @@ -1439,7 +1539,7 @@ def combat_power(player: dict) -> int: def max_hp(player: dict) -> int: - return 40 + 4 * skill_level(player, "combat") + return 40 + 4 * skill_level(player, "combat") + int(gear_bonus(player, "hp")) def regen(world: dict, player: dict, now: float, boost: float = 1) -> None: @@ -1447,6 +1547,7 @@ def regen(world: dict, player: dict, now: float, boost: float = 1) -> None: if now > player["hp_at"]: top = max_hp(player) boost *= 2 if fitted(world, "enchanted_hearths") else 1 + boost *= 1 + gear_bonus(player, "regen") / 100 per_second = top * REGEN_PER_HOUR * boost * speed(world) / HOUR player["hp"] = min(top, player["hp"] + (now - player["hp_at"]) * per_second) player["hp_at"] = now @@ -1473,8 +1574,9 @@ def fighter(player: dict, blessed: dict, brew: dict, enemy: dict | None = None) 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"], + "accuracy": (10 + 2 * power) * style["accuracy"] * (1 + gear_bonus(player, "accuracy") / 100), + "max_hit": max(2, round((2 + power / 3) * damage * lands)), + "crit": style["crit"] + gear_bonus(player, "crit") / 100, "first": style.get("first", False), "affinity": lands, "block": BLOCK_CHANCE if player["equip"].get("shield") else 0, "evasion": 10 + 2 * (skill_level(player, "combat") + defence_bonus(player)), @@ -1839,7 +1941,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R cls = CLASSES[player["cls"]] lucky = bool(action["outputs"]) and ( action["skill"] in cls["craft"] or (player["cls"] == "miner" and action["skill"] == "mining") - ) and rng.random() < LUCKY_CHANCE + ) and rng.random() < LUCKY_CHANCE + gear_bonus(player, "masterwork") / 100 outputs = {item: qty * (2 if lucky else 1) for item, qty in action["outputs"].items()} if lucky: summary["lucky"] += 1 @@ -1847,11 +1949,19 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R for item, qty in action["outputs"].items(): outputs[item] += qty summary["lucky"] += 1 + if action["skill"] in GATHERING and rng.random() < gear_bonus(player, "double") / 100: + for item, qty in action["outputs"].items(): + outputs[item] += qty + summary["lucky"] += 1 mods = skill_mods(world, action) if mods["yield"] != 1: for item, qty in list(outputs.items()): exact = qty * mods["yield"] outputs[item] = int(exact) + (1 if rng.random() < exact - int(exact) else 0) + for item, qty, odds in action.get("finds", []): + # A gem in the rock: a masterwork (a prospector's double ore) turns up two. + if rng.random() < odds: + outputs[item] = outputs.get(item, 0) + qty * (2 if lucky else 1) xp *= mods["xp"] if "enemy" in action: @@ -1959,6 +2069,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R def gain_xp(world: dict, player: dict, skill: str, xp: float, now: float, summary: dict | None = None) -> None: before = skill_level(player, skill) + xp *= 1 + gear_bonus(player, "xp") / 100 player["xp"][skill] += xp after = skill_level(player, skill) if summary is not None: @@ -2079,7 +2190,7 @@ def _order_sources() -> tuple[dict, dict]: if action["skill"] in CRAFTING: craft.setdefault(item, action["id"]) elif action["skill"] in GATHERING: - for output in action["outputs"]: + for output in [*action["outputs"], *(item for item, _qty, _odds in action.get("finds", []))]: gather.setdefault(output, action["id"]) return craft, gather @@ -3169,6 +3280,9 @@ FITTINGS = [ for _weapon in _weapon_actions(): ACTIONS.append(_weapon) ACTION_BY_ID[_weapon["id"]] = _weapon +for _jewel in _jewel_actions(): + ACTIONS.append(_jewel) + ACTION_BY_ID[_jewel["id"]] = _jewel FITTING_BY_ID = {} for _id, _name, _building, _blevel, _level, _inputs, _effect, _glyph, _tint in FITTINGS: FITTING_BY_ID[_id] = {"id": _id, "name": _name, "building": _building, "level": _blevel, "effect": _effect} @@ -3748,7 +3862,7 @@ 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"} +REAGENT_GLYPHS = {"heavy": "chain-mail", "medium": "scale-mail", "light": "yarn", "jewel": "crystal-shine"} WEAPON_GLYPHS = {"hammer": "warhammer", "staff": "wizard-staff", "bow": "pocket-bow"} HARDWARE_GLYPHS = ["screw", "nails", "door-handle", "metal-plate", "crossed-chains", "padlock"] GLYPHS = { @@ -3759,6 +3873,8 @@ GLYPHS = { "bones": ("broken-bone", "#e9dfc6"), "big_bones": ("crossed-bones", "#e9dfc6"), "dragon_bones": ("dinosaur-bones", "#a6d98f"), "studded_body": ("leather-armor", "#a3825a"), "coal": ("ore", "#5d626b"), "water_barrel": ("barrel", "#5aa0d8"), + "silver_ore": ("ore", "#dfe4ec"), "silver_bar": ("metal-bar", "#dfe4ec"), + "gold_ore": ("ore", "#f0c040"), "gold_bar": ("gold-bar", "#f0c040"), "thatch": ("high-grass", "#d8c070"), "flax": ("flax", "#8fb0d8"), "rope": ("rope-coil", "#c9a66b"), "hide_glue": ("honey-jar", "#c08a40"), "provisions": ("knapsack", "#a08050"), "raw_shrimp": ("shrimp", "#f39a82"), "raw_trout": ("flatfish", "#9fb5c8"), "raw_salmon": ("salmon", "#f08a6a"), @@ -3770,6 +3886,7 @@ GLYPHS = { SKILL_GLYPHS = { "mining": ("mining", "#b8b2aa"), "woodcutting": ("wood-axe", "#c98a4a"), "farming": ("wheat", "#e3c060"), "hunting": ("deer-head", "#c49a6c"), "fishing": ("fishing-pole", "#5aa0d8"), "smithing": ("anvil", "#d07a3c"), + "jewelcrafting": ("gems", "#e070c0"), "construction": ("hammer-nails", "#d1a46a"), "cooking": ("cooking-pot", "#e0703a"), "leatherworking": ("sewing-needle", "#d6a26e"), "herblore": ("cauldron", "#7fbf5a"), "combat": ("crossed-swords", "#e05a5a"), "prayer": ("prayer", "#e9dfc6"), @@ -3819,7 +3936,7 @@ UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "mag "archery-target", "dodging", "shield-reflect", "cape", "gem-pendant", "gauntlet", "ring", "quiver", "leather-boot", "crystal-ball", "hot-meal", "round-bottom-flask", "visored-helm", "breastplate", "leg-armor", "pointy-sword", "round-shield", - *(kind["glyph"] for kind in DAMAGE_TYPES.values())] + *(kind["glyph"] for kind in DAMAGE_TYPES.values()), *(stat["glyph"] for stat in JEWEL_STATS.values())] def _assign_glyphs() -> None: @@ -3858,6 +3975,10 @@ def _assign_glyphs() -> None: ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint) for herb, tint in HERB_TINTS.items(): ITEMS[herb].update(glyph="herbs-bundle", tint=tint) + for gem, _name, _value, tint, glyph in GEMS: + ITEMS[gem].update(glyph=glyph, tint=tint) + for key, *_rest, piece_glyph in JEWELLERY: + ITEMS[f"{gem}_{key}"].update(glyph=piece_glyph, tint=tint) flasks = ["round-bottom-flask", "round-bottom-flask", "standing-potion", "standing-potion", "bubbling-flask", "bubbling-flask"] for potion, *_rest, skill, effect in POTIONS: @@ -3877,6 +3998,9 @@ def _assign_glyphs() -> None: elif action["outputs"]: first = ITEMS[next(iter(action["outputs"]))] action["glyph"], action["tint"] = first["glyph"], first["tint"] + elif action.get("finds"): + first = ITEMS[action["finds"][0][0]] + action["glyph"], action["tint"] = first["glyph"], first["tint"] elif action.get("special") in ("bury", "offer"): bone = ITEMS[next(iter(action["inputs"]))] action["glyph"] = "tombstone" if action["special"] == "bury" else "candle-holder" @@ -3913,6 +4037,6 @@ def content() -> dict: "order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()), "equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED, "styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE, - "resistant": RESISTANT, "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE, + "resistant": RESISTANT, "jewel_stats": JEWEL_STATS, "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE, "regen_per_hour": REGEN_PER_HOUR, } diff --git a/static/app.js b/static/app.js index a6f96dd..d3146f0 100644 --- a/static/app.js +++ b/static/app.js @@ -657,13 +657,20 @@ function animateFight(now) { }); } +// A piece of jewellery's bonus, e.g. "+8% accuracy". +function jewelText(it) { + const st = C.jewel_stats[it.jewel]; + return `+${it[it.jewel]}${st.unit} ${st.name.toLowerCase()}`; +} + // 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`]; + const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`, it.ward && `+${it.ward} ward`, + it.jewel && !["power", "ward"].includes(it.jewel) && jewelText(it)]; return stats.filter(Boolean).join(", ") + (it.weight ? ` (${C.weights[it.weight].name.toLowerCase()})` : ""); } -const GEAR_SLOTS = ["weapon", "shield", "helmet", "body", "legs"]; +const GEAR_SLOTS = ["weapon", "shield", "helmet", "body", "legs", "ring", "amulet", "trinket"]; // A weapon's or armour piece's stats in a small grid. Weapons: damage type and power | 1H or 2H | swing // time; armour: armour | weight | ward. Below: what it needs to wear, and any bonus for you. @@ -684,6 +691,9 @@ function gearChips(it, who = S.me) { row = [cell(glyph("checked-shield", "#d4dae2"), it.armour, `+${it.armour} armour: less damage from blows`), cell("", C.weights[it.weight].name, `${C.weights[it.weight].name} armour`, "muted"), cell(glyph("magic-shield", "#a77be8"), it.ward, `+${it.ward} ward: less damage from magic`)]; + } else if (it.jewel) { + const st = C.jewel_stats[it.jewel]; + row = [cell(glyph(st.glyph, st.tint), `+${it[it.jewel]}${st.unit}`, jewelText(it)), "", ""]; } let req = "", bonus = ""; if (it.req) { @@ -708,7 +718,8 @@ function itemStats(id) { parts.push(`${it.two_handed ? "two-handed " : ""}${st.name}, ${st.interval}s${trade ? ` (${C.classes[trade].name.toLowerCase()})` : ""}`); } 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.reagent) parts.push(it.reagent === "jewel" ? "Reagent for jewellery" : `Reagent for ${C.weights[it.reagent].name.toLowerCase()} armour`); + if (it.jewel && !["power", "ward"].includes(it.jewel)) parts.push(jewelText(it)); 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.potion) parts.push(`${it.potion.text}, ${C.potion_charges} uses`); @@ -735,15 +746,17 @@ function actionCard(a) { let io = ""; if (Object.keys(a.inputs).length) { io += Object.entries(a.inputs).map(([id, q]) => itemChip(id, q, me.bank[id] || 0)).join("") - + (Object.keys(a.outputs).length ? `→` : ""); + + (Object.keys(a.outputs).length || a.finds ? `→` : ""); } io += Object.entries(a.outputs).map(([id, q]) => itemChip(id, q)).join(""); + const chance = ([id, q, odds]) => `${item(id).icon} ${Math.round(odds * 100)}%`; + if (a.finds) io += a.finds.map(chance).join(""); let extra = ""; if (a.enemy) { const o = me.fight_odds[a.id], e = a.enemy; extra = `${e.boss ? `