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 --- 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 + 11 files changed, 1230 insertions(+), 18 deletions(-) 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 (limited to 'internal') 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