@g3nomaly/g3n-mods 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +7 -1
  2. package/README.md +12 -5
  3. package/mods/agent-adventure/.claude-plugin/plugin.json +23 -0
  4. package/mods/agent-adventure/hooks/content.ts +263 -0
  5. package/mods/agent-adventure/hooks/engine.ts +1125 -0
  6. package/mods/agent-adventure/hooks/hooks.json +1 -0
  7. package/mods/agent-adventure/hooks/register.tsx +465 -0
  8. package/mods/agent-adventure/hooks/sprite.ts +424 -0
  9. package/mods/agent-adventure/types/index.d.ts +248 -0
  10. package/mods/agent-adventure/view/panes.json +8 -0
  11. package/mods/agent-adventure/view/view.css +230 -0
  12. package/mods/agent-adventure/view/view.ts +308 -0
  13. package/mods/agents-panel/.claude-plugin/plugin.json +9 -1
  14. package/mods/agents-panel/hooks/register.tsx +18 -4
  15. package/mods/agents-panel/view/panes.json +8 -0
  16. package/mods/context-bar/.claude-plugin/plugin.json +9 -1
  17. package/mods/context-bar/hooks/register.tsx +4 -3
  18. package/mods/context-bar/view/panes.json +8 -0
  19. package/mods/custom-crab-agents/hooks/register.tsx +15 -1
  20. package/mods/delete-guard/.claude-plugin/plugin.json +9 -1
  21. package/mods/delete-guard/hooks/register.tsx +15 -5
  22. package/mods/delete-guard/view/panes.json +8 -0
  23. package/mods/focus-mode/.claude-plugin/plugin.json +6 -0
  24. package/mods/focus-mode/hooks/register.tsx +7 -4
  25. package/mods/focus-mode/view/panes.json +8 -0
  26. package/mods/mini-game/.claude-plugin/plugin.json +9 -1
  27. package/mods/mini-game/hooks/register.tsx +21 -7
  28. package/mods/mini-game/view/panes.json +8 -0
  29. package/mods/tool-timeline/.claude-plugin/plugin.json +9 -1
  30. package/mods/tool-timeline/hooks/register.tsx +18 -4
  31. package/mods/tool-timeline/view/panes.json +8 -0
  32. package/mods/you-should-know/.claude-plugin/plugin.json +3 -3
  33. package/mods/you-should-know/hooks/register.tsx +173 -45
  34. package/mods/you-should-know/types/index.d.ts +13 -4
  35. package/package.json +2 -1
@@ -4,6 +4,12 @@
4
4
  "name": "g3ntrovert"
5
5
  },
6
6
  "plugins": [
7
+ {
8
+ "name": "agent-adventure",
9
+ "source": "./mods/agent-adventure",
10
+ "description": "An idle roguelike in a pane: each running agent fights as a hero, and the party rests in the tavern when none run.",
11
+ "version": "0.1.0"
12
+ },
7
13
  {
8
14
  "name": "agents-panel",
9
15
  "source": "./mods/agents-panel",
@@ -61,7 +67,7 @@
61
67
  {
62
68
  "name": "you-should-know",
63
69
  "source": "./mods/you-should-know",
64
- "description": "A card under long replies that flags their key point: a headline and one supporting line.",
70
+ "description": "A compact card under a reply: the questions it asks you word for word, and for long replies what changed and what it concluded, each line tagged.",
65
71
  "version": "0.1.0"
66
72
  }
67
73
  ]
package/README.md CHANGED
@@ -20,6 +20,7 @@ Then open the project in VS Code and trust the workspace. New sessions load the
20
20
 
21
21
  | Mod | What it does | Command |
22
22
  |---|---|---|
23
+ | `agent-adventure` | An idle roguelike to watch while agents work. Each running agent fights as a hero: the main thread and every subagent. With none running, the heroes rest in the tavern. A run is a dungeon of fights, two forks (a safe trail or a dangerous one with double XP and a relic), a camp and a boss. Heroes keep their level, skills and trophies between runs. A party wipe costs every hero 5 levels. Every 10 levels a hero picks one of two skills, up to three. Haiku names new heroes and makes the picks. Agents can name their own hero with the `hero` tool, which is cosmetic only. The game never changes what agents do | `/agent-adventure` |
23
24
  | `agents-panel` | The agents in `.claude/agents` with their model and a run button. A running agent shows a spinner, and a toast says when it starts and finishes | `/agents-panel` |
24
25
  | `context-bar` | One bar of context-window use split by source, with the total and where compaction starts | `/context-bar` |
25
26
  | `custom-crab-agents` | Adds each subagent to the agents panel as a crab drawn as Claude's mascot, walking while it works and blinking when done: its role (from its name or type) picks its hat and what it holds, such as a reviewer's deerstalker and magnifying glass, a tester's work helmet and wrench, a designer's top hat and brush. Shows model, effort, steps, context, tokens, cost and time. Click anywhere on a crab to open its whole conversation in a tab and message it there. Needs `agents-panel`, which installing it adds | `/agents-panel` |
@@ -29,15 +30,19 @@ Then open the project in VS Code and trust the workspace. New sessions load the
29
30
  | `hide-personal-data` | Masks names, emails, phone numbers, IBANs and card numbers in replies. Hover a mask to read it | `/hide-personal-data on \| off` |
30
31
  | `mini-game` | Minesweeper, or whack-a-mole when you want zero thinking: hit the mole before it hides, three misses and the round ends, and it speeds up as you score. Both keep a high score. Switch games with the tabs in its pane | `/mini-game [mines \| mole]` |
31
32
  | `tool-timeline` | The last ten tool calls, one line each, with their durations, in a pane that keeps one height. Click a call to see it in full in the box under them. A denied call is struck through, next to the call that replaced it | `/tool-timeline` |
32
- | `you-should-know` | A card under long replies with their key point, a headline and one supporting line. It costs one small Haiku call per reply of 1500 characters or more | none |
33
+ | `you-should-know` | A compact card under a reply, one tagged line per point. **QUESTION** lines quote the questions the reply asks you, word for word. **CHANGE**, **CONCLUSION** and **FACT** lines come from one small Haiku call that reads your request beside the reply: what was added, removed or changed, the direct answer to what you asked, and costs or decisions only, never caveats. It runs only for replies of 800 characters or more | none |
33
34
 
34
- For `agents-panel`, `context-bar` and `tool-timeline`, the command opens the pane and running it again closes it.
35
+ Every mod with a pane shows it in the mods viewer. Only `context-bar` also opens it in the Claude Code chat when a session starts. Each pane mod has a `showInClaudeCode` option, **Show in Claude Code** in `/config`, that turns this on or off. A mod's command opens its pane in the chat whatever the option says, and for `agents-panel`, `context-bar` and `tool-timeline` running it again closes it there. A `delete-guard` card waiting on an answer also opens in the chat, and leaves once answered.
35
36
 
36
- Three mods take settings, declared under `userConfig` in their `.claude-plugin/plugin.json`:
37
+ Mods take settings, declared under `userConfig` in their `.claude-plugin/plugin.json`:
38
+
39
+ - `agent-adventure`, `agents-panel`, `context-bar`, `delete-guard`, `focus-mode`, `mini-game` and `tool-timeline`: `showInClaudeCode`, whether the pane opens in the Claude Code chat at session start (on for `context-bar` only).
40
+
41
+ - `agent-adventure`: `model`, the model that names heroes and picks skills and routes (`haiku`); `off` uses the game's own picks.
37
42
 
38
43
  - `focus-mode`: `minutes`, the block length before you pick one (25).
39
44
  - `hide-personal-data`: `names`, extra names to always mask, separated by commas.
40
- - `you-should-know`: `minChars`, the shortest reply that gets a card (1500), and `model`, the model that picks the key point (`haiku`).
45
+ - `you-should-know`: `minChars`, the shortest reply Haiku reads for changes, conclusions and facts (800); questions get a card at any length, and `model`, the model that picks the key point (`haiku`).
41
46
 
42
47
  ## Requirements
43
48
 
@@ -57,6 +62,7 @@ mods/<name>/
57
62
  ├── hooks/register.tsx # exports `register`
58
63
  ├── types/index.d.ts
59
64
  └── view/ # optional: how the VS Code viewer draws it
65
+ ├── panes.json # { "panes": [{ "id", "title" }] }: the panes the viewer shows
60
66
  ├── view.ts # exports `mount(root, api)`
61
67
  └── view.css
62
68
  ```
@@ -65,7 +71,7 @@ mods/<name>/
65
71
 
66
72
  On every surface but the VS Code viewer, a mod draws with the engine's elements (`Box`, `Text`, `Button`, `Svg`). In the viewer it can draw with its own HTML and CSS instead, as `tool-timeline`, `agents-panel` and `custom-crab-agents` do:
67
73
 
68
- 1. **The hook hands over data.** On `e.surface === 'vscode'`, the render hook returns a view box: a `Box` keyed `g3n-view:<mod>` holding the data as JSON in one `Text`, and a hidden `Input` keyed `g3n-act:<mod>` that receives the view's actions.
74
+ 1. **The hook hands over data.** When the viewer draws the pane, the render hook returns a view box: a `Box` keyed `g3n-view:<mod>` holding the data as JSON in one `Text`, and a hidden `Input` keyed `g3n-act:<mod>` that receives the view's actions. The viewer's relay sets the prop `g3nViewer: true` on every pane the viewer draws. The Claude Code extension draws on the `vscode` surface too but never sets it, so check both: `e.surface === 'vscode'` and `e.props.g3nViewer === true`.
69
75
  2. **The view draws it.** `view/view.ts` exports `mount(root, api)`. `root` is a shadow root of the mod's own, so `view.css` applies only there and the editor's theme variables still reach it. `mount` returns `{ update(data) }`, which gets the data each time the pane is drawn. Keep elements across updates, and animations and anything typed in the view keep going.
70
76
  3. **Actions go back to the hook.** `api.act(name, arg)` arrives at the hidden Input's `onSubmit` as JSON of `{ name, arg }`.
71
77
 
@@ -80,6 +86,7 @@ Run `bun install` at the repository root first, then these from `packages/g3n-mo
80
86
  ```sh
81
87
  bun run build # rewrites .claude-plugin/marketplace.json from each mod's plugin.json
82
88
  bun run typecheck
89
+ bun run test # the tests in tests/, such as agent-adventure's game rules
83
90
  bun run validate # runs `claude plugin validate` on every mod
84
91
  bun run types # copies the mod API types into types/claude-code.d.ts
85
92
  ```
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "agent-adventure",
3
+ "version": "0.1.0",
4
+ "description": "An idle roguelike in a pane: each running agent fights as a hero, and the party rests in the tavern when none run.",
5
+ "author": {
6
+ "name": "g3ntrovert"
7
+ },
8
+ "types": "./types/index.d.ts",
9
+ "userConfig": {
10
+ "showInClaudeCode": {
11
+ "type": "boolean",
12
+ "title": "Show in Claude Code",
13
+ "default": false,
14
+ "description": "Open this mod's pane in the Claude Code chat when a session starts. The mods viewer shows it either way."
15
+ },
16
+ "model": {
17
+ "type": "string",
18
+ "title": "Model",
19
+ "default": "haiku",
20
+ "description": "Model that names new heroes, picks skills and routes. off for the game's own picks"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,263 @@
1
+ // packages/g3n-mods/mods/agent-adventure/hooks/content.ts
2
+ import type { ClassId, RegionId, SkillDef, StatId } from '../types'
3
+
4
+ /**
5
+ * The game's tables: classes, skills, enemies, regions and relics. Plain data only, so the engine, the hooks module
6
+ * and the view can all import it.
7
+ */
8
+
9
+ /** Stats at level 1 and their growth per level. Crit and evade are chances from 0 to 1 and do not grow. */
10
+ type Stats = { hp: number; atk: number; def: number; spd: number; crit: number; evade: number }
11
+
12
+ export type ClassDef = {
13
+ name: string
14
+ /** One line on how it fights */
15
+ role: string
16
+ base: Stats
17
+ growth: Omit<Stats, 'spd' | 'crit' | 'evade'>
18
+ basic: SkillDef
19
+ /** The skills it can be offered, two at a time, every 10 levels */
20
+ pool: SkillDef[]
21
+ /** An effect it starts every fight with */
22
+ opening?: { kind: 'taunt'; turns: number }
23
+ }
24
+
25
+ /** Builds a skill; `id` is the class or enemy prefix and the name. */
26
+ function skill(prefix: string, name: string, kind: SkillDef['kind'], power: number, cd: number, blurb: string, extra: Partial<SkillDef> = {}): SkillDef {
27
+ return { id: `${prefix}.${name.toLowerCase().replace(/[^a-z]+/g, '-')}`, name, kind, power, cd, blurb, ...extra }
28
+ }
29
+
30
+ /** Builds a passive skill that raises `stat`. */
31
+ function passive(prefix: string, name: string, stat: StatId, power: number, blurb: string): SkillDef {
32
+ return skill(prefix, name, 'passive', power, 0, blurb, { stat })
33
+ }
34
+
35
+ export const CLASSES: Readonly<Record<ClassId, ClassDef>> = {
36
+ fighter: {
37
+ name: 'Fighter',
38
+ role: 'Durable front line with heavy blows',
39
+ base: { hp: 44, atk: 8, def: 5, spd: 5, crit: 0.06, evade: 0.03 },
40
+ growth: { hp: 7, atk: 1.5, def: 0.9 },
41
+ basic: skill('fighter', 'Slash', 'strike', 1, 0, 'A solid sword swing'),
42
+ pool: [
43
+ skill('fighter', 'Cleave', 'cleave', 0.75, 3, 'Hits every enemy'),
44
+ skill('fighter', 'Power Strike', 'strike', 2.1, 3, 'One crushing blow'),
45
+ skill('fighter', 'Second Wind', 'healSelf', 0.3, 5, 'Recovers 30% HP'),
46
+ skill('fighter', 'Battle Cry', 'rally', 0.2, 5, 'Party deals 20% more damage', { turns: 3 }),
47
+ skill('fighter', 'Shield Bash', 'stun', 0.8, 4, 'Hits and stuns'),
48
+ passive('fighter', 'Iron Skin', 'def', 0.25, '+25% defense'),
49
+ ],
50
+ },
51
+ healer: {
52
+ name: 'Healer',
53
+ role: 'Mends wounds and raises the fallen',
54
+ base: { hp: 34, atk: 6, def: 3, spd: 5, crit: 0.04, evade: 0.05 },
55
+ growth: { hp: 5, atk: 1.2, def: 0.5 },
56
+ basic: skill('healer', 'Mend', 'mend', 0.24, 0, 'Heals the most hurt ally, or smites when no one is'),
57
+ pool: [
58
+ skill('healer', 'Prayer', 'healAll', 0.18, 4, 'Heals the whole party'),
59
+ skill('healer', 'Resurrect', 'revive', 0.4, 6, 'Raises a fallen ally'),
60
+ skill('healer', 'Sanctuary', 'shield', 0.3, 5, 'Party takes 30% less damage', { turns: 2 }),
61
+ skill('healer', 'Holy Light', 'strike', 1.8, 3, 'A searing beam'),
62
+ skill('healer', 'Blessing', 'rally', 0.15, 5, 'Party deals 15% more damage', { turns: 3 }),
63
+ passive('healer', 'Grace', 'hp', 0.2, '+20% max HP'),
64
+ ],
65
+ },
66
+ monk: {
67
+ name: 'Monk',
68
+ role: 'Quick combos and hard to hit',
69
+ base: { hp: 38, atk: 7, def: 3, spd: 6, crit: 0.08, evade: 0.12 },
70
+ growth: { hp: 6, atk: 1.4, def: 0.6 },
71
+ basic: skill('monk', 'Palm', 'multi', 0.55, 0, 'Two quick strikes', { hits: 2 }),
72
+ pool: [
73
+ skill('monk', 'Flurry', 'multi', 0.5, 3, 'Four strikes on random enemies', { hits: 4 }),
74
+ skill('monk', 'Stunning Fist', 'stun', 1, 4, 'Hits and stuns'),
75
+ skill('monk', 'Inner Peace', 'healSelf', 0.35, 5, 'Recovers 35% HP'),
76
+ skill('monk', 'Tiger Strike', 'strike', 1.9, 3, 'One fierce blow'),
77
+ skill('monk', 'Chi Wave', 'cleave', 0.6, 3, 'Hits every enemy'),
78
+ passive('monk', 'Wind Step', 'evade', 0.1, '+10% evasion'),
79
+ ],
80
+ },
81
+ ranger: {
82
+ name: 'Ranger',
83
+ role: 'Accurate shots from range',
84
+ base: { hp: 32, atk: 8, def: 2, spd: 6, crit: 0.15, evade: 0.06 },
85
+ growth: { hp: 5, atk: 1.5, def: 0.4 },
86
+ basic: skill('ranger', 'Shot', 'strike', 1, 0, 'An arrow at one enemy'),
87
+ pool: [
88
+ skill('ranger', 'Volley', 'cleave', 0.65, 3, 'Arrows on every enemy'),
89
+ skill('ranger', 'Aimed Shot', 'strike', 2.2, 4, 'One careful, heavy shot'),
90
+ skill('ranger', 'Poison Arrow', 'burn', 0.35, 3, 'Hits and poisons', { turns: 3 }),
91
+ skill('ranger', 'Snare', 'stun', 0.6, 4, 'Hits and roots'),
92
+ skill('ranger', 'Rapid Fire', 'multi', 0.6, 3, 'Three quick arrows', { hits: 3 }),
93
+ passive('ranger', 'Hawk Eye', 'crit', 0.15, '+15% critical chance'),
94
+ ],
95
+ },
96
+ mage: {
97
+ name: 'Mage',
98
+ role: 'Area damage, fragile',
99
+ base: { hp: 28, atk: 9, def: 1.5, spd: 5, crit: 0.08, evade: 0.04 },
100
+ growth: { hp: 4, atk: 1.8, def: 0.3 },
101
+ basic: skill('mage', 'Bolt', 'strike', 1.1, 0, 'A crackling bolt'),
102
+ pool: [
103
+ skill('mage', 'Fireball', 'cleave', 0.9, 3, 'Burns every enemy'),
104
+ skill('mage', 'Ignite', 'burn', 0.5, 3, 'Sets one enemy alight', { turns: 3 }),
105
+ skill('mage', 'Arcane Blast', 'strike', 2.4, 4, 'One huge blast'),
106
+ skill('mage', 'Chain Lightning', 'multi', 0.8, 3, 'Three arcs on random enemies', { hits: 3 }),
107
+ skill('mage', 'Mana Shield', 'shield', 0.25, 5, 'Party takes 25% less damage', { turns: 2 }),
108
+ passive('mage', 'Arcane Mind', 'atk', 0.2, '+20% attack'),
109
+ ],
110
+ },
111
+ guardian: {
112
+ name: 'Guardian',
113
+ role: 'Draws attacks and shields allies',
114
+ base: { hp: 52, atk: 6, def: 6, spd: 4, crit: 0.04, evade: 0.02 },
115
+ growth: { hp: 8, atk: 1.1, def: 1.1 },
116
+ basic: skill('guardian', 'Bash', 'strike', 0.85, 0, 'A shield to the face'),
117
+ opening: { kind: 'taunt', turns: 2 },
118
+ pool: [
119
+ skill('guardian', 'Provoke', 'taunt', 0.3, 4, 'Draws every attack and braces', { turns: 3 }),
120
+ skill('guardian', 'Bulwark', 'shield', 0.35, 5, 'Party takes 35% less damage', { turns: 2 }),
121
+ skill('guardian', 'Lay On Hands', 'heal', 0.3, 4, 'Heals the most hurt ally'),
122
+ skill('guardian', 'Earthshaker', 'cleave', 0.6, 4, 'Hits every enemy'),
123
+ skill('guardian', 'Rally Banner', 'rally', 0.15, 5, 'Party deals 15% more damage', { turns: 3 }),
124
+ passive('guardian', 'Shield Wall', 'def', 0.3, '+30% defense'),
125
+ ],
126
+ },
127
+ rogue: {
128
+ name: 'Rogue',
129
+ role: 'Fast, critical single-target damage',
130
+ base: { hp: 32, atk: 8, def: 2, spd: 7, crit: 0.2, evade: 0.1 },
131
+ growth: { hp: 5, atk: 1.6, def: 0.4 },
132
+ basic: skill('rogue', 'Stab', 'strike', 1, 0, 'A quick dagger'),
133
+ pool: [
134
+ skill('rogue', 'Backstab', 'strike', 2.5, 4, 'A vicious strike'),
135
+ skill('rogue', 'Fan of Knives', 'cleave', 0.6, 3, 'Knives at every enemy'),
136
+ skill('rogue', 'Toxin', 'burn', 0.4, 3, 'Hits and poisons', { turns: 3 }),
137
+ skill('rogue', 'Cheap Shot', 'stun', 0.7, 4, 'Hits and stuns'),
138
+ skill('rogue', 'Flurry of Blades', 'multi', 0.7, 3, 'Three cuts on random enemies', { hits: 3 }),
139
+ passive('rogue', 'Shadowstep', 'evade', 0.12, '+12% evasion'),
140
+ ],
141
+ },
142
+ }
143
+
144
+ export const CLASS_IDS = Object.keys(CLASSES) as ClassId[]
145
+
146
+ /** Every learnable skill by id. */
147
+ export const SKILLS: Readonly<Record<string, SkillDef>> = Object.fromEntries(
148
+ CLASS_IDS.flatMap(cls => CLASSES[cls].pool).map(one => [one.id, one]),
149
+ )
150
+
151
+ /** Most skills a hero holds, besides its basic action. */
152
+ export const MAX_SKILLS = 3
153
+
154
+ /** An enemy kind: its stats as multiples of the level baseline, and an optional special. */
155
+ export type EnemyDef = {
156
+ name: string
157
+ hp: number
158
+ atk: number
159
+ def: number
160
+ spd: number
161
+ crit: number
162
+ evade: number
163
+ basic: SkillDef
164
+ special?: SkillDef
165
+ }
166
+
167
+ /** A boss: an enemy with a second phase below half HP. */
168
+ export type BossDef = EnemyDef & { phase2: { line: string; atkPct: number; special: SkillDef } }
169
+
170
+ const hit = (prefix: string, name: string) => skill(prefix, name, 'strike', 1, 0, '')
171
+
172
+ export const ENEMIES: Readonly<Record<string, EnemyDef>> = {
173
+ slime: { name: 'Slime', hp: 1.1, atk: 0.8, def: 0.6, spd: 3, crit: 0.03, evade: 0, basic: hit('slime', 'Squelch') },
174
+ wolf: { name: 'Wolf', hp: 0.8, atk: 1.05, def: 0.8, spd: 7, crit: 0.12, evade: 0.08, basic: hit('wolf', 'Bite') },
175
+ goblin: { name: 'Goblin', hp: 0.9, atk: 1, def: 1, spd: 5, crit: 0.06, evade: 0.05, basic: hit('goblin', 'Stab') },
176
+ shaman: {
177
+ name: 'Goblin Shaman', hp: 0.9, atk: 0.9, def: 0.9, spd: 5, crit: 0.05, evade: 0.05, basic: hit('shaman', 'Zap'),
178
+ special: skill('shaman', 'Hex Mend', 'heal', 0.3, 3, ''),
179
+ },
180
+ bat: { name: 'Cave Bat', hp: 0.6, atk: 0.9, def: 0.5, spd: 8, crit: 0.08, evade: 0.15, basic: hit('bat', 'Screech') },
181
+ spider: {
182
+ name: 'Spider', hp: 0.85, atk: 0.9, def: 0.8, spd: 6, crit: 0.06, evade: 0.06, basic: hit('spider', 'Bite'),
183
+ special: skill('spider', 'Venom', 'burn', 0.35, 3, '', { turns: 3 }),
184
+ },
185
+ golem: { name: 'Rock Golem', hp: 1.5, atk: 1.1, def: 1.6, spd: 2, crit: 0.02, evade: 0, basic: hit('golem', 'Pound') },
186
+ skeleton: { name: 'Skeleton', hp: 0.9, atk: 1, def: 1.1, spd: 5, crit: 0.06, evade: 0.04, basic: hit('skeleton', 'Rattle Slash') },
187
+ ghost: { name: 'Ghost', hp: 0.7, atk: 0.95, def: 0.4, spd: 6, crit: 0.05, evade: 0.22, basic: hit('ghost', 'Chill Touch') },
188
+ cultist: {
189
+ name: 'Cultist', hp: 0.85, atk: 1, def: 0.8, spd: 5, crit: 0.05, evade: 0.05, basic: hit('cultist', 'Dark Bolt'),
190
+ special: skill('cultist', 'Hellfire', 'burn', 0.45, 3, '', { turns: 2 }),
191
+ },
192
+ knight: {
193
+ name: 'Bone Knight', hp: 1.3, atk: 1.15, def: 1.5, spd: 4, crit: 0.06, evade: 0.03, basic: hit('knight', 'Cleaver'),
194
+ special: skill('knight', 'Sweep', 'cleave', 0.7, 3, ''),
195
+ },
196
+ }
197
+
198
+ export const BOSSES: Readonly<Record<string, BossDef>> = {
199
+ goblinKing: {
200
+ name: 'Grub the Goblin King', hp: 1, atk: 1, def: 1.1, spd: 4, crit: 0.08, evade: 0.04, basic: hit('king', 'Club'),
201
+ special: skill('king', 'Ground Slam', 'cleave', 0.8, 3, ''),
202
+ phase2: { line: 'Grub throws his crown aside and roars!', atkPct: 0.3, special: skill('king', 'Frenzy', 'multi', 0.7, 2, '', { hits: 3 }) },
203
+ },
204
+ wyrm: {
205
+ name: 'The Crystal Wyrm', hp: 1.1, atk: 1, def: 1.3, spd: 4, crit: 0.06, evade: 0.03, basic: hit('wyrm', 'Claw'),
206
+ special: skill('wyrm', 'Shard Breath', 'cleave', 0.85, 3, ''),
207
+ phase2: { line: 'The Wyrm\'s crystals blaze with light!', atkPct: 0.25, special: skill('wyrm', 'Prism Beam', 'burn', 0.6, 2, '', { turns: 3 }) },
208
+ },
209
+ lich: {
210
+ name: 'The Lich', hp: 0.95, atk: 1.1, def: 0.9, spd: 5, crit: 0.06, evade: 0.08, basic: hit('lich', 'Soul Bolt'),
211
+ special: skill('lich', 'Life Drain', 'drain', 1.2, 3, ''),
212
+ phase2: { line: 'The Lich\'s phylactery cracks; death pours out!', atkPct: 0.3, special: skill('lich', 'Death Nova', 'cleave', 0.9, 2, '') },
213
+ },
214
+ }
215
+
216
+ export type RegionDef = { name: string; enemies: string[]; elites: string[]; boss: keyof typeof BOSSES }
217
+
218
+ export const REGIONS: Readonly<Record<RegionId, RegionDef>> = {
219
+ woods: { name: 'Mossy Woods', enemies: ['slime', 'wolf', 'goblin'], elites: ['shaman', 'wolf'], boss: 'goblinKing' },
220
+ caves: { name: 'Echo Caves', enemies: ['bat', 'spider', 'golem'], elites: ['golem', 'spider'], boss: 'wyrm' },
221
+ crypt: { name: 'Sunken Crypt', enemies: ['skeleton', 'ghost', 'cultist'], elites: ['knight', 'cultist'], boss: 'lich' },
222
+ }
223
+
224
+ export const REGION_IDS = Object.keys(REGIONS) as RegionId[]
225
+
226
+ /** A run-only relic: a party-wide bonus until the run ends. */
227
+ export type RelicDef = {
228
+ name: string
229
+ icon: string
230
+ blurb: string
231
+ atkPct?: number
232
+ defPct?: number
233
+ spd?: number
234
+ crit?: number
235
+ /** Share of max HP healed at the start of each own turn */
236
+ regen?: number
237
+ /** Once per run, a wipe raises the party instead */
238
+ isFeather?: boolean
239
+ }
240
+
241
+ export const RELICS: Readonly<Record<string, RelicDef>> = {
242
+ ember: { name: 'Ember Charm', icon: '🔥', blurb: '+15% attack', atkPct: 0.15 },
243
+ oak: { name: 'Oak Ward', icon: '🌳', blurb: '+20% defense', defPct: 0.2 },
244
+ boots: { name: 'Swift Boots', icon: '👢', blurb: '+1 speed', spd: 1 },
245
+ moss: { name: 'Mending Moss', icon: '🌿', blurb: 'Heal 3% HP each turn', regen: 0.03 },
246
+ coin: { name: 'Lucky Coin', icon: '🪙', blurb: '+10% critical chance', crit: 0.1 },
247
+ feather: { name: 'Phoenix Feather', icon: '🪶', blurb: 'Once, a wipe raises the party instead', isFeather: true },
248
+ }
249
+
250
+ /** First halves and second halves of the names the game gives a hero when the model gives none. */
251
+ export const NAME_PARTS: readonly [readonly string[], readonly string[]] = [
252
+ ['Bran', 'Elo', 'Thal', 'Mira', 'Kor', 'Wren', 'Ash', 'Iso', 'Dun', 'Sel', 'Fen', 'Rho', 'Tam', 'Vey', 'Orl', 'Pip'],
253
+ ['noc', 'wen', 'ric', 'dra', 'vin', 'ley', 'mir', 'bel', 'dor', 'ka', 'sin', 'tov', 'ra', 'lin', 'ger', 'by'],
254
+ ]
255
+
256
+ /** What heroes say in the tavern; `{friend}` is a hero they like. */
257
+ export const TAVERN_LINES: readonly string[] = [
258
+ 'Another round!', 'My feet still smell of slime.', 'Anyone seen my other boot?', 'To the next dungeon!',
259
+ 'This stew is mostly turnip.', 'I could take that boss. Probably.', '*polishes gear*', '*hums an old song*',
260
+ 'Cards, anyone?', '*dozes by the fire*', 'Remember that time with the bats?', 'The bard here is awful.',
261
+ ]
262
+ export const FRIEND_LINES: readonly string[] = ['A toast to {friend}!', '{friend}, you saved my hide back there.', 'I\'d follow {friend} into any crypt.']
263
+ export const RIVAL_LINES: readonly string[] = ['{friend} snores like a golem.', 'I did more damage than {friend}.']