# 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 # utility mod or combat with default target trigger # combat mod on specific mob cast solar bolt # alias, prefix matching trigger low process # utility mod trigger transport town # teleport trigger enchant 1 # enchant an inventory item trigger em grab # pick up ground item via science trigger superheat # 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 `, 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 ` or `trigger low process ` 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 . You receive 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 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 ` 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 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 ` 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 and produce a ."` ### 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 ."` 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 ` 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 and it becomes a !"` 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 ` or via autocast during combat): 1. **Level check:** Player Science level >= mod.Level. If not: `"You need level science to trigger ."` 2. **Junk cost check:** Call `hasJunkCost(p, mod)`. If not: `"You don't have enough junk to trigger ."` 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 `), 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 `: 1. If player is already in combat: switch to using this mod as the current autocast. Output: `"You switch to triggering ."` 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 ' 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: 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: 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: 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: 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: 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: 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 []" // 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 ", 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 ") 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 ") 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 ", 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 [target] cast [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 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 ' or 'cast ' - 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 ' or set 'autocast ' then 'attack ' 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 ```