aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/scavenging.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-19 18:33:43 -0400
committerhistoria <[not public]>2026-06-19 18:33:43 -0400
commit2bec24859af8f03c80a0a58e3240519942450167 (patch)
treedcb8baa5591fdeca51e179a63a08d46b014cb85c /skill_plans/scavenging.md
parent0ea4eec5dd2122c6704216971eb1249276297867 (diff)
downloadthehouseoficarus-2bec24859af8f03c80a0a58e3240519942450167.tar.gz
readme
Diffstat (limited to 'skill_plans/scavenging.md')
-rw-r--r--skill_plans/scavenging.md950
1 files changed, 0 insertions, 950 deletions
diff --git a/skill_plans/scavenging.md b/skill_plans/scavenging.md
deleted file mode 100644
index 5dc7d39..0000000
--- a/skill_plans/scavenging.md
+++ /dev/null
@@ -1,950 +0,0 @@
-# Scavenging Skill Implementation Plan
-
-## 1. Overview
-
-Scavenging is the Runecrafting analog for The House of Icarus. The skill has two phases:
-
-1. **Mining scrap** — Gather `scrap_metal` from scrap pile objects using a pickaxe (Mining skill). This already works via `data/behaviors/mine_scrap.yaml` and `data/objects/scrap_pile.yaml`. The item `data/items/scrap_metal.yaml` exists and is non-stackable.
-2. **Identifying junk** — Take scrap to an altar station. With the correct identifier tool in inventory, type `id` (or `use <altar>`). On the next tick, ALL scrap in the player's inventory is converted into the corresponding junk type. Output quantity is `scrap_count × multiplier`, where multiplier increases with Scavenging level (like OSRS double runes).
-
-There are 4 junk types, each requiring a specific identifier tool and altar:
-
-| Junk Type | Identifier Tool | Altar Station | Level Req | XP/scrap |
-|-----------|----------------|---------------|-----------|----------|
-| Solarjunk | Solar Identifier | Solar Altar | 1 | 5 |
-| Hydrojunk | Hydro Identifier | Hydro Altar | 14 | 8 |
-| Ecojunk | Eco Identifier | Eco Altar | 28 | 12 |
-| Biojunk | Bio Identifier | Bio Altar | 44 | 16 |
-
-The Scavenging skill constant already exists in `internal/player/player.go:28` as `Scavenging SkillName = "scavenging"` with abbreviation `"scv"`.
-
----
-
-## 2. Commands
-
-### New Command: `id`
-
-| Property | Value |
-|----------|-------|
-| Command | `id` |
-| Aliases | `identify` |
-| Class | `ClassActive` |
-| Handler | `g.doIdentify(sess, args)` |
-| Action Type | `"identify"` |
-| ActionState | `ActionIdentifying` |
-| Ticks | 1 (instant conversion on next tick) |
-
-**Behavior:** When the player types `id`:
-1. Cancel any active action.
-2. Scan the room for an altar object (`solar_altar`, `hydro_altar`, `eco_altar`, `bio_altar`).
-3. If no altar found: `"There is no altar here."`
-4. Determine the junk type from the altar type.
-5. Check the player has the matching identifier tool in inventory or equipment.
-6. If no identifier: `"You need a <type> identifier to use this altar."`
-7. Check the player has at least one `scrap_metal` in inventory.
-8. If no scrap: `"You don't have any scrap metal."`
-9. Check player's Scavenging level meets the junk's level requirement.
-10. If too low: `"You need level <N> scavenging to identify <junk type>."`
-11. Create a 1-tick `"identify"` action.
-12. On advance: count all `scrap_metal` slots, remove them, calculate multiplier, add junk, award XP.
-
-**`use <altar>` path:** The `id` command is the primary interface. Additionally, `use` on an altar without specifying an item should route to the same `doIdentify` logic. This can be handled by giving each altar object a `behavior` that points to a behavior YAML with `type: use`, OR by special-casing altar IDs in the `use` command handler. The recommended approach is to add `use_interactions` on each altar object YAML that triggers the identify logic (see Section 6).
-
-### Classification and Dispatch Changes
-
-**File: `internal/game/game.go`**
-
-In `classifyCommand()` at line 136, add `"id"` and `"identify"` to the `ClassActive` list:
-
-```go
-case "get", "take", "grab", "pick", "drop",
- "attack", "kill",
- "north", "n", "south", "s", "east", "e",
- "west", "w", "up", "u", "down", "d",
- "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft",
- "id", "identify":
- return ClassActive
-```
-
-In `executeCommand()` (around line 249), add a case:
-
-```go
-case "id", "identify":
- g.doIdentify(sess, strings.Join(args, " "))
- return
-```
-
----
-
-## 3. New Files to Create
-
-### Go Files
-
-| File | Purpose |
-|------|---------|
-| `internal/game/cmd_id.go` | `doIdentify()` handler — validate altar, identifier, scrap; create action |
-| `internal/game/action_identify.go` | `startIdentify()` and `advanceIdentify()` — the conversion logic |
-
-### YAML Files — Items
-
-| File | Description |
-|------|-------------|
-| `data/items/solar_identifier.yaml` | Solar Identifier tool |
-| `data/items/hydro_identifier.yaml` | Hydro Identifier tool |
-| `data/items/eco_identifier.yaml` | Eco Identifier tool |
-| `data/items/bio_identifier.yaml` | Bio Identifier tool |
-| `data/items/solarjunk.yaml` | Solarjunk (stackable output) |
-| `data/items/hydrojunk.yaml` | Hydrojunk (stackable output) |
-| `data/items/ecojunk.yaml` | Ecojunk (stackable output) |
-| `data/items/biojunk.yaml` | Biojunk (stackable output) |
-
-### YAML Files — Objects
-
-| File | Description |
-|------|-------------|
-| `data/objects/solar_altar.yaml` | Solar Altar station |
-| `data/objects/hydro_altar.yaml` | Hydro Altar station |
-| `data/objects/eco_altar.yaml` | Eco Altar station |
-| `data/objects/bio_altar.yaml` | Bio Altar station |
-
-### YAML Files — Help
-
-| File | Description |
-|------|-------------|
-| `data/help/id.yaml` | Help for the `id` command |
-| `data/help/scavenging.yaml` | Help for the Scavenging skill |
-
----
-
-## 4. Code Changes to Existing Files
-
-### `internal/game/game.go`
-
-1. **`classifyCommand()`** (line 136): Add `"id"`, `"identify"` to `ClassActive` case.
-2. **`executeCommand()`** (line 249): Add `case "id", "identify":` dispatching to `g.doIdentify(sess, strings.Join(args, " "))`.
-
-### `internal/game/action_state.go`
-
-Add the new ActionType constant:
-
-```go
-ActionIdentifying ActionType = "identifying"
-```
-
-Add a case in `Description()`:
-
-```go
-case ActionIdentifying:
- return "identifying scrap at " + a.TargetName
-```
-
-### `internal/game/action.go`
-
-In `AdvanceActions()` (line 225), add a case in the switch:
-
-```go
-case "identify":
- g.advanceIdentify(sess, p)
-```
-
-### `internal/game/game.go` — `ProcessQueuedCommands()`
-
-In the `ActionState` clearing logic (line 472), the `ActionIdentifying` type is a 1-tick action like `ActionPickingUp`. It does NOT need to persist in the list of long-running actions. After the advance tick fires and completes, the action is nil'd and the state clears naturally. No changes needed here — the `default` branch at line 474 already clears non-persistent action states.
-
----
-
-## 5. Items — Full YAML Definitions
-
-### `data/items/scrap_metal.yaml` (ALREADY EXISTS — verify)
-
-```yaml
-id: scrap_metal
-name: scrap metal
-color: "243"
-description: "A chunk of salvaged scrap metal."
-value: 2
-stackable: false
-```
-
-This already exists at `data/items/scrap_metal.yaml`. No changes needed. Scrap metal is intentionally non-stackable (like rune essence in OSRS — each piece takes an inventory slot, limiting how many you can carry per trip to 28).
-
-### `data/items/solar_identifier.yaml`
-
-```yaml
-id: solar_identifier
-name: solar identifier
-color: "220"
-description: "A handheld scanner calibrated to isolate solar-frequency signatures in scrap metal. Required to produce solarjunk at a solar altar."
-value: 500
-```
-
-### `data/items/hydro_identifier.yaml`
-
-```yaml
-id: hydro_identifier
-name: hydro identifier
-color: "39"
-description: "A handheld scanner calibrated to isolate hydro-frequency signatures in scrap metal. Required to produce hydrojunk at a hydro altar."
-value: 1000
-```
-
-### `data/items/eco_identifier.yaml`
-
-```yaml
-id: eco_identifier
-name: eco identifier
-color: "34"
-description: "A handheld scanner calibrated to isolate eco-frequency signatures in scrap metal. Required to produce ecojunk at an eco altar."
-value: 2500
-```
-
-### `data/items/bio_identifier.yaml`
-
-```yaml
-id: bio_identifier
-name: bio identifier
-color: "196"
-description: "A handheld scanner calibrated to isolate bio-frequency signatures in scrap metal. Required to produce biojunk at a bio altar."
-value: 5000
-```
-
-### `data/items/solarjunk.yaml`
-
-```yaml
-id: solarjunk
-name: solarjunk
-color: "220"
-description: "A fragment of reclaimed solar circuitry. Used as a universal energy medium."
-value: 10
-stackable: true
-```
-
-### `data/items/hydrojunk.yaml`
-
-```yaml
-id: hydrojunk
-name: hydrojunk
-color: "39"
-description: "A fragment of reclaimed hydro circuitry. Used as a universal energy medium."
-value: 18
-stackable: true
-```
-
-### `data/items/ecojunk.yaml`
-
-```yaml
-id: ecojunk
-name: ecojunk
-color: "34"
-description: "A fragment of reclaimed eco circuitry. Used as a universal energy medium."
-value: 30
-stackable: true
-```
-
-### `data/items/biojunk.yaml`
-
-```yaml
-id: biojunk
-name: biojunk
-color: "196"
-description: "A fragment of reclaimed bio circuitry. Used as a universal energy medium."
-value: 50
-stackable: true
-```
-
----
-
-## 6. Objects — Full YAML Definitions
-
-### `data/objects/solar_altar.yaml`
-
-```yaml
-id: solar_altar
-name: solar altar
-color: "220"
-description: "A humming altar of golden circuitry. Place scrap metal here with a solar identifier to produce solarjunk. Type 'id' to begin."
-```
-
-### `data/objects/hydro_altar.yaml`
-
-```yaml
-id: hydro_altar
-name: hydro altar
-color: "39"
-description: "A pulsing altar of blue crystalline conduits. Place scrap metal here with a hydro identifier to produce hydrojunk. Type 'id' to begin."
-```
-
-### `data/objects/eco_altar.yaml`
-
-```yaml
-id: eco_altar
-name: eco altar
-color: "34"
-description: "A thrumming altar of green organic circuits. Place scrap metal here with an eco identifier to produce ecojunk. Type 'id' to begin."
-```
-
-### `data/objects/bio_altar.yaml`
-
-```yaml
-id: bio_altar
-name: bio altar
-color: "196"
-description: "A sinister altar of red biological interfaces. Place scrap metal here with a bio identifier to produce biojunk. Type 'id' to begin."
-```
-
----
-
-## 7. Rooms
-
-### Existing Room: Room 9 — Scavenging Post
-
-Currently at `data/rooms/9.yaml`:
-
-```yaml
-id: 9
-name: "Scavenging Post"
-description: "A cluttered yard full of scrap metal and discarded technology. This area is not yet accessible."
-exits:
- east: 10
- west: 8
-```
-
-**Update room 9** to add scrap pile objects and a useful description:
-
-```yaml
-id: 9
-name: "Scavenging Post"
-description: "A cluttered yard full of {243}scrap metal{/} and discarded technology. Piles of salvageable debris are scattered across the ground. The air smells of ozone and rust."
-exits:
- east: 10
- west: 8
-objects:
- - id: scrap_pile
- - id: scrap_pile
- - id: scrap_pile
-```
-
-### New Altar Rooms
-
-Create 4 new rooms for the altars. These should branch off from or near the Scavenging Post area. The exact room IDs depend on the next available IDs in the codebase. Check `data/rooms/` for the highest existing ID and use the next sequential numbers.
-
-**Suggested room layout:**
-- Room 9 (Scavenging Post) — hub with scrap piles
-- New rooms branching from room 9 or nearby rooms for each altar
-
-Example new room files (IDs are placeholders — use next available):
-
-#### `data/rooms/<next_id>.yaml` — Solar Altar Chamber
-
-```yaml
-id: <next_id>
-name: "Solar Altar Chamber"
-description: "A circular chamber bathed in warm {220}golden light{/}. At its center stands a humming {220 bold}solar altar{/}, its surface etched with fractal circuit patterns that glow softly."
-exits:
- south: 9
-objects:
- - id: solar_altar
-```
-
-#### `data/rooms/<next_id+1>.yaml` — Hydro Altar Chamber
-
-```yaml
-id: <next_id+1>
-name: "Hydro Altar Chamber"
-description: "Condensation drips from the ceiling of this cool, blue-lit chamber. A {39 bold}hydro altar{/} dominates the room, its crystalline surface rippling with patterns like flowing water."
-exits:
- south: 9
-objects:
- - id: hydro_altar
-```
-
-#### `data/rooms/<next_id+2>.yaml` — Eco Altar Chamber
-
-```yaml
-id: <next_id+2>
-name: "Eco Altar Chamber"
-description: "Bioluminescent moss covers the walls of this overgrown chamber. An {34 bold}eco altar{/} rises from the floor, entwined with living circuitry that pulses with green light."
-exits:
- south: 9
-objects:
- - id: eco_altar
-```
-
-#### `data/rooms/<next_id+3>.yaml` — Bio Altar Chamber
-
-```yaml
-id: <next_id+3>
-name: "Bio Altar Chamber"
-description: "The air here is thick and warm. A {196 bold}bio altar{/} throbs at the center of the room, its surface covered in red organic membranes threaded with copper wire."
-exits:
- south: 9
-objects:
- - id: bio_altar
-```
-
-**Update room 9 exits** to add north exits (or other directions) to the altar rooms. The exact exit directions depend on the room layout. Add exits from room 9 to each altar room, e.g.:
-
-```yaml
-exits:
- east: 10
- west: 8
- north: <solar_altar_room_id>
-```
-
-Or create an intermediate hub room that branches to all 4 altars.
-
-**To determine actual room IDs:** Run `ls data/rooms/ | sort -n | tail -5` to find the highest existing room ID, then use sequential IDs after that.
-
----
-
-## 8. Mechanics — The `id` Command
-
-### Flow Diagram
-
-```
-Player types "id" (or "identify")
- │
- ├── CancelAction(p)
- │
- ├── Scan room for altar objects
- │ └── Check: solar_altar, hydro_altar, eco_altar, bio_altar
- │ └── If none found: "There is no altar here." → return
- │
- ├── Determine altar type → junk mapping
- │ ├── solar_altar → solarjunk (req: solar_identifier, level 1, 5 XP)
- │ ├── hydro_altar → hydrojunk (req: hydro_identifier, level 14, 8 XP)
- │ ├── eco_altar → ecojunk (req: eco_identifier, level 28, 12 XP)
- │ └── bio_altar → biojunk (req: bio_identifier, level 44, 16 XP)
- │
- ├── Check identifier tool in inventory or equipment
- │ └── If missing: "You need a <name> to use this altar." → return
- │
- ├── Check player has scrap_metal in inventory
- │ └── If none: "You don't have any scrap metal." → return
- │
- ├── Check Scavenging level ≥ required level
- │ └── If too low: "You need level <N> scavenging to do that." → return
- │
- └── Create action:
- p.Action = &action.Action{
- Type: "identify",
- TargetName: <altar_name>,
- Data: {
- "altar_type": <altar_def_id>,
- "junk_id": <junk_item_id>,
- "xp_per": <xp_per_scrap>,
- "level_req": <level_req>,
- },
- WaitLeft: 1,
- }
- p.ActionState = &ActionState{Type: ActionIdentifying, TargetName: <altar_name>}
-```
-
-### Advance Logic (next tick)
-
-```
-advanceIdentify(sess, p):
- │
- ├── Read action data (junk_id, xp_per, level_req)
- │
- ├── Count all scrap_metal in inventory
- │ └── Iterate slots 0-27, sum quantities where ItemID == "scrap_metal"
- │
- ├── Calculate multiplier from Scavenging level (see Section 11)
- │ └── totalJunk = scrapCount * multiplier
- │
- ├── Check inventory space for junk (junk is stackable, so only need 1 free slot
- │ if player doesn't already have that junk type)
- │ └── If no existing stack AND no free slot:
- │ "Your inventory is too full!" → CancelAction → return
- │
- ├── Remove all scrap_metal from inventory
- │ └── Iterate slots 0-27, nil any slot with ItemID == "scrap_metal"
- │
- ├── Add junk to inventory
- │ └── Find existing stack of junk_id → add totalJunk to quantity
- │ └── OR find free slot → set to {ItemID: junk_id, Quantity: totalJunk}
- │
- ├── Award XP: totalXP = scrapCount * xp_per
- │ └── p.AddSkillXP(player.Scavenging, totalXP)
- │ └── Check for level-up, output message if so
- │
- ├── Output message:
- │ "The altar hums. You identify <scrapCount> scrap metal into <totalJunk> <junk_name>."
- │ └── If xp_drops enabled: " (+<totalXP>xp scv)"
- │
- ├── SaveCharacter(p)
- │
- └── CancelAction(p)
-```
-
----
-
-## 9. Action Lifecycle — Implementation Details
-
-### `internal/game/cmd_id.go`
-
-```go
-package game
-
-import (
- "fmt"
-
- "thehouseoficarus/internal/action"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-type altarConfig struct {
- AltarID string
- IdentifierID string
- JunkID string
- LevelReq int
- XPPer int
-}
-
-var altarConfigs = []altarConfig{
- {"solar_altar", "solar_identifier", "solarjunk", 1, 5},
- {"hydro_altar", "hydro_identifier", "hydrojunk", 14, 8},
- {"eco_altar", "eco_identifier", "ecojunk", 28, 12},
- {"bio_altar", "bio_identifier", "biojunk", 44, 16},
-}
-
-func (g *Game) doIdentify(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
-
- var found *altarConfig
- for _, ac := range altarConfigs {
- if defID, _ := g.findStation(p.RoomID, []string{ac.AltarID}); defID != "" {
- found = &ac
- break
- }
- }
- if found == nil {
- sess.WriteLine("There is no altar here.")
- return
- }
-
- identifierName := found.IdentifierID
- if def, err := g.ItemStore.Load(found.IdentifierID); err == nil {
- identifierName = def.Name
- }
- if !p.HasItem(found.IdentifierID) {
- sess.WriteLine(fmt.Sprintf("You need a %s to use this altar.", identifierName))
- return
- }
-
- scrapCount := g.countItem(p, "scrap_metal")
- if scrapCount == 0 {
- sess.WriteLine("You don't have any scrap metal.")
- return
- }
-
- scavLevel := p.Level(player.Scavenging)
- if scavLevel < found.LevelReq {
- junkName := found.JunkID
- if def, err := g.ItemStore.Load(found.JunkID); err == nil {
- junkName = def.Name
- }
- sess.WriteLine(fmt.Sprintf("You need level %d scavenging to identify %s.", found.LevelReq, junkName))
- return
- }
-
- altarName := found.AltarID
- if def, err := g.ObjectStore.Load(found.AltarID); err == nil {
- altarName = def.Name
- }
-
- p.Action = &action.Action{
- Type: "identify",
- TargetID: found.AltarID,
- TargetName: altarName,
- WaitLeft: 1,
- Data: map[string]any{
- "junk_id": found.JunkID,
- "xp_per": found.XPPer,
- "level_req": found.LevelReq,
- },
- }
- p.ActionState = &ActionState{Type: ActionIdentifying, TargetName: altarName}
-}
-
-// countItem counts total quantity of an item across all inventory slots.
-// For non-stackable items, each slot with that item contributes 1.
-func (g *Game) countItem(p *player.Player, itemID string) int {
- total := 0
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == itemID {
- total += slot.Quantity
- }
- }
- return total
-}
-```
-
-**Note on `countItem`:** Check if `player.Player` already has a `CountItem` method. Looking at the codebase, `p.CountItem` is used in production code (`recipe.HasAllItemsQty(p.CountItem)`). Use that if it already returns a count for a given item ID. If `CountItem(id string) int` exists, use `p.CountItem("scrap_metal")` instead of a custom helper.
-
-### `internal/game/action_identify.go`
-
-```go
-package game
-
-import (
- "fmt"
-
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) {
- junkID := p.Action.Data["junk_id"].(string)
- xpPer := p.Action.Data["xp_per"].(int)
-
- scrapCount := 0
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == "scrap_metal" {
- scrapCount += slot.Quantity
- }
- }
-
- if scrapCount == 0 {
- sess.WriteLine("You don't have any scrap metal.")
- g.CancelAction(p)
- return
- }
-
- scavLevel := p.Level(player.Scavenging)
- multiplier := junkMultiplier(junkID, scavLevel)
- totalJunk := scrapCount * multiplier
-
- hasExistingStack := false
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == junkID {
- hasExistingStack = true
- break
- }
- }
- if !hasExistingStack && p.FirstFreeSlot() == -1 {
- sess.WriteLine("Your inventory is too full!")
- g.CancelAction(p)
- return
- }
-
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == "scrap_metal" {
- p.SetInvSlot(i, nil)
- }
- }
-
- placed := false
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == junkID {
- slot.Quantity += totalJunk
- placed = true
- break
- }
- }
- if !placed {
- freeSlot := p.FirstFreeSlot()
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: junkID, Quantity: totalJunk})
- }
-
- totalXP := scrapCount * xpPer
- junkName := junkID
- if def, err := g.ItemStore.Load(junkID); err == nil {
- junkName = def.Name
- }
-
- msg := fmt.Sprintf("The altar hums. You identify %d scrap metal into %d %s.", scrapCount, totalJunk, junkName)
-
- if newLevel := p.AddSkillXP(player.Scavenging, totalXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d scavenging! ***", newLevel)))
- }
-
- if p.OptionBool("xp_drops") && totalXP > 0 {
- msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp scv)", totalXP))
- }
- sess.WriteLine(msg)
-
- g.AccountStore.SaveCharacter(p)
- g.CancelAction(p)
-}
-```
-
-**Note on `xpPer` type:** When data is stored via `map[string]any` and read back after JSON/YAML round-tripping through the action system, integer values may come back as `float64` (if the action data is ever serialized). Check how other action handlers read integers from `Data`. Looking at `action_search.go:42`, it uses `data["slot_idx"].(int)` — so direct int assertion is fine since action data is never serialized to disk. However, `action_production.go:310` reads `wait` as `float64` — this is because `map[string]any` stores numbers from YAML as float64. Since we're setting these values directly in Go code (not loading from YAML), `int` assertion is correct. But to be safe, consider storing as `float64` and reading as `float64`, then converting:
-
-```go
-xpPer := int(p.Action.Data["xp_per"].(float64))
-```
-
-This is safer. Check the `advanceSearch` pattern — it reads `int` directly because it was set as `int` in `startSearch`. Since `doIdentify` also sets `int` values directly, the `.(int)` assertion should work. But be aware: if the game ever serializes/deserializes actions, this could break. Follow whichever pattern the codebase uses consistently.
-
----
-
-## 10. XP Table
-
-XP is awarded per scrap converted, not per junk produced. This means multipliers give more output but not more XP per scrap.
-
-| Junk Type | XP per scrap | Level Req |
-|-----------|-------------|-----------|
-| Solarjunk | 5 | 1 |
-| Hydrojunk | 8 | 14 |
-| Ecojunk | 12 | 28 |
-| Biojunk | 16 | 44 |
-
-**XP examples (28 inventory slots = 28 scrap per trip):**
-
-| Junk Type | XP per trip (28 scrap) |
-|-----------|----------------------|
-| Solarjunk | 140 |
-| Hydrojunk | 224 |
-| Ecojunk | 336 |
-| Biojunk | 448 |
-
-For reference, level 99 requires 13,034,431 XP (RSC table). At 448 XP per trip (biojunk), that's ~29,094 trips — comparable to OSRS Runecrafting grind.
-
----
-
-## 11. Level Multipliers
-
-At higher Scavenging levels, each scrap metal produces multiple junk (like OSRS double/triple runes).
-
-### Multiplier Table
-
-| Scavenging Level | Solarjunk | Hydrojunk | Ecojunk | Biojunk |
-|-----------------|-----------|-----------|---------|---------|
-| 1 | 1x | — | — | — |
-| 14 | 1x | 1x | — | — |
-| 22 | 2x | 1x | — | — |
-| 28 | 2x | 1x | 1x | — |
-| 36 | 2x | 2x | 1x | — |
-| 44 | 3x | 2x | 1x | 1x |
-| 56 | 3x | 2x | 2x | 1x |
-| 66 | 4x | 3x | 2x | 1x |
-| 78 | 4x | 3x | 3x | 2x |
-| 88 | 5x | 4x | 3x | 2x |
-| 99 | 6x | 4x | 3x | 3x |
-
-### Implementation
-
-```go
-type junkMultiplierEntry struct {
- Level int
- Multiplier int
-}
-
-var junkMultipliers = map[string][]junkMultiplierEntry{
- "solarjunk": {
- {1, 1}, {22, 2}, {44, 3}, {66, 4}, {88, 5}, {99, 6},
- },
- "hydrojunk": {
- {14, 1}, {36, 2}, {66, 3}, {88, 4},
- },
- "ecojunk": {
- {28, 1}, {56, 2}, {78, 3},
- },
- "biojunk": {
- {44, 1}, {78, 2}, {99, 3},
- },
-}
-
-func junkMultiplier(junkID string, level int) int {
- entries, ok := junkMultipliers[junkID]
- if !ok {
- return 1
- }
- mult := 1
- for _, e := range entries {
- if level >= e.Level {
- mult = e.Multiplier
- }
- }
- return mult
-}
-```
-
-Place this in `cmd_id.go` or `action_identify.go` — whichever file contains the `advanceIdentify` function.
-
----
-
-## 12. Mining Scrap — Verification
-
-The existing setup:
-
-- **Behavior:** `data/behaviors/mine_scrap.yaml` — skill: mining, level 1, XP 5, base_wait 10, requires pickaxe, 55% base success + 1% per level (cap 95%), drops `scrap_metal` with depletion, 50-tick respawn.
-- **Object:** `data/objects/scrap_pile.yaml` — name "scrap pile", behavior `mine_scrap`.
-- **Item:** `data/items/scrap_metal.yaml` — non-stackable, value 2.
-
-**Verify these work by:**
-1. Ensure room 9 has `scrap_pile` objects listed.
-2. Test: `mine scrap` or `mine pile` should start gathering.
-3. Test: Successful gather produces `scrap_metal` in inventory.
-4. Test: Depletion and respawn work correctly.
-
-**Potential adjustment:** The `base_wait: 10` (10 ticks = 6 seconds at 600ms tick) is reasonable for scrap mining. The success rate starting at 55% and capping at 95% is fine. Consider whether the behavior should use `skill: scavenging` instead of `skill: mining`. The TODO says "Mine 'scrap' in scrap pile objects" which implies Mining skill is correct (Scavenging is for the identification phase). However, this is a design decision — if scrap mining should train Scavenging, change the behavior to `skill: scavenging`.
-
-**Recommendation:** Keep `skill: mining` for scrap mining (consistent with the mining skill being used for all pick-based gathering). The Scavenging skill is trained exclusively through identification at altars. This parallels OSRS where Mining trains Mining and Runecrafting trains Runecrafting.
-
----
-
-## 13. Help Files
-
-### `data/help/id.yaml`
-
-```yaml
-name: "id"
-category: "Commands"
-description: |
- Identify scrap metal at an altar to produce junk.
-
- Usage: id
- identify
-
- Stand at an altar (solar, hydro, eco, or bio) with the matching
- identifier tool in your inventory and scrap metal. Type 'id' to
- convert all your scrap metal into junk on the next game tick.
-
- Each altar type requires a specific identifier:
- Solar Altar → solar identifier → solarjunk (level 1)
- Hydro Altar → hydro identifier → hydrojunk (level 14)
- Eco Altar → eco identifier → ecojunk (level 28)
- Bio Altar → bio identifier → biojunk (level 44)
-
- At higher scavenging levels, each scrap produces multiple junk.
- See 'help scavenging' for the full multiplier table.
-```
-
-### `data/help/scavenging.yaml`
-
-```yaml
-name: "scavenging"
-category: "Skills"
-description: |
- Scavenging is the art of reclaiming useful circuitry from salvaged scrap.
-
- Step 1: Mine scrap metal from scrap piles (requires a pickaxe, uses
- the Mining skill).
- Step 2: Take scrap metal to an altar with the matching identifier tool.
- Step 3: Type 'id' to convert all scrap metal into junk.
-
- Junk types and requirements:
- Solarjunk — level 1, solar identifier, solar altar (5 XP/scrap)
- Hydrojunk — level 14, hydro identifier, hydro altar (8 XP/scrap)
- Ecojunk — level 28, eco identifier, eco altar (12 XP/scrap)
- Biojunk — level 44, bio identifier, bio altar (16 XP/scrap)
-
- Higher scavenging levels produce more junk per scrap:
- Solarjunk: 2x at 22, 3x at 44, 4x at 66, 5x at 88, 6x at 99
- Hydrojunk: 2x at 36, 3x at 66, 4x at 88
- Ecojunk: 2x at 56, 3x at 78
- Biojunk: 2x at 78, 3x at 99
-
- Scrap metal is not stackable — you can carry 28 per trip (one per
- inventory slot). Each trip converts all scrap in one tick.
-```
-
----
-
-## 14. Implementation Checklist
-
-### Phase 1: Core Files (minimal working feature)
-
-- [ ] Create `internal/game/cmd_id.go` with `doIdentify()`, `altarConfigs`, `junkMultiplier()`
-- [ ] Create `internal/game/action_identify.go` with `advanceIdentify()`
-- [ ] Edit `internal/game/action_state.go`: add `ActionIdentifying` constant and `Description()` case
-- [ ] Edit `internal/game/game.go` → `classifyCommand()`: add `"id"`, `"identify"` to `ClassActive`
-- [ ] Edit `internal/game/game.go` → `executeCommand()`: add `case "id", "identify":`
-- [ ] Edit `internal/game/action.go` → `AdvanceActions()`: add `case "identify":`
-
-### Phase 2: YAML Data
-
-- [ ] Create `data/items/solar_identifier.yaml`
-- [ ] Create `data/items/hydro_identifier.yaml`
-- [ ] Create `data/items/eco_identifier.yaml`
-- [ ] Create `data/items/bio_identifier.yaml`
-- [ ] Create `data/items/solarjunk.yaml`
-- [ ] Create `data/items/hydrojunk.yaml`
-- [ ] Create `data/items/ecojunk.yaml`
-- [ ] Create `data/items/biojunk.yaml`
-- [ ] Create `data/objects/solar_altar.yaml`
-- [ ] Create `data/objects/hydro_altar.yaml`
-- [ ] Create `data/objects/eco_altar.yaml`
-- [ ] Create `data/objects/bio_altar.yaml`
-
-### Phase 3: Rooms
-
-- [ ] Update `data/rooms/9.yaml` — add scrap pile objects, update description, add exits to altar rooms
-- [ ] Create altar room YAML files (4 rooms) — determine next available room IDs
-- [ ] Ensure exits are bidirectional (altar rooms exit back to room 9 or a hub)
-
-### Phase 4: Help
-
-- [ ] Create `data/help/id.yaml`
-- [ ] Create `data/help/scavenging.yaml`
-
-### Phase 5: Verify
-
-- [ ] `make build` — compiles without errors
-- [ ] `make vet` — no vet warnings
-- [ ] `make test` — all tests pass
-- [ ] Manual test: mine scrap at room 9, walk to altar, `id`, verify conversion
-- [ ] Test: identifier not in inventory → correct error message
-- [ ] Test: no scrap in inventory → correct error message
-- [ ] Test: no altar in room → correct error message
-- [ ] Test: level too low → correct error message
-- [ ] Test: inventory full (no room for junk) → correct error message
-- [ ] Test: multiplier works at higher levels
-- [ ] Test: XP is awarded correctly
-- [ ] Test: level-up message displays
-- [ ] Test: `help id` and `help scavenging` display correctly
-
----
-
-## 15. Design Decisions & Edge Cases
-
-### Why not use the recipe/production system?
-
-The production system (`action_production.go`) processes items one at a time in a loop with "How many?" prompts and per-cycle timing. Scavenging converts ALL scrap in a single tick with level-based multipliers. The mechanics are fundamentally different:
-- Production: consume 1 input → wait N ticks → produce 1 output → repeat
-- Identify: consume ALL scrap → wait 1 tick → produce (scrap × multiplier) junk
-
-A custom action type (`identify`) is cleaner than bending the production system.
-
-### Scrap is non-stackable (intentional)
-
-Like rune essence in OSRS, scrap metal fills one inventory slot per piece. This limits trips to 28 scrap maximum, creating a meaningful gameplay loop of mine → walk → identify → repeat.
-
-### Identifier tools are NOT consumed
-
-Identifiers are reusable tools (like a talisman in OSRS). Players buy/find them once and keep them. They take 1 inventory slot, reducing max scrap per trip to 27.
-
-### What if the player has multiple types of identifiers?
-
-The `id` command scans for altars in the room, not identifiers. It finds which altar is present, then checks for the matching identifier. Only one altar type per room. No ambiguity.
-
-### What if multiple altars are in the same room?
-
-The first matching altar in `altarConfigs` order wins (solar → hydro → eco → bio). In practice, each room should have exactly one altar.
-
-### Junk items are stackable
-
-Unlike scrap, junk stacks in a single inventory slot. A player can accumulate unlimited junk in one slot. This is intentional — junk is the "rune" equivalent and will be consumed by future skills/spells.
-
-### XP is per scrap, not per junk
-
-If a player converts 28 scrap at 3x multiplier, they get 84 junk but only 28 × (xp_per) XP. The multiplier rewards efficiency (more output per trip) without inflating XP rates.
-
-### The `countItem` helper
-
-Before implementing, check if `player.Player` already has a `CountItem(id string) int` method. The production system uses `p.CountItem` — see `action_production.go:221` where `recipe.HasAllItemsQty(p.CountItem)` passes it as a function. If `CountItem` exists, use it directly instead of writing a new helper. If it returns a count, `p.CountItem("scrap_metal")` gives the total.
-
-### Action cancellation
-
-The `identify` action is a 1-tick action. If the player moves or starts another action before it fires, the action is cancelled naturally (CancelAction is called). The scrap is NOT consumed until `advanceIdentify` runs, so cancellation is safe — no items lost.