From fbd1c30d7b9777c9df6653f0c393afa7a7dfa519 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 19 Jun 2026 03:05:46 -0400 Subject: feat: science skill roughly implemented --- TODO.md | 3 +- data/help/autocast.yaml | 21 ++ data/help/mods.yaml | 16 + data/help/science.yaml | 32 ++ data/help/trigger.yaml | 24 ++ data/items/abyssal_bracelet.yaml | 6 + data/items/advanced_bio_deck.yaml | 11 + data/items/advanced_eco_deck.yaml | 11 + data/items/advanced_hydro_deck.yaml | 11 + data/items/advanced_solar_deck.yaml | 11 + data/items/basic_deck.yaml | 10 + data/items/binding_necklace.yaml | 6 + data/items/bio_deck.yaml | 11 + data/items/bracelet_of_clay.yaml | 6 + data/items/bracelet_of_slaughter.yaml | 6 + data/items/diamond_bolts.yaml | 6 +- data/items/diamond_bolts_e.yaml | 9 + data/items/diamond_bracelet.yaml | 4 +- data/items/diamond_necklace.yaml | 4 +- data/items/diamond_ring.yaml | 4 +- data/items/digsite_pendant.yaml | 6 + data/items/eco_deck.yaml | 11 + data/items/emerald_bolts.yaml | 8 +- data/items/emerald_bolts_e.yaml | 9 + data/items/emerald_bracelet.yaml | 6 +- data/items/emerald_necklace.yaml | 6 +- data/items/emerald_ring.yaml | 6 +- data/items/hydro_deck.yaml | 11 + data/items/inoculation_bracelet.yaml | 6 + data/items/necklace_of_passage.yaml | 6 + data/items/nutrient_bar.yaml | 8 + data/items/phoenix_necklace.yaml | 6 + data/items/ring_of_dueling.yaml | 6 + data/items/ring_of_forging.yaml | 6 + data/items/ring_of_life.yaml | 6 + data/items/ring_of_recoil.yaml | 6 + data/items/ruby_bolts.yaml | 6 +- data/items/ruby_bolts_e.yaml | 9 + data/items/ruby_bracelet.yaml | 4 +- data/items/ruby_necklace.yaml | 4 +- data/items/ruby_ring.yaml | 4 +- data/items/sapphire_bolts.yaml | 8 +- data/items/sapphire_bolts_e.yaml | 9 + data/items/sapphire_bracelet.yaml | 6 +- data/items/sapphire_necklace.yaml | 6 +- data/items/sapphire_ring.yaml | 6 +- data/items/solar_deck.yaml | 11 + internal/action/recipe.go | 20 ++ internal/config/config.go | 1 + internal/game/action_state.go | 3 + internal/game/cmd_attack.go | 61 +++- internal/game/cmd_autocast.go | 54 +++ internal/game/cmd_mods.go | 111 ++++++ internal/game/cmd_trigger.go | 651 ++++++++++++++++++++++++++++++++++ internal/game/game.go | 13 +- internal/game/science.go | 332 +++++++++++++++++ internal/object/item.go | 1 + internal/player/player.go | 1 + 58 files changed, 1583 insertions(+), 63 deletions(-) create mode 100644 data/help/autocast.yaml create mode 100644 data/help/mods.yaml create mode 100644 data/help/science.yaml create mode 100644 data/help/trigger.yaml create mode 100644 data/items/abyssal_bracelet.yaml create mode 100644 data/items/advanced_bio_deck.yaml create mode 100644 data/items/advanced_eco_deck.yaml create mode 100644 data/items/advanced_hydro_deck.yaml create mode 100644 data/items/advanced_solar_deck.yaml create mode 100644 data/items/basic_deck.yaml create mode 100644 data/items/binding_necklace.yaml create mode 100644 data/items/bio_deck.yaml create mode 100644 data/items/bracelet_of_clay.yaml create mode 100644 data/items/bracelet_of_slaughter.yaml create mode 100644 data/items/diamond_bolts_e.yaml create mode 100644 data/items/digsite_pendant.yaml create mode 100644 data/items/eco_deck.yaml create mode 100644 data/items/emerald_bolts_e.yaml create mode 100644 data/items/hydro_deck.yaml create mode 100644 data/items/inoculation_bracelet.yaml create mode 100644 data/items/necklace_of_passage.yaml create mode 100644 data/items/nutrient_bar.yaml create mode 100644 data/items/phoenix_necklace.yaml create mode 100644 data/items/ring_of_dueling.yaml create mode 100644 data/items/ring_of_forging.yaml create mode 100644 data/items/ring_of_life.yaml create mode 100644 data/items/ring_of_recoil.yaml create mode 100644 data/items/ruby_bolts_e.yaml create mode 100644 data/items/sapphire_bolts_e.yaml create mode 100644 data/items/solar_deck.yaml create mode 100644 internal/game/cmd_autocast.go create mode 100644 internal/game/cmd_mods.go create mode 100644 internal/game/cmd_trigger.go create mode 100644 internal/game/science.go 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 + 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 ' or 'cast ' + - Every mod costs a combination of junk items + - All mods also cost 1 scrap metal, UNLESS you have a deck + equipped + - Wielding a deck (science weapon) removes the scrap + requirement + - Elemental decks also provide unlimited supply of their junk + type + + Junk types: + solarjunk, hydrojunk, ecojunk, biojunk (from scavenging) + chaosjunk, deathjunk, bloodjunk (combat mod components) + lawjunk (transport), cosmicjunk (enchantment) + naturejunk (processing) + + Decks: basic deck, solar deck, hydro deck, eco deck, bio deck + Advanced versions of each elemental deck also exist. + + Combat: Use 'trigger ' or set 'autocast ' + then 'attack ' + + Type 'mods' to see all mods you can currently use. + + See also: trigger, autocast, mods, scavenging 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 [target] + cast [target] + + Trigger a science mod. Combat mods target a mob and initiate + science-based combat. Utility mods act on items in your + inventory or on yourself. + + Examples: + trigger bio strike goblin - Attack 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 ' 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 ", 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 ") + 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 ") + 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 ", 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"` -- cgit v1.2.3