aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-19 03:05:46 -0400
committerhistoria <[not public]>2026-06-19 03:05:46 -0400
commitfbd1c30d7b9777c9df6653f0c393afa7a7dfa519 (patch)
tree7d4dd5b1fb1a5f868a330caf009ac8dc4e583025
parentdea4a06764c507699cf9adcd1fd0a572d227aff0 (diff)
downloadthehouseoficarus-fbd1c30d7b9777c9df6653f0c393afa7a7dfa519.tar.gz
feat: science skill roughly implemented
-rw-r--r--TODO.md3
-rw-r--r--data/help/autocast.yaml21
-rw-r--r--data/help/mods.yaml16
-rw-r--r--data/help/science.yaml32
-rw-r--r--data/help/trigger.yaml24
-rw-r--r--data/items/abyssal_bracelet.yaml6
-rw-r--r--data/items/advanced_bio_deck.yaml11
-rw-r--r--data/items/advanced_eco_deck.yaml11
-rw-r--r--data/items/advanced_hydro_deck.yaml11
-rw-r--r--data/items/advanced_solar_deck.yaml11
-rw-r--r--data/items/basic_deck.yaml10
-rw-r--r--data/items/binding_necklace.yaml6
-rw-r--r--data/items/bio_deck.yaml11
-rw-r--r--data/items/bracelet_of_clay.yaml6
-rw-r--r--data/items/bracelet_of_slaughter.yaml6
-rw-r--r--data/items/diamond_bolts.yaml6
-rw-r--r--data/items/diamond_bolts_e.yaml9
-rw-r--r--data/items/diamond_bracelet.yaml4
-rw-r--r--data/items/diamond_necklace.yaml4
-rw-r--r--data/items/diamond_ring.yaml4
-rw-r--r--data/items/digsite_pendant.yaml6
-rw-r--r--data/items/eco_deck.yaml11
-rw-r--r--data/items/emerald_bolts.yaml8
-rw-r--r--data/items/emerald_bolts_e.yaml9
-rw-r--r--data/items/emerald_bracelet.yaml6
-rw-r--r--data/items/emerald_necklace.yaml6
-rw-r--r--data/items/emerald_ring.yaml6
-rw-r--r--data/items/hydro_deck.yaml11
-rw-r--r--data/items/inoculation_bracelet.yaml6
-rw-r--r--data/items/necklace_of_passage.yaml6
-rw-r--r--data/items/nutrient_bar.yaml8
-rw-r--r--data/items/phoenix_necklace.yaml6
-rw-r--r--data/items/ring_of_dueling.yaml6
-rw-r--r--data/items/ring_of_forging.yaml6
-rw-r--r--data/items/ring_of_life.yaml6
-rw-r--r--data/items/ring_of_recoil.yaml6
-rw-r--r--data/items/ruby_bolts.yaml6
-rw-r--r--data/items/ruby_bolts_e.yaml9
-rw-r--r--data/items/ruby_bracelet.yaml4
-rw-r--r--data/items/ruby_necklace.yaml4
-rw-r--r--data/items/ruby_ring.yaml4
-rw-r--r--data/items/sapphire_bolts.yaml8
-rw-r--r--data/items/sapphire_bolts_e.yaml9
-rw-r--r--data/items/sapphire_bracelet.yaml6
-rw-r--r--data/items/sapphire_necklace.yaml6
-rw-r--r--data/items/sapphire_ring.yaml6
-rw-r--r--data/items/solar_deck.yaml11
-rw-r--r--internal/action/recipe.go20
-rw-r--r--internal/config/config.go1
-rw-r--r--internal/game/action_state.go3
-rw-r--r--internal/game/cmd_attack.go61
-rw-r--r--internal/game/cmd_autocast.go54
-rw-r--r--internal/game/cmd_mods.go111
-rw-r--r--internal/game/cmd_trigger.go651
-rw-r--r--internal/game/game.go13
-rw-r--r--internal/game/science.go332
-rw-r--r--internal/object/item.go1
-rw-r--r--internal/player/player.go1
58 files changed, 1583 insertions, 63 deletions
diff --git a/TODO.md b/TODO.md
index b7309dd..67c77f1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -16,6 +16,7 @@ Detailed implementation plans for each skill/system are in `skill_plans/`.
- [x] Step 3: Combat Feel — Aggressive mobs (auto-attack on room enter, OSRS combat level rule), equipment skill requirements on all tiered gear
- [x] Step 4: Technology — 25 techs (attack/str/def/ranged/science boosts, 3 protection techs, utility techs), Battery drain, 1-tick flicking, tech command, TechTick, charging station, score/prompt integration
- [x] Step 5: Scavenging — mine_scrap behavior verified. Created 10 junk items (solar, hydro, cosmic, eco, chaos, bio, nature, law, death, blood — all stackable) with matching identifier tools and altar objects. `id`/`identify` command (Active): scans room for altar, checks for matching identifier in inventory, converts all scrap to junk on next tick with level-based multipliers, awards Scavenging XP. 10 altar rooms (150-159) branching from Scavenging Post (room 9). Help files created.
+- [x] Step 6: Science — 40 mods (20 combat, 2 processing, 3 utility, 7 transport, 4 enchant, 4 chip), ModDef/AllMods in science.go. `trigger`/`cast` command (Active). `autocast`/`auto` (Instant). `mods`/`modlist` (Instant). Autocast in startCombat with 5-tick science speed and melee fallback. `scienceAttack()` with elemental weakness +30%. Junk cost system with deck exemptions. `ProvidesJunk` on ItemDef, `AutocastMod` on Player, `FindByInput` on RecipeStore, `science_mod` color target. 9 deck items, 24 jewelry items, 8 bolt items, nutrient_bar. Help files created.
---
@@ -31,7 +32,7 @@ See `skill_plans/scavenging.md`. The simplest new skill — mine scrap (already
See `skill_plans/science.md`. Players "trigger mods" (cast spells) powered by junk (runes). Decks (staves) provide unlimited supply of one junk type and remove scrap requirement. Depends on combat overhaul + scavenging.
-- [ ] **Science System** — Define ~40 mods in Go: 20 combat mods (5 levels each of solar/hydro/eco/bio strikes), ~20 utility mods (low/high level processing, bones to nutrients, electromagnetic grab, superheat item, transport/teleport mods, jewelry enchants, bolt chipping). Add `ProvidesJunk string` field to ItemDef. Create deck items (basic, solar, hydro, eco, bio — each provides unlimited of one junk type; any deck removes scrap requirement). Add `trigger`/`cast` command (Active for combat, instant for utility), `autocast` command (Instant), `mods` command (Instant — shows known mods and junk costs). Integrate autocast into combat loop in `cmd_attack.go`. Science combat uses Science level + science_attack for accuracy, mod's MaxHit for damage. Elemental weakness gives +30% accuracy. Award Science XP per cast.
+- [x] Step 6: Science — 40 mods (20 combat, 2 processing, 3 utility, 7 transport, 4 enchant, 4 chip), ModDef/AllMods in science.go. `trigger`/`cast` command (Active) routes to combat/transport/processing/utility/enchant handlers. `autocast`/`auto` command (Instant) sets science combat autocast. `mods`/`modlist` command (Instant) shows available mods with effective junk costs. Integrated autocast into `startCombat()` in cmd_attack.go with 5-tick science speed and melee fallback on junk depletion. `scienceAttack()` with Science level + science_attack roll, elemental weakness +30% accuracy, mod MaxHit damage. Junk cost system: deck-equipped removes scrap_metal, elemental deck provides unlimited junk type. Added `ProvidesJunk` to ItemDef, `AutocastMod` to Player, `FindByInput` to RecipeStore, `science_mod` color target. 9 deck items, 24 jewelry items, 8 bolt items, nutrient_bar item. Help files created.
---
diff --git a/data/help/autocast.yaml b/data/help/autocast.yaml
new file mode 100644
index 0000000..9b4fc40
--- /dev/null
+++ b/data/help/autocast.yaml
@@ -0,0 +1,21 @@
+name: "autocast"
+category: "Commands"
+description: |
+ Set a mod to autocast during combat.
+
+ 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
diff --git a/data/help/mods.yaml b/data/help/mods.yaml
new file mode 100644
index 0000000..ed9dbcb
--- /dev/null
+++ b/data/help/mods.yaml
@@ -0,0 +1,16 @@
+name: "mods"
+category: "Commands"
+description: |
+ Display available science mods.
+
+ Usage: mods
+ modlist
+
+ 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
diff --git a/data/help/science.yaml b/data/help/science.yaml
new file mode 100644
index 0000000..7681310
--- /dev/null
+++ b/data/help/science.yaml
@@ -0,0 +1,32 @@
+name: "science"
+category: "Skills"
+description: |
+ 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
diff --git a/data/help/trigger.yaml b/data/help/trigger.yaml
new file mode 100644
index 0000000..0e6823d
--- /dev/null
+++ b/data/help/trigger.yaml
@@ -0,0 +1,24 @@
+name: "trigger"
+category: "Commands"
+description: |
+ Trigger a science mod.
+
+ 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 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 without 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 costs.
+
+ See also: autocast, mods, science
diff --git a/data/items/abyssal_bracelet.yaml b/data/items/abyssal_bracelet.yaml
new file mode 100644
index 0000000..3b424c3
--- /dev/null
+++ b/data/items/abyssal_bracelet.yaml
@@ -0,0 +1,6 @@
+id: abyssal_bracelet
+name: abyssal bracelet
+color: "255"
+description: "An enchanted diamond bracelet that increases scavenging output."
+value: 6000
+equip_slot: hands
diff --git a/data/items/advanced_bio_deck.yaml b/data/items/advanced_bio_deck.yaml
new file mode 100644
index 0000000..1411a16
--- /dev/null
+++ b/data/items/advanced_bio_deck.yaml
@@ -0,0 +1,11 @@
+id: advanced_bio_deck
+name: advanced bio deck
+color: "196 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_attack: 20
+provides_junk: biojunk
diff --git a/data/items/advanced_eco_deck.yaml b/data/items/advanced_eco_deck.yaml
new file mode 100644
index 0000000..c844f47
--- /dev/null
+++ b/data/items/advanced_eco_deck.yaml
@@ -0,0 +1,11 @@
+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_attack: 20
+provides_junk: ecojunk
diff --git a/data/items/advanced_hydro_deck.yaml b/data/items/advanced_hydro_deck.yaml
new file mode 100644
index 0000000..2910931
--- /dev/null
+++ b/data/items/advanced_hydro_deck.yaml
@@ -0,0 +1,11 @@
+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_attack: 20
+provides_junk: hydrojunk
diff --git a/data/items/advanced_solar_deck.yaml b/data/items/advanced_solar_deck.yaml
new file mode 100644
index 0000000..002845c
--- /dev/null
+++ b/data/items/advanced_solar_deck.yaml
@@ -0,0 +1,11 @@
+id: advanced_solar_deck
+name: advanced solar deck
+color: "220 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_attack: 20
+provides_junk: solarjunk
diff --git a/data/items/basic_deck.yaml b/data/items/basic_deck.yaml
new file mode 100644
index 0000000..ef2549a
--- /dev/null
+++ b/data/items/basic_deck.yaml
@@ -0,0 +1,10 @@
+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_attack: 5
diff --git a/data/items/binding_necklace.yaml b/data/items/binding_necklace.yaml
new file mode 100644
index 0000000..e9e7559
--- /dev/null
+++ b/data/items/binding_necklace.yaml
@@ -0,0 +1,6 @@
+id: binding_necklace
+name: binding necklace
+color: "34"
+description: "An enchanted emerald necklace. Provides a 100 percent success rate when identifying junk at altars."
+value: 1200
+equip_slot: neck
diff --git a/data/items/bio_deck.yaml b/data/items/bio_deck.yaml
new file mode 100644
index 0000000..8de2036
--- /dev/null
+++ b/data/items/bio_deck.yaml
@@ -0,0 +1,11 @@
+id: bio_deck
+name: bio deck
+color: "196"
+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_attack: 10
+provides_junk: biojunk
diff --git a/data/items/bracelet_of_clay.yaml b/data/items/bracelet_of_clay.yaml
new file mode 100644
index 0000000..d8cd881
--- /dev/null
+++ b/data/items/bracelet_of_clay.yaml
@@ -0,0 +1,6 @@
+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
diff --git a/data/items/bracelet_of_slaughter.yaml b/data/items/bracelet_of_slaughter.yaml
new file mode 100644
index 0000000..2bbbbec
--- /dev/null
+++ b/data/items/bracelet_of_slaughter.yaml
@@ -0,0 +1,6 @@
+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
diff --git a/data/items/diamond_bolts.yaml b/data/items/diamond_bolts.yaml
index f3e8d83..efe8013 100644
--- a/data/items/diamond_bolts.yaml
+++ b/data/items/diamond_bolts.yaml
@@ -1,9 +1,9 @@
id: diamond_bolts
name: diamond bolts
color: "255"
-description: "Adamant bolts tipped with diamond."
-value: 230
+description: "Bolts tipped with diamond. Can be enchanted via science."
+value: 130
stackable: true
equip_slot: ammo
stats:
- ranged_strength: 52
+ ranged_attack: 9
diff --git a/data/items/diamond_bolts_e.yaml b/data/items/diamond_bolts_e.yaml
new file mode 100644
index 0000000..93ade4c
--- /dev/null
+++ b/data/items/diamond_bolts_e.yaml
@@ -0,0 +1,9 @@
+id: diamond_bolts_e
+name: diamond bolts e
+color: "255 bold"
+description: "Enchanted diamond-tipped bolts. Have a chance to ignore the target's defense."
+value: 180
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_attack: 10
diff --git a/data/items/diamond_bracelet.yaml b/data/items/diamond_bracelet.yaml
index c4fcb85..5ba5778 100644
--- a/data/items/diamond_bracelet.yaml
+++ b/data/items/diamond_bracelet.yaml
@@ -1,6 +1,6 @@
id: diamond_bracelet
name: diamond bracelet
color: "255"
-description: "A gold bracelet set with a diamond."
-value: 190
+description: "A bracelet set with a diamond. Can be enchanted."
+value: 1500
equip_slot: hands
diff --git a/data/items/diamond_necklace.yaml b/data/items/diamond_necklace.yaml
index e900756..5629b8c 100644
--- a/data/items/diamond_necklace.yaml
+++ b/data/items/diamond_necklace.yaml
@@ -1,6 +1,6 @@
id: diamond_necklace
name: diamond necklace
color: "255"
-description: "A gold necklace set with a diamond."
-value: 180
+description: "A necklace set with a diamond. Can be enchanted."
+value: 1600
equip_slot: neck
diff --git a/data/items/diamond_ring.yaml b/data/items/diamond_ring.yaml
index 7980d2a..54023c3 100644
--- a/data/items/diamond_ring.yaml
+++ b/data/items/diamond_ring.yaml
@@ -1,6 +1,6 @@
id: diamond_ring
name: diamond ring
color: "255"
-description: "A gold ring set with a diamond."
-value: 170
+description: "A ring set with a diamond. Can be enchanted."
+value: 1500
equip_slot: ring
diff --git a/data/items/digsite_pendant.yaml b/data/items/digsite_pendant.yaml
new file mode 100644
index 0000000..b28e3e3
--- /dev/null
+++ b/data/items/digsite_pendant.yaml
@@ -0,0 +1,6 @@
+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
diff --git a/data/items/eco_deck.yaml b/data/items/eco_deck.yaml
new file mode 100644
index 0000000..ef1a991
--- /dev/null
+++ b/data/items/eco_deck.yaml
@@ -0,0 +1,11 @@
+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_attack: 10
+provides_junk: ecojunk
diff --git a/data/items/emerald_bolts.yaml b/data/items/emerald_bolts.yaml
index b123d92..1afdcd0 100644
--- a/data/items/emerald_bolts.yaml
+++ b/data/items/emerald_bolts.yaml
@@ -1,9 +1,9 @@
id: emerald_bolts
name: emerald bolts
-color: "83"
-description: "Mithril bolts tipped with emerald."
-value: 65
+color: "34"
+description: "Bolts tipped with emerald. Can be enchanted via science."
+value: 40
stackable: true
equip_slot: ammo
stats:
- ranged_strength: 39
+ ranged_attack: 5
diff --git a/data/items/emerald_bolts_e.yaml b/data/items/emerald_bolts_e.yaml
new file mode 100644
index 0000000..4cb7cbd
--- /dev/null
+++ b/data/items/emerald_bolts_e.yaml
@@ -0,0 +1,9 @@
+id: emerald_bolts_e
+name: emerald bolts e
+color: "34 bold"
+description: "Enchanted emerald-tipped bolts. Have a chance to poison the target."
+value: 55
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_attack: 6
diff --git a/data/items/emerald_bracelet.yaml b/data/items/emerald_bracelet.yaml
index 91fa223..04e4979 100644
--- a/data/items/emerald_bracelet.yaml
+++ b/data/items/emerald_bracelet.yaml
@@ -1,6 +1,6 @@
id: emerald_bracelet
name: emerald bracelet
-color: "83"
-description: "A gold bracelet set with a emerald."
-value: 105
+color: "34"
+description: "A bracelet set with an emerald. Can be enchanted."
+value: 400
equip_slot: hands
diff --git a/data/items/emerald_necklace.yaml b/data/items/emerald_necklace.yaml
index 2f87002..2107ca4 100644
--- a/data/items/emerald_necklace.yaml
+++ b/data/items/emerald_necklace.yaml
@@ -1,6 +1,6 @@
id: emerald_necklace
name: emerald necklace
-color: "83"
-description: "A gold necklace set with a emerald."
-value: 100
+color: "34"
+description: "A necklace set with an emerald. Can be enchanted."
+value: 450
equip_slot: neck
diff --git a/data/items/emerald_ring.yaml b/data/items/emerald_ring.yaml
index f70a059..253f872 100644
--- a/data/items/emerald_ring.yaml
+++ b/data/items/emerald_ring.yaml
@@ -1,6 +1,6 @@
id: emerald_ring
name: emerald ring
-color: "83"
-description: "A gold ring set with a emerald."
-value: 95
+color: "34"
+description: "A ring set with an emerald. Can be enchanted."
+value: 400
equip_slot: ring
diff --git a/data/items/hydro_deck.yaml b/data/items/hydro_deck.yaml
new file mode 100644
index 0000000..0e417a2
--- /dev/null
+++ b/data/items/hydro_deck.yaml
@@ -0,0 +1,11 @@
+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_attack: 10
+provides_junk: hydrojunk
diff --git a/data/items/inoculation_bracelet.yaml b/data/items/inoculation_bracelet.yaml
new file mode 100644
index 0000000..9a3a149
--- /dev/null
+++ b/data/items/inoculation_bracelet.yaml
@@ -0,0 +1,6 @@
+id: inoculation_bracelet
+name: inoculation bracelet
+color: "196"
+description: "An enchanted ruby bracelet that provides resistance to poison."
+value: 2500
+equip_slot: hands
diff --git a/data/items/necklace_of_passage.yaml b/data/items/necklace_of_passage.yaml
new file mode 100644
index 0000000..b5630ed
--- /dev/null
+++ b/data/items/necklace_of_passage.yaml
@@ -0,0 +1,6 @@
+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
diff --git a/data/items/nutrient_bar.yaml b/data/items/nutrient_bar.yaml
new file mode 100644
index 0000000..d33d7de
--- /dev/null
+++ b/data/items/nutrient_bar.yaml
@@ -0,0 +1,8 @@
+id: nutrient_bar
+name: nutrient bar
+color: "220"
+description: "A compressed bar of processed nutrients. Restores a small amount of health."
+value: 5
+stackable: true
+heal_value: 2
+eat_message: "You eat the nutrient bar."
diff --git a/data/items/phoenix_necklace.yaml b/data/items/phoenix_necklace.yaml
new file mode 100644
index 0000000..b53fafb
--- /dev/null
+++ b/data/items/phoenix_necklace.yaml
@@ -0,0 +1,6 @@
+id: phoenix_necklace
+name: phoenix necklace
+color: "255"
+description: "An enchanted diamond necklace that restores HP when you drop below 20 percent health."
+value: 5500
+equip_slot: neck
diff --git a/data/items/ring_of_dueling.yaml b/data/items/ring_of_dueling.yaml
new file mode 100644
index 0000000..0c11e87
--- /dev/null
+++ b/data/items/ring_of_dueling.yaml
@@ -0,0 +1,6 @@
+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
diff --git a/data/items/ring_of_forging.yaml b/data/items/ring_of_forging.yaml
new file mode 100644
index 0000000..d0792a1
--- /dev/null
+++ b/data/items/ring_of_forging.yaml
@@ -0,0 +1,6 @@
+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
diff --git a/data/items/ring_of_life.yaml b/data/items/ring_of_life.yaml
new file mode 100644
index 0000000..ab0b6e3
--- /dev/null
+++ b/data/items/ring_of_life.yaml
@@ -0,0 +1,6 @@
+id: ring_of_life
+name: ring of life
+color: "255"
+description: "An enchanted diamond ring that teleports you to safety when your HP drops critically low."
+value: 5000
+equip_slot: ring
diff --git a/data/items/ring_of_recoil.yaml b/data/items/ring_of_recoil.yaml
new file mode 100644
index 0000000..c143f68
--- /dev/null
+++ b/data/items/ring_of_recoil.yaml
@@ -0,0 +1,6 @@
+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
diff --git a/data/items/ruby_bolts.yaml b/data/items/ruby_bolts.yaml
index cf50f5e..b83bec2 100644
--- a/data/items/ruby_bolts.yaml
+++ b/data/items/ruby_bolts.yaml
@@ -1,9 +1,9 @@
id: ruby_bolts
name: ruby bolts
color: "196"
-description: "Adamant bolts tipped with ruby."
-value: 130
+description: "Bolts tipped with ruby. Can be enchanted via science."
+value: 75
stackable: true
equip_slot: ammo
stats:
- ranged_strength: 49
+ ranged_attack: 7
diff --git a/data/items/ruby_bolts_e.yaml b/data/items/ruby_bolts_e.yaml
new file mode 100644
index 0000000..14954c2
--- /dev/null
+++ b/data/items/ruby_bolts_e.yaml
@@ -0,0 +1,9 @@
+id: ruby_bolts_e
+name: ruby bolts e
+color: "196 bold"
+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:
+ ranged_attack: 8
diff --git a/data/items/ruby_bracelet.yaml b/data/items/ruby_bracelet.yaml
index 4a9a509..e5f3f70 100644
--- a/data/items/ruby_bracelet.yaml
+++ b/data/items/ruby_bracelet.yaml
@@ -1,6 +1,6 @@
id: ruby_bracelet
name: ruby bracelet
color: "196"
-description: "A gold bracelet set with a ruby."
-value: 140
+description: "A bracelet set with a ruby. Can be enchanted."
+value: 800
equip_slot: hands
diff --git a/data/items/ruby_necklace.yaml b/data/items/ruby_necklace.yaml
index 2c1315d..e181777 100644
--- a/data/items/ruby_necklace.yaml
+++ b/data/items/ruby_necklace.yaml
@@ -1,6 +1,6 @@
id: ruby_necklace
name: ruby necklace
color: "196"
-description: "A gold necklace set with a ruby."
-value: 135
+description: "A necklace set with a ruby. Can be enchanted."
+value: 850
equip_slot: neck
diff --git a/data/items/ruby_ring.yaml b/data/items/ruby_ring.yaml
index 9582c0a..cb0cfde 100644
--- a/data/items/ruby_ring.yaml
+++ b/data/items/ruby_ring.yaml
@@ -1,6 +1,6 @@
id: ruby_ring
name: ruby ring
color: "196"
-description: "A gold ring set with a ruby."
-value: 130
+description: "A ring set with a ruby. Can be enchanted."
+value: 800
equip_slot: ring
diff --git a/data/items/sapphire_bolts.yaml b/data/items/sapphire_bolts.yaml
index e465e91..a7102c9 100644
--- a/data/items/sapphire_bolts.yaml
+++ b/data/items/sapphire_bolts.yaml
@@ -1,9 +1,9 @@
id: sapphire_bolts
name: sapphire bolts
-color: "69"
-description: "Mithril bolts tipped with sapphire."
-value: 40
+color: "39"
+description: "Bolts tipped with sapphire. Can be enchanted via science."
+value: 20
stackable: true
equip_slot: ammo
stats:
- ranged_strength: 36
+ ranged_attack: 3
diff --git a/data/items/sapphire_bolts_e.yaml b/data/items/sapphire_bolts_e.yaml
new file mode 100644
index 0000000..dc378cc
--- /dev/null
+++ b/data/items/sapphire_bolts_e.yaml
@@ -0,0 +1,9 @@
+id: sapphire_bolts_e
+name: sapphire bolts e
+color: "39 bold"
+description: "Enchanted sapphire-tipped bolts. Have a chance to drain the target's Science level."
+value: 30
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_attack: 4
diff --git a/data/items/sapphire_bracelet.yaml b/data/items/sapphire_bracelet.yaml
index 54d7c45..a2996eb 100644
--- a/data/items/sapphire_bracelet.yaml
+++ b/data/items/sapphire_bracelet.yaml
@@ -1,6 +1,6 @@
id: sapphire_bracelet
name: sapphire bracelet
-color: "69"
-description: "A gold bracelet set with a sapphire."
-value: 95
+color: "39"
+description: "A bracelet set with a sapphire. Can be enchanted."
+value: 200
equip_slot: hands
diff --git a/data/items/sapphire_necklace.yaml b/data/items/sapphire_necklace.yaml
index 80deb37..e553973 100644
--- a/data/items/sapphire_necklace.yaml
+++ b/data/items/sapphire_necklace.yaml
@@ -1,6 +1,6 @@
id: sapphire_necklace
name: sapphire necklace
-color: "69"
-description: "A gold necklace set with a sapphire."
-value: 90
+color: "39"
+description: "A necklace set with a sapphire. Can be enchanted."
+value: 250
equip_slot: neck
diff --git a/data/items/sapphire_ring.yaml b/data/items/sapphire_ring.yaml
index 013a730..40e8cc5 100644
--- a/data/items/sapphire_ring.yaml
+++ b/data/items/sapphire_ring.yaml
@@ -1,6 +1,6 @@
id: sapphire_ring
name: sapphire ring
-color: "69"
-description: "A gold ring set with a sapphire."
-value: 80
+color: "39"
+description: "A ring set with a sapphire. Can be enchanted."
+value: 200
equip_slot: ring
diff --git a/data/items/solar_deck.yaml b/data/items/solar_deck.yaml
new file mode 100644
index 0000000..72b7e9e
--- /dev/null
+++ b/data/items/solar_deck.yaml
@@ -0,0 +1,11 @@
+id: solar_deck
+name: solar deck
+color: "220"
+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_attack: 10
+provides_junk: solarjunk
diff --git a/internal/action/recipe.go b/internal/action/recipe.go
index fee3434..d1acaa5 100644
--- a/internal/action/recipe.go
+++ b/internal/action/recipe.go
@@ -75,6 +75,26 @@ func (r *RecipeDef) MatchesEntry(itemID string) bool {
return false
}
+func (s *RecipeStore) FindByInput(recipeType string, itemID string) *RecipeDef {
+ all, err := s.LoadAll()
+ if err != nil {
+ return nil
+ }
+ for i := range all {
+ if recipeType != "" && all[i].Type != recipeType {
+ continue
+ }
+ for _, e := range all[i].Consume {
+ for _, id := range e.Items {
+ if id == itemID {
+ return &all[i]
+ }
+ }
+ }
+ }
+ return nil
+}
+
func (s *RecipeStore) FindByItem(itemID, recipeType string) ([]RecipeDef, error) {
all, err := s.LoadAll()
if err != nil {
diff --git a/internal/config/config.go b/internal/config/config.go
index a9f0020..932af4a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -50,6 +50,7 @@ func DefaultColors() ColorsConfig {
"visual_first_tick": "196 bold",
"battery": "45",
"tech_depleted": "196",
+ "science_mod": "99",
}
}
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index 0dc3d32..596eb4e 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -25,6 +25,7 @@ const (
ActionCleaning ActionType = "cleaning"
ActionMixing ActionType = "mixing"
ActionIdentifying ActionType = "identifying"
+ ActionTriggering ActionType = "triggering"
)
type ActionState struct {
@@ -82,6 +83,8 @@ func (a *ActionState) Description() string {
return "mixing " + a.TargetName
case ActionIdentifying:
return "identifying scrap at " + a.TargetName
+ case ActionTriggering:
+ return "triggering " + a.TargetName
}
return ""
}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 401837f..29bff30 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -124,24 +124,39 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
combat.EnterCombat(p.Name, mob.InstanceID)
+ autocastActive := p.AutocastMod != ""
playerSpeed := g.playerWeaponSpeed(p)
-
- attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
- var styleParts []string
- if attBonus > 0 {
- styleParts = append(styleParts, fmt.Sprintf("+%d atk", attBonus))
- }
- if strBonus > 0 {
- styleParts = append(styleParts, fmt.Sprintf("+%d str", strBonus))
+ if autocastActive {
+ playerSpeed = 5.0
}
- if defBonus > 0 {
- styleParts = append(styleParts, fmt.Sprintf("+%d def", defBonus))
- }
- styleStr := ""
- if len(styleParts) > 0 {
- styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")"
+
+ if !autocastActive {
+ attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+ var styleParts []string
+ if attBonus > 0 {
+ styleParts = append(styleParts, fmt.Sprintf("+%d atk", attBonus))
+ }
+ if strBonus > 0 {
+ styleParts = append(styleParts, fmt.Sprintf("+%d str", strBonus))
+ }
+ if defBonus > 0 {
+ styleParts = append(styleParts, fmt.Sprintf("+%d def", defBonus))
+ }
+ styleStr := ""
+ if len(styleParts) > 0 {
+ styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")"
+ }
+ sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", g.colorize(sess, "mob_name", mobDisplayName(mob, true)), styleStr))
+ } else {
+ mod := GetMod(p.AutocastMod)
+ modName := p.AutocastMod
+ if mod != nil {
+ modName = mod.Name
+ }
+ sess.WriteLine(fmt.Sprintf("\nYou attack %s with %s!",
+ g.colorize(sess, "mob_name", mobDisplayName(mob, true)),
+ g.colorize(sess, "science_mod", modName)))
}
- sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", g.colorize(sess, "mob_name", mobDisplayName(mob, true)), styleStr))
p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name}
g.Ticks.Subscribe(engine.ToTicks(playerSpeed), func() bool {
@@ -154,7 +169,21 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
g.endCombat(sess, p, currentMob)
return false
}
- g.playerAttack(sess, p, currentMob)
+ 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
diff --git a/internal/game/cmd_autocast.go b/internal/game/cmd_autocast.go
new file mode 100644
index 0000000..205fd8d
--- /dev/null
+++ b/internal/game/cmd_autocast.go
@@ -0,0 +1,54 @@
+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))
+}
diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go
new file mode 100644
index 0000000..b9161e3
--- /dev/null
+++ b/internal/game/cmd_mods.go
@@ -0,0 +1,111 @@
+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
+ 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, ", ")
+}
diff --git a/internal/game/cmd_trigger.go b/internal/game/cmd_trigger.go
new file mode 100644
index 0000000..8e3a84e
--- /dev/null
+++ b/internal/game/cmd_trigger.go
@@ -0,0 +1,651 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+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
+ }
+
+ 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)
+ 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 cs := combat.GetCombat(p.Name); cs != nil {
+ p.AutocastMod = mod.ID
+ sess.WriteLine(fmt.Sprintf("You switch to triggering %s.", mod.Name))
+ return
+ }
+
+ 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)
+
+ 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))
+
+ p.RoomID = mod.Destination
+ if g.Hub != nil {
+ g.Hub.LeaveRoom(sess)
+ 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
+ }
+
+ 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
+ }
+ }
+
+ 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)
+
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == "bones" {
+ slot.ItemID = "nutrient_bar"
+ }
+ }
+
+ 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
+ }
+
+ groundItems := g.World.GroundItems(p.RoomID)
+ var matchedID string
+ for itemID := range groundItems {
+ def, _ := g.ItemStore.Load(itemID)
+ if def != nil && def.MatchesName(targetArg) {
+ matchedID = itemID
+ break
+ }
+ if strings.HasPrefix(itemID, strings.ToLower(targetArg)) {
+ matchedID = itemID
+ break
+ }
+ }
+
+ if matchedID == "" {
+ sess.WriteLine("You don't see that here.")
+ return
+ }
+
+ if p.Action != nil {
+ g.CancelAction(p)
+ }
+
+ g.consumeJunkCost(p, mod)
+
+ qty := groundItems[matchedID]
+ g.World.RemoveGroundItem(p.RoomID, matchedID, qty)
+
+ def, _ := g.ItemStore.Load(matchedID)
+ name := matchedID
+ if def != nil {
+ name = def.Name
+ }
+
+ freeSlot := p.FirstFreeSlot()
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: matchedID, Quantity: 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)
+
+ 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
+ }
+
+ slot, inv := g.findInventoryItem(p, targetArg)
+ if slot < 0 {
+ sess.WriteLine("You don't have that item.")
+ return
+ }
+
+ recipe := g.RecipeStore.FindByInput("smelt", inv.ItemID)
+ if recipe == nil {
+ sess.WriteLine("You can't superheat that.")
+ return
+ }
+
+ 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
+ }
+
+ for _, e := range recipe.Consume {
+ for _, itemID := range e.Items {
+ qty := e.Qty
+ if qty <= 0 {
+ qty = 1
+ }
+ if p.CountItem(itemID) < qty {
+ def, _ := g.ItemStore.Load(itemID)
+ name := itemID
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You need %d %s.", qty, name))
+ return
+ }
+ }
+ }
+
+ if p.Action != nil {
+ g.CancelAction(p)
+ }
+
+ g.consumeJunkCost(p, mod)
+
+ for _, e := range recipe.Consume {
+ for _, itemID := range e.Items {
+ qty := e.Qty
+ if qty <= 0 {
+ qty = 1
+ }
+ p.RemoveItem(itemID, qty)
+ }
+ }
+
+ outputQty := recipe.OutputQty
+ if outputQty <= 0 {
+ outputQty = 1
+ }
+ placed := false
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == recipe.Output {
+ slot.Quantity += outputQty
+ placed = true
+ break
+ }
+ }
+ if !placed {
+ freeSlot := p.FirstFreeSlot()
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty})
+ }
+
+ 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)
+ outputName := recipe.Output
+ 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) {
+ if chipInfo, ok := chipMap[mod.ID]; ok {
+ g.triggerChipBolts(sess, p, mod, chipInfo)
+ return
+ }
+
+ enchants, ok := enchantMap[mod.ID]
+ if !ok {
+ sess.WriteLine("That enchantment has no known recipes.")
+ return
+ }
+
+ if targetArg == "" {
+ 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)
+
+ 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(inv.ItemID)
+ inputName := inv.ItemID
+ 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)
+ placed := false
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == chip.Output {
+ slot.Quantity += chip.Qty
+ placed = true
+ break
+ }
+ }
+ if !placed {
+ freeSlot := p.FirstFreeSlot()
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: chip.Output, Quantity: 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)))
+ }
+}
+
+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
+ }
+
+ if g.processConsumeQueue(p, sess) {
+ p.ActionState = &ActionState{Type: ActionEating, TargetName: "food"}
+ return true
+ }
+
+ g.consumeJunkCost(p, mod)
+
+ equipSciBonus := g.totalEquipScienceAttack(p)
+ attRoll := (p.Level(player.Science) + 8) * (equipSciBonus + 64)
+
+ mobSciDef := mob.ScienceDefense
+ defRoll := (mob.Defense + 9) * (mobSciDef + 64)
+
+ if mob.Weakness == mod.Element {
+ attRoll = attRoll * 13 / 10
+ }
+
+ if combat.HitCheck(attRoll, defRoll) {
+ dmg := combat.RollDamage(mod.MaxHit)
+ if dmg < 0 {
+ dmg = 0
+ }
+ 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) 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
+}
diff --git a/internal/game/game.go b/internal/game/game.go
index dff78d5..9d8a2c9 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -141,7 +141,8 @@ func classifyCommand(cmd string) CommandClass {
"map", "option", "options", "alias", "unalias",
"description", "desc", "queued", "color", "colors",
"colortable", "prompt", "style", "stats",
- "tech", "t":
+ "tech", "t",
+ "autocast", "auto", "mods", "modlist":
return ClassInstant
case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield":
return ClassFree
@@ -150,7 +151,8 @@ func classifyCommand(cmd string) CommandClass {
"north", "n", "south", "s", "east", "e",
"west", "w", "up", "u", "down", "d",
"quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "mix",
- "id", "identify":
+ "id", "identify",
+ "trigger", "cast":
return ClassActive
case "eat", "fletch", "clean":
return ClassFree
@@ -401,6 +403,13 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
case "id", "identify":
g.doIdentify(sess, strings.Join(args, " "))
return
+ 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
case "talk", "speak", "ask":
g.CancelAction(p)
if len(args) == 0 {
diff --git a/internal/game/science.go b/internal/game/science.go
new file mode 100644
index 0000000..38cb138
--- /dev/null
+++ b/internal/game/science.go
@@ -0,0 +1,332 @@
+package game
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+)
+
+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
+ Destination int
+}
+
+var AllMods []*ModDef
+
+var modByID map[string]*ModDef
+
+func GetMod(id string) *ModDef {
+ return modByID[id]
+}
+
+func FindMod(input string) *ModDef {
+ if m, ok := modByID[input]; ok {
+ return m
+ }
+ lower := strings.ToLower(strings.ReplaceAll(input, " ", "_"))
+ for _, m := range AllMods {
+ if strings.HasPrefix(m.ID, lower) {
+ return m
+ }
+ }
+ lowerSpace := strings.ToLower(input)
+ for _, m := range AllMods {
+ if strings.HasPrefix(strings.ToLower(m.Name), lowerSpace) {
+ return m
+ }
+ }
+ return nil
+}
+
+type chipEntry struct {
+ Input string
+ Output string
+ Qty int
+}
+
+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",
+ },
+}
+
+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},
+}
+
+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)
+
+ if hasDeck {
+ delete(cost, "scrap_metal")
+ }
+
+ 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, ", ")
+}
+
+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.ScienceAttack
+ }
+ }
+ return total
+}
+
+func init() {
+ AllMods = []*ModDef{
+ {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"},
+
+ {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"},
+
+ {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"},
+
+ {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"},
+
+ {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"},
+
+ {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"},
+
+ {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},
+
+ {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"},
+
+ {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"},
+ }
+
+ modByID = make(map[string]*ModDef, len(AllMods))
+ for _, m := range AllMods {
+ modByID[m.ID] = m
+ }
+}
diff --git a/internal/object/item.go b/internal/object/item.go
index 60d9ff9..6f0faac 100644
--- a/internal/object/item.go
+++ b/internal/object/item.go
@@ -58,6 +58,7 @@ type ItemDef struct {
EatMessage string `yaml:"eat_message"`
MadeFrom []MadeFromEntry `yaml:"made_from,omitempty"`
Ticks float64 `yaml:"ticks"`
+ ProvidesJunk string `yaml:"provides_junk,omitempty"`
}
type MadeFromEntry struct {
diff --git a/internal/player/player.go b/internal/player/player.go
index a970a00..dbbd4da 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -177,6 +177,7 @@ type Player struct {
MoveDirection string `yaml:"-"`
MoveTarget int `yaml:"-"`
VisualTickCurrent int `yaml:"-"`
+ AutocastMod string `yaml:"-"`
Battery float64 `yaml:"battery"`
ActiveTechs map[string]bool `yaml:"-"`
QuickTech string `yaml:"quick_tech,omitempty"`