diff options
| author | historia <[not public]> | 2026-06-19 02:08:55 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-19 02:08:55 -0400 |
| commit | 458916fb78dcef3ff77cb29b7ba98d9f646819fc (patch) | |
| tree | 19cd8b8eef41a1aa00d155bf7953ad2770f91a4c /skill_plans/scavenging.md | |
| parent | 97a1a908b9e6e6d3516628c139c9b64262b4fe08 (diff) | |
| download | thehouseoficarus-458916fb78dcef3ff77cb29b7ba98d9f646819fc.tar.gz | |
feat: rough pharmacy skill added
Diffstat (limited to 'skill_plans/scavenging.md')
| -rw-r--r-- | skill_plans/scavenging.md | 950 |
1 files changed, 950 insertions, 0 deletions
diff --git a/skill_plans/scavenging.md b/skill_plans/scavenging.md new file mode 100644 index 0000000..5dc7d39 --- /dev/null +++ b/skill_plans/scavenging.md @@ -0,0 +1,950 @@ +# 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. |
