diff options
| author | historia <[not public]> | 2026-06-19 18:33:43 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-19 18:33:43 -0400 |
| commit | 2bec24859af8f03c80a0a58e3240519942450167 (patch) | |
| tree | dcb8baa5591fdeca51e179a63a08d46b014cb85c /skill_plans/scavenging.md | |
| parent | 0ea4eec5dd2122c6704216971eb1249276297867 (diff) | |
| download | thehouseoficarus-2bec24859af8f03c80a0a58e3240519942450167.tar.gz | |
readme
Diffstat (limited to 'skill_plans/scavenging.md')
| -rw-r--r-- | skill_plans/scavenging.md | 950 |
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. |
