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