aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/science.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/science.md
parent0ea4eec5dd2122c6704216971eb1249276297867 (diff)
downloadthehouseoficarus-2bec24859af8f03c80a0a58e3240519942450167.tar.gz
readme
Diffstat (limited to 'skill_plans/science.md')
-rw-r--r--skill_plans/science.md3115
1 files changed, 0 insertions, 3115 deletions
diff --git a/skill_plans/science.md b/skill_plans/science.md
deleted file mode 100644
index b3c1cb7..0000000
--- a/skill_plans/science.md
+++ /dev/null
@@ -1,3115 +0,0 @@
-# Science Skill Implementation Plan
-
-## 1. Overview
-
-Science is the Magic equivalent in The House of Icarus. The terminology mapping is:
-
-| OSRS | House of Icarus |
-|------|-----------------|
-| Spells | Mods (modules) |
-| Casting | Triggering |
-| Runes | Junk |
-| Staves | Decks |
-| Rune Essence | Scrap (`scrap_metal`) |
-| Spellbook | Mod list |
-| Body/Mind runes | Not implemented (too low-level) |
-| Fire/Water/Earth/Air runes | Solarjunk/Hydrojunk/Ecojunk/Biojunk |
-| Chaos/Death/Blood/Law/Cosmic/Nature runes | Chaosjunk/Deathjunk/Bloodjunk/Lawjunk/Cosmicjunk/Naturejunk |
-
-**Core mechanic:** Every mod costs a combination of junk types plus 1 scrap. Wielding ANY deck (weapon_type: science) in main_hand removes the scrap requirement. Wielding a specific elemental deck ALSO provides an unlimited supply of that deck's base junk type (e.g., Solar Deck provides unlimited solarjunk).
-
-**Existing code:**
-- `Science SkillName = "science"` at `internal/player/player.go:14`
-- `SkillAbbr["science"] = "sci"` at `internal/player/player.go:46`
-- `WeaponScience WeaponType = "science"` at `internal/object/item.go:30`
-- `ScienceBonus int` on `ItemStats` at `internal/object/item.go:71` (currently unused)
-- `CombatLevel()` includes `+0.125 * Science` at `internal/player/player.go:325`
-- No magic/science system, combat formulas, autocast, or mod definitions exist
-
-**Dependencies:**
-- `scavenging.md` must be implemented first for base junk types (solarjunk, hydrojunk, ecojunk, biojunk) and scrap_metal
-- `combat.md` changes needed for science attack/defense bonuses on items and mobs (see Section 7)
-- The 6 higher-tier junk types (chaos, death, blood, law, cosmic, nature) require additional altars added to scavenging — see Section 11
-
----
-
-## 2. Commands
-
-### `trigger` / `cast` (ClassActive)
-
-| Property | Value |
-|----------|-------|
-| Command | `trigger` |
-| Aliases | `cast` |
-| Class | `ClassActive` |
-| Handler | `g.doTrigger(sess, args)` |
-| File | `internal/game/cmd_trigger.go` |
-
-Always ClassActive. For combat mods, initiates science combat (autocasting loop). For utility mods, executes on the next tick (1-tick action).
-
-**Usage:**
-```
-trigger <mod_name> # utility mod or combat with default target
-trigger <mod_name> <target> # combat mod on specific mob
-cast solar bolt # alias, prefix matching
-trigger low process # utility mod
-trigger transport town # teleport
-trigger enchant 1 <jewelry> # enchant an inventory item
-trigger em grab <ground_item> # pick up ground item via science
-trigger superheat <ore> # smelt without furnace
-```
-
-### `autocast` / `auto` (ClassInstant)
-
-| Property | Value |
-|----------|-------|
-| Command | `autocast` |
-| Aliases | `auto` |
-| Class | `ClassInstant` |
-| Handler | `g.doAutocast(sess, args)` |
-| File | `internal/game/cmd_autocast.go` |
-
-Sets the autocast mod for science combat. When autocast is set and player uses `attack <mob>`, each combat tick triggers the autocast mod instead of a melee attack.
-
-**Usage:**
-```
-autocast solar bolt # set autocast to solar_bolt (prefix match)
-autocast off # disable autocast
-auto hydro surge # alias
-autocast # show current autocast
-```
-
-### `mods` / `modlist` (ClassInstant)
-
-| Property | Value |
-|----------|-------|
-| Command | `mods` |
-| Aliases | `modlist` |
-| Class | `ClassInstant` |
-| Handler | `g.doMods(sess)` |
-| File | `internal/game/cmd_mods.go` |
-
-Displays all mods the player has the Science level to use, organized by category, with junk costs.
-
-### Classification Changes
-
-**File: `internal/game/game.go`, `classifyCommand()` at line 136:**
-
-Add to `ClassInstant` case:
-```go
-case "say", "score", "sc", "inventory", "i", "inv",
- "look", "l", "exits", "help",
- "map", "option", "options", "alias", "unalias",
- "description", "desc", "queued", "color", "colors",
- "colortable", "prompt", "style",
- "autocast", "auto", "mods", "modlist":
- return ClassInstant
-```
-
-Add to `ClassActive` case:
-```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",
- "trigger", "cast":
- return ClassActive
-```
-
-### Dispatch Changes
-
-**File: `internal/game/game.go`, `executeCommand()` at line 249:**
-
-Add cases:
-```go
-case "autocast", "auto":
- g.doAutocast(sess, strings.Join(args, " "))
-case "mods", "modlist":
- g.doMods(sess)
-case "trigger", "cast":
- g.doTrigger(sess, strings.Join(args, " "))
- return
-```
-
----
-
-## 3. Mod Definition Structure
-
-Mods are hardcoded in Go (not YAML-driven). Defined in `internal/game/science.go`.
-
-```go
-package game
-
-type ModCategory string
-
-const (
- ModCombat ModCategory = "combat"
- ModUtility ModCategory = "utility"
- ModEnchant ModCategory = "enchant"
- ModProcessing ModCategory = "processing"
- ModTransport ModCategory = "transport"
-)
-
-type ModDef struct {
- ID string
- Name string
- Level int
- MaxHit int
- BaseXP float64
- JunkCost map[string]int
- Category ModCategory
- Element string
- TargetType string // "mob", "inventory", "self", "ground_item"
-}
-
-var AllMods []*ModDef
-
-var modByID map[string]*ModDef
-
-func init() {
- modByID = make(map[string]*ModDef, len(AllMods))
- for _, m := range AllMods {
- modByID[m.ID] = m
- }
-}
-
-func GetMod(id string) *ModDef {
- return modByID[id]
-}
-
-func FindMod(input string) *ModDef {
- // Exact match first
- if m, ok := modByID[input]; ok {
- return m
- }
- // Prefix match on ID (underscores stripped for matching)
- lower := strings.ToLower(strings.ReplaceAll(input, " ", "_"))
- for _, m := range AllMods {
- if strings.HasPrefix(m.ID, lower) {
- return m
- }
- }
- // Prefix match on Name
- lowerSpace := strings.ToLower(input)
- for _, m := range AllMods {
- if strings.HasPrefix(strings.ToLower(m.Name), lowerSpace) {
- return m
- }
- }
- return nil
-}
-```
-
----
-
-## 4. Combat Mods
-
-All combat mods have `Category: ModCombat`, `TargetType: "mob"`. The `Element` field determines elemental weakness bonuses.
-
-All junk costs below include `"scrap_metal": 1` which is removed if the player has ANY deck equipped.
-
-### Bio Strikes (Air spell equivalents — lowest level)
-
-| ID | Name | Level | Max Hit | Junk Cost | XP |
-|---|---|---|---|---|---|
-| `bio_strike` | Bio Strike | 1 | 4 | 2 biojunk, 1 scrap | 5.5 |
-| `bio_bolt` | Bio Bolt | 17 | 9 | 2 biojunk, 1 chaosjunk, 1 scrap | 13.5 |
-| `bio_blast` | Bio Blast | 41 | 13 | 3 biojunk, 1 chaosjunk, 1 deathjunk, 1 scrap | 25.5 |
-| `bio_wave` | Bio Wave | 62 | 17 | 5 biojunk, 1 deathjunk, 1 bloodjunk, 1 scrap | 36.0 |
-| `bio_surge` | Bio Surge | 81 | 21 | 7 biojunk, 1 bloodjunk, 1 scrap | 44.0 |
-
-```go
-{ID: "bio_strike", Name: "Bio Strike", Level: 1, MaxHit: 4, BaseXP: 5.5,
- JunkCost: map[string]int{"biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
-{ID: "bio_bolt", Name: "Bio Bolt", Level: 17, MaxHit: 9, BaseXP: 13.5,
- JunkCost: map[string]int{"biojunk": 2, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
-{ID: "bio_blast", Name: "Bio Blast", Level: 41, MaxHit: 13, BaseXP: 25.5,
- JunkCost: map[string]int{"biojunk": 3, "chaosjunk": 1, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
-{ID: "bio_wave", Name: "Bio Wave", Level: 62, MaxHit: 17, BaseXP: 36.0,
- JunkCost: map[string]int{"biojunk": 5, "deathjunk": 1, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
-{ID: "bio_surge", Name: "Bio Surge", Level: 81, MaxHit: 21, BaseXP: 44.0,
- JunkCost: map[string]int{"biojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
-```
-
-### Hydro Strikes (Water spell equivalents)
-
-| ID | Name | Level | Max Hit | Junk Cost | XP |
-|---|---|---|---|---|---|
-| `hydro_strike` | Hydro Strike | 5 | 6 | 3 hydrojunk, 1 ecojunk, 1 scrap | 7.5 |
-| `hydro_bolt` | Hydro Bolt | 23 | 10 | 3 hydrojunk, 2 ecojunk, 1 scrap | 16.5 |
-| `hydro_blast` | Hydro Blast | 47 | 14 | 5 hydrojunk, 3 ecojunk, 1 chaosjunk, 1 scrap | 28.5 |
-| `hydro_wave` | Hydro Wave | 65 | 18 | 7 hydrojunk, 5 ecojunk, 1 deathjunk, 1 scrap | 37.5 |
-| `hydro_surge` | Hydro Surge | 85 | 22 | 10 hydrojunk, 7 ecojunk, 1 bloodjunk, 1 scrap | 46.0 |
-
-```go
-{ID: "hydro_strike", Name: "Hydro Strike", Level: 5, MaxHit: 6, BaseXP: 7.5,
- JunkCost: map[string]int{"hydrojunk": 3, "ecojunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
-{ID: "hydro_bolt", Name: "Hydro Bolt", Level: 23, MaxHit: 10, BaseXP: 16.5,
- JunkCost: map[string]int{"hydrojunk": 3, "ecojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
-{ID: "hydro_blast", Name: "Hydro Blast", Level: 47, MaxHit: 14, BaseXP: 28.5,
- JunkCost: map[string]int{"hydrojunk": 5, "ecojunk": 3, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
-{ID: "hydro_wave", Name: "Hydro Wave", Level: 65, MaxHit: 18, BaseXP: 37.5,
- JunkCost: map[string]int{"hydrojunk": 7, "ecojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
-{ID: "hydro_surge", Name: "Hydro Surge", Level: 85, MaxHit: 22, BaseXP: 46.0,
- JunkCost: map[string]int{"hydrojunk": 10, "ecojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
-```
-
-### Eco Strikes (Earth spell equivalents)
-
-| ID | Name | Level | Max Hit | Junk Cost | XP |
-|---|---|---|---|---|---|
-| `eco_strike` | Eco Strike | 9 | 7 | 2 ecojunk, 2 biojunk, 1 scrap | 9.5 |
-| `eco_bolt` | Eco Bolt | 29 | 11 | 3 ecojunk, 2 biojunk, 1 scrap | 19.5 |
-| `eco_blast` | Eco Blast | 53 | 15 | 4 ecojunk, 3 biojunk, 1 chaosjunk, 1 scrap | 31.5 |
-| `eco_wave` | Eco Wave | 70 | 19 | 7 ecojunk, 5 biojunk, 1 deathjunk, 1 scrap | 40.0 |
-| `eco_surge` | Eco Surge | 90 | 23 | 10 ecojunk, 7 biojunk, 1 bloodjunk, 1 scrap | 48.5 |
-
-```go
-{ID: "eco_strike", Name: "Eco Strike", Level: 9, MaxHit: 7, BaseXP: 9.5,
- JunkCost: map[string]int{"ecojunk": 2, "biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
-{ID: "eco_bolt", Name: "Eco Bolt", Level: 29, MaxHit: 11, BaseXP: 19.5,
- JunkCost: map[string]int{"ecojunk": 3, "biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
-{ID: "eco_blast", Name: "Eco Blast", Level: 53, MaxHit: 15, BaseXP: 31.5,
- JunkCost: map[string]int{"ecojunk": 4, "biojunk": 3, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
-{ID: "eco_wave", Name: "Eco Wave", Level: 70, MaxHit: 19, BaseXP: 40.0,
- JunkCost: map[string]int{"ecojunk": 7, "biojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
-{ID: "eco_surge", Name: "Eco Surge", Level: 90, MaxHit: 23, BaseXP: 48.5,
- JunkCost: map[string]int{"ecojunk": 10, "biojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
-```
-
-### Solar Strikes (Fire spell equivalents — highest level)
-
-| ID | Name | Level | Max Hit | Junk Cost | XP |
-|---|---|---|---|---|---|
-| `solar_strike` | Solar Strike | 13 | 8 | 3 solarjunk, 2 ecojunk, 1 scrap | 11.5 |
-| `solar_bolt` | Solar Bolt | 35 | 12 | 4 solarjunk, 3 ecojunk, 1 scrap | 22.5 |
-| `solar_blast` | Solar Blast | 59 | 16 | 5 solarjunk, 4 ecojunk, 1 chaosjunk, 1 scrap | 34.5 |
-| `solar_wave` | Solar Wave | 75 | 20 | 7 solarjunk, 5 ecojunk, 1 deathjunk, 1 scrap | 42.5 |
-| `solar_surge` | Solar Surge | 95 | 24 | 10 solarjunk, 7 ecojunk, 1 bloodjunk, 1 scrap | 51.0 |
-
-```go
-{ID: "solar_strike", Name: "Solar Strike", Level: 13, MaxHit: 8, BaseXP: 11.5,
- JunkCost: map[string]int{"solarjunk": 3, "ecojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
-{ID: "solar_bolt", Name: "Solar Bolt", Level: 35, MaxHit: 12, BaseXP: 22.5,
- JunkCost: map[string]int{"solarjunk": 4, "ecojunk": 3, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
-{ID: "solar_blast", Name: "Solar Blast", Level: 59, MaxHit: 16, BaseXP: 34.5,
- JunkCost: map[string]int{"solarjunk": 5, "ecojunk": 4, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
-{ID: "solar_wave", Name: "Solar Wave", Level: 75, MaxHit: 20, BaseXP: 42.5,
- JunkCost: map[string]int{"solarjunk": 7, "ecojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
-{ID: "solar_surge", Name: "Solar Surge", Level: 95, MaxHit: 24, BaseXP: 51.0,
- JunkCost: map[string]int{"solarjunk": 10, "ecojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
-```
-
----
-
-## 5. Utility Mods
-
-### Processing Mods (Alchemy equivalents)
-
-| ID | Name | Level | Junk Cost | XP | Effect |
-|---|---|---|---|---|---|
-| `low_process` | Low Level Processing | 21 | 3 naturejunk, 1 solarjunk, 1 scrap | 31.0 | Convert inventory item to credits at 50% of `value` |
-| `high_process` | High Level Processing | 55 | 5 naturejunk, 1 solarjunk, 1 scrap | 65.0 | Convert inventory item to credits at 100% of `value` |
-
-```go
-{ID: "low_process", Name: "Low Level Processing", Level: 21, MaxHit: 0, BaseXP: 31.0,
- JunkCost: map[string]int{"naturejunk": 3, "solarjunk": 1, "scrap_metal": 1},
- Category: ModProcessing, Element: "", TargetType: "inventory"},
-{ID: "high_process", Name: "High Level Processing", Level: 55, MaxHit: 0, BaseXP: 65.0,
- JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap_metal": 1},
- Category: ModProcessing, Element: "", TargetType: "inventory"},
-```
-
-**Implementation:**
-1. Player types `trigger low_process <item>` or `trigger low process <item>`
-2. `doTrigger` resolves the mod via `FindMod`
-3. Finds the target item in player inventory via `findInventoryMatches`
-4. Checks junk cost (see Section 16)
-5. Creates a 1-tick action
-6. On advance: consume junk, remove 1 of the item, add credits (`item.Value / 2` for low, `item.Value` for high)
-7. Output: `"You process the <item>. You receive <N> credits."`
-8. Award XP to Science
-
-### Bones to Nutrients
-
-| ID | Name | Level | Junk Cost | XP | Effect |
-|---|---|---|---|---|---|
-| `bones_to_nutrients` | Bones to Nutrients | 15 | 2 naturejunk, 2 ecojunk, 1 scrap | 25.0 | Convert ALL `bones` in inventory to `nutrient_bar` |
-
-```go
-{ID: "bones_to_nutrients", Name: "Bones to Nutrients", Level: 15, MaxHit: 0, BaseXP: 25.0,
- JunkCost: map[string]int{"naturejunk": 2, "ecojunk": 2, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "self"},
-```
-
-**Implementation:**
-1. `trigger bones to nutrients` (no target argument needed)
-2. Count all `bones` items in inventory
-3. If 0: `"You don't have any bones."`
-4. Consume junk cost (one-time cost, not per bone)
-5. Replace each `bones` inventory slot with `nutrient_bar` (new item, stackable, heal_value: 2)
-6. Output: `"You convert <N> bones into nutrient bars."`
-7. Award `25.0 * N` XP to Science (XP per bone converted)
-
-### Electromagnetic Grab (Telekinetic Grab equivalent)
-
-| ID | Name | Level | Junk Cost | XP | Effect |
-|---|---|---|---|---|---|
-| `em_grab` | Electromagnetic Grab | 33 | 1 lawjunk, 1 biojunk, 1 scrap | 43.0 | Pick up a ground item, bypassing reservation |
-
-```go
-{ID: "em_grab", Name: "Electromagnetic Grab", Level: 33, MaxHit: 0, BaseXP: 43.0,
- JunkCost: map[string]int{"lawjunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "ground_item"},
-```
-
-**Implementation:**
-1. `trigger em grab <item>`
-2. Find matching ground item in room via existing `findGroundMatches` logic
-3. Consume junk cost
-4. Pick up item to inventory (bypass reservation — do NOT check `ReservedFor`)
-5. If inventory full: `"Your inventory is full."`
-6. Output: `"You magnetically pull the <item> toward you."`
-7. Award XP
-
-### Superheat Item
-
-| ID | Name | Level | Junk Cost | XP | Effect |
-|---|---|---|---|---|---|
-| `superheat` | Superheat Item | 43 | 4 naturejunk, 1 solarjunk, 1 scrap | 53.0 | Smelt ore into bar without furnace |
-
-```go
-{ID: "superheat", Name: "Superheat Item", Level: 43, MaxHit: 0, BaseXP: 53.0,
- JunkCost: map[string]int{"naturejunk": 4, "solarjunk": 1, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "inventory"},
-```
-
-**Implementation:**
-1. `trigger superheat <ore>`
-2. Find matching item in inventory
-3. Look up the smelting recipe that uses this ore (search `RecipeStore` for type "smelt" recipes containing this item)
-4. If no recipe: `"You can't superheat that."`
-5. Check player has all required items for the recipe in inventory
-6. Check player meets the recipe's skill requirement
-7. Consume junk cost + recipe inputs
-8. Add recipe output to inventory
-9. Award `53.0` XP to Science + the recipe's smithing XP
-10. Output: `"You superheat the <ore> and produce a <bar>."`
-
-### Transport Mods (Teleport equivalents)
-
-| ID | Name | Level | Junk Cost | XP | Destination |
-|---|---|---|---|---|---|
-| `transport_town` | Transport: Town Square | 25 | 1 lawjunk, 1 solarjunk, 1 biojunk, 1 scrap | 27.0 | Room 1 (Town Square) |
-| `transport_forge` | Transport: Forge | 31 | 1 lawjunk, 1 ecojunk, 1 scrap | 35.0 | Room 12 (Forge) |
-| `transport_mine` | Transport: Mining Pit | 37 | 1 lawjunk, 1 ecojunk, 1 solarjunk, 1 scrap | 40.0 | Room 6 (Mining Pit) |
-| `transport_forest` | Transport: Forest | 45 | 1 lawjunk, 1 ecojunk, 1 biojunk, 1 scrap | 48.0 | Room 22 (Forest area) |
-| `transport_scavenge` | Transport: Scavenging Post | 51 | 1 lawjunk, 1 naturejunk, 1 scrap | 52.0 | Room 9 (Scavenging Post) |
-| `transport_deep_mine` | Transport: Deep Mine | 61 | 2 lawjunk, 1 ecojunk, 1 scrap | 60.0 | Room 7 (Deep mining area) |
-| `transport_fishing` | Transport: Fishing Dock | 55 | 1 lawjunk, 1 hydrojunk, 1 biojunk, 1 scrap | 56.0 | Room 10 (Fishing area) |
-
-```go
-{ID: "transport_town", Name: "Transport: Town Square", Level: 25, MaxHit: 0, BaseXP: 27.0,
- JunkCost: map[string]int{"lawjunk": 1, "solarjunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self"},
-{ID: "transport_forge", Name: "Transport: Forge", Level: 31, MaxHit: 0, BaseXP: 35.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self"},
-{ID: "transport_mine", Name: "Transport: Mining Pit", Level: 37, MaxHit: 0, BaseXP: 40.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "solarjunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self"},
-{ID: "transport_forest", Name: "Transport: Forest", Level: 45, MaxHit: 0, BaseXP: 48.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self"},
-{ID: "transport_scavenge", Name: "Transport: Scavenging Post", Level: 51, MaxHit: 0, BaseXP: 52.0,
- JunkCost: map[string]int{"lawjunk": 1, "naturejunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self"},
-{ID: "transport_deep_mine", Name: "Transport: Deep Mine", Level: 61, MaxHit: 0, BaseXP: 60.0,
- JunkCost: map[string]int{"lawjunk": 2, "ecojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self"},
-{ID: "transport_fishing", Name: "Transport: Fishing Dock", Level: 55, MaxHit: 0, BaseXP: 56.0,
- JunkCost: map[string]int{"lawjunk": 1, "hydrojunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self"},
-```
-
-**Transport Implementation:**
-1. `trigger transport town`
-2. Check level, check junk cost
-3. If player is in combat: `"You can't teleport during combat!"`
-4. Cancel any active action
-5. Create a 3-tick action (`ActionTriggering`)
-6. Tick 1: `"You begin activating the transport module..."`
-7. Tick 2: `"The world shimmers around you..."`
-8. Tick 3: Consume junk, teleport player, award XP
-9. If player takes damage (mob hit) during the cast, cancel: `"Your transport was interrupted!"`
-10. On completion: move player to destination room, `g.Hub.EnterRoom(sess, destRoom)`, `g.doLook(sess)`
-11. Output: `"You materialize at <room_name>."`
-
-The `ModDef` needs a `Destination int` field for transport mods. Add this to the struct:
-
-```go
-type ModDef struct {
- ID string
- Name string
- Level int
- MaxHit int
- BaseXP float64
- JunkCost map[string]int
- Category ModCategory
- Element string
- TargetType string
- Destination int // room ID for transport mods (0 = N/A)
-}
-```
-
-Set `Destination` on each transport mod:
-- `transport_town`: `Destination: 1`
-- `transport_forge`: `Destination: 12`
-- `transport_mine`: `Destination: 6`
-- `transport_forest`: `Destination: 22`
-- `transport_scavenge`: `Destination: 9`
-- `transport_deep_mine`: `Destination: 7`
-- `transport_fishing`: `Destination: 10`
-
----
-
-## 6. Enchant Mods
-
-### Jewelry Enchantment
-
-| ID | Name | Level | Junk Cost | XP | Effect |
-|---|---|---|---|---|---|
-| `enchant_1` | Enchant Level 1 | 7 | 1 cosmicjunk, 1 hydrojunk, 1 scrap | 17.5 | Enchant sapphire jewelry |
-| `enchant_2` | Enchant Level 2 | 27 | 1 cosmicjunk, 3 biojunk, 1 scrap | 37.0 | Enchant emerald jewelry |
-| `enchant_3` | Enchant Level 3 | 49 | 1 cosmicjunk, 5 solarjunk, 1 scrap | 59.0 | Enchant ruby jewelry |
-| `enchant_4` | Enchant Level 4 | 57 | 1 cosmicjunk, 10 ecojunk, 1 scrap | 67.0 | Enchant diamond jewelry |
-
-```go
-{ID: "enchant_1", Name: "Enchant Level 1", Level: 7, MaxHit: 0, BaseXP: 17.5,
- JunkCost: map[string]int{"cosmicjunk": 1, "hydrojunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-{ID: "enchant_2", Name: "Enchant Level 2", Level: 27, MaxHit: 0, BaseXP: 37.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "biojunk": 3, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-{ID: "enchant_3", Name: "Enchant Level 3", Level: 49, MaxHit: 0, BaseXP: 59.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "solarjunk": 5, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-{ID: "enchant_4", Name: "Enchant Level 4", Level: 57, MaxHit: 0, BaseXP: 67.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "ecojunk": 10, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-```
-
-**Enchantment mapping** — hardcoded in `science.go`:
-
-```go
-var enchantMap = map[string]map[string]string{
- "enchant_1": {
- "sapphire_ring": "ring_of_recoil",
- "sapphire_necklace": "necklace_of_passage",
- "sapphire_bracelet": "bracelet_of_clay",
- },
- "enchant_2": {
- "emerald_ring": "ring_of_dueling",
- "emerald_necklace": "binding_necklace",
- "emerald_bracelet": "bracelet_of_slaughter",
- },
- "enchant_3": {
- "ruby_ring": "ring_of_forging",
- "ruby_necklace": "digsite_pendant",
- "ruby_bracelet": "inoculation_bracelet",
- },
- "enchant_4": {
- "diamond_ring": "ring_of_life",
- "diamond_necklace": "phoenix_necklace",
- "diamond_bracelet": "abyssal_bracelet",
- },
-}
-```
-
-**Implementation:**
-1. `trigger enchant 1 <jewelry item>`
-2. Find matching item in inventory
-3. Check the item is a valid input for the enchant level (look up `enchantMap[mod.ID][item.ID]`)
-4. If not valid: `"You can't enchant that with this mod."`
-5. Consume junk, remove unenchanted item, add enchanted item
-6. Output: `"You enchant the <item> and it becomes a <result>!"`
-7. Award XP
-
-### Bolt Chipping (Bolt Enchantment equivalents)
-
-| ID | Name | Level | Junk Cost | XP | Effect |
-|---|---|---|---|---|---|
-| `chip_sapphire` | Chip Sapphire Bolts | 4 | 1 cosmicjunk, 1 hydrojunk, 1 scrap | 9.0 | Enchant 10 sapphire bolts |
-| `chip_emerald` | Chip Emerald Bolts | 27 | 1 cosmicjunk, 3 biojunk, 1 scrap | 37.0 | Enchant 10 emerald bolts |
-| `chip_ruby` | Chip Ruby Bolts | 49 | 1 cosmicjunk, 5 solarjunk, 1 bloodjunk, 1 scrap | 59.0 | Enchant 10 ruby bolts |
-| `chip_diamond` | Chip Diamond Bolts | 57 | 1 cosmicjunk, 10 ecojunk, 1 scrap | 67.0 | Enchant 10 diamond bolts |
-
-```go
-{ID: "chip_sapphire", Name: "Chip Sapphire Bolts", Level: 4, MaxHit: 0, BaseXP: 9.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "hydrojunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-{ID: "chip_emerald", Name: "Chip Emerald Bolts", Level: 27, MaxHit: 0, BaseXP: 37.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "biojunk": 3, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-{ID: "chip_ruby", Name: "Chip Ruby Bolts", Level: 49, MaxHit: 0, BaseXP: 59.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "solarjunk": 5, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-{ID: "chip_diamond", Name: "Chip Diamond Bolts", Level: 57, MaxHit: 0, BaseXP: 67.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "ecojunk": 10, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-```
-
-**Bolt chip mapping** — hardcoded in `science.go`:
-
-```go
-var chipMap = map[string]struct {
- Input string
- Output string
- Qty int
-}{
- "chip_sapphire": {"sapphire_bolts", "sapphire_bolts_e", 10},
- "chip_emerald": {"emerald_bolts", "emerald_bolts_e", 10},
- "chip_ruby": {"ruby_bolts", "ruby_bolts_e", 10},
- "chip_diamond": {"diamond_bolts", "diamond_bolts_e", 10},
-}
-```
-
-**Implementation:**
-1. `trigger chip sapphire` (no target needed — auto-finds bolts)
-2. Check player has at least 10 of the input bolt type
-3. Consume junk + 10 bolts, add 10 enchanted bolts
-4. If fewer than 10: `"You need at least 10 sapphire bolts."`
-5. Output: `"You chip 10 sapphire bolts with arcane circuitry."`
-6. Award XP
-
----
-
-## 7. Science Combat Mechanic
-
-### Attack Flow
-
-When a player triggers a combat mod (via `trigger <mod> <mob>` or via autocast during combat):
-
-1. **Level check:** Player Science level >= mod.Level. If not: `"You need level <N> science to trigger <mod>."`
-2. **Junk cost check:** Call `hasJunkCost(p, mod)`. If not: `"You don't have enough junk to trigger <mod>."`
-3. **Consume junk:** Call `consumeJunkCost(p, mod)`. Removes junk from inventory (respecting `provides_junk` and deck scrap exemption).
-4. **Attack roll:** `ScienceAttackRoll = (scienceLevel + 8) * (equipScienceAttack + 64)`
- - `scienceLevel` = `p.Level(player.Science)`
- - `equipScienceAttack` = sum of `ScienceBonus` from ALL equipped items (existing field on `ItemStats`, currently unused)
- - No style bonus for science (science doesn't use attack styles)
-5. **Defense roll:** `MobDefenseRoll = (mobDefLevel + 9) * (mobScienceDefense + 64)`
- - `mobDefLevel` = `mob.Defense` (existing field)
- - `mobScienceDefense` = new field on `MobDef` / `MobInstance` (see Section 7.1)
-6. **Elemental weakness:** If `mob.Weakness == mod.Element`, multiply `ScienceAttackRoll` by 1.3 (30% accuracy bonus)
-7. **Hit check:** `combat.HitCheck(scienceAttackRoll, mobDefenseRoll)`
-8. **Damage:** If hit, `dmg = 1 + rand.Intn(mod.MaxHit)`. Max hit comes from the mod definition, NOT equipment.
-9. **XP:** Award `mod.BaseXP` to Science, `mod.BaseXP * 0.33` to Hitpoints
-
-### Attack Speed
-
-Science combat attack speed is always **5 ticks** (same as OSRS magic). This is the mod trigger speed, regardless of the equipped deck's `speed` field. The deck's `speed` field is only used if the player melees with the deck (which would be unusual but allowed).
-
-### Autocast Attack Replacement
-
-When autocast is set and the player attacks a mob (via `attack <mob>`), the `startCombat` function detects `p.AutocastMod != ""` and uses the science combat path:
-
-1. The player attack subscriber (in `startCombat`, the first `Ticks.Subscribe`) checks `p.AutocastMod`
-2. If autocast is set: call `g.scienceAttack(sess, p, mob, autocastMod)` instead of `g.playerAttack(sess, p, mob)`
-3. If `scienceAttack` returns false (out of junk), disable autocast: `p.AutocastMod = ""`, output `"You've run out of junk. Switching to melee."`, then call `g.playerAttack(sess, p, mob)` for this tick and all future ticks
-4. Attack speed when autocasting: use 5 ticks (science speed), NOT the weapon's melee speed
-
-### Direct `trigger` Combat
-
-When the player types `trigger solar bolt <mob>`:
-
-1. If player is already in combat: switch to using this mod as the current autocast. Output: `"You switch to triggering <mod>."`
-2. If not in combat: start combat with the target mob using science combat (same as `doAttack` but with science path). Set `p.AutocastMod = mod.ID`.
-
-### New Combat Functions
-
-**File: `internal/game/cmd_trigger.go`**
-
-```go
-func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.MobInstance, mod *ModDef) bool {
- if p.Level(player.Science) < mod.Level {
- sess.WriteLine(fmt.Sprintf("You need level %d science to trigger %s.", mod.Level, mod.Name))
- return false
- }
- if !g.hasJunkCost(p, mod) {
- return false // signal out of junk
- }
-
- if g.processConsumeQueue(p, sess) {
- p.ActionState = &ActionState{Type: ActionEating, TargetName: "food"}
- return true // ate food this tick, still have junk
- }
-
- g.consumeJunkCost(p, mod)
-
- equipSciBonus := g.totalEquipScienceAttack(p)
- attRoll := (p.Level(player.Science) + 8) * (equipSciBonus + 64)
-
- mobSciDef := mob.ScienceDefense // new field
- defRoll := (mob.Defense + 9) * (mobSciDef + 64)
-
- if mob.Weakness == mod.Element {
- attRoll = attRoll * 13 / 10 // +30% accuracy
- }
-
- if combat.HitCheck(attRoll, defRoll) {
- dmg := combat.RollDamage(mod.MaxHit)
- mob.HP -= dmg
- if mob.HP < 0 {
- mob.HP = 0
- }
- if mob.HP < mob.MaxHP && mob.HP > 0 {
- mob.StartRegen()
- }
-
- sciXP := int(mod.BaseXP)
- hpXP := int(mod.BaseXP * 0.33)
- var gains []xpGain
- var leveledUp []player.SkillName
-
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- leveledUp = append(leveledUp, player.Science)
- }
- gains = append(gains, xpGain{string(player.Science), sciXP})
-
- if newLevel := p.AddSkillXP(player.Hitpoints, hpXP); newLevel > 0 {
- leveledUp = append(leveledUp, player.Hitpoints)
- }
- gains = append(gains, xpGain{string(player.Hitpoints), hpXP})
-
- g.AccountStore.SaveCharacter(p)
-
- for _, skill := range leveledUp {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill)))
- }
-
- mobName := mobDisplayName(mob, true)
- prefix := fmt.Sprintf(" %s hits %s for %s damage.",
- g.colorize(sess, "science_mod", mod.Name),
- g.colorize(sess, "mob_name", mobName),
- g.colorize(sess, "damage", fmt.Sprint(dmg)))
- hpPart := fmt.Sprintf("[%s/%dhp]", g.colorize(sess, "enemy_hp", fmt.Sprint(mob.HP)), mob.MaxHP)
- line := prefix + " " + hpPart
- if p.OptionBool("xp_drops") && len(gains) > 0 {
- var parts []string
- for _, gain := range gains {
- parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)]))
- }
- line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")")
- }
- sess.WriteLine(line)
- } else {
- sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" %s fails to connect.", mod.Name)))
- }
- return true
-}
-
-func (g *Game) totalEquipScienceAttack(p *player.Player) int {
- total := 0
- for _, itemID := range p.Equipment {
- def, err := g.ItemStore.Load(itemID)
- if err == nil {
- total += def.Stats.ScienceBonus
- }
- }
- return total
-}
-```
-
-### 7.1 Mob Science Defense and Weakness Fields
-
-Add two new fields to `MobDef` and `MobInstance`:
-
-**File: `internal/world/mob.go`**
-
-In `MobDef` struct (after `Defense` field at line 29):
-```go
-ScienceDefense int `yaml:"science_defense"`
-Weakness string `yaml:"weakness"`
-```
-
-In `MobInstance` struct (after `Defense` field at line 48):
-```go
-ScienceDefense int
-Weakness string
-```
-
-In the mob instantiation logic (wherever `MobInstance` is created from `MobDef`), copy these fields:
-```go
-inst.ScienceDefense = def.ScienceDefense
-inst.Weakness = def.Weakness
-```
-
-Mob YAML example with weakness:
-```yaml
-id: fire_elemental
-name: fire elemental
-weakness: hydro # weak to hydro (water) mods — +30% accuracy
-science_defense: 20
-```
-
----
-
-## 8. Autocast System
-
-### Player Field
-
-**File: `internal/player/player.go`**
-
-Add to `Player` struct (after `VisualTickCurrent` at line 174):
-```go
-AutocastMod string `yaml:"-"`
-```
-
-The `yaml:"-"` tag means autocast is NOT saved to character YAML. Autocast resets on logout.
-
-### `cmd_autocast.go`
-
-```go
-package game
-
-import (
- "fmt"
- "strings"
-
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) doAutocast(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- input = strings.TrimSpace(input)
-
- if input == "" {
- if p.AutocastMod == "" {
- sess.WriteLine("No autocast mod set. Use 'autocast <mod>' to set one.")
- } else {
- mod := GetMod(p.AutocastMod)
- if mod == nil {
- sess.WriteLine("Autocast: none (invalid mod)")
- p.AutocastMod = ""
- } else {
- sess.WriteLine(fmt.Sprintf("Autocast: %s (Lv%d)", mod.Name, mod.Level))
- }
- }
- return
- }
-
- if strings.ToLower(input) == "off" {
- p.AutocastMod = ""
- sess.WriteLine("Autocast disabled.")
- return
- }
-
- mod := FindMod(strings.ToLower(input))
- if mod == nil {
- sess.WriteLine("Unknown mod.")
- return
- }
-
- if mod.Category != ModCombat {
- sess.WriteLine("You can only autocast combat mods.")
- return
- }
-
- if p.Level(player.Science) < mod.Level {
- sess.WriteLine(fmt.Sprintf("You need level %d science to autocast %s.", mod.Level, mod.Name))
- return
- }
-
- p.AutocastMod = mod.ID
- sess.WriteLine(fmt.Sprintf("Autocast set to: %s", mod.Name))
-}
-```
-
-### Integration with `cmd_attack.go`
-
-**File: `internal/game/cmd_attack.go`**
-
-Modify `startCombat()` to detect autocast. The key change is in the player attack subscriber:
-
-Replace the player attack subscriber in `startCombat` (lines 138-154):
-
-```go
-playerSpeed := g.playerWeaponSpeed(p)
-autocastActive := p.AutocastMod != ""
-if autocastActive {
- playerSpeed = 5.0 // science combat speed
-}
-
-// ... (existing style display code, but skip style display if autocasting)
-
-g.Ticks.Subscribe(engine.ToTicks(playerSpeed), func() bool {
- cs := combat.GetCombat(p.Name)
- if cs == nil || !cs.Active {
- return false
- }
- currentMob := g.MobStore.GetInstance(cs.MobID)
- if currentMob == nil || currentMob.HP <= 0 {
- g.endCombat(sess, p, currentMob)
- return false
- }
-
- if p.AutocastMod != "" {
- mod := GetMod(p.AutocastMod)
- if mod != nil {
- if !g.scienceAttack(sess, p, currentMob, mod) {
- p.AutocastMod = ""
- sess.WriteLine("You've run out of junk. Switching to melee.")
- g.playerAttack(sess, p, currentMob)
- }
- } else {
- p.AutocastMod = ""
- g.playerAttack(sess, p, currentMob)
- }
- } else {
- g.playerAttack(sess, p, currentMob)
- }
-
- if currentMob.HP <= 0 {
- g.endCombat(sess, p, currentMob)
- return false
- }
- return true
-})
-```
-
-Also modify the initial combat message in `startCombat`:
-```go
-if autocastActive {
- mod := GetMod(p.AutocastMod)
- sess.WriteLine(fmt.Sprintf("\nYou attack %s with %s!",
- g.colorize(sess, "mob_name", mobDisplayName(mob, true)),
- g.colorize(sess, "science_mod", mod.Name)))
-} else {
- // existing style display
- sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", ...))
-}
-```
-
-### XP Distribution for Science Combat
-
-Science combat does NOT use attack styles. XP is always:
-- `mod.BaseXP` to Science
-- `mod.BaseXP * 0.33` to Hitpoints
-
-This replaces the melee XP distribution in `awardCombatXP`. The `scienceAttack` function handles XP directly (see Section 7 code).
-
----
-
-## 9. Deck Items
-
-### `provides_junk` Field
-
-**File: `internal/object/item.go`**
-
-Add to `ItemDef` struct (after `Ticks` field at line 58):
-```go
-ProvidesJunk string `yaml:"provides_junk"`
-```
-
-### Deck YAML Definitions
-
-#### `data/items/basic_deck.yaml`
-
-```yaml
-id: basic_deck
-name: basic deck
-color: "245"
-description: "A simple programmable deck. Removes the scrap requirement for triggering mods, but provides no elemental junk."
-value: 500
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 5
-```
-
-#### `data/items/solar_deck.yaml`
-
-```yaml
-id: solar_deck
-name: solar deck
-color: "196"
-description: "A programmable deck pulsing with solar energy. Provides unlimited solarjunk and removes the scrap requirement."
-value: 1500
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 10
-provides_junk: solarjunk
-```
-
-#### `data/items/hydro_deck.yaml`
-
-```yaml
-id: hydro_deck
-name: hydro deck
-color: "39"
-description: "A programmable deck infused with hydro circuitry. Provides unlimited hydrojunk and removes the scrap requirement."
-value: 1500
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 10
-provides_junk: hydrojunk
-```
-
-#### `data/items/eco_deck.yaml`
-
-```yaml
-id: eco_deck
-name: eco deck
-color: "34"
-description: "A programmable deck threaded with eco-organic circuits. Provides unlimited ecojunk and removes the scrap requirement."
-value: 1500
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 10
-provides_junk: ecojunk
-```
-
-#### `data/items/bio_deck.yaml`
-
-```yaml
-id: bio_deck
-name: bio deck
-color: "208"
-description: "A programmable deck infused with bio-synthetic membranes. Provides unlimited biojunk and removes the scrap requirement."
-value: 1500
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 10
-provides_junk: biojunk
-```
-
-#### `data/items/advanced_solar_deck.yaml`
-
-```yaml
-id: advanced_solar_deck
-name: advanced solar deck
-color: "196 bold"
-description: "A high-powered solar deck with enhanced circuitry. Provides unlimited solarjunk and removes the scrap requirement."
-value: 15000
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 20
-provides_junk: solarjunk
-```
-
-#### `data/items/advanced_hydro_deck.yaml`
-
-```yaml
-id: advanced_hydro_deck
-name: advanced hydro deck
-color: "39 bold"
-description: "A high-powered hydro deck with enhanced circuitry. Provides unlimited hydrojunk and removes the scrap requirement."
-value: 15000
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 20
-provides_junk: hydrojunk
-```
-
-#### `data/items/advanced_eco_deck.yaml`
-
-```yaml
-id: advanced_eco_deck
-name: advanced eco deck
-color: "34 bold"
-description: "A high-powered eco deck with enhanced circuitry. Provides unlimited ecojunk and removes the scrap requirement."
-value: 15000
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 20
-provides_junk: ecojunk
-```
-
-#### `data/items/advanced_bio_deck.yaml`
-
-```yaml
-id: advanced_bio_deck
-name: advanced bio deck
-color: "208 bold"
-description: "A high-powered bio deck with enhanced circuitry. Provides unlimited biojunk and removes the scrap requirement."
-value: 15000
-equip_slot: main_hand
-weapon_type: science
-speed: 5
-stats:
- science_bonus: 20
-provides_junk: biojunk
-```
-
----
-
-## 10. New Junk Items (Higher-tier)
-
-The base 4 junk types (solarjunk, hydrojunk, ecojunk, biojunk) are already defined in `scavenging.md`. These are the 6 additional junk types needed for science.
-
-#### `data/items/chaosjunk.yaml`
-
-```yaml
-id: chaosjunk
-name: chaosjunk
-color: "198"
-description: "A volatile fragment of unstable circuitry that crackles with chaotic energy. Used for mid-level science mods."
-value: 75
-stackable: true
-```
-
-#### `data/items/deathjunk.yaml`
-
-```yaml
-id: deathjunk
-name: deathjunk
-color: "231"
-description: "A cold, pale fragment of dead circuitry that absorbs light. Used for high-level science mods."
-value: 150
-stackable: true
-```
-
-#### `data/items/bloodjunk.yaml`
-
-```yaml
-id: bloodjunk
-name: bloodjunk
-color: "124"
-description: "A dark crimson fragment of circuitry that pulses as if alive. Used for the most powerful science mods."
-value: 300
-stackable: true
-```
-
-#### `data/items/lawjunk.yaml`
-
-```yaml
-id: lawjunk
-name: lawjunk
-color: "33"
-description: "A precisely calibrated fragment of navigation circuitry. Used for transport mods."
-value: 200
-stackable: true
-```
-
-#### `data/items/cosmicjunk.yaml`
-
-```yaml
-id: cosmicjunk
-name: cosmicjunk
-color: "99"
-description: "A shimmering fragment of cosmic circuitry that bends light around it. Used for enchantment mods."
-value: 120
-stackable: true
-```
-
-#### `data/items/naturejunk.yaml`
-
-```yaml
-id: naturejunk
-name: naturejunk
-color: "76"
-description: "A fragment of bio-organic circuitry intertwined with living matter. Used for processing and conversion mods."
-value: 100
-stackable: true
-```
-
----
-
-## 11. Junk Sources — Higher-tier Altars
-
-The 6 higher-tier junk types are produced at new altars via the Scavenging skill's `id` command (same mechanic as base junk). Each altar requires a corresponding identifier tool and a minimum Scavenging level.
-
-**This is a dependency on the scavenging.md plan.** The scavenging plan currently defines 4 altars (solar, hydro, eco, bio). 6 more must be added:
-
-| Junk Type | Identifier Tool | Altar Station | Scavenging Level | XP/scrap |
-|-----------|----------------|---------------|-------------------|----------|
-| Chaosjunk | Chaos Identifier | Chaos Altar | 35 | 20 |
-| Cosmicjunk | Cosmic Identifier | Cosmic Altar | 27 | 14 |
-| Naturejunk | Nature Identifier | Nature Altar | 44 | 22 |
-| Lawjunk | Law Identifier | Law Altar | 54 | 28 |
-| Deathjunk | Death Identifier | Death Altar | 65 | 35 |
-| Bloodjunk | Blood Identifier | Blood Altar | 77 | 45 |
-
-### New Identifier Items
-
-#### `data/items/chaos_identifier.yaml`
-
-```yaml
-id: chaos_identifier
-name: chaos identifier
-color: "198"
-description: "A handheld scanner calibrated to isolate chaos-frequency signatures in scrap metal. Required to produce chaosjunk at a chaos altar."
-value: 3000
-```
-
-#### `data/items/cosmic_identifier.yaml`
-
-```yaml
-id: cosmic_identifier
-name: cosmic identifier
-color: "99"
-description: "A handheld scanner calibrated to isolate cosmic-frequency signatures in scrap metal. Required to produce cosmicjunk at a cosmic altar."
-value: 2000
-```
-
-#### `data/items/nature_identifier.yaml`
-
-```yaml
-id: nature_identifier
-name: nature identifier
-color: "76"
-description: "A handheld scanner calibrated to isolate nature-frequency signatures in scrap metal. Required to produce naturejunk at a nature altar."
-value: 4000
-```
-
-#### `data/items/law_identifier.yaml`
-
-```yaml
-id: law_identifier
-name: law identifier
-color: "33"
-description: "A handheld scanner calibrated to isolate law-frequency signatures in scrap metal. Required to produce lawjunk at a law altar."
-value: 6000
-```
-
-#### `data/items/death_identifier.yaml`
-
-```yaml
-id: death_identifier
-name: death identifier
-color: "231"
-description: "A handheld scanner calibrated to isolate death-frequency signatures in scrap metal. Required to produce deathjunk at a death altar."
-value: 10000
-```
-
-#### `data/items/blood_identifier.yaml`
-
-```yaml
-id: blood_identifier
-name: blood identifier
-color: "124"
-description: "A handheld scanner calibrated to isolate blood-frequency signatures in scrap metal. Required to produce bloodjunk at a blood altar."
-value: 20000
-```
-
-### New Altar Objects
-
-#### `data/objects/chaos_altar.yaml`
-
-```yaml
-id: chaos_altar
-name: chaos altar
-color: "198"
-description: "A crackling altar of unstable circuitry. Sparks arc between exposed conductors. Place scrap metal here with a chaos identifier to produce chaosjunk. Type 'id' to begin."
-```
-
-#### `data/objects/cosmic_altar.yaml`
-
-```yaml
-id: cosmic_altar
-name: cosmic altar
-color: "99"
-description: "A shimmering altar that seems to bend the space around it. Place scrap metal here with a cosmic identifier to produce cosmicjunk. Type 'id' to begin."
-```
-
-#### `data/objects/nature_altar.yaml`
-
-```yaml
-id: nature_altar
-name: nature altar
-color: "76"
-description: "A living altar of intertwined organic circuitry and vines. Place scrap metal here with a nature identifier to produce naturejunk. Type 'id' to begin."
-```
-
-#### `data/objects/law_altar.yaml`
-
-```yaml
-id: law_altar
-name: law altar
-color: "33"
-description: "A precisely geometric altar with perfectly aligned conductors. Place scrap metal here with a law identifier to produce lawjunk. Type 'id' to begin."
-```
-
-#### `data/objects/death_altar.yaml`
-
-```yaml
-id: death_altar
-name: death altar
-color: "231"
-description: "A pale, lifeless altar that absorbs all warmth from the air. Place scrap metal here with a death identifier to produce deathjunk. Type 'id' to begin."
-```
-
-#### `data/objects/blood_altar.yaml`
-
-```yaml
-id: blood_altar
-name: blood altar
-color: "124"
-description: "A dark crimson altar with channels that pulse like veins. Place scrap metal here with a blood identifier to produce bloodjunk. Type 'id' to begin."
-```
-
-### New Altar Rooms
-
-These rooms should branch off from the scavenging area or be placed in harder-to-reach locations. Use next available room IDs. Example layouts:
-
-```yaml
-# Cosmic Altar Chamber
-id: <next_id>
-name: "Cosmic Altar Chamber"
-description: "The walls of this chamber shimmer with an otherworldly iridescence. A {99 bold}cosmic altar{/} hovers slightly above the ground at the center, its surface rippling like a mirage."
-exits:
- south: 9
-objects:
- - id: cosmic_altar
-```
-
-```yaml
-# Chaos Altar Chamber
-id: <next_id>
-name: "Chaos Altar Chamber"
-description: "Sparks arc unpredictably across the walls of this unstable chamber. A {198 bold}chaos altar{/} sits at the center, crackling with volatile energy."
-exits:
- south: 9
-objects:
- - id: chaos_altar
-```
-
-```yaml
-# Nature Altar Chamber
-id: <next_id>
-name: "Nature Altar Chamber"
-description: "Vines and moss cover every surface. The air is thick and humid. A {76 bold}nature altar{/} rises from the ground, pulsing with organic circuitry."
-exits:
- south: 9
-objects:
- - id: nature_altar
-```
-
-```yaml
-# Law Altar Chamber
-id: <next_id>
-name: "Law Altar Chamber"
-description: "This chamber is perfectly symmetrical. Every surface is polished to a mirror finish. A {33 bold}law altar{/} stands at the exact center, its geometric perfection almost unsettling."
-exits:
- south: 9
-objects:
- - id: law_altar
-```
-
-```yaml
-# Death Altar Chamber
-id: <next_id>
-name: "Death Altar Chamber"
-description: "The temperature drops sharply as you enter this pale, silent chamber. A {231}death altar{/} dominates the room, its surface cold to the touch and utterly devoid of light."
-exits:
- south: 9
-objects:
- - id: death_altar
-```
-
-```yaml
-# Blood Altar Chamber
-id: <next_id>
-name: "Blood Altar Chamber"
-description: "The walls seem to breathe in this unsettling chamber. A {124 bold}blood altar{/} pulses at the center, its surface covered in dark crimson channels that flow like living veins."
-exits:
- south: 9
-objects:
- - id: blood_altar
-```
-
-**Alternative junk sources (mob drops, future shops):** Higher-tier junk can also drop from mobs. Example mob drop entries:
-```yaml
-drops:
- loot:
- - item_id: chaosjunk
- weight: 10
- quantity: 3
-```
-
----
-
-## 12. `provides_junk` and Deck Scrap Exemption
-
-### Junk Cost Checking Logic
-
-**File: `internal/game/science.go`**
-
-```go
-func (g *Game) hasDeckEquipped(p *player.Player) bool {
- itemID, ok := p.Equipment[object.SlotMainHand]
- if !ok {
- return false
- }
- def, err := g.ItemStore.Load(itemID)
- if err != nil {
- return false
- }
- return def.WeaponType == object.WeaponScience
-}
-
-func (g *Game) equippedProvidesJunk(p *player.Player) string {
- itemID, ok := p.Equipment[object.SlotMainHand]
- if !ok {
- return ""
- }
- def, err := g.ItemStore.Load(itemID)
- if err != nil {
- return ""
- }
- return def.ProvidesJunk
-}
-
-func (g *Game) effectiveJunkCost(p *player.Player, mod *ModDef) map[string]int {
- cost := make(map[string]int)
- for k, v := range mod.JunkCost {
- cost[k] = v
- }
-
- hasDeck := g.hasDeckEquipped(p)
-
- // Any deck removes scrap requirement
- if hasDeck {
- delete(cost, "scrap_metal")
- }
-
- // Specific deck provides unlimited elemental junk
- providesJunk := g.equippedProvidesJunk(p)
- if providesJunk != "" {
- delete(cost, providesJunk)
- }
-
- return cost
-}
-
-func (g *Game) hasJunkCost(p *player.Player, mod *ModDef) bool {
- cost := g.effectiveJunkCost(p, mod)
- for itemID, qty := range cost {
- if p.CountItem(itemID) < qty {
- return false
- }
- }
- return true
-}
-
-func (g *Game) consumeJunkCost(p *player.Player, mod *ModDef) bool {
- cost := g.effectiveJunkCost(p, mod)
- for itemID, qty := range cost {
- if !p.RemoveItem(itemID, qty) {
- return false
- }
- }
- g.AccountStore.SaveCharacter(p)
- return true
-}
-
-func (g *Game) junkCostString(p *player.Player, mod *ModDef) string {
- cost := g.effectiveJunkCost(p, mod)
- if len(cost) == 0 {
- return "free"
- }
- var parts []string
- for itemID, qty := range cost {
- def, _ := g.ItemStore.Load(itemID)
- name := itemID
- if def != nil {
- name = def.Name
- }
- if qty > 1 {
- parts = append(parts, fmt.Sprintf("%d %s", qty, name))
- } else {
- parts = append(parts, name)
- }
- }
- sort.Strings(parts)
- return strings.Join(parts, ", ")
-}
-```
-
----
-
-## 13. Utility Mod Implementation Details
-
-### `doTrigger` Handler
-
-**File: `internal/game/cmd_trigger.go`**
-
-```go
-package game
-
-import (
- "fmt"
- "strings"
-
- "thehouseoficarus/internal/combat"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) doTrigger(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- input = strings.TrimSpace(input)
-
- if input == "" {
- sess.WriteLine("Trigger what? Type 'mods' to see available mods.")
- return
- }
-
- // Parse: "trigger <mod> [<target>]"
- // Try longest prefix match for mod name, remainder is target
- mod, targetArg := g.parseTriggerArgs(input)
- if mod == nil {
- sess.WriteLine("Unknown mod. Type 'mods' to see available mods.")
- return
- }
-
- if p.Level(player.Science) < mod.Level {
- sess.WriteLine(fmt.Sprintf("You need level %d science to trigger %s.", mod.Level, mod.Name))
- return
- }
-
- if !g.hasJunkCost(p, mod) {
- sess.WriteLine(fmt.Sprintf("You don't have enough junk to trigger %s.", mod.Name))
- return
- }
-
- switch mod.Category {
- case ModCombat:
- g.triggerCombatMod(sess, p, mod, targetArg)
- case ModTransport:
- g.triggerTransport(sess, p, mod)
- case ModProcessing:
- g.triggerProcessing(sess, p, mod, targetArg)
- case ModUtility:
- g.triggerUtility(sess, p, mod, targetArg)
- case ModEnchant:
- g.triggerEnchant(sess, p, mod, targetArg)
- }
-}
-
-func (g *Game) parseTriggerArgs(input string) (*ModDef, string) {
- lower := strings.ToLower(input)
-
- // Try matching progressively longer prefixes
- words := strings.Fields(lower)
- for i := len(words); i > 0; i-- {
- candidate := strings.Join(words[:i], " ")
- mod := FindMod(candidate)
- if mod != nil {
- target := strings.TrimSpace(strings.Join(words[i:], " "))
- return mod, target
- }
- }
- return nil, ""
-}
-
-func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
- // If already in combat, switch autocast to this mod
- if cs := combat.GetCombat(p.Name); cs != nil {
- p.AutocastMod = mod.ID
- sess.WriteLine(fmt.Sprintf("You switch to triggering %s.", mod.Name))
- return
- }
-
- // Not in combat — need a target
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- var mobTarget string
- if targetArg == "" {
- mobTarget = g.resolveDefaultMob(p.RoomID)
- if mobTarget == "" {
- sess.WriteLine("Trigger on what?")
- return
- }
- } else {
- mobTarget = targetArg
- }
-
- mob := g.findMob(sess, mobTarget, p.RoomID)
- if mob == nil {
- return
- }
-
- if mob.HP <= 0 {
- sess.WriteLine("That is already dead.")
- return
- }
-
- if mob.Protected {
- sess.WriteLine(fmt.Sprintf("You can't attack %s!", mobDisplayName(mob, true)))
- return
- }
-
- if combat.IsMobInCombat(mob.InstanceID) {
- sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false)))
- return
- }
-
- p.AutocastMod = mod.ID
- g.startCombat(sess, p, mob)
-}
-
-func (g *Game) triggerTransport(sess *net.Session, p *player.Player, mod *ModDef) {
- if combat.GetCombat(p.Name) != nil {
- sess.WriteLine("You can't teleport during combat!")
- return
- }
-
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- g.consumeJunkCost(p, mod)
-
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf("+%dxp sci", sciXP)))
- }
- g.AccountStore.SaveCharacter(p)
-
- // Broadcast departure
- if g.Hub != nil {
- for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
- if other != sess {
- other.WriteLine(fmt.Sprintf("\n%s teleports away.", p.Name))
- }
- }
- }
-
- sess.WriteLine(fmt.Sprintf("\nYou activate %s...", mod.Name))
-
- oldRoom := p.RoomID
- p.RoomID = mod.Destination
- if g.Hub != nil {
- g.Hub.LeaveRoom(sess, oldRoom)
- g.Hub.EnterRoom(sess, p.RoomID)
- }
-
- room, _ := g.World.LoadRoom(p.RoomID)
- destName := fmt.Sprintf("room %d", p.RoomID)
- if room != nil {
- destName = room.Name
- }
- sess.WriteLine(fmt.Sprintf("You materialize at %s.", destName))
- g.AccountStore.SaveCharacter(p)
- g.doLook(sess)
-}
-
-func (g *Game) triggerProcessing(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
- if combat.GetCombat(p.Name) != nil {
- sess.WriteLine("You can't do that during combat!")
- return
- }
-
- if targetArg == "" {
- sess.WriteLine(fmt.Sprintf("Usage: trigger %s <item>", strings.ReplaceAll(mod.ID, "_", " ")))
- return
- }
-
- // Find item in inventory
- slot, inv := g.findInventoryItem(p, targetArg)
- if slot < 0 {
- sess.WriteLine("You don't have that item.")
- return
- }
-
- itemDef, err := g.ItemStore.Load(inv.ItemID)
- if err != nil || itemDef.Value <= 0 {
- sess.WriteLine("That item has no value.")
- return
- }
-
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- g.consumeJunkCost(p, mod)
-
- creditValue := itemDef.Value
- if mod.ID == "low_process" {
- creditValue = itemDef.Value / 2
- if creditValue < 1 {
- creditValue = 1
- }
- }
-
- // Remove 1 of the item
- if inv.Quantity > 1 {
- inv.Quantity--
- } else {
- p.SetInvSlot(slot, nil)
- }
-
- p.Credits += creditValue
-
- sciXP := int(mod.BaseXP)
- var leveledUp []player.SkillName
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- leveledUp = append(leveledUp, player.Science)
- }
- g.AccountStore.SaveCharacter(p)
-
- for _, skill := range leveledUp {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill)))
- }
-
- sess.WriteLine(fmt.Sprintf("You process the %s. You receive %s credits.",
- itemDef.Name, g.colorize(sess, "credits_pickup", fmt.Sprint(creditValue))))
-
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf("+%dxp sci", sciXP)))
- }
-}
-
-func (g *Game) triggerUtility(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
- switch mod.ID {
- case "bones_to_nutrients":
- g.triggerBonesToNutrients(sess, p, mod)
- case "em_grab":
- g.triggerEmGrab(sess, p, mod, targetArg)
- case "superheat":
- g.triggerSuperheat(sess, p, mod, targetArg)
- }
-}
-
-func (g *Game) triggerBonesToNutrients(sess *net.Session, p *player.Player, mod *ModDef) {
- if combat.GetCombat(p.Name) != nil {
- sess.WriteLine("You can't do that during combat!")
- return
- }
-
- boneCount := p.CountItem("bones")
- if boneCount == 0 {
- sess.WriteLine("You don't have any bones.")
- return
- }
-
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- g.consumeJunkCost(p, mod)
-
- // Replace all bones with nutrient_bar
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == "bones" {
- slot.ItemID = "nutrient_bar"
- // Quantity stays the same (bones are non-stackable, qty=1 each)
- }
- }
-
- sciXP := int(mod.BaseXP) * boneCount
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
- g.AccountStore.SaveCharacter(p)
-
- sess.WriteLine(fmt.Sprintf("You convert %d bones into nutrient bars.", boneCount))
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf("+%dxp sci", sciXP)))
- }
-}
-
-func (g *Game) triggerEmGrab(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
- if targetArg == "" {
- sess.WriteLine("Grab what? Usage: trigger em grab <item>")
- return
- }
-
- if p.FirstFreeSlot() < 0 {
- sess.WriteLine("Your inventory is full.")
- return
- }
-
- // Find ground item (bypass reservation)
- items := g.World.GroundItems(p.RoomID)
- var matchIdx int = -1
- for i, gi := range items {
- def, _ := g.ItemStore.Load(gi.ItemID)
- if def != nil && def.MatchesName(targetArg) {
- matchIdx = i
- break
- }
- // Also try raw ID match
- if strings.HasPrefix(gi.ItemID, strings.ToLower(targetArg)) {
- matchIdx = i
- break
- }
- }
-
- if matchIdx < 0 {
- sess.WriteLine("You don't see that here.")
- return
- }
-
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- g.consumeJunkCost(p, mod)
-
- gi := items[matchIdx]
- g.World.RemoveGroundItem(p.RoomID, matchIdx)
- g.addToInventory(p, gi.ItemID, gi.Quantity)
-
- def, _ := g.ItemStore.Load(gi.ItemID)
- name := gi.ItemID
- if def != nil {
- name = def.Name
- }
-
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
- g.AccountStore.SaveCharacter(p)
-
- sess.WriteLine(fmt.Sprintf("You magnetically pull the %s toward you.", name))
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf("+%dxp sci", sciXP)))
- }
-}
-
-func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
- if combat.GetCombat(p.Name) != nil {
- sess.WriteLine("You can't do that during combat!")
- return
- }
-
- if targetArg == "" {
- sess.WriteLine("Superheat what? Usage: trigger superheat <ore>")
- return
- }
-
- // Find the ore in inventory
- slot, inv := g.findInventoryItem(p, targetArg)
- if slot < 0 {
- sess.WriteLine("You don't have that item.")
- return
- }
-
- // Find a smelting recipe that uses this item
- recipe := g.RecipeStore.FindByInput("smelt", inv.ItemID)
- if recipe == nil {
- sess.WriteLine("You can't superheat that.")
- return
- }
-
- // Check skill level for the recipe
- if recipe.Level > 0 && p.Level(player.SkillName(recipe.Skill)) < recipe.Level {
- sess.WriteLine(fmt.Sprintf("You need level %d %s to smelt that.", recipe.Level, recipe.Skill))
- return
- }
-
- // Check all recipe inputs are available
- for _, inputItem := range recipe.Inputs {
- if p.CountItem(inputItem.ID) < inputItem.Qty {
- def, _ := g.ItemStore.Load(inputItem.ID)
- name := inputItem.ID
- if def != nil {
- name = def.Name
- }
- sess.WriteLine(fmt.Sprintf("You need %d %s.", inputItem.Qty, name))
- return
- }
- }
-
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- g.consumeJunkCost(p, mod)
-
- // Consume recipe inputs
- for _, inputItem := range recipe.Inputs {
- p.RemoveItem(inputItem.ID, inputItem.Qty)
- }
-
- // Add recipe output
- g.addToInventory(p, recipe.Output.ID, recipe.Output.Qty)
-
- // Award Science XP + Smithing XP
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
- if recipe.XP > 0 {
- if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(player.SkillName(recipe.Skill)), recipe.Skill)))
- }
- }
- g.AccountStore.SaveCharacter(p)
-
- outputDef, _ := g.ItemStore.Load(recipe.Output.ID)
- outputName := recipe.Output.ID
- if outputDef != nil {
- outputName = outputDef.Name
- }
- inputDef, _ := g.ItemStore.Load(inv.ItemID)
- inputName := inv.ItemID
- if inputDef != nil {
- inputName = inputDef.Name
- }
-
- sess.WriteLine(fmt.Sprintf("You superheat the %s and produce a %s.", inputName, outputName))
- if p.OptionBool("xp_drops") {
- parts := []string{fmt.Sprintf("+%dxp sci", sciXP)}
- if recipe.XP > 0 {
- parts = append(parts, fmt.Sprintf("+%dxp %s", recipe.XP, player.SkillAbbr[player.SkillName(recipe.Skill)]))
- }
- sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")"))
- }
-}
-
-func (g *Game) triggerEnchant(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
- // Check for bolt chip mods first
- if chipInfo, ok := chipMap[mod.ID]; ok {
- g.triggerChipBolts(sess, p, mod, chipInfo)
- return
- }
-
- // Jewelry enchantment
- enchants, ok := enchantMap[mod.ID]
- if !ok {
- sess.WriteLine("That enchantment has no known recipes.")
- return
- }
-
- if targetArg == "" {
- // List valid targets
- sess.WriteLine(fmt.Sprintf("Enchant what? Use: trigger %s <jewelry item>", strings.ReplaceAll(mod.ID, "_", " ")))
- return
- }
-
- slot, inv := g.findInventoryItem(p, targetArg)
- if slot < 0 {
- sess.WriteLine("You don't have that item.")
- return
- }
-
- outputID, ok := enchants[inv.ItemID]
- if !ok {
- sess.WriteLine("You can't enchant that with this mod.")
- return
- }
-
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- g.consumeJunkCost(p, mod)
-
- // Replace item
- inv.ItemID = outputID
-
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
- g.AccountStore.SaveCharacter(p)
-
- outputDef, _ := g.ItemStore.Load(outputID)
- outputName := outputID
- if outputDef != nil {
- outputName = outputDef.Name
- }
- inputDef, _ := g.ItemStore.Load(targetArg)
- inputName := targetArg
- if inputDef != nil {
- inputName = inputDef.Name
- }
-
- sess.WriteLine(fmt.Sprintf("You enchant the %s and it becomes a %s!", inputName, outputName))
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf("+%dxp sci", sciXP)))
- }
-}
-
-func (g *Game) triggerChipBolts(sess *net.Session, p *player.Player, mod *ModDef, chip chipEntry) {
- count := p.CountItem(chip.Input)
- if count < chip.Qty {
- inputDef, _ := g.ItemStore.Load(chip.Input)
- name := chip.Input
- if inputDef != nil {
- name = inputDef.Name
- }
- sess.WriteLine(fmt.Sprintf("You need at least %d %s.", chip.Qty, name))
- return
- }
-
- if p.Action != nil {
- g.CancelAction(p)
- }
-
- g.consumeJunkCost(p, mod)
-
- p.RemoveItem(chip.Input, chip.Qty)
- g.addToInventory(p, chip.Output, chip.Qty)
-
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
- g.AccountStore.SaveCharacter(p)
-
- inputDef, _ := g.ItemStore.Load(chip.Input)
- name := chip.Input
- if inputDef != nil {
- name = inputDef.Name
- }
-
- sess.WriteLine(fmt.Sprintf("You chip %d %s with arcane circuitry.", chip.Qty, name))
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf("+%dxp sci", sciXP)))
- }
-}
-
-// Helper: find an inventory item by name/prefix
-func (g *Game) findInventoryItem(p *player.Player, input string) (int, *player.InventorySlot) {
- lower := strings.ToLower(strings.TrimSpace(input))
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot == nil {
- continue
- }
- def, err := g.ItemStore.Load(slot.ItemID)
- if err != nil {
- continue
- }
- if def.MatchesName(lower) {
- return i, slot
- }
- }
- return -1, nil
-}
-```
-
-### Helper types for chip map
-
-```go
-type chipEntry struct {
- Input string
- Output string
- Qty int
-}
-
-var chipMap = map[string]chipEntry{
- "chip_sapphire": {"sapphire_bolts", "sapphire_bolts_e", 10},
- "chip_emerald": {"emerald_bolts", "emerald_bolts_e", 10},
- "chip_ruby": {"ruby_bolts", "ruby_bolts_e", 10},
- "chip_diamond": {"diamond_bolts", "diamond_bolts_e", 10},
-}
-```
-
----
-
-## 14. Score Page / Mods List
-
-### `cmd_mods.go`
-
-**File: `internal/game/cmd_mods.go`**
-
-```go
-package game
-
-import (
- "fmt"
- "sort"
- "strings"
-
- "thehouseoficarus/internal/color"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) doMods(sess *net.Session) {
- p := sess.Player.(*player.Player)
- mode := g.colorMode(sess)
- sciLevel := p.Level(player.Science)
-
- categories := []struct {
- Name string
- Cat ModCategory
- }{
- {"Combat", ModCombat},
- {"Processing", ModProcessing},
- {"Utility", ModUtility},
- {"Transport", ModTransport},
- {"Enchantment", ModEnchant},
- }
-
- sess.WriteLine("")
-
- anyMods := false
- for _, cat := range categories {
- var mods []*ModDef
- for _, m := range AllMods {
- if m.Category == cat.Cat && m.Level <= sciLevel {
- mods = append(mods, m)
- }
- }
- if len(mods) == 0 {
- continue
- }
-
- sort.Slice(mods, func(i, j int) bool {
- return mods[i].Level < mods[j].Level
- })
-
- t := &Table{Title: cat.Name + " Mods", Columns: []string{
- color.Render(mode, color.Parse("75"), "Mod"),
- color.Render(mode, color.Parse("230"), "Lv"),
- color.Render(mode, color.Parse("245"), "Cost"),
- color.Render(mode, color.Parse("222"), "XP"),
- }}
-
- for _, m := range mods {
- costStr := g.modCostDisplay(p, m)
- xpStr := fmt.Sprintf("%.1f", m.BaseXP)
- if m.MaxHit > 0 {
- xpStr += fmt.Sprintf(" (max %d)", m.MaxHit)
- }
- t.Rows = append(t.Rows, []string{
- color.Render(mode, color.Parse("75"), m.Name),
- color.Render(mode, color.Parse("230"), fmt.Sprint(m.Level)),
- color.Render(mode, color.Parse("245"), costStr),
- color.Render(mode, color.Parse("222"), xpStr),
- })
- }
-
- for _, line := range t.Render(p.OptionBool("unicode")) {
- sess.WriteLine(line)
- }
- anyMods = true
- }
-
- if !anyMods {
- sess.WriteLine("You don't know any mods yet. Train Science to unlock mods.")
- }
-
- if p.AutocastMod != "" {
- mod := GetMod(p.AutocastMod)
- if mod != nil {
- sess.WriteLine(fmt.Sprintf("\nAutocast: %s", g.colorize(sess, "science_mod", mod.Name)))
- }
- }
-}
-
-func (g *Game) modCostDisplay(p *player.Player, mod *ModDef) string {
- cost := g.effectiveJunkCost(p, mod)
- if len(cost) == 0 {
- return "free"
- }
- var parts []string
- // Sort keys for consistent display
- keys := make([]string, 0, len(cost))
- for k := range cost {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, itemID := range keys {
- qty := cost[itemID]
- def, _ := g.ItemStore.Load(itemID)
- name := itemID
- if def != nil {
- name = def.Name
- }
- if qty > 1 {
- parts = append(parts, fmt.Sprintf("%d %s", qty, name))
- } else {
- parts = append(parts, name)
- }
- }
- return strings.Join(parts, ", ")
-}
-```
-
-**Display format example:**
-
-```
-Combat Mods
-┌───────────────┬────┬──────────────────────────────┬──────────────┐
-│ Mod │ Lv │ Cost │ XP │
-├───────────────┼────┼──────────────────────────────┼──────────────┤
-│ Bio Strike │ 1 │ 2 biojunk │ 5.5 (max 4) │
-│ Hydro Strike │ 5 │ 3 hydrojunk, 1 ecojunk │ 7.5 (max 6) │
-│ ... │ │ │ │
-└───────────────┴────┴──────────────────────────────┴──────────────┘
-
-Autocast: Solar Bolt
-```
-
-Note: the "Cost" column reflects the effective cost after deck bonuses. If the player has a solar deck equipped, solarjunk and scrap_metal are removed from cost display.
-
----
-
-## 15. Code Changes — Complete File List
-
-### New Files
-
-| File | Purpose |
-|------|---------|
-| `internal/game/science.go` | `ModDef` struct, `AllMods` slice, `modByID` map, `FindMod`, `GetMod`, `enchantMap`, `chipMap`, `chipEntry`, junk cost helpers (`hasDeckEquipped`, `equippedProvidesJunk`, `effectiveJunkCost`, `hasJunkCost`, `consumeJunkCost`, `junkCostString`) |
-| `internal/game/cmd_trigger.go` | `doTrigger` handler, `parseTriggerArgs`, `triggerCombatMod`, `triggerTransport`, `triggerProcessing`, `triggerUtility`, `triggerEnchant`, `triggerBonesToNutrients`, `triggerEmGrab`, `triggerSuperheat`, `triggerChipBolts`, `scienceAttack`, `totalEquipScienceAttack`, `findInventoryItem` |
-| `internal/game/cmd_autocast.go` | `doAutocast` handler |
-| `internal/game/cmd_mods.go` | `doMods` handler, `modCostDisplay` |
-| `data/items/basic_deck.yaml` | Basic Deck item |
-| `data/items/solar_deck.yaml` | Solar Deck item |
-| `data/items/hydro_deck.yaml` | Hydro Deck item |
-| `data/items/eco_deck.yaml` | Eco Deck item |
-| `data/items/bio_deck.yaml` | Bio Deck item |
-| `data/items/advanced_solar_deck.yaml` | Advanced Solar Deck item |
-| `data/items/advanced_hydro_deck.yaml` | Advanced Hydro Deck item |
-| `data/items/advanced_eco_deck.yaml` | Advanced Eco Deck item |
-| `data/items/advanced_bio_deck.yaml` | Advanced Bio Deck item |
-| `data/items/chaosjunk.yaml` | Chaosjunk item |
-| `data/items/deathjunk.yaml` | Deathjunk item |
-| `data/items/bloodjunk.yaml` | Bloodjunk item |
-| `data/items/lawjunk.yaml` | Lawjunk item |
-| `data/items/cosmicjunk.yaml` | Cosmicjunk item |
-| `data/items/naturejunk.yaml` | Naturejunk item |
-| `data/items/nutrient_bar.yaml` | Nutrient Bar item (bones_to_nutrients output) |
-| `data/items/chaos_identifier.yaml` | Chaos Identifier tool |
-| `data/items/cosmic_identifier.yaml` | Cosmic Identifier tool |
-| `data/items/nature_identifier.yaml` | Nature Identifier tool |
-| `data/items/law_identifier.yaml` | Law Identifier tool |
-| `data/items/death_identifier.yaml` | Death Identifier tool |
-| `data/items/blood_identifier.yaml` | Blood Identifier tool |
-| `data/objects/chaos_altar.yaml` | Chaos Altar object |
-| `data/objects/cosmic_altar.yaml` | Cosmic Altar object |
-| `data/objects/nature_altar.yaml` | Nature Altar object |
-| `data/objects/law_altar.yaml` | Law Altar object |
-| `data/objects/death_altar.yaml` | Death Altar object |
-| `data/objects/blood_altar.yaml` | Blood Altar object |
-| `data/help/trigger.yaml` | Help for trigger command |
-| `data/help/autocast.yaml` | Help for autocast command |
-| `data/help/mods.yaml` | Help for mods command |
-| `data/help/science.yaml` | Help for Science skill |
-| New altar rooms (6 YAML files) | Rooms containing each new altar |
-
-### Modified Files
-
-| File | Changes |
-|------|---------|
-| `internal/object/item.go` | Add `ProvidesJunk string` field to `ItemDef` (after `Ticks` at line 58). Add `ScienceAttack int` and `ScienceDamage int` to `ItemStats` if needed for future granularity (currently `ScienceBonus` covers it). |
-| `internal/player/player.go` | Add `AutocastMod string` field to `Player` struct with `yaml:"-"` tag (after `VisualTickCurrent` at line 174). |
-| `internal/world/mob.go` | Add `ScienceDefense int` and `Weakness string` fields to `MobDef` (after `Defense` at line 29). Add same fields to `MobInstance` (after `Defense` at line 48). Copy fields in mob instantiation. |
-| `internal/game/game.go` | Add `"trigger"`, `"cast"` to `ClassActive` case in `classifyCommand()` (line 146). Add `"autocast"`, `"auto"`, `"mods"`, `"modlist"` to `ClassInstant` case (line 138). Add dispatch cases in `executeCommand()`: `case "trigger", "cast":` → `g.doTrigger(...)`, `case "autocast", "auto":` → `g.doAutocast(...)`, `case "mods", "modlist":` → `g.doMods(...)`. |
-| `internal/game/cmd_attack.go` | Modify `startCombat()` to check `p.AutocastMod`: if set, use 5-tick science speed and call `scienceAttack` instead of `playerAttack`. Handle junk depletion fallback to melee. Modify initial combat message for autocast. |
-| `internal/game/action_state.go` | Add `ActionTriggering ActionType = "triggering"` constant. Add case in `Description()`: `case ActionTriggering: return "triggering " + a.TargetName`. |
-| `internal/game/game.go` (`ProcessQueuedCommands`) | Add `ActionTriggering` to the persistent action types list in the switch at line 472 (if transport mods use multi-tick actions). |
-
-### Nutrient Bar Item
-
-#### `data/items/nutrient_bar.yaml`
-
-```yaml
-id: nutrient_bar
-name: nutrient bar
-color: "220"
-description: "A compressed bar of processed nutrients. Restores a small amount of health."
-value: 5
-heal_value: 2
-eat_message: "You eat the nutrient bar."
-```
-
----
-
-## 16. Junk Checking Helper — Detailed Logic
-
-The junk cost system must handle 3 layers:
-1. **Base cost:** From `mod.JunkCost` (includes `scrap_metal: 1` on every mod)
-2. **Deck exemption:** If ANY deck equipped (weapon_type == "science"), remove `scrap_metal` from cost
-3. **Provides junk:** If equipped deck has `provides_junk: "solarjunk"`, remove `solarjunk` from cost
-
-### Step-by-step for `hasJunkCost`:
-
-```
-1. Copy mod.JunkCost into a new map
-2. Check if player has a science weapon in main_hand
- - If yes: delete "scrap_metal" from cost map
- - If yes AND weapon has provides_junk: delete that junk from cost map
-3. For each remaining (junk_id, qty) in cost map:
- - If p.CountItem(junk_id) < qty: return false
-4. Return true
-```
-
-### Step-by-step for `consumeJunkCost`:
-
-```
-1. Compute effective cost (same as hasJunkCost)
-2. For each (junk_id, qty) in effective cost:
- - p.RemoveItem(junk_id, qty)
-3. Save character
-```
-
-### Edge cases:
-
-- **Stacking:** Junk items are stackable, so `CountItem` returns the total across all inventory slots. `RemoveItem` handles removing across multiple slots.
-- **Multiple junk types from same deck:** A deck only provides ONE junk type. No deck provides multiple types.
-- **No deck, no scrap:** If player has no deck and no scrap in inventory, all mods fail with "You don't have enough junk."
-- **Zero-cost:** After deck reductions, if the effective cost map is empty, the mod is free to cast. This is intended (e.g., bio_strike with a bio deck costs nothing — same as OSRS air strike with staff of air + no rune essence needed).
-
----
-
-## 17. Enchanted Items (Output definitions)
-
-These items are the output of enchantment mods. They need item YAML files.
-
-### Ring of Recoil (from sapphire ring)
-
-```yaml
-id: ring_of_recoil
-name: ring of recoil
-color: "39"
-description: "An enchanted sapphire ring that reflects a portion of melee damage back to the attacker."
-value: 500
-equip_slot: ring
-stats:
- defense_bonus: 0
-```
-
-### Necklace of Passage (from sapphire necklace)
-
-```yaml
-id: necklace_of_passage
-name: necklace of passage
-color: "39"
-description: "An enchanted sapphire necklace that can teleport the wearer to various locations."
-value: 750
-equip_slot: neck
-```
-
-### Ring of Dueling (from emerald ring)
-
-```yaml
-id: ring_of_dueling
-name: ring of dueling
-color: "34"
-description: "An enchanted emerald ring used for teleporting to dueling arenas."
-value: 1000
-equip_slot: ring
-```
-
-### Ring of Forging (from ruby ring)
-
-```yaml
-id: ring_of_forging
-name: ring of forging
-color: "196"
-description: "An enchanted ruby ring that prevents ore from failing to smelt."
-value: 2000
-equip_slot: ring
-```
-
-### Ring of Life (from diamond ring)
-
-```yaml
-id: ring_of_life
-name: ring of life
-color: "231"
-description: "An enchanted diamond ring that teleports you to safety when your HP drops critically low."
-value: 5000
-equip_slot: ring
-```
-
-### Binding Necklace (from emerald necklace)
-
-```yaml
-id: binding_necklace
-name: binding necklace
-color: "34"
-description: "An enchanted emerald necklace. Provides a 100% success rate when identifying junk at altars."
-value: 1200
-equip_slot: neck
-```
-
-### Digsite Pendant (from ruby necklace)
-
-```yaml
-id: digsite_pendant
-name: digsite pendant
-color: "196"
-description: "An enchanted ruby necklace that can teleport you to dig sites."
-value: 2500
-equip_slot: neck
-```
-
-### Phoenix Necklace (from diamond necklace)
-
-```yaml
-id: phoenix_necklace
-name: phoenix necklace
-color: "231"
-description: "An enchanted diamond necklace that restores HP when you drop below 20% health."
-value: 5500
-equip_slot: neck
-```
-
-### Bracelets
-
-```yaml
-id: bracelet_of_clay
-name: bracelet of clay
-color: "39"
-description: "An enchanted sapphire bracelet. Softens clay for easier crafting."
-value: 500
-equip_slot: hands
-```
-
-```yaml
-id: bracelet_of_slaughter
-name: bracelet of slaughter
-color: "34"
-description: "An enchanted emerald bracelet that provides bonus XP on kills."
-value: 1200
-equip_slot: hands
-```
-
-```yaml
-id: inoculation_bracelet
-name: inoculation bracelet
-color: "196"
-description: "An enchanted ruby bracelet that provides resistance to poison."
-value: 2500
-equip_slot: hands
-```
-
-```yaml
-id: abyssal_bracelet
-name: abyssal bracelet
-color: "231"
-description: "An enchanted diamond bracelet that increases scavenging output."
-value: 6000
-equip_slot: hands
-```
-
-### Enchanted Bolts
-
-```yaml
-id: sapphire_bolts_e
-name: sapphire bolts (e)
-color: "39"
-description: "Enchanted sapphire-tipped bolts. Have a chance to drain the target's Science level."
-value: 30
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 4
-```
-
-```yaml
-id: emerald_bolts_e
-name: emerald bolts (e)
-color: "34"
-description: "Enchanted emerald-tipped bolts. Have a chance to poison the target."
-value: 55
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 6
-```
-
-```yaml
-id: ruby_bolts_e
-name: ruby bolts (e)
-color: "196"
-description: "Enchanted ruby-tipped bolts. Have a chance to deal extra damage based on the target's remaining HP."
-value: 100
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 8
-```
-
-```yaml
-id: diamond_bolts_e
-name: diamond bolts (e)
-color: "231"
-description: "Enchanted diamond-tipped bolts. Have a chance to ignore the target's defense."
-value: 180
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 10
-```
-
-### Unenchanted Jewelry (prerequisites — needed if not already in game)
-
-These items need to exist for the enchantment system to work. Create if they don't already exist:
-
-```yaml
-# data/items/sapphire_ring.yaml
-id: sapphire_ring
-name: sapphire ring
-color: "39"
-description: "A ring set with a sapphire. Can be enchanted."
-value: 200
-equip_slot: ring
-
-# data/items/sapphire_necklace.yaml
-id: sapphire_necklace
-name: sapphire necklace
-color: "39"
-description: "A necklace set with a sapphire. Can be enchanted."
-value: 250
-equip_slot: neck
-
-# data/items/sapphire_bracelet.yaml
-id: sapphire_bracelet
-name: sapphire bracelet
-color: "39"
-description: "A bracelet set with a sapphire. Can be enchanted."
-value: 200
-equip_slot: hands
-
-# data/items/emerald_ring.yaml
-id: emerald_ring
-name: emerald ring
-color: "34"
-description: "A ring set with an emerald. Can be enchanted."
-value: 400
-equip_slot: ring
-
-# data/items/emerald_necklace.yaml
-id: emerald_necklace
-name: emerald necklace
-color: "34"
-description: "A necklace set with an emerald. Can be enchanted."
-value: 450
-equip_slot: neck
-
-# data/items/emerald_bracelet.yaml
-id: emerald_bracelet
-name: emerald bracelet
-color: "34"
-description: "A bracelet set with an emerald. Can be enchanted."
-value: 400
-equip_slot: hands
-
-# data/items/ruby_ring.yaml
-id: ruby_ring
-name: ruby ring
-color: "196"
-description: "A ring set with a ruby. Can be enchanted."
-value: 800
-equip_slot: ring
-
-# data/items/ruby_necklace.yaml
-id: ruby_necklace
-name: ruby necklace
-color: "196"
-description: "A necklace set with a ruby. Can be enchanted."
-value: 850
-equip_slot: neck
-
-# data/items/ruby_bracelet.yaml
-id: ruby_bracelet
-name: ruby bracelet
-color: "196"
-description: "A bracelet set with a ruby. Can be enchanted."
-value: 800
-equip_slot: hands
-
-# data/items/diamond_ring.yaml
-id: diamond_ring
-name: diamond ring
-color: "231"
-description: "A ring set with a diamond. Can be enchanted."
-value: 1500
-equip_slot: ring
-
-# data/items/diamond_necklace.yaml
-id: diamond_necklace
-name: diamond necklace
-color: "231"
-description: "A necklace set with a diamond. Can be enchanted."
-value: 1600
-equip_slot: neck
-
-# data/items/diamond_bracelet.yaml
-id: diamond_bracelet
-name: diamond bracelet
-color: "231"
-description: "A bracelet set with a diamond. Can be enchanted."
-value: 1500
-equip_slot: hands
-```
-
-### Unenchanted Bolts (prerequisites)
-
-```yaml
-# data/items/sapphire_bolts.yaml
-id: sapphire_bolts
-name: sapphire bolts
-color: "39"
-description: "Bolts tipped with sapphire. Can be enchanted via science."
-value: 20
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 3
-
-# data/items/emerald_bolts.yaml
-id: emerald_bolts
-name: emerald bolts
-color: "34"
-description: "Bolts tipped with emerald. Can be enchanted via science."
-value: 40
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 5
-
-# data/items/ruby_bolts.yaml
-id: ruby_bolts
-name: ruby bolts
-color: "196"
-description: "Bolts tipped with ruby. Can be enchanted via science."
-value: 75
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 7
-
-# data/items/diamond_bolts.yaml
-id: diamond_bolts
-name: diamond bolts
-color: "231"
-description: "Bolts tipped with diamond. Can be enchanted via science."
-value: 130
-stackable: true
-equip_slot: ammo
-stats:
- attack_bonus: 9
-```
-
----
-
-## 18. `RecipeStore.FindByInput` — New Method
-
-The `superheat` mod needs to find a smelting recipe given an input item. Add a helper method to `RecipeStore`:
-
-**File: `internal/action/recipe.go`** (or wherever RecipeStore is defined)
-
-```go
-func (s *RecipeStore) FindByInput(recipeType string, itemID string) *Recipe {
- // Search all recipes of the given type for one that uses itemID as an input
- recipes := s.LoadAll(recipeType)
- for _, r := range recipes {
- for _, input := range r.Inputs {
- if input.ID == itemID {
- return r
- }
- }
- }
- return nil
-}
-```
-
-If `RecipeStore` doesn't have a `LoadAll` method that filters by type, add one. The existing `RecipeStore` likely loads recipes from `data/recipes/` YAML files. Check the actual implementation to determine the exact approach.
-
----
-
-## 19. Rooms — Deck and Junk Sources
-
-### Deck Spawn Locations
-
-Decks can be found as spawns, mob drops, or shop purchases. For initial implementation, place basic decks as ground spawns:
-
-**Update room 1 (Town Square) or a magic shop room:**
-
-```yaml
-spawns:
- - item_id: basic_deck
- quantity: 1
- respawn_ticks: 120
-```
-
-Elemental decks should be rarer — place in harder areas or as mob drops. Advanced decks should only come from high-level content.
-
-### Higher-tier Junk Altar Rooms
-
-See Section 11 for full room definitions. Place them branching off from the scavenging area or in a dedicated "altar wing."
-
----
-
-## 20. Help Files
-
-### `data/help/trigger.yaml`
-
-```yaml
-id: trigger
-title: "Trigger"
-aliases:
- - cast
-body: |
- Usage: trigger <mod> [target]
- cast <mod> [target]
-
- Trigger a science mod. Combat mods target a mob and initiate science-based combat.
- Utility mods act on items in your inventory or on yourself.
-
- Examples:
- trigger bio strike goblin - Attack a goblin with Bio Strike
- trigger low process iron ore - Convert iron ore to credits
- trigger transport town - Teleport to Town Square
- trigger enchant 1 sapphire ring - Enchant a sapphire ring
- trigger superheat copper ore - Smelt copper ore without a furnace
- trigger em grab bones - Pick up bones from the ground
-
- All mods cost junk (and 1 scrap metal unless you have a deck equipped).
- Type 'mods' to see your available mods and their costs.
-
- See also: autocast, mods, science
-```
-
-### `data/help/autocast.yaml`
-
-```yaml
-id: autocast
-title: "Autocast"
-aliases:
- - auto
-body: |
- Usage: autocast <mod>
- autocast off
- autocast
-
- Set a combat mod to automatically trigger each attack tick during combat.
- When autocast is active, attacking a mob will use science combat instead
- of melee, consuming junk each tick.
-
- If you run out of junk, autocast disables and you switch to melee attacks.
-
- autocast - Show current autocast setting
- autocast solar bolt - Set autocast to Solar Bolt
- autocast off - Disable autocast
-
- See also: trigger, mods, science
-```
-
-### `data/help/mods.yaml`
-
-```yaml
-id: mods
-title: "Mods"
-aliases:
- - modlist
-body: |
- Usage: mods
-
- Display all science mods you have the level to use, organized by
- category (Combat, Processing, Utility, Transport, Enchantment).
-
- Shows the junk cost for each mod (adjusted for your equipped deck).
- Also shows your current autocast setting.
-
- See also: trigger, autocast, science
-```
-
-### `data/help/science.yaml`
-
-```yaml
-id: science
-title: "Science"
-body: |
- Science is the skill that powers mods — powerful modules that can be
- triggered for combat, teleportation, item processing, and enchanting.
-
- Key concepts:
- - Mods are triggered with 'trigger <mod>' or 'cast <mod>'
- - Every mod costs a combination of junk items
- - All mods also cost 1 scrap metal, UNLESS you have a deck equipped
- - Wielding a deck (science weapon) removes the scrap requirement
- - Elemental decks also provide unlimited supply of their junk type
-
- Junk types: solarjunk, hydrojunk, ecojunk, biojunk (from scavenging)
- chaosjunk, deathjunk, bloodjunk (combat mod components)
- lawjunk (transport), cosmicjunk (enchantment), naturejunk (processing)
-
- Decks: basic deck, solar deck, hydro deck, eco deck, bio deck
- Advanced versions of each elemental deck also exist.
-
- Combat: Use 'trigger <mod> <mob>' or set 'autocast <mod>' then 'attack <mob>'
-
- Type 'mods' to see all mods you can currently use.
-
- See also: trigger, autocast, mods, scavenging
-```
-
----
-
-## 21. Implementation Order — Step-by-step Checklist
-
-### Phase 1: Core Infrastructure
-
-- [ ] 1. Add `ProvidesJunk string` field to `ItemDef` in `internal/object/item.go`
-- [ ] 2. Add `AutocastMod string` field (yaml:"-") to `Player` in `internal/player/player.go`
-- [ ] 3. Add `ScienceDefense int` and `Weakness string` to `MobDef` and `MobInstance` in `internal/world/mob.go`
-- [ ] 4. Copy `ScienceDefense` and `Weakness` in mob instantiation logic
-- [ ] 5. Add `ActionTriggering ActionType = "triggering"` to `internal/game/action_state.go`
-- [ ] 6. Add `Description()` case for `ActionTriggering`
-- [ ] 7. Run `make vet` to verify no compile errors
-
-### Phase 2: Mod Definitions
-
-- [ ] 8. Create `internal/game/science.go` with `ModDef`, `AllMods`, `modByID`, `FindMod`, `GetMod`
-- [ ] 9. Define all 20 combat mods (bio/hydro/eco/solar × strike/bolt/blast/wave/surge)
-- [ ] 10. Define all 7 transport mods
-- [ ] 11. Define all 2 processing mods (low_process, high_process)
-- [ ] 12. Define 3 utility mods (bones_to_nutrients, em_grab, superheat)
-- [ ] 13. Define 4 enchant mods (enchant_1 through enchant_4)
-- [ ] 14. Define 4 chip mods (chip_sapphire through chip_diamond)
-- [ ] 15. Define `enchantMap` and `chipMap`
-- [ ] 16. Implement junk cost helpers (`hasDeckEquipped`, `equippedProvidesJunk`, `effectiveJunkCost`, `hasJunkCost`, `consumeJunkCost`)
-- [ ] 17. Run `make vet`
-
-### Phase 3: Commands
-
-- [ ] 18. Create `internal/game/cmd_mods.go` with `doMods`
-- [ ] 19. Create `internal/game/cmd_autocast.go` with `doAutocast`
-- [ ] 20. Create `internal/game/cmd_trigger.go` with `doTrigger`, `parseTriggerArgs`, all trigger sub-handlers, `scienceAttack`, `totalEquipScienceAttack`, `findInventoryItem`
-- [ ] 21. Update `classifyCommand()` in `game.go` — add trigger/cast to ClassActive, autocast/auto/mods/modlist to ClassInstant
-- [ ] 22. Update `executeCommand()` in `game.go` — add dispatch cases for all new commands
-- [ ] 23. Run `make vet`
-
-### Phase 4: Combat Integration
-
-- [ ] 24. Modify `startCombat()` in `cmd_attack.go` to detect `p.AutocastMod` and use science combat path
-- [ ] 25. Handle autocast speed (5 ticks for science) vs melee weapon speed
-- [ ] 26. Handle junk depletion → fallback to melee with message
-- [ ] 27. Update initial combat message for autocast mode
-- [ ] 28. Add `ActionTriggering` to persistent actions list in `ProcessQueuedCommands` if needed
-- [ ] 29. Run `make test`
-
-### Phase 5: Data Files — Junk Items
-
-- [ ] 30. Create `data/items/chaosjunk.yaml`
-- [ ] 31. Create `data/items/deathjunk.yaml`
-- [ ] 32. Create `data/items/bloodjunk.yaml`
-- [ ] 33. Create `data/items/lawjunk.yaml`
-- [ ] 34. Create `data/items/cosmicjunk.yaml`
-- [ ] 35. Create `data/items/naturejunk.yaml`
-- [ ] 36. Create `data/items/nutrient_bar.yaml`
-
-### Phase 6: Data Files — Deck Items
-
-- [ ] 37. Create `data/items/basic_deck.yaml`
-- [ ] 38. Create `data/items/solar_deck.yaml`
-- [ ] 39. Create `data/items/hydro_deck.yaml`
-- [ ] 40. Create `data/items/eco_deck.yaml`
-- [ ] 41. Create `data/items/bio_deck.yaml`
-- [ ] 42. Create `data/items/advanced_solar_deck.yaml`
-- [ ] 43. Create `data/items/advanced_hydro_deck.yaml`
-- [ ] 44. Create `data/items/advanced_eco_deck.yaml`
-- [ ] 45. Create `data/items/advanced_bio_deck.yaml`
-
-### Phase 7: Data Files — Enchanting Prerequisites
-
-- [ ] 46. Create all unenchanted jewelry items (12 files: sapphire/emerald/ruby/diamond × ring/necklace/bracelet)
-- [ ] 47. Create all enchanted jewelry items (12 files)
-- [ ] 48. Create all unenchanted bolt items (4 files)
-- [ ] 49. Create all enchanted bolt items (4 files)
-
-### Phase 8: Data Files — Higher-tier Altars (Scavenging extension)
-
-- [ ] 50. Create 6 new identifier items
-- [ ] 51. Create 6 new altar objects
-- [ ] 52. Create 6 new altar rooms (use next available room IDs)
-- [ ] 53. Update altar config in scavenging `doIdentify` logic to recognize new altars
-- [ ] 54. Place deck spawns in appropriate rooms
-
-### Phase 9: Data Files — Help
-
-- [ ] 55. Create `data/help/trigger.yaml`
-- [ ] 56. Create `data/help/autocast.yaml`
-- [ ] 57. Create `data/help/mods.yaml`
-- [ ] 58. Create `data/help/science.yaml`
-
-### Phase 10: Testing and Polish
-
-- [ ] 59. Run `make test` — all existing tests pass
-- [ ] 60. Run `make vet` — no warnings
-- [ ] 61. Manual test: trigger bio strike on a mob at level 1
-- [ ] 62. Manual test: autocast solar bolt, attack mob, verify science combat
-- [ ] 63. Manual test: run out of junk during autocast, verify melee fallback
-- [ ] 64. Manual test: equip solar deck, verify solarjunk and scrap removed from costs
-- [ ] 65. Manual test: trigger low process on an item, verify credits
-- [ ] 66. Manual test: trigger transport town, verify teleport
-- [ ] 67. Manual test: mods command displays correct costs
-- [ ] 68. Manual test: enchant a sapphire ring
-- [ ] 69. Manual test: superheat an ore
-- [ ] 70. Manual test: bones to nutrients
-- [ ] 71. Manual test: em grab a ground item
-
----
-
-## 22. Dependencies
-
-| Dependency | Status | Required For |
-|------------|--------|-------------|
-| `scavenging.md` — base junk types (solarjunk, hydrojunk, ecojunk, biojunk, scrap_metal) | Must be implemented first | All mods use these as costs |
-| `scavenging.md` — `doIdentify` logic | Must be implemented first | Higher-tier altars reuse the same mechanic |
-| `scavenging.md` — needs update for 6 new altars | Update needed | Chaosjunk, deathjunk, bloodjunk, lawjunk, cosmicjunk, naturejunk production |
-| `ItemStats.ScienceBonus` field | Already exists (unused) | Science attack roll uses this |
-| `WeaponScience` weapon type | Already exists | Deck detection |
-| `combat.HitCheck`, `combat.RollDamage` | Already exist | Science combat reuses these |
-| `player.Science` skill constant | Already exists | Level checks, XP |
-| Crafting system (for jewelry) | May need implementation | Unenchanted jewelry creation |
-| Fletching system (for bolts) | May need implementation | Unenchanted bolt creation |
-
-### Circular dependency note
-
-The enchantment and bolt chipping mods require unenchanted jewelry and bolts to exist. These come from Crafting (jewelry) and Fletching (bolts). If those skills aren't implemented yet, the enchant/chip items can still be defined in YAML and placed as mob drops or ground spawns for testing. The enchantment system itself will work regardless — it just needs the input items to exist in inventory.
-
----
-
-## 23. Complete `AllMods` Definition
-
-For reference, the complete `AllMods` slice in `internal/game/science.go`:
-
-```go
-var AllMods = []*ModDef{
- // Bio Strikes (Air equivalents)
- {ID: "bio_strike", Name: "Bio Strike", Level: 1, MaxHit: 4, BaseXP: 5.5,
- JunkCost: map[string]int{"biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_bolt", Name: "Bio Bolt", Level: 17, MaxHit: 9, BaseXP: 13.5,
- JunkCost: map[string]int{"biojunk": 2, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_blast", Name: "Bio Blast", Level: 41, MaxHit: 13, BaseXP: 25.5,
- JunkCost: map[string]int{"biojunk": 3, "chaosjunk": 1, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_wave", Name: "Bio Wave", Level: 62, MaxHit: 17, BaseXP: 36.0,
- JunkCost: map[string]int{"biojunk": 5, "deathjunk": 1, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_surge", Name: "Bio Surge", Level: 81, MaxHit: 21, BaseXP: 44.0,
- JunkCost: map[string]int{"biojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
-
- // Hydro Strikes (Water equivalents)
- {ID: "hydro_strike", Name: "Hydro Strike", Level: 5, MaxHit: 6, BaseXP: 7.5,
- JunkCost: map[string]int{"hydrojunk": 3, "ecojunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_bolt", Name: "Hydro Bolt", Level: 23, MaxHit: 10, BaseXP: 16.5,
- JunkCost: map[string]int{"hydrojunk": 3, "ecojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_blast", Name: "Hydro Blast", Level: 47, MaxHit: 14, BaseXP: 28.5,
- JunkCost: map[string]int{"hydrojunk": 5, "ecojunk": 3, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_wave", Name: "Hydro Wave", Level: 65, MaxHit: 18, BaseXP: 37.5,
- JunkCost: map[string]int{"hydrojunk": 7, "ecojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_surge", Name: "Hydro Surge", Level: 85, MaxHit: 22, BaseXP: 46.0,
- JunkCost: map[string]int{"hydrojunk": 10, "ecojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
-
- // Eco Strikes (Earth equivalents)
- {ID: "eco_strike", Name: "Eco Strike", Level: 9, MaxHit: 7, BaseXP: 9.5,
- JunkCost: map[string]int{"ecojunk": 2, "biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_bolt", Name: "Eco Bolt", Level: 29, MaxHit: 11, BaseXP: 19.5,
- JunkCost: map[string]int{"ecojunk": 3, "biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_blast", Name: "Eco Blast", Level: 53, MaxHit: 15, BaseXP: 31.5,
- JunkCost: map[string]int{"ecojunk": 4, "biojunk": 3, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_wave", Name: "Eco Wave", Level: 70, MaxHit: 19, BaseXP: 40.0,
- JunkCost: map[string]int{"ecojunk": 7, "biojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_surge", Name: "Eco Surge", Level: 90, MaxHit: 23, BaseXP: 48.5,
- JunkCost: map[string]int{"ecojunk": 10, "biojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
-
- // Solar Strikes (Fire equivalents)
- {ID: "solar_strike", Name: "Solar Strike", Level: 13, MaxHit: 8, BaseXP: 11.5,
- JunkCost: map[string]int{"solarjunk": 3, "ecojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_bolt", Name: "Solar Bolt", Level: 35, MaxHit: 12, BaseXP: 22.5,
- JunkCost: map[string]int{"solarjunk": 4, "ecojunk": 3, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_blast", Name: "Solar Blast", Level: 59, MaxHit: 16, BaseXP: 34.5,
- JunkCost: map[string]int{"solarjunk": 5, "ecojunk": 4, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_wave", Name: "Solar Wave", Level: 75, MaxHit: 20, BaseXP: 42.5,
- JunkCost: map[string]int{"solarjunk": 7, "ecojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_surge", Name: "Solar Surge", Level: 95, MaxHit: 24, BaseXP: 51.0,
- JunkCost: map[string]int{"solarjunk": 10, "ecojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
-
- // Processing Mods
- {ID: "low_process", Name: "Low Level Processing", Level: 21, MaxHit: 0, BaseXP: 31.0,
- JunkCost: map[string]int{"naturejunk": 3, "solarjunk": 1, "scrap_metal": 1},
- Category: ModProcessing, Element: "", TargetType: "inventory"},
- {ID: "high_process", Name: "High Level Processing", Level: 55, MaxHit: 0, BaseXP: 65.0,
- JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap_metal": 1},
- Category: ModProcessing, Element: "", TargetType: "inventory"},
-
- // Utility Mods
- {ID: "bones_to_nutrients", Name: "Bones to Nutrients", Level: 15, MaxHit: 0, BaseXP: 25.0,
- JunkCost: map[string]int{"naturejunk": 2, "ecojunk": 2, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "self"},
- {ID: "em_grab", Name: "Electromagnetic Grab", Level: 33, MaxHit: 0, BaseXP: 43.0,
- JunkCost: map[string]int{"lawjunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "ground_item"},
- {ID: "superheat", Name: "Superheat Item", Level: 43, MaxHit: 0, BaseXP: 53.0,
- JunkCost: map[string]int{"naturejunk": 4, "solarjunk": 1, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "inventory"},
-
- // Transport Mods
- {ID: "transport_town", Name: "Transport: Town Square", Level: 25, MaxHit: 0, BaseXP: 27.0,
- JunkCost: map[string]int{"lawjunk": 1, "solarjunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 1},
- {ID: "transport_forge", Name: "Transport: Forge", Level: 31, MaxHit: 0, BaseXP: 35.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 12},
- {ID: "transport_mine", Name: "Transport: Mining Pit", Level: 37, MaxHit: 0, BaseXP: 40.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "solarjunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 6},
- {ID: "transport_forest", Name: "Transport: Forest", Level: 45, MaxHit: 0, BaseXP: 48.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 22},
- {ID: "transport_scavenge", Name: "Transport: Scavenging Post", Level: 51, MaxHit: 0, BaseXP: 52.0,
- JunkCost: map[string]int{"lawjunk": 1, "naturejunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 9},
- {ID: "transport_deep_mine", Name: "Transport: Deep Mine", Level: 61, MaxHit: 0, BaseXP: 60.0,
- JunkCost: map[string]int{"lawjunk": 2, "ecojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 7},
- {ID: "transport_fishing", Name: "Transport: Fishing Dock", Level: 55, MaxHit: 0, BaseXP: 56.0,
- JunkCost: map[string]int{"lawjunk": 1, "hydrojunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 10},
-
- // Enchant Mods
- {ID: "enchant_1", Name: "Enchant Level 1", Level: 7, MaxHit: 0, BaseXP: 17.5,
- JunkCost: map[string]int{"cosmicjunk": 1, "hydrojunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "enchant_2", Name: "Enchant Level 2", Level: 27, MaxHit: 0, BaseXP: 37.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "biojunk": 3, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "enchant_3", Name: "Enchant Level 3", Level: 49, MaxHit: 0, BaseXP: 59.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "solarjunk": 5, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "enchant_4", Name: "Enchant Level 4", Level: 57, MaxHit: 0, BaseXP: 67.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "ecojunk": 10, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-
- // Chip Bolt Mods
- {ID: "chip_sapphire", Name: "Chip Sapphire Bolts", Level: 4, MaxHit: 0, BaseXP: 9.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "hydrojunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "chip_emerald", Name: "Chip Emerald Bolts", Level: 27, MaxHit: 0, BaseXP: 37.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "biojunk": 3, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "chip_ruby", Name: "Chip Ruby Bolts", Level: 49, MaxHit: 0, BaseXP: 59.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "solarjunk": 5, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "chip_diamond", Name: "Chip Diamond Bolts", Level: 57, MaxHit: 0, BaseXP: 67.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "ecojunk": 10, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-}
-```
-
-Total mods: 20 combat + 2 processing + 3 utility + 7 transport + 4 enchant + 4 chip = **40 mods**.
-
----
-
-## 24. `ScienceBonus` Usage Clarification
-
-The existing `ItemStats.ScienceBonus` field at `internal/object/item.go:71` is currently unused. With this implementation:
-
-- `ScienceBonus` on `ItemStats` is used as the **science attack bonus** for the attack roll calculation
-- It is analogous to `AttackBonus` for melee
-- All equipped items' `ScienceBonus` values are summed via `totalEquipScienceAttack()`
-- Decks have `science_bonus: 10` (basic) or `science_bonus: 20` (advanced) in their stats
-- Other equipment can also have `science_bonus` to boost science accuracy (e.g., mystic robes equivalent)
-
-**No new `ItemStats` fields are needed.** The existing `ScienceBonus` field covers the science attack roll. The max hit for science combat comes entirely from the mod definition, not from equipment (same as OSRS magic).
-
----
-
-## 25. CombatLevel Update
-
-The existing `CombatLevel()` at `internal/player/player.go:314` already includes Science at `+0.125`. However, OSRS uses a dominant-style formula where magic competes with melee/ranged. For a more accurate OSRS formula:
-
-```go
-func (p *Player) CombatLevel() int {
- base := 0.25 * float64(p.Level(Defense)+p.Level(Hitpoints)+p.Level(Technology))
- att := float64(p.Level(Attack))
- str := float64(p.Level(Strength))
- melee := 0.325 * (att + str)
- ranged := 0.325 * float64(p.Level(Ranged)) * 1.5
- science := 0.325 * float64(p.Level(Science)) * 1.5
-
- dominant := melee
- if ranged > dominant {
- dominant = ranged
- }
- if science > dominant {
- dominant = science
- }
-
- return int(base + dominant)
-}
-```
-
-**This change is OPTIONAL.** The current formula works. Update only if the game design wants science to compete with melee/ranged for combat level dominance. If updated, remove the existing `+0.125 * Science` line and add science to the dominant-style calculation.
-
----
-
-## 26. Color Target
-
-Add a new color target for science mod names:
-
-**File: `internal/config/colors.go`** (or wherever color targets are defined)
-
-Add `"science_mod"` as a configurable color target with a default of `"99"` (purple/cosmic).
-
-This allows players to customize the color of mod names in combat output via the `color` command:
-```
-color science_mod 39
-```