Repository navigation
Releases: duysqubix/shellquest
Release list
v1.28.0
v1.28.0 — Potions That Actually Heal, and an Arena You Can Survive
Your potions were dead weight. A Legendary draught healed a flat 22 HP — about 3% of an endgame health bar — while the cheapest common and the rarest legendary restored nearly the same trivial amount. And once an arena run turned against you, there was nothing to do but watch yourself get knocked out. This release fixes both: potions now scale with your max HP, and between arena rounds you can pay a healer or quaff a potion to stay in the fight.
🧪 Potions Scale With You
Healing is no longer a flat number frozen at level 1. Every potion now restores a percentage of your max HP, driven by its rarity/power — so the same potion that patches a scratch early game delivers a real heal at level 150.
~ $ sq drink Code-Cordial
🧪 You drink the Code-Cordial! Restored +63 HP. HP: 163/300
That Rare potion healed 63 HP on a 300-HP hero — where before it would have restored a laughable 7. The full curve:
| Potion | @ Level 50 (300 HP) | @ Level 150 (800 HP) | Old (flat) |
|---|---|---|---|
| Common | 9–36 HP | 24–96 HP | 1–4 |
| Rare | 45–90 HP | 120–240 HP | 5–10 |
| Legendary | 135–225 HP | 360–600 HP | 15–25 |
Common potions are honest topups; a Legendary can now claw back most of your bar. Item definitions and prices are unchanged — only what they do changed.
⚕️ The Arena Healer
Between arena rounds you can now visit a healer to restore yourself to full, for a price pinned to the stakes: 25% of the run's entry fee. It's reusable — pay again as often as you can afford — so a deep, punishing run becomes a question of managing your purse, not just your HP.
Choose [1-3]: 3
The healer binds your wounds with practiced hands. +183 HP. HP: 300/300 (150 gold)
🍶 Quaff Mid-Arena
Carrying potions into the arena finally means something. Pick option 4 between rounds and your hero quaffs their strongest potion on the spot — no digging through the inventory.
Choose [1-4]: 4
🧪 You quaff the Heart of the Hardware! +198 HP. HP: 285/300
Healer gold and quaffed potions are sunk costs: they stick even if you're knocked out next round (only a hard Ctrl+C abort rolls the whole run back).
💰 The Net Preview Knows What You Spent
The Cash Out preview's (net) figure — what you'd actually walk away with — now subtracts every healer visit, and updates live the moment you pay.
💰 Cash Out now: +72 gold, +250 XP (net: -528 gold)
The healer binds your wounds... +183 HP. HP: 300/300 (150 gold)
💰 Cash Out now: +72 gold, +250 XP (net: -678 gold)
Heal once, watch the net drop by exactly the healer's fee. No surprises when the gold actually lands.
⚠️ Balance Note
Potion healing is now a percentage of max HP instead of a flat 1–25. This is a large buff — potions go from near-useless to a genuine survivability tool at every level. Arena runs are more forgiving if you bring gold and potions, and more of a gold sink if you lean on the healer.
Saves are unchanged. Potion names, prices, and drop rates are unchanged. No migration needed.
Full Changelog: v1.27.0...v1.28.0
v1.27.0
v1.27.0 — Watch Both Health Bars at Once
In the arena you could always see the monster's HP ticking down on every swing — but to check your own health you had to wait for the enemy to land a hit and read it off their line. By then it might be too late to cash out. Now your HP rides along on every attack you make.
❤ Your HP, On Every Swing
Each player-attack line that shows the monster's (HP: …) now also carries your own HP, marked with a distinct ❤ and color-tiered like a real health bar — green when you're healthy, yellow when you're hurt, red when you're one bad roll from a knockout. The monster's bar stays red; yours stands apart so you can read both at a glance, mid-fight, without doing the math.
Your blade bites deep into the Deadlock Demon for 78 damage! (HP: 76/154) ❤ 200/200
The Deadlock Demon strikes with fury! You take 58 damage! HP: 142/200
CRITICAL! Your blade cleaves the Deadlock Demon for 160 damage! (HP: 0/154) ❤ 142/200
Watch the overlay update in real time: you open at ❤ 200/200, the Demon clips you for 58, and your very next swing already reads ❤ 142/200. The danger is no longer hidden between the lines — it's right there on the blow you just landed, so the Continue-or-Cash-Out call is one you make with both bars in view.
Saves are unchanged. CLI surface is unchanged. No migration needed.
Full Changelog: v1.26.0...v1.27.0
v1.26.0
v1.26.0 — The Arena Tells You If You're Actually Ahead
The cash-out preview always showed you the pot — but not whether the pot had cleared your entry fee yet. You could bank "+72 gold" feeling like a winner, having forgotten you paid 600 to walk in. Now every cash-out line shows your net — exactly what you'd walk away with, fee and all.
💰 Net Gold in the Cash-Out Preview
After each won round the gauntlet now appends your net position: total gold earned this run minus the entry fee. Negative while you're still digging out of the fee, then it climbs into the green as you push deeper. The decision to Continue or Cash Out is finally an honest one — you can see whether bailing right now actually leaves you ahead.
Round 1 cleared! Current HP: 600/600
💰 Cash Out now: +72 gold, +250 XP (net: -528 gold)
1) Continue
2) Cash Out
That -528 is the truth the old line hid: a 600-gold entry fee against 72 gold in the pot. Keep clearing rounds and watch it recover in real time:
💰 Cash Out now: +72 gold, +250 XP (net: -528 gold)
💰 Cash Out now: +180 gold, +600 XP (net: -420 gold)
💰 Cash Out now: +330 gold, +1200 XP (net: -270 gold)
💰 Cash Out now: +540 gold, +2000 XP (net: -60 gold)
The net is color-coded — red while you're underwater, green once the run has paid for itself, dimmed at exactly break-even. No mental math mid-fight, no nasty surprise when the gold actually lands in your purse. The number you see is the number you keep.
Saves are unchanged. CLI surface is unchanged. No migration needed.
Full Changelog: v1.25.0...v1.26.0
v1.25.0
v1.25.0 — The Void, and the Great Rebalance
The biggest release since the arena landed. Two headline features and a world that grew underneath them.
First: The Void — a daily, procedurally-generated dungeon that opens through a portal in your home directory. A real maze of directories on your disk, a hidden scroll buried deep inside it, and a once-a-day shot at Rare-or-better loot for the player who finds it.
Second: the Great Rebalance — a full, simulation-validated retune of the game from level 1 to level 150. Leveling was too fast. Fresh characters were too strong. High-level characters were functionally immortal. The arena tiers paid out like a slot machine stuck on win. Bosses were flavor, not threats. Every one of those was diagnosed against the real numbers, fixed, and re-validated against a Docker-isolated simulation harness that drove thousands of characters through the game. None of these numbers are guesses.
And underneath both: 22 new zones and ~70 new recognized commands that landed during this cycle, widening the world your shell maps onto.
🌀 The Void — A Daily Procedural Dungeon
There is a new command: sq quest.
Run it from your home directory and a quest-giver opens a portal — a rift at $HOME that leads into The Void, a procedurally-generated maze of directories that materializes under ~/.shellquest/the_void. It is a real on-disk labyrinth: bounded in depth, threaded with one-way symlinks, strictly contained inside the shellquest home so it can never escape into the rest of your filesystem.
Somewhere deep in that maze, at a leaf the generator chose, a lost_scroll waits. Your job is to find it.
~ $ sq quest
🌀 A portal tears open in the corner of your home directory...
The Void yawns. Somewhere inside, a lost scroll holds the day's phrase.
~ $ cd ~/.shellquest/the_void
# ...wander the maze, read directories, hunt the scroll...
~ $ cat .../null_gallery/lost_scroll_4271.txt
"the prompt flickers"
~ $ cd ~ && sq quest answer "the prompt flickers"
💎 The Void yields. You claim a Rare-or-better relic, XP, and gold.The rules of the Void:
- It reshuffles every UTC midnight. Each day is a fresh maze with a fresh scroll and a fresh phrase. The old maze is blown away — no orphaned dungeons pile up on your disk.
- One claim per day. Answer correctly and the day's reward is yours. Come back tomorrow for a new one.
- Wrong answers cost nothing. Guess as many times as you like; the Void is patient.
- The reward floor is Rare. No Common scraps from the Void — only Rare, Epic, or Legendary loot, plus bonus XP and gold.
The Void is also a combat zone. While you crawl the maze, you face an 18-strong roster of Void-native horrors — the Null Wraith, the Echo Eater, the Orphaned Inode, the Rift-Mouthed Daemon, the Zero-Width Horror, and more — whose difficulty scales with both your level and how deep into the maze you've wandered. The deeper you go, the more dangerous it gets.
There's a manual for all of it: sq help quest (also reachable as sq help void).
⚖️ The Great Rebalance
A player noticed something while playing: a brand-new character hit level 3 in about five minutes, and a level-2 Ranger had an attack stat that felt absurd. That observation kicked off a full diagnostic pass over the entire progression curve, early game to end game. Every number below was validated against a simulation harness — one Docker container per character, thousands of runs — before it shipped. The fixes confirmed the data.
Leveling is no longer candy
The early XP curve was trivially generous. A single affinity event (a git commit, a cargo build) handed out ~25–37 XP, and the level-1 threshold was just 25 — so one good command was an entire level. Reaching level 6 took roughly 28 commands.
Two things changed. First, the early curve was steepened: the level-1 threshold is now 70 XP, and the early band grows as level × 30 + 40 (up from level × 15 + 10). Reaching level 6 took roughly 63 commands after that change alone — and the whole curve, all the way to 150, was re-checked to climb monotonically: no level is ever cheaper to clear than the one before it. (The previous early-curve patch had accidentally made level 11 easier than level 10; that discontinuity is gone.)
Second, on top of the steeper curve, every XP award in the game is now halved at the source — a single global knob divides the XP you earn from overworld events, combat kills, the Void, bosses, quests, and the arena. The number you see is the number you gain (display and reality always match), and a successful event never grants nothing — the smallest award still floors at 1 XP. Where a git commit used to hand you most of a level, it now hands you a healthy chunk of one.
| Reach | Old cumulative XP | New cumulative XP |
|---|---|---|
| Level 2 | 25 | 70 |
| Level 3 | 65 | 170 |
| Level 4 | 120 | 300 |
| Level 6 | 275 | 870 |
The threshold table above is only half the story — because XP income is also cut, the real-world climb is slower still than the cumulative numbers alone suggest. Leveling now feels earned from the very first command instead of raining down like candy.
Fresh characters aren't over-powered
Attack power used to read STR + DEX/2 + gear. For the DEX-built classes (Ranger, Rogue) that double-dipped a high DEX into both their dodge and their attack, so a level-2 Dwarf Ranger swung for 21 with no weapon equipped.
The DEX contribution to attack was reduced to a third: attack_power = STR + DEX/3 + gear. That same Dwarf Ranger now reads ATK 17 at level 1 — strong, but no longer a bare-handed powerhouse. Your status sheet reflects the new math directly.
Combat is dangerous again — at every level
This was the deepest fix. In simulation, high-level characters were effectively immortal: across 198 overworld encounters at level 100, the death rate was 0%. Not low — zero. The cause was mechanical: a high-level character's DEX had grown so large that it collapsed every enemy's chance to hit down to a natural-20-only roll. Stack enough dodge and nothing could touch you.
So enemy accuracy now has a floor that high player DEX cannot fully erase — a cap on how much dodge advantage you can hold over an enemy in the overworld. The result, measured at level 100 after the change:
| Class | Death rate before | Death rate after |
|---|---|---|
| Necromancer | 0% | 3.3% |
| Ranger | 0% | 7.1% |
| Rogue | 0% | 6.5% |
| Warrior | 0% | 4.9% |
| Wizard | 0% | 2.6% |
Every class now takes real risk at high level, and — critically — the risk is fair across classes. No build is immortal; no build is a death-trap. The early and mid game were validated the same way: levels 1–20 land in a healthy 3–10% death band, with the monster-danger scaling and per-level HP floors retuned to keep the pressure consistent as you climb.
The arena pays out like a gamble, not a guarantee
The five arena tiers were re-simulated end to end. The Pit was paying out negative on average (the entry fee outran the reward). The deeper tiers were the opposite — the Colosseum cleared at a 2% knockout rate while handing over thousands of gold. Neither is a gamble.
All five tiers were retuned so each is a genuine risk/reward decision at its unlock level — meaningful knockout risk, an economy that rewards a clean run and stings on a wipe, and deeper tiers that demand more and pay more. The wave escalation from v1.24 stays; the fee and reward curves around it were rebalanced against the data.
Bosses are threats, not flavor
A world boss used to be a cutscene. In simulation, a level-100 character beat bosses taking near-zero damage — 74 boss fights, all wins, no danger. Boss HP and attack were lifted substantially (the roster now runs 440–540 HP), and the same dodge-advantage cap was applied to boss fights. A mid-to-high-level character can still win — but now a boss costs you something on the way down.
🗺️ A Wider World
Two changes from earlier in this cycle ship here too:
- +22 new themed zones. The filesystem map your
$PWDlands on grew — more of your real directories now have their own danger level, color, and flavor, from system paths to caches to build artifacts. - +~70 new recognized commands. Many more of the commands you actually type now trigger class-aware events, each with its own flavor per class.
🔧 Behind the Scenes
None of the rebalance above would have been trustworthy without tooling. This cycle the balance simulator was rebuilt and extended:
- Dockerized simulation — one container per simulated character, for true filesystem isolation now that
sqreaches the real disk (the Void needed this). - Pre-leveled and pre-prestiged character seeding — the simulator can now spawn a character directly at level 100, or level 150 with prestige 3, instead of grinding one there from scratch. This made end-game and Godslayer-tier validation tractable (minutes instead of hours).
SQ_DEBUGtelemetry,sq items --json/sq bestiary --jsoncatalog dumps, and an interactive A/B-compare dashboard — the instrumentation that turned "it feels too easy" into a table of numbers.
These are developer tools; they are not shipped in the sq binary and don't change how you play. They're why the numbers in this release are claims you can check rather than vibes.
One Honest Caveat
The Godslayer's Court — the 50-round, level-150, prestige-3 endgame tier — was validated for entry and deep-round survival, but its crown-clear (winning all 50 rounds) was not provable in simulation: the sim's strategy never voluntarily cashes out, so it can't model the deliberate human decision of when to bank and walk. Entry, escalating risk, and the economy are confirmed by data. Whether a perfectly-played human clears the crown is, as it should be, left to you.
Every fight h...
v1.24.0
v1.24.0 — The Arena Has Waves Now
Last release made arena combat hit back. This one makes each fight harder than the last.
In v1.23 every round of a tier felt roughly the same — monsters scaled a little per round but the curve was nearly flat. You'd clear all 10 rounds of the Gauntlet at full HP because rounds 1 and 10 played out the same way. That's not what the user asked for, and the data didn't lie: a L150 prestige-3 Godslayer was clearing the entire 49-round Godslayer's Court at 39% HP, taking 1% of max HP per round on average. The arena's headline tier was a victory lap.
v1.24 introduces wave-based escalation — a step function on enemy HP, accuracy, and crit chance that turns each tier into a deliberate gauntlet where round 10 is something you genuinely flex about clearing.
⚔️ Wave-Based Combat
Every arena round is now mapped to a difficulty wave. The wave determines three independent multipliers that compound:
| Wave | Rounds | HP multiplier | Hit cap | Enemy crit% |
|---|---|---|---|---|
| Warmup | 1-3 | ×1.0 | 18 (10% land) | 0% |
| Building | 4-6 | ×1.4 | 17 (15% land) | 3% |
| Tense | 7-9 | ×2.0 | 15 (25% land) | 6% |
| Boss-tier | 10-15 | ×3.0 | 14 (30% land) | 8% |
| Brutal | 16-25 | ×4.5 | 12 (40% land) | 12% |
| Nightmare | 26-40 | ×6.5 | 10 (50% land) | 18% |
| Endgame | 41+ | ×9.0 | 8 (65% land) | 22% |
Each row is a wall. Rounds 1-3 are a warmup so newbies can ease in. Round 4 is the first wall — monster HP nearly doubles. Round 7 is the second wall — monsters now have a 25% land rate against your defense regardless of how high you stacked it, and they have a 6% chance to crit you for double damage. Round 10 is the boss-tier wall — almost no one will clear it on their first run.
For your L26 Warrior with a Rare+4 weapon, the Gauntlet now looks like this in real numbers:
| Round | Enemy HP | Enemy hit% | Enemy crit% | Realistic swings to kill |
|---|---|---|---|---|
| 1 | 127 | 10% | 0% | 3 |
| 5 | 207 | 15% | 3% | 5 |
| 7 | 322 | 25% | 6% | 8 |
| 10 | 707 | 30% | 8% | 17 |
The cumulative damage in a single Gauntlet attempt — even when you cash out at round 7 — now eats 30-50% of your HP. Pushing to round 10 means real risk of KO.
💥 Enemy Critical Hits
Late-round enemies can now crit you for double damage with a new message variant:
CRITICAL! The Heap Corruptor crushes your guard for 88 damage! HP: 60/190
CRITICAL! The Page Fault Phantom lands a devastating blow! 76 damage! HP: 14/180
CRITICAL! The Race Condition Reaper eviscerates you for 92 damage! HP: 0/152
Crit chance is 0% in the warmup waves so the early game is unchanged, then ramps up: 3% in wave 2, 6% in wave 3, 8% in boss-tier (wave 4), all the way to 22% in endgame waves. A boss-tier 30-swing fight at 8% crit chance gives you roughly 2.4 expected crit damage events — enough to randomly swing a marginal fight into a KO.
The crit message is distinct (red CRITICAL! prefix, different action verbs) so you can tell when it happens.
🎯 Accuracy Cap Ramps Per Wave
In v1.23 the enemy hit threshold was capped at a fixed 18 across all rounds, giving a permanent 10-15% land rate regardless of how deep you pushed. That's why round 25 of Abyssal felt the same as round 5 — the enemy hit rate was identical.
In v1.24 the cap is per-wave. Warmup cap is 18 (10% land); by Wave 4 (round 10) it's 14 (30%); by Wave 6 (round 26) it's 10 (50%). A defense-stacked tank still benefits from gear, but the cap eats into them progressively as they push deeper into a tier.
🧪 The Black-Mirror Balance Simulator (dev-tool)
The fixes in this release weren't tuned by feel. They came out of a Python harness at dev-tools/balance-sim/ that runs hundreds of compressed shellquest playthroughs in parallel and persists every metric to SQLite. Think of it as the Hang the DJ episode for shellquest: a thousand simulated lives across class × race × strategy combinations, each one driving a real sq binary through tick → equip → enchant → arena cycles, recording every action, every drop, every arena attempt for later analysis.
The simulator validated this release end-to-end. For the v1.24-soft tuning we shipped:
- Pit (tier 1) — 13/13 Wizards survived, 6/6 Rogues, all clearing 4 rounds. The entry tier still works for newbies. ✓
- Gauntlet (tier 2) — most attempts end in KO around round 7-9 (~0-30% survival rate). Pushing to round 10 is a real flex. ✓
- Enemy crits fire on schedule — 0.14-0.50 crits per arena attempt depending on tier depth. ✓
- No class is broken — all 5 classes reach L40 in 1300-2100 ticks, similar progression curves. ✓
The harness lives outside the shellquest binary — it's a Python tool that calls sq as a subprocess, parses save.json directly, and aggregates 24-run sweeps in ~4 minutes wall clock with --parallel 4. Future balance changes get tested with empirical data instead of intuition.
Usage:
cd dev-tools/balance-sim
# Run a small validation sweep
python3 runner.py --runs 3 --classes Warrior Wizard --strategies greedy balanced \
--target-level 40 --max-ticks 2500 --parallel 4 \
--tuning-label my-test
# Generate the report
python3 report.py --tuning-label my-test --output reports/my-test.mdSee dev-tools/balance-sim/README.md for the full reference.
🧪 Tests
Seven new arena tuning tests pin the wave-based behaviour:
enemy_hp_at_round_ten_is_meaningfully_harder_than_round_one— locks in ≥4× HP scaling at round 10.enemy_hp_curve_is_monotonic_non_decreasing_through_round_fifty— guards against accidental wave-table inversions.enemy_hit_cap_grows_stricter_in_later_waves— locks in the per-wave cap ramp.high_defense_player_takes_more_hits_in_late_rounds_than_early— verifies the cap actually changes hit rate end-to-end.enemy_does_not_crit_in_warmup_waves— exhaustive check that rounds 1-3 are crit-free.enemy_can_crit_in_late_waves_but_not_always— confirms the crit roll is probabilistic, not deterministic.enemy_crit_message_contains_critical_marker_and_doubled_damage— locks in the newtournament_enemy_critmessage contract.
Total: 361 passing, up from 354.
🩹 Migration & Behaviour
- No save format changes. Existing characters continue working without touching disk state.
- Arena combat will feel dramatically different on the next
sq arenarun, especially past round 4. - Newbie Pit experience preserved (rounds 1-3 unchanged from v1.23).
- The
enemy_hit_threshold_capfield inArenaCombatTuningand the no-roundcompute_enemy_hitwrapper were removed (the wave function now owns these). External consumers (none in-tree) would need to usecompute_enemy_hit_at_round(..., round)andarena_wave(round).hit_threshold_cap. - All combat surfaces (arena + boss + overworld tick) still share the crit formula change from v1.23.
v1.23.0
v1.23.0 — Combat Clarity & Balance
A two-part themed release: first, you can finally see what your gear actually does to your numbers. Second, the arena combat that was supposed to challenge you at level 25 (and 60, and 100, and 150) actually does. Both halves of this release came out of the same empirical session: an arena diagnostic the user requested after noticing they were one-shotting monsters that should have been threats.
The fixes shipped here are not guesses. The arena combat math was modeled against compute_player_hit / compute_enemy_hit, the crit threshold formula, and the enemy HP scaling formula directly from src/arena.rs. Predictions were then validated empirically against a 6-character progression simulation harness that drove sq arena end-to-end at character levels L3, L15, L25, L60, L100, and L150 with class-appropriate gear from the real loot tables. The data confirmed the diagnosis. The fixes confirmed the data.
👁️ Now You Can See What Your Gear Does
Status used to show STR: 41 DEX: 33 INT: 31 and your gear, but it never showed how those translated into the numbers that actually drive combat. You had to reverse-engineer attack_power = STR + DEX/2 + weapon.power + weapon.enchant_level mentally to know whether the new Rare ring you just picked up was an upgrade. No more.
│ STR: 28 DEX: 26 INT: 24
│ ATK: 39 (+16) DEF: 8 (+22)
│ Gold: 749
│
│ Weapon: Scythe of Segfault (+12) [Rare] [Enchanted +4]
│ Armor: Breastplate of Bcrypt (+12) [Rare] [Enchanted +4]
│ Ring: Kernel-Key (+9) [Rare]
│
│ Gear bonuses:
│ Attack: +16 (weapon 12 +4 enchant)
│ Defense: +22 (armor 12 +4 enchant, ring 9)
ATK shows your base attack power from STR/DEX, with the gear contribution in parentheses. Same for DEF. The Gear bonuses block breaks down the +16 attack into "weapon 12 +4 enchant" so you know exactly which slot is pulling its weight when you compare items.
This is purely display. No mechanics changed. But the comparison "is this +1 power Rare a better weapon than my +3 enchanted Uncommon?" is now answered by looking at one line.
🔍 sq identify Now Shows What Each Stat Affects
When you sq identify an item, the panel now includes an Affects: line spelling out which derived stats that gear contributes to. A weapon affects ATK. Armor and rings affect DEF. Enchant levels stack into the same totals. The wording is short and consistent so quick comparisons stay quick.
⚔️ The Arena Got Real
This is the headline change. Three compounding bugs in the arena combat math had turned every tier past L15 into a victory lap. Empirical data from the 6-character simulation made it impossible to keep ignoring:
| Character | Lvl | DEF | Old enemy hit% | New enemy hit% | Old kill time | New kill time | Old dmg taken | New dmg taken |
|---|---|---|---|---|---|---|---|---|
| Newbie | 3 | 8 | 60% | 50% | 3.5 swings | 5.0 swings | 56% HP | 56% HP |
| Apprentice | 15 | 14 | 0% | 18% | 2.0 | 3.75 | 0% HP | 54% HP |
| Adept | 25 | 24 | 7% | 7% (capped) | 2.6 | 4.33 | 27% HP | 49% HP |
| Veteran | 60 | 43 | 7% | 3-15% | 2.1 | 3.36 | 25% HP | 17% HP |
| Champion | 100 | 67 | 3% | 11% | 2.5 | 3.75 | 19% HP | 186% HP ¹ |
| Godslayer | 150 P3 | 102 | 3% | 10% | 2.6 | 4.05 | 61% HP | 305% HP ¹ |
¹ Cumulative damage taken exceeds max HP because recovery between rounds is now meaningful — you take ~40 damage per round but recover ~55, slowly bleeding red into the late tier instead of cruising at full HP. Champion and Godslayer's Court are no longer victory laps; they're attrition wars.
The fixes themselves are surgical:
Fix A — enemy_hit_threshold is capped at 18
The old formula 8 + DEF/2 + DEX/5 had no upper bound. Once your defense climbed past ~24 (somewhere around L25-L30 with average gear), the threshold rose above 20, and the only way an enemy could land a hit was rolling a natural 20 — a fixed 5% rate that never moved no matter how strong they got. The new enemy_hit_threshold_cap = 18 field in ArenaCombatTuning clamps the threshold so monsters always have at least a ~10-15% landing rate. Newbie-tier defense values stay below the cap so the early game is unchanged.
Fix B — Crit threshold ramps with INT instead of saturating
The old (20 - INT/4).max(15) formula saturated at INT 20, meaning every character past the lowest stats had a flat 30% crit rate and additional INT was wasted. The new (20 - INT/8).max(13) ramps slower (low-INT characters get less crit benefit, restoring the "scrappy" feel of new chars) and caps later at 13 (so high-INT mages still claw out to 35% crit, but the cap actually feels earned). This change applies in three places: arena combat, boss combat, and overworld tick combat — wherever the player rolls a crit.
Fix C — Enemy HP scales harder
The old enemy_hp = 20 + 10×round + max_hp/4 + 20×prestige formula scaled slower than player damage growth, so kill time stayed at 2-3 swings across L15 → L150. The new constants enemy_hp_per_round: 20, enemy_hp_max_hp_divisor: 2, enemy_hp_per_prestige: 50 lift kill time to a healthy 3-5 swings in early-to-mid game, and 5-9 swings in deep Godslayer's Court rounds where the per-round addend really stacks up.
The three fixes compound: enemies actually land hits sometimes (Fix A), your crit advantage is no longer guaranteed (Fix B), and the fight lasts long enough for both to matter (Fix C). The arena returns to being a challenge.
🧪 Internals & Testing
Five new arena tuning tests in src/arena.rs:
enemy_hit_threshold_caps_at_eighteen_for_high_defense— locks in Fix A.enemy_hit_threshold_cap_preserves_low_defense_dynamic_range— confirms Newbie-tier defense values stay below the cap so the early game doesn't shift under Fix A.crit_threshold_ramps_with_intelligence_and_caps_at_thirteen— locks in Fix B's ramp shape across the INT range.enemy_hp_scales_with_new_tuning_constants— guards the Fix C constants against accidental rollback.
Plus implicit coverage of the gear-bonus display via the existing attack_power_* and defense_* tests in src/character.rs. Total: 354 passing, up from 350.
The 6-character simulation harness lives at /tmp/sq-sim/sim.py in this session. It's not committed to the repo — diagnostic tooling, not production code — but the methodology and per-character output are reproducible: manufacture a save at the target progression milestone with realistic gear from the loot tables, drive sq arena via tmux with SQ_NO_PACING=1, capture and parse combat output, aggregate per-tier hit rate / crit rate / kill time / damage taken. If a future rebalance pass is needed, the harness rebuilds in minutes.
🩹 Migration & Behaviour
- No save format changes. Existing characters continue working without touching disk state.
- Arena combat will feel noticeably different on the next
sq arenarun. Expect to actually take damage. Expect monsters to live past the second swing. - Newbie experience (L1-L5 in The Pit) is preserved: empirically, Pit round 1 still ends at ~50% HP. The fixes only kick in once your DEF/INT have grown past the early-game thresholds.
- All commits since v1.22.0 are bundled here:
523ae90(gear bonuses display),ffbe51a(ATK/DEF stat line), the arena rebalance, plus44c95b3(release cadence convention in AGENTS.md that triggered batching this work into one themed release instead of seven small ones).
v1.22.0
v1.22.0 — The Bestiary Comes Alive
Forty monsters. One hundred sixty items. All the AI-generated placeholders from before v1.21 are replaced with curated content. The infrastructure has been ready since v1.21.0 (zone-aware spawning) and v1.21.1 (HP-pool multi-turn combat); v1.22 is the content drop those releases were waiting for.
🐛 The Bestiary, Filled Out
The Horror and Boss-adjacent tiers were placeholders in v1.21 (high-danger zones gracefully fell back to Hunter tier). They're real now. 40 monsters across 5 tiers, 8 per tier, drafted by a writing-category agent and tuned to the HP/ATK/XP ranges from docs/balance-analysis.md §6.1.
Tier 1 — Vermin (zones 1-2)
Small filesystem creatures, single-hit kills, low XP. The intro tier.
Symlink Slug · Dotfile Dust-Mite · Cache Cricket · Tempfile Tadpole
Log-File Larva · Socket Snail · Inode Inchworm · Permission Pupa
Tier 2 — Bruiser (zones 2-3)
Process spirits, fight-back energy, 2-3 hit kills.
Zombie Process Zealot · Background Job Brawler · Daemon Duelist · Shell-Script Shaman
Pipe-Line Pugilist · Env-Var Vandal · Cron-Job Crusader · Std-Error Specter
Tier 3 — Hunter (zones 3-4)
Memory dwellers with predator aesthetic, 3-5 hit kills.
Heap-Alloc Hound · Stack-Trace Stalker · Pointer Panther · Overflow Basilisk
Garbage-Collector Gryphon · Memory-Leak Manticore · Segfault Shark · Dangling-Pointer Dingo
Tier 4 — Horror (zones 4-5)
Kernel terrors, serious threats, 5-8 hit kills.
Kernel-Panic Kraken · Interrupt-Handler Hydra · System-Call Siren · Page-Fault Phantom
Deadlock Dragon · Race-Condition Reaper · Root-Kit Revenant · Scheduler-Shadow Stalker
Tier 5 — Boss-adjacent (zones 5+)
Architectural entities, the kind you flee. 10+ hit kills.
The Monolith Manifestation · Architectural-Debt Archon · The Legacy-Code Lich
Distributed-System Djinn · The Microservice Medusa · Cloud-Native Chimera
The Immutable-State Idol · Entropy-Engine Elemental
The 5 named bosses (Kernel Panic, Lord of /dev/null, SIGKILL Supreme, The Infinite Loop, The Memory Corruption) are unchanged — they're a separate roster from this bestiary and continue to spawn at their existing 1/500 rate via boss::maybe_spawn.
⚔️ Walking Into node_modules Now Means Something
Before v1.22, the danger-5 zone (node_modules) fell back to Hunter tier because Horror and Boss-adjacent slots were empty. Now those slots are populated.
$ cd ~/projects/myrepo/node_modules
$ rm -rf some_orphan_thing
🪤 A Deadlock Dragon ascends from the depths! (HP 150, ATK 22)
...
⚔️ The Deadlock Dragon falls! Glorious victory! +130 XP
A level-100 character will need 3-5 turns to kill a Boss-adjacent. A level-30 character will probably die. Don't cd node_modules casually anymore.
💎 Loot Table, Reframed
160 items across the 5 rarities × 4 slots × 8 items grid. Naming intensity scales with rarity:
Common is mundane: Rusty Crowbar, Cardboard Box, Soda Tab, Lukewarm Water. The bottom rung; you'll sell most of these via sq sell junk.
Uncommon is decent: Polished Wrench, Reinforced Vest, Silver Band, Fresh Espresso. Real tools. Real protein.
Rare is the named tools tier — single-word evocative concepts: Bit-Blitzer, Logic-Lash, Kernel-Cloak, Code-Cordial.
Epic is darker fantasy — multi-word with attitude: Void-Pointer Voulge, Mantle of the Mainframe, Sigil of the Sysadmin, Essence of the Epoch.
Legendary is the "The X-of-Y" construction — the kind of name that makes you sit up when it drops: The Final Commit of Fate, The Aegis of the Absolute Path, The Loop of the Lost Link, The Tear of the Tired Tech-Lead.
$ cd ~ && sq shop
🏪 The Terminal Bazaar
──────────────────────────────────────────────────
1. Dull Letter Opener (+1 Weapon) [Common] — 10 gold
2. Cardboard Box (+2 Armor) [Common] — 15 gold
3. Stained Apron (+3 Armor) [Common] — 20 gold
4. Soda Tab (+4 Ring) [Common] — 25 gold
5. Half-Empty Juice Box (+3 Potion) [Common] — 20 gold
Prices reflect the v1.19 multiplier ladder (5/8/13/22/35). The flagship "max-enchant a Legendary for 30,000 gold" math from v1.18 still holds — your Legendary just has a way better name now.
⚙️ How This Shipped Without Disruption
This is the content release. The mechanics were already in place:
- v1.21.0 added the
MonsterTierenum + zone gating - v1.21.1 added HP pools + multi-turn combat
- v1.22.0 swaps the actual content
Each release was small enough to ship cleanly on its own. The whole bestiary overhaul took four releases, each with its own user-facing release notes.
The writing-category agent that drafted the 40 monsters + 160 items did so as a parallel team member during v1.21 development. Designer reviewed and approved the proposal at docs/v1.22-bestiary-naming-proposal.md. v1.22 is a string-substitution layer on top of v1.21's infrastructure — zero schema changes, zero combat-math changes.
⚙ Compatibility
Saves are unchanged. No schema migration. No CLI changes.
If your save references old monsters by name (it doesn't — names are runtime strings, not save data) or old items by name (it does — every item in your inventory is a name-tagged record), your old items remain in your inventory. The Pike of Ping you found before v1.22 is still a Pike of Ping. New drops will come from the new pool.
A future sq sell legacy command (not in this release) could let you sell off pre-v1.22 items in bulk. For now, your old items mingle with new ones until you sell or replace them.
Full Changelog: v1.21.1...v1.22.0
v1.21.1
v1.21.1 — Combat Has Depth Now
v1.21.0 added the tier system but combat still resolved in a single d20 check. A Hunter died from one hit; an Imp died from one hit; the only thing that varied was which monster the zone picked. The mechanics matched the tier nomenclature only by accident.
v1.21.1 closes that gap. Every monster now has an HP pool. Combat is a multi-turn loop within a single tick — player swings, monster swings back if still alive, repeat until someone hits zero. The boss-combat depth (crits, class signatures, signature bonuses) now applies to every fight, not just the 0.1% boss encounters.
⚔️ Multi-Turn Combat in One Tick
Each sq tick that triggers a fight now runs the full battle internally:
turn 1: player swings → 12 dmg → Hunter 26/38 HP
turn 1: Hunter swings → 5 dmg → player 145/150 HP
turn 2: player swings → CRIT → 24 dmg → Hunter 2/38 HP ← Wizard arcane burn applied
turn 3: player swings → 11 dmg → Hunter dies
You don't see this turn-by-turn output — combat reports a single summary line, same as before. But the outcome is different: damage accumulates, kills can take 2-5 swings, and tougher monsters live up to their tier.
Vermin (HP 1-3) still die in one player swing for any character that can land a hit. The depth shows up against Bruiser+ tiers.
🩸 You'll Take More Damage Per Fight
This is the visible change. In single-roll combat, you either won cleanly or won-with-damage in one outcome. Now multi-turn combat means monsters get multiple swings if they don't die instantly.
old: Wizard fights Hunter → 1 roll → maybe 8 dmg → done
new: Wizard fights Hunter → 3 turns → 8+11+0 = 19 dmg → done
For your character this means:
- Vermin tier: no change (one-shot kills, no damage)
- Bruiser tier: ~1-2 turns of damage exchange at mid-game
- Hunter tier: ~2-4 turns; cumulative damage of 10-30 is normal
- Horror / Boss-adjacent: empty in v1.21, populating in v1.22
You'll see the combat_tough message variant (which already existed for the old TOUGH outcome) much more often. That's expected — it's now used for "won but took N damage in the fight" instead of just "won the single-roll TOUGH branch".
If this feels too punishing, the healer (1 HP / 30s from $HOME) is unchanged and absorbs the new damage rate gracefully.
✨ Class Signatures Fire Per-Turn
Wizard's arcane burn, Warrior's battle frenzy, Ranger's mark prey, Rogue's shadow strike — all of these previously fired at most once per combat (one roll = one trigger check). They now fire on every player attack within the multi-turn loop.
For multi-turn fights this is a real damage boost:
- Wizard: arcane burn applies on every swing — sustained DPS goes up against high-HP monsters
- Warrior: battle frenzy triggers per-turn once HP drops below 60% (the v1.19 threshold change), so in a long fight you can chain it
- Necromancer: soul-drain heals you on every kill, so multi-encounter sessions in
/tmpactually sustain you - Rogue: shadow strike still saves the nat-1 fumble per swing — more swings, more saves
This means class identity matters more than it did. A Wizard with INT 30 against a Hunter is now noticeably better than a Wizard with INT 10.
🎯 Crits Land Per-Turn Now Too
The crit formula (crit_threshold = max(15, 20 - INT/4), x2 damage) used to apply once per fight. Now per swing. INT-built classes get more reliable burst windows in multi-turn fights.
🧹 Code Health
combat() and combat_elite() were near-duplicate single-roll resolvers. Merged into a single combat() that takes an is_elite: bool parameter and dispatches to the right message variant internally. Net effect: 160 lines collapse to one HP-pool loop. Future combat-resolution changes touch one function, not two.
The encounter pipeline (random_monster_for_zone → EncounterProfile → apply_elite_pressure → combat) now threads HP through end-to-end. Elite monsters get 1.5× HP in addition to their existing 1.6× ATK + 2× XP — they live longer to justify their existing reward bump.
⚙ Compatibility
Saves are unchanged. No schema migration. No CLI changes. The change is purely in combat resolution math. Old saves load identically; the new fights apply going forward.
This is genuinely a difficulty change. Mid-game players will see noticeably more damage per fight at first. It evens out as you level (your max HP scales faster than monster damage does), but the first few sessions after upgrade may feel like the game pushes back harder. That's the point — combat used to be a single coin flip; now it's a sustained exchange.
Full Changelog: v1.21.0...v1.21.1
v1.21.0
v1.21.0 — Monsters Fit Their Environment Now
Before this release, your $HOME directory and node_modules rolled from the same uniform-random monster pool. A fresh level-1 character could greet the day by walking into node_modules and meeting an Enraged Buffer Overflow Beast on tick 2. That's no longer how spawning works.
Each zone now spawns from a tier-appropriate monster pool. Low-danger zones see weak monsters; deep zones see strong ones. The pool you fight reflects where you are.
🗺️ The Tier System
Five tiers slot into the danger ladder:
| Tier | Vibe | Zones |
|---|---|---|
| Vermin | Small filesystem creatures | danger 1 ($HOME) and danger 2 (src/, /etc, target/, test) |
| Bruiser | Process spirits | danger 2 and danger 3 (/tmp, /var, .git) |
| Hunter | Memory dwellers | danger 3 and danger 4 (/dev) |
| Horror (coming v1.22) | Kernel terrors | danger 4 and danger 5 (node_modules) |
| Boss-adjacent (coming v1.22) | Architectural entities | danger 5+ |
The 10 monsters that already exist in the game are now slotted into Vermin (Syntax Error Snake), Bruiser (5 monsters), and Hunter (4 monsters) based on their attack range. Horror and Boss-adjacent tiers ship empty in v1.21 — they'll fill out in v1.22 when the full 40-monster bestiary lands (8 per tier, themed naming pass already drafted, pending designer review at docs/v1.22-bestiary-naming-proposal.md).
For now, danger-5 zones (node_modules) gracefully fall back to the Hunter tier, so the high-danger experience is preserved — you'll still meet a Buffer Overflow Beast in the abyss.
🏠 What This Fixes Right Now
The first-tick deadliness problem flagged in docs/balance-analysis.md A1 is solved. A fresh L1 Wizard-Goblin in $HOME can no longer encounter a 36-ATK Enraged Buffer Overflow Beast on tick 2. They'll meet a Syntax Error Snake — atk 4, tier-appropriate, beatable.
$ cd ~ && sq tick --cmd "rm foo" --cwd ~ --exit-code 0
🪤 Snake hisses from the shadows!
⚔️ You vanquish the Syntax Error Snake for 6 XP.
vs. the previous-world possibility:
$ cd ~ && sq tick --cmd "rm foo" --cwd ~ --exit-code 0
🪤 Enraged Buffer Overflow Beast attacks! (atk 36)
💀 You died...
That second screenshot no longer exists at level 1 in your home directory.
⚙️ What's Under the Hood
Three pieces shipped:
MonsterTierenum (events.rs) with five variants. Public so tests + future v1.22 content can use it.MONSTER_POOLstatic array with tier metadata on every entry. Replaces the prior inlinerandom_monster()tuple list.tiers_for_danger(danger_level) -> &[MonsterTier]— the pure function that decides which tiers a zone can spawn. Zone struct stays untouched (no circular import with events.rs).
8 new tests cover the tier filtering invariants. Total test count 333 → 341.
Combat resolution is unchanged in v1.21. Monsters still die in a single hit, the d20 hit/dodge check works exactly the same. The HP-pool refactor that turns combat into multi-turn fights is the next session's work and ships as v1.21.1.
⚙ Compatibility
Saves are unchanged. No schema migration. No CLI changes. The tier system is purely server-side — the monster you fight just has a different name distribution depending on which zone you're standing in.
Full Changelog: v1.20.1...v1.21.0
v1.20.1
v1.20.1 — Aliases Stop Hiding from --help
Three small post-v1.20 carryovers, none of which deserve a minor bump on their own. Bundled.
🆔 Existing Aliases Now Visible
When sq identify shipped in v1.20 with the id alias, it surfaced in sq --help as [aliases: id]. The older aliases — stat for status, inv for inventory, wear for equip, unequip for remove — were hidden (functional but undocumented in --help). New players couldn't discover them without hunting through sq help.
All five aliases are now visible in sq --help:
status View your character sheet (includes inventory) [aliases: stat]
inventory Check your inventory [aliases: inv]
equip Equip armor or ring from inventory [aliases: wear]
remove Remove equipped weapon, armor, or ring (puts it back in inventory) [aliases: unequip]
identify Show full stats and modifiers for an inventory or equipped item [aliases: id]
No new aliases were added. No existing aliases were changed. They just announce themselves now.
🧪 Help Suggestion Tests Now Property-Based
Internal cleanup. The fixtures that asserted the exact ordered list of typo suggestions (e.g. ["identify", "reset", "remove"] for re) were brittle by design — every new short alias risked churn. Refactored to assert the invariant directly: suggestions must be sorted ascending by edit distance, with alphabetical tiebreak when distances match. Same correctness guarantee, no fixture churn when topics shift.
No user-visible behavior change.
📜 Documentation
- The cargo build cache quirk that bit two manual QA cycles (
cargo build --bin sqreports "Fresh" while the on-disk binary is stale) is now documented inAGENTS.mdwith the workaround (cargo clean -p shellquest). - The post-v1.18
sq identifywork surfaced that max-enchanted Legendary items have a sell-enchant-bonus that exceeds their base sell value (the proportion of recovery climbs from 0% at +0 to 67% at +5). Documented indocs/balance-analysis.mdas a v1.22+ observation. Net economy is unchanged — enchant-then-sell is still ~10% recovery on investment.
⚙ Compatibility
Saves are unchanged. No CLI behavior changes (the aliases worked before; they just weren't advertised). No migration. Drop-in upgrade.
Full Changelog: v1.20.0...v1.20.1