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")