From 5facec9ee8674db1e08d93d3b8f4ce8978958e7f Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 20 Oct 2026 20:26:54 +0100 Subject: [PATCH] Rings of precision become rings of haste Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- game.py | 33 +++++++++++++++++++++++++++------ static/app.js | 4 ++-- tests/test_game.py | 16 +++++++++++++--- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c05adba..df25190 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Alpha. One season, bandit raids plus two friendly events, six buildings. 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 + area. Rings for offence (swing speed, 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, diff --git a/game.py b/game.py index d6446a1..9fcc733 100644 --- a/game.py +++ b/game.py @@ -221,7 +221,7 @@ PROSPECT = ["stone", "iron_ore", "coal", "mithril_ore", "adamantite_ore", "runit # 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"), + ("haste", "ring", "ring of haste", "haste", [5, 7, 8, 9, 10, 11], 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"), @@ -231,9 +231,9 @@ JEWELLERY = [ ("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. +# How each jewellery bonus reads: name, unit, glyph, tint (and a sign, if it takes away). JEWEL_STATS = { - "accuracy": {"name": "Accuracy", "unit": "%", "glyph": "archery-target", "tint": "#e0b04a"}, + "haste": {"name": "Swing time", "unit": "%", "glyph": "stopwatch", "tint": "#9fb3c8", "sign": "-"}, "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"}, @@ -1033,7 +1033,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows # --- world --------------------------------------------------------------- -WORLD_VERSION = 19 +WORLD_VERSION = 20 def new_world(now: float, speed: float = 1.0) -> dict: @@ -1187,9 +1187,29 @@ def migrate(world: dict, now: float | None = None) -> dict: for player in world["players"].values(): player.setdefault("pinned", []) world["version"] = 19 + if world["version"] < 20: + _precision_to_haste(world) + world["version"] = 20 return world +def _precision_to_haste(world: dict) -> None: + """Rings of precision became rings of haste: same gem, same slot.""" + rename = lambda item: item.replace("_precision", "_haste") if item and item.endswith("_precision") else item + swap = lambda items: {rename(item): qty for item, qty in items.items()} + world["city"]["stockpile"] = swap(world["city"]["stockpile"]) + for player in world["players"].values(): + player["bank"] = swap(player["bank"]) + player["equip"] = {slot: rename(item) for slot, item in player["equip"].items()} + action = player["action"] + if action: + for key in ("id", "resume"): + if key in action: + action[key] = rename(action[key]) + for order in world.get("orders", []): + order["item"], order["action"] = rename(order["item"]), rename(order["action"]) + + # 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. @@ -1578,8 +1598,9 @@ def fighter(player: dict, blessed: dict, brew: dict, enemy: dict | None = None) damage = style["hit"] * (CLASS_STYLE_BONUS if CLASS_STYLE.get(player["cls"]) == style_id else 1) 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"] * (1 + gear_bonus(player, "accuracy") / 100), + return {"style": style_id, "damage": style["damage"], + "interval": round(style["interval"] * (1 - gear_bonus(player, "haste") / 100), 2), + "accuracy": (10 + 2 * power) * style["accuracy"], "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, diff --git a/static/app.js b/static/app.js index 2045331..61b501d 100644 --- a/static/app.js +++ b/static/app.js @@ -670,7 +670,7 @@ 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()}`; + return `${st.sign || "+"}${it[it.jewel]}${st.unit} ${st.name.toLowerCase()}`; } // Power, armour and ward, and the armour's weight. @@ -703,7 +703,7 @@ function gearChips(it, who = S.me) { 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)), "", ""]; + row = [cell(glyph(st.glyph, st.tint), `${st.sign || "+"}${it[it.jewel]}${st.unit}`, jewelText(it)), "", ""]; } let req = "", bonus = ""; if (it.req) { diff --git a/tests/test_game.py b/tests/test_game.py index 44ec793..6ae5c83 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -1650,7 +1650,7 @@ class JewelcraftingTest(unittest.TestCase): self.ann = self.world["players"]["ann"] def test_recipes_take_a_band_a_gem_and_past_quartz_a_shard(self): - self.assertEqual(game.ACTION_BY_ID["jc_quartz_precision"]["inputs"], {"copper_bar": 1, "quartz": 1}) + self.assertEqual(game.ACTION_BY_ID["jc_quartz_haste"]["inputs"], {"copper_bar": 1, "quartz": 1}) self.assertEqual(game.ACTION_BY_ID["jc_garnet_vigour"]["inputs"], {"silver_bar": 2, "garnet": 1, "watchtower_shard": 2}) self.assertEqual(game.ACTION_BY_ID["jc_diamond_charm"]["inputs"], @@ -1679,9 +1679,9 @@ class JewelcraftingTest(unittest.TestCase): p["xp"]["combat"] = game.XP_TABLE[20] before = game.fighter(p, game.NO_BLESSING, {}) hp = game.max_hp(p) - p["equip"].update(ring="garnet_precision", amulet="garnet_vigour", trinket="garnet_hourglass") + p["equip"].update(ring="garnet_haste", amulet="garnet_vigour", trinket="garnet_hourglass") after = game.fighter(p, game.NO_BLESSING, {}) - self.assertAlmostEqual(after["accuracy"], before["accuracy"] * 1.08) + self.assertAlmostEqual(after["interval"], round(before["interval"] * 0.93, 2)) self.assertEqual(game.max_hp(p), hp + 10) game.gain_xp(self.world, p, "mining", 100, T0) self.assertAlmostEqual(p["xp"]["mining"], 103) @@ -1707,3 +1707,13 @@ class JewelcraftingTest(unittest.TestCase): self.assertEqual(self.ann["xp"]["jewelcrafting"], 0) self.assertEqual({slot: self.ann["equip"][slot] for slot in ("ring", "amulet", "trinket")}, {"ring": None, "amulet": None, "trinket": None}) + + def test_rings_of_precision_became_haste(self): + self.world["version"] = 19 + self.ann["bank"] = {"garnet_precision": 2, "garnet": 1} + self.ann["equip"]["ring"] = "quartz_precision" + self.ann["action"] = {"id": "jc_garnet_precision", "start": T0} + game.migrate(self.world, T0) + self.assertEqual(self.ann["bank"], {"garnet_haste": 2, "garnet": 1}) + self.assertEqual(self.ann["equip"]["ring"], "quartz_haste") + self.assertEqual(self.ann["action"]["id"], "jc_garnet_haste")