aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_trigger.go
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 /internal/game/cmd_trigger.go
parentdea4a06764c507699cf9adcd1fd0a572d227aff0 (diff)
downloadthehouseoficarus-fbd1c30d7b9777c9df6653f0c393afa7a7dfa519.tar.gz
feat: science skill roughly implemented
Diffstat (limited to 'internal/game/cmd_trigger.go')
-rw-r--r--internal/game/cmd_trigger.go651
1 files changed, 651 insertions, 0 deletions
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
+}