mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
fix(understone): rebalance early game and add inn turn refresh
The opening hours were a gold/HP death spiral: a fresh hero spent more gold healing a fight than the kill paid, mid-tier foes out-damaged a starting HP bar, the map gave no read on where harder foes spawned, and a spent day left the player idle until the UTC rollover. This eases the on-ramp across both shipped worlds. Economy - Healer drops from 2 to 1 gold per HP, so topping up no longer outruns income. - Starting purse 20 -> 37: enough to buy the cheapest armor and one potion up front, a one-point DEF bump plus a heal cushion the player chooses to spend. Combat - Tier 2-4 common foes lose 1 ATK each, trimming the burst that could halve or end a fresh hero in a single bout. Rares and the boss are untouched. Wayfinding - The road glyph changes from "=" to a shaded path that reads as one continuous road in every orientation; "=" only looked right horizontally and broke into stacked dashes on vertical runs. Cinder's basalt path gets the same treatment with a crosshatch glyph free in its palette. Rest - Sleeping at the inn now rolls a spent adventurer into a fresh day's turns (it already fully heals). The top-up fires only at zero turns, so it never banks past the daily cap. Verified: full suite green (+2 rest tests, ruff/mypy clean); the greedy balance bot still clears the world 11/12 seeds at an unchanged pace on both packs.
This commit is contained in:
@@ -161,6 +161,35 @@ def test_rest_heals_and_charges(tmp_path: Path, clock: object) -> None:
|
|||||||
assert "full health" in out.lower()
|
assert "full health" in out.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_when_spent_restores_a_fresh_days_turns(tmp_path: Path, clock: object) -> None:
|
||||||
|
"""Sleeping at the inn with no turns left rolls into a fresh day's allowance."""
|
||||||
|
game = _game(tmp_path, clock)
|
||||||
|
game.join("Brandr")
|
||||||
|
player = game.players["Brandr"]
|
||||||
|
daily = game.world.settings.daily_turns
|
||||||
|
player.turns_left = 0 # spent for the day
|
||||||
|
player.hp = 5
|
||||||
|
game.move("Brandr", "", "west", 2) # step into the inn
|
||||||
|
out = game.action("Brandr", "rest", "", "")
|
||||||
|
assert player.turns_left == daily # a fresh day's turns restored
|
||||||
|
assert player.hp == player.max_hp # and fully mended
|
||||||
|
assert f"/{daily} ]" in out # footer reflects the refreshed budget
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_with_turns_in_hand_never_inflates_the_budget(tmp_path: Path, clock: object) -> None:
|
||||||
|
"""Resting mid-day mends but adds no turns — the top-up only fires at zero."""
|
||||||
|
game = _game(tmp_path, clock)
|
||||||
|
game.join("Brandr")
|
||||||
|
player = game.players["Brandr"]
|
||||||
|
daily = game.world.settings.daily_turns
|
||||||
|
player.turns_left = daily - 3 # turns still in hand
|
||||||
|
player.hp = 5
|
||||||
|
game.move("Brandr", "", "west", 2) # step into the inn
|
||||||
|
game.action("Brandr", "rest", "", "")
|
||||||
|
assert player.turns_left == daily - 3 # unchanged: no farming past the cap
|
||||||
|
assert player.hp == player.max_hp # but the heal still lands
|
||||||
|
|
||||||
|
|
||||||
def test_fight_spends_a_turn_and_credits(tmp_path: Path, clock: object) -> None:
|
def test_fight_spends_a_turn_and_credits(tmp_path: Path, clock: object) -> None:
|
||||||
game = _game(tmp_path, clock)
|
game = _game(tmp_path, clock)
|
||||||
game.join("Brandr")
|
game.join("Brandr")
|
||||||
|
|||||||
@@ -1015,16 +1015,28 @@ class Game:
|
|||||||
return self._overworld_frame(player, lines=["You step back out into the open air."])
|
return self._overworld_frame(player, lines=["You step back out into the open air."])
|
||||||
|
|
||||||
def _rest(self, player: Player) -> str:
|
def _rest(self, player: Player) -> str:
|
||||||
|
# Settle any pending day rollover first, so a rest taken as the first act
|
||||||
|
# of a new day is the ordinary refresh, not the spent-turns top-up below.
|
||||||
|
self._ensure_day(player)
|
||||||
cost = self.world.settings.rest_cost
|
cost = self.world.settings.rest_cost
|
||||||
if leveling.rest(player, cost):
|
if not leveling.rest(player, cost):
|
||||||
# A night's rest is a private errand, not Herald news; persist only.
|
|
||||||
self._persist(player)
|
|
||||||
return self._location_menu(
|
return self._location_menu(
|
||||||
player, lines=[f"You sleep deeply and wake at full health. (-{cost} gold)"]
|
player, lines=[f"You can't afford the {cost}-gold bed. (You have {player.gold}.)"]
|
||||||
)
|
)
|
||||||
return self._location_menu(
|
# A night at the inn always mends. Once the day's turns are spent it also
|
||||||
player, lines=[f"You can't afford the {cost}-gold bed. (You have {player.gold}.)"]
|
# rolls the sleeper into a fresh day's allowance, so a spent adventurer can
|
||||||
)
|
# press on rather than idling until the dawn rollover. The top-up only
|
||||||
|
# fires at zero, so it never banks turns past the daily cap.
|
||||||
|
line = f"You sleep deeply and wake at full health. (-{cost} gold)"
|
||||||
|
if player.turns_left <= 0:
|
||||||
|
player.turns_left = self.world.settings.daily_turns
|
||||||
|
line = (
|
||||||
|
f"You sleep through to a new dawn, waking at full health "
|
||||||
|
f"and ready to venture out anew. (-{cost} gold)"
|
||||||
|
)
|
||||||
|
# A night's rest is a private errand, not Herald news; persist only.
|
||||||
|
self._persist(player)
|
||||||
|
return self._location_menu(player, lines=[line])
|
||||||
|
|
||||||
# -- the Vault: bank gold at the inn (safe from ambush) --------------
|
# -- the Vault: bank gold at the inn (safe from ambush) --------------
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"tier": 2,
|
"tier": 2,
|
||||||
"name": "Goblin",
|
"name": "Goblin",
|
||||||
"hp": 12,
|
"hp": 12,
|
||||||
"atk": 5,
|
"atk": 4,
|
||||||
"def": 1,
|
"def": 1,
|
||||||
"xp": 18,
|
"xp": 18,
|
||||||
"gold": 7
|
"gold": 7
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"tier": 2,
|
"tier": 2,
|
||||||
"name": "Bandit Scout",
|
"name": "Bandit Scout",
|
||||||
"hp": 14,
|
"hp": 14,
|
||||||
"atk": 6,
|
"atk": 5,
|
||||||
"def": 1,
|
"def": 1,
|
||||||
"xp": 20,
|
"xp": 20,
|
||||||
"gold": 9
|
"gold": 9
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"tier": 3,
|
"tier": 3,
|
||||||
"name": "Forest Wolf",
|
"name": "Forest Wolf",
|
||||||
"hp": 20,
|
"hp": 20,
|
||||||
"atk": 8,
|
"atk": 7,
|
||||||
"def": 2,
|
"def": 2,
|
||||||
"xp": 35,
|
"xp": 35,
|
||||||
"gold": 14
|
"gold": 14
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"tier": 3,
|
"tier": 3,
|
||||||
"name": "Bog Stalker",
|
"name": "Bog Stalker",
|
||||||
"hp": 22,
|
"hp": 22,
|
||||||
"atk": 9,
|
"atk": 8,
|
||||||
"def": 2,
|
"def": 2,
|
||||||
"xp": 38,
|
"xp": 38,
|
||||||
"gold": 16
|
"gold": 16
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"tier": 4,
|
"tier": 4,
|
||||||
"name": "Cave Troll",
|
"name": "Cave Troll",
|
||||||
"hp": 38,
|
"hp": 38,
|
||||||
"atk": 12,
|
"atk": 11,
|
||||||
"def": 4,
|
"def": 4,
|
||||||
"xp": 70,
|
"xp": 70,
|
||||||
"gold": 30
|
"gold": 30
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"tier": 4,
|
"tier": 4,
|
||||||
"name": "Barrow Wight",
|
"name": "Barrow Wight",
|
||||||
"hp": 35,
|
"hp": 35,
|
||||||
"atk": 13,
|
"atk": 12,
|
||||||
"def": 4,
|
"def": 4,
|
||||||
"xp": 65,
|
"xp": 65,
|
||||||
"gold": 28
|
"gold": 28
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"=": {
|
"=": {
|
||||||
"key": "road",
|
"key": "road",
|
||||||
"glyph": "=",
|
"glyph": "▒",
|
||||||
"walkable": true,
|
"walkable": true,
|
||||||
"encounter_rate": 0.02,
|
"encounter_rate": 0.02,
|
||||||
"color": "road"
|
"color": "road"
|
||||||
|
|||||||
@@ -100,8 +100,8 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"daily_turns": 10,
|
"daily_turns": 10,
|
||||||
"rest_cost": 15,
|
"rest_cost": 15,
|
||||||
"heal_cost_per_hp": 2,
|
"heal_cost_per_hp": 1,
|
||||||
"starting_gold": 20,
|
"starting_gold": 37,
|
||||||
"starting_weapon": "rusty_dagger",
|
"starting_weapon": "rusty_dagger",
|
||||||
"starting_armor": "cloth_tunic",
|
"starting_armor": "cloth_tunic",
|
||||||
"start_hp": 20,
|
"start_hp": 20,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"tier": 2,
|
"tier": 2,
|
||||||
"name": "Ember Imp",
|
"name": "Ember Imp",
|
||||||
"hp": 12,
|
"hp": 12,
|
||||||
"atk": 5,
|
"atk": 4,
|
||||||
"def": 1,
|
"def": 1,
|
||||||
"xp": 18,
|
"xp": 18,
|
||||||
"gold": 7
|
"gold": 7
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"tier": 2,
|
"tier": 2,
|
||||||
"name": "Slag Scuttler",
|
"name": "Slag Scuttler",
|
||||||
"hp": 14,
|
"hp": 14,
|
||||||
"atk": 6,
|
"atk": 5,
|
||||||
"def": 1,
|
"def": 1,
|
||||||
"xp": 20,
|
"xp": 20,
|
||||||
"gold": 9
|
"gold": 9
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"tier": 3,
|
"tier": 3,
|
||||||
"name": "Magma Hound",
|
"name": "Magma Hound",
|
||||||
"hp": 20,
|
"hp": 20,
|
||||||
"atk": 8,
|
"atk": 7,
|
||||||
"def": 2,
|
"def": 2,
|
||||||
"xp": 35,
|
"xp": 35,
|
||||||
"gold": 14
|
"gold": 14
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"tier": 3,
|
"tier": 3,
|
||||||
"name": "Obsidian Lurker",
|
"name": "Obsidian Lurker",
|
||||||
"hp": 22,
|
"hp": 22,
|
||||||
"atk": 9,
|
"atk": 8,
|
||||||
"def": 2,
|
"def": 2,
|
||||||
"xp": 38,
|
"xp": 38,
|
||||||
"gold": 16
|
"gold": 16
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"tier": 4,
|
"tier": 4,
|
||||||
"name": "Basalt Golem",
|
"name": "Basalt Golem",
|
||||||
"hp": 38,
|
"hp": 38,
|
||||||
"atk": 12,
|
"atk": 11,
|
||||||
"def": 4,
|
"def": 4,
|
||||||
"xp": 70,
|
"xp": 70,
|
||||||
"gold": 30
|
"gold": 30
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"tier": 4,
|
"tier": 4,
|
||||||
"name": "Ashen Wraith",
|
"name": "Ashen Wraith",
|
||||||
"hp": 35,
|
"hp": 35,
|
||||||
"atk": 13,
|
"atk": 12,
|
||||||
"def": 4,
|
"def": 4,
|
||||||
"xp": 65,
|
"xp": 65,
|
||||||
"gold": 28
|
"gold": 28
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"=": {
|
"=": {
|
||||||
"key": "basalt",
|
"key": "basalt",
|
||||||
"glyph": "=",
|
"glyph": "▩",
|
||||||
"walkable": true,
|
"walkable": true,
|
||||||
"encounter_rate": 0.02,
|
"encounter_rate": 0.02,
|
||||||
"color": "road"
|
"color": "road"
|
||||||
|
|||||||
@@ -100,8 +100,8 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"daily_turns": 10,
|
"daily_turns": 10,
|
||||||
"rest_cost": 15,
|
"rest_cost": 15,
|
||||||
"heal_cost_per_hp": 2,
|
"heal_cost_per_hp": 1,
|
||||||
"starting_gold": 20,
|
"starting_gold": 37,
|
||||||
"starting_weapon": "charred_shiv",
|
"starting_weapon": "charred_shiv",
|
||||||
"starting_armor": "scorched_rags",
|
"starting_armor": "scorched_rags",
|
||||||
"start_hp": 20,
|
"start_hp": 20,
|
||||||
|
|||||||
Reference in New Issue
Block a user