diff options
Diffstat (limited to 'internal/game')
79 files changed, 2243 insertions, 1475 deletions
diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go index 4d8b65e..c31d7d5 100644 --- a/internal/game/action_burn.go +++ b/internal/game/action_burn.go @@ -3,6 +3,7 @@ package game import ( "fmt" "math/rand" + "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/engine" @@ -11,6 +12,18 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeBurn(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + g.doBurn(sess, p, strings.Join(args, " ")) +} + +func (g *Game) executeStoke(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + g.doStoke(sess, p, strings.Join(args, " ")) +} + func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) { if p.Action != nil { g.cancelAction(p) diff --git a/internal/game/action_steal.go b/internal/game/action_steal.go index ee5456c..3510ce7 100644 --- a/internal/game/action_steal.go +++ b/internal/game/action_steal.go @@ -16,6 +16,16 @@ import ( "thehouseoficarus/internal/world" ) +func (g *Game) executeSteal(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + g.doSteal(sess, "") + } else { + g.doSteal(sess, strings.Join(args, " ")) + } +} + var stallGuardTalk = &action.TalkConfig{ Nodes: map[string]action.TalkNode{ "start": { diff --git a/internal/game/aggro.go b/internal/game/aggro.go deleted file mode 100644 index 2a9fff4..0000000 --- a/internal/game/aggro.go +++ /dev/null @@ -1,53 +0,0 @@ -package game - -import ( - "fmt" - - "thehouseoficarus/internal/combat" - "thehouseoficarus/internal/engine" - "thehouseoficarus/internal/net" -) - -func (g *Game) checkAggro(sess *net.Session) { - p := sess.Player - - if combat.GetCombat(p.Name) != nil { - return - } - - playerLevel := p.CombatLevel() - mobs := g.MobStore.MobsInRoom(p.RoomID) - - for _, mob := range mobs { - if !mob.Aggressive || mob.HP <= 0 || mob.Protected { - continue - } - if combat.IsMobInCombat(mob.InstanceID) { - continue - } - mobLevel := mobCombatLevel(mob) - if playerLevel > mobLevel*2 { - continue - } - aggMob := mob - g.Ticks.Subscribe(engine.ToTicks(1), func() bool { - if combat.GetCombat(p.Name) != nil { - return false - } - if aggMob.HP <= 0 || aggMob.RoomID != p.RoomID { - return false - } - if combat.IsMobInCombat(aggMob.InstanceID) { - return false - } - attacker := aggMob.Name - if !aggMob.Unique { - attacker = "The " + aggMob.Name - } - sess.WriteLine(fmt.Sprintf("\n%s attacks you!", g.colorize(sess, "mob_name", attacker))) - g.startCombat(sess, p, aggMob) - return false - }) - break - } -} diff --git a/internal/game/cmd_alias.go b/internal/game/cmd_alias.go index e7e4e12..890b904 100644 --- a/internal/game/cmd_alias.go +++ b/internal/game/cmd_alias.go @@ -8,6 +8,14 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeAlias(sess *net.Session, args []string, rawInput string) { + g.doAlias(sess, args) +} + +func (g *Game) executeUnalias(sess *net.Session, args []string, rawInput string) { + g.doUnalias(sess, args) +} + func (g *Game) doAlias(sess *net.Session, args []string) { if sess.Account == nil { sess.WriteLine("No account loaded.") diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index b0ce214..222e3c3 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -608,6 +608,20 @@ func (g *Game) stopCombat(playerName string) { } } +func (g *Game) executeAttack(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + p := sess.Player + target := g.resolveDefaultMob(p.RoomID) + if target == "" { + sess.WriteLine("Attack what?") + return + } + g.doAttack(sess, target) + } else { + g.doAttack(sess, strings.Join(args, " ")) + } +} + func (g *Game) respawnMob(instanceID string) { inst := g.MobStore.GetInstance(instanceID) if inst == nil { diff --git a/internal/game/cmd_autotrigger.go b/internal/game/cmd_autotrigger.go index 3c41735..e2ecd1a 100644 --- a/internal/game/cmd_autotrigger.go +++ b/internal/game/cmd_autotrigger.go @@ -52,3 +52,7 @@ func (g *Game) doAutotrigger(sess *net.Session, input string) { p.AutotriggerMod = mod.ID sess.WriteLine(fmt.Sprintf("Autotrigger set to: %s", mod.Name)) } + +func (g *Game) executeAutotrigger(sess *net.Session, args []string, rawInput string) { + g.doAutotrigger(sess, strings.Join(args, " ")) +} diff --git a/internal/game/cmd_bank.go b/internal/game/cmd_bank.go index 0785b81..d91e115 100644 --- a/internal/game/cmd_bank.go +++ b/internal/game/cmd_bank.go @@ -10,6 +10,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeBank(sess *net.Session, args []string, rawInput string) { + g.doBank(sess) +} + func (g *Game) handleBankInput(sess *net.Session, input string) { if sess.Player == nil { sess.State = net.StateGame diff --git a/internal/game/cmd_clean.go b/internal/game/cmd_clean.go index 1f00d75..47896bb 100644 --- a/internal/game/cmd_clean.go +++ b/internal/game/cmd_clean.go @@ -11,6 +11,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeClean(sess *net.Session, args []string, rawInput string) { + g.doClean(sess, strings.Join(args, " ")) +} + func (g *Game) doClean(sess *net.Session, input string) { p := sess.Player diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index 3fb7716..257c965 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -128,6 +128,10 @@ func (g *Game) colorSource(sess *net.Session, category string) string { return "builtin" } +func (g *Game) executeColor(sess *net.Session, args []string, rawInput string) { + g.doColor(sess, strings.Join(args, " ")) +} + var colorCategoryOrder = []string{ "room_name", "room_number", diff --git a/internal/game/cmd_colortable.go b/internal/game/cmd_colortable.go index 5aa36d9..86299dc 100644 --- a/internal/game/cmd_colortable.go +++ b/internal/game/cmd_colortable.go @@ -37,6 +37,10 @@ func (g *Game) doColortable(sess *net.Session) { writeANSITable(sess, mode) } +func (g *Game) executeColortable(sess *net.Session, args []string, rawInput string) { + g.doColortable(sess) +} + func writeSectionHeader(sess *net.Session, title string, unicode bool) { if unicode { sess.WriteLine(fmt.Sprintf("── %s ──", title)) diff --git a/internal/game/cmd_drink.go b/internal/game/cmd_consume.go index 44ef991..0a7d335 100644 --- a/internal/game/cmd_drink.go +++ b/internal/game/cmd_consume.go @@ -11,6 +11,149 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeEat(sess *net.Session, args []string, rawInput string) { + g.doEat(sess, strings.Join(args, " ")) +} + +func (g *Game) executeDrink(sess *net.Session, args []string, rawInput string) { + g.doDrink(sess, args) +} + +func (g *Game) doEat(sess *net.Session, input string) { + p := sess.Player + + if input == "" { + sess.WriteLine("Eat what?") + return + } + + qty, itemName := parseQty(input) + _ = qty + + matches := g.findInventoryMatches(itemName, p) + if len(matches) == 0 { + sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemName)) + return + } + + unique := uniqueItemNames(matches) + if len(unique) > 1 { + g.showWhichOne(sess, matches) + return + } + + itemID := matches[0].ID + def, _ := g.ItemStore.Load(itemID) + + if def == nil || def.EatMessage == "" { + sess.WriteLine("You can't eat that.") + return + } + + if p.ConsumeCooldown > 0 { + sess.WriteLine("You're still digesting your last meal.") + return + } + + g.cancelRest(p.Name) + + if p.Action == nil && len(p.WalkSequence) == 0 { + g.doEatNow(sess, p, itemID, def) + return + } + + g.cancelAction(p) + + g.consumeQueue[p.Name] = &QueuedCommand{ + Session: sess, + Command: "eat", + Args: itemID, + Timestamp: time.Now(), + } + + if !p.OptionBool("queue_silently") { + sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", g.itemColorize(sess, def, def.Name))) + } +} + +func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def *object.ItemDef) { + if def == nil { + var err error + def, err = g.ItemStore.Load(itemID) + if err != nil || def.EatMessage == "" { + return + } + } + + p.RemoveItem(itemID, 1) + + p.HP += def.HealValue + maxHP := p.MaxHP() + if p.HP > maxHP { + p.HP = maxHP + } + if p.HP < 0 { + p.HP = 0 + } + + if def.HealValue != 0 { + p.StartRegen() + } + + p.ConsumeCooldown = 3 + g.AccountStore.SaveCharacter(p) + + p.ActionState = &ActionState{Type: ActionEating, TargetName: def.Name} + + sess.WriteLine(g.colorize(sess, "eat_food", def.EatMessage)) + + if p.HP <= 0 { + g.endCombat(sess, p, nil) + } +} + +func (g *Game) processConsumeQueue(p *player.Player, sess *net.Session) bool { + qc, ok := g.consumeQueue[p.Name] + if !ok { + return false + } + delete(g.consumeQueue, p.Name) + + itemID := qc.Args + def, err := g.ItemStore.Load(itemID) + + if qc.Command == "drink" { + if err != nil || (def.PotionEffect == "" && def.HealValue <= 0) { + return false + } + if p.ConsumeCooldown > 0 { + return false + } + if !p.HasItem(itemID) { + sess.WriteLine("You no longer have that to drink.") + return false + } + g.applyPotion(sess, p, itemID, def) + return true + } + + if err != nil || def.EatMessage == "" { + return false + } + + if p.ConsumeCooldown > 0 { + return false + } + + if !p.HasItem(itemID) { + sess.WriteLine("You no longer have that to eat.") + return false + } + + g.doEatNow(sess, p, itemID, def) + return true +} + func (g *Game) doDrink(sess *net.Session, args []string) { p := sess.Player diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go index 5b78745..288afb0 100644 --- a/internal/game/cmd_cook.go +++ b/internal/game/cmd_cook.go @@ -2,12 +2,17 @@ package game import ( "fmt" + "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) +func (g *Game) executeCook(sess *net.Session, args []string, rawInput string) { + g.doCook(sess, strings.Join(args, " ")) +} + func (g *Game) doCook(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) diff --git a/internal/game/cmd_craft.go b/internal/game/cmd_craft.go index 7f8808b..447d980 100644 --- a/internal/game/cmd_craft.go +++ b/internal/game/cmd_craft.go @@ -8,6 +8,14 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeCraft(sess *net.Session, args []string, rawInput string) { + g.doCraft(sess, strings.Join(args, " ")) +} + +func (g *Game) executeConstruct(sess *net.Session, args []string, rawInput string) { + g.doConstruct(sess, strings.Join(args, " ")) +} + func (g *Game) doCraft(sess *net.Session, input string) { g.doGenericStationSkill(sess, input, "crafting", player.Crafting, "last_craft", "Crafting", "craft", "Craft") } diff --git a/internal/game/cmd_description.go b/internal/game/cmd_description.go index ced5dcc..5d0c5c1 100644 --- a/internal/game/cmd_description.go +++ b/internal/game/cmd_description.go @@ -6,6 +6,10 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeDescription(sess *net.Session, args []string, rawInput string) { + g.doDescription(sess) +} + func (g *Game) doDescription(sess *net.Session) { p := sess.Player sess.WriteLine("") diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go index d3141e3..551b014 100644 --- a/internal/game/cmd_drop.go +++ b/internal/game/cmd_drop.go @@ -180,6 +180,20 @@ func (g *Game) doDrop(sess *net.Session, input string) { } } +func (g *Game) executeDrop(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + sess.WriteLine("Drop what?") + } else if args[0] == "all" { + if len(args) == 1 { + g.doDropAll(sess) + } else { + g.doDropAllNamed(sess, strings.Join(args[1:], " ")) + } + } else { + g.doDrop(sess, strings.Join(args, " ")) + } +} + func (g *Game) handleDropAllConfirm(sess *net.Session, input string) { p := sess.Player if p == nil { diff --git a/internal/game/cmd_eat.go b/internal/game/cmd_eat.go deleted file mode 100644 index f94ade3..0000000 --- a/internal/game/cmd_eat.go +++ /dev/null @@ -1,145 +0,0 @@ -package game - -import ( - "fmt" - "time" - - "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" - "thehouseoficarus/internal/player" -) - -func (g *Game) doEat(sess *net.Session, input string) { - p := sess.Player - - if input == "" { - sess.WriteLine("Eat what?") - return - } - - qty, itemName := parseQty(input) - _ = qty - - matches := g.findInventoryMatches(itemName, p) - if len(matches) == 0 { - sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemName)) - return - } - - unique := uniqueItemNames(matches) - if len(unique) > 1 { - g.showWhichOne(sess, matches) - return - } - - itemID := matches[0].ID - def, _ := g.ItemStore.Load(itemID) - - if def == nil || def.EatMessage == "" { - sess.WriteLine("You can't eat that.") - return - } - - if p.ConsumeCooldown > 0 { - sess.WriteLine("You're still digesting your last meal.") - return - } - - g.cancelRest(p.Name) - - if p.Action == nil && len(p.WalkSequence) == 0 { - g.doEatNow(sess, p, itemID, def) - return - } - - g.cancelAction(p) - - g.consumeQueue[p.Name] = &QueuedCommand{ - Session: sess, - Command: "eat", - Args: itemID, - Timestamp: time.Now(), - } - - if !p.OptionBool("queue_silently") { - sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", g.itemColorize(sess, def, def.Name))) - } -} - -func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def *object.ItemDef) { - if def == nil { - var err error - def, err = g.ItemStore.Load(itemID) - if err != nil || def.EatMessage == "" { - return - } - } - - p.RemoveItem(itemID, 1) - - p.HP += def.HealValue - maxHP := p.MaxHP() - if p.HP > maxHP { - p.HP = maxHP - } - if p.HP < 0 { - p.HP = 0 - } - - if def.HealValue != 0 { - p.StartRegen() - } - - p.ConsumeCooldown = 3 - g.AccountStore.SaveCharacter(p) - - p.ActionState = &ActionState{Type: ActionEating, TargetName: def.Name} - - sess.WriteLine(g.colorize(sess, "eat_food", def.EatMessage)) - - if p.HP <= 0 { - g.endCombat(sess, p, nil) - } -} - -func (g *Game) processConsumeQueue(p *player.Player, sess *net.Session) bool { - qc, ok := g.consumeQueue[p.Name] - if !ok { - return false - } - delete(g.consumeQueue, p.Name) - - itemID := qc.Args - def, err := g.ItemStore.Load(itemID) - - if qc.Command == "drink" { - if err != nil || (def.PotionEffect == "" && def.HealValue <= 0) { - return false - } - if p.ConsumeCooldown > 0 { - return false - } - if !p.HasItem(itemID) { - sess.WriteLine("You no longer have that to drink.") - return false - } - g.applyPotion(sess, p, itemID, def) - return true - } - - if err != nil || def.EatMessage == "" { - return false - } - - if p.ConsumeCooldown > 0 { - return false - } - - if !p.HasItem(itemID) { - sess.WriteLine("You no longer have that to eat.") - return false - } - - g.doEatNow(sess, p, itemID, def) - return true -} diff --git a/internal/game/cmd_estate_directory.go b/internal/game/cmd_estate_directory.go index c79135f..dbc79f6 100644 --- a/internal/game/cmd_estate_directory.go +++ b/internal/game/cmd_estate_directory.go @@ -186,7 +186,7 @@ func (g *Game) getOwnedHouseRoom(p *player.Player, flagKey string) int { } houseMap := map[string]int{ - "local_neighborhood": 200, + "local_neighborhood": 300, } return houseMap[v] } diff --git a/internal/game/cmd_farm.go b/internal/game/cmd_farm.go index 5ef36d9..791cb6d 100644 --- a/internal/game/cmd_farm.go +++ b/internal/game/cmd_farm.go @@ -9,6 +9,64 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executePlant(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + sess.WriteLine("Plant what?") + } else { + g.doPlant(sess, strings.Join(args, " ")) + } +} + +func (g *Game) executeHarvest(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + g.doHarvest(sess, "") + } else { + g.doHarvest(sess, strings.Join(args, " ")) + } +} + +func (g *Game) executeRake(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + g.doRake(sess, "") + } else { + g.doRake(sess, strings.Join(args, " ")) + } +} + +func (g *Game) executeWater(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + g.doWater(sess, "") + } else { + g.doWater(sess, strings.Join(args, " ")) + } +} + +func (g *Game) executeCure(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + g.doCure(sess, "") + } else { + g.doCure(sess, strings.Join(args, " ")) + } +} + +func (g *Game) executeInspect(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + g.doInspect(sess, "") + } else { + g.doInspect(sess, strings.Join(args, " ")) + } +} + func (g *Game) doPlant(sess *net.Session, input string) { p := sess.Player diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go index 1f6a075..e218dca 100644 --- a/internal/game/cmd_fletch.go +++ b/internal/game/cmd_fletch.go @@ -9,6 +9,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeFletch(sess *net.Session, args []string, rawInput string) { + g.doFletch(sess, strings.Join(args, " ")) +} + var fletchLogItems = map[string]bool{ "logs": true, "oak_logs": true, "willow_logs": true, "maple_logs": true, "yew_logs": true, "magic_logs": true, diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go index bae721b..f3485fe 100644 --- a/internal/game/cmd_get.go +++ b/internal/game/cmd_get.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "strings" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" @@ -419,3 +420,17 @@ func (g *Game) findInventoryMatches(input string, p *player.Player) []itemMatch } }) } + +func (g *Game) executeGet(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + sess.WriteLine("Get what?") + } else if args[0] == "all" { + if len(args) == 1 { + g.doGetAll(sess) + } else { + g.doGetAllNamed(sess, strings.Join(args[1:], " ")) + } + } else { + g.doGet(sess, strings.Join(args, " ")) + } +} diff --git a/internal/game/cmd_handlers.go b/internal/game/cmd_handlers.go deleted file mode 100644 index b8db359..0000000 --- a/internal/game/cmd_handlers.go +++ /dev/null @@ -1,370 +0,0 @@ -package game - -import ( - "fmt" - "strings" - - "thehouseoficarus/internal/net" -) - -func (g *Game) executeGet(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - sess.WriteLine("Get what?") - } else if args[0] == "all" { - if len(args) == 1 { - g.doGetAll(sess) - } else { - g.doGetAllNamed(sess, strings.Join(args[1:], " ")) - } - } else { - g.doGet(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeDrop(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - sess.WriteLine("Drop what?") - } else if args[0] == "all" { - if len(args) == 1 { - g.doDropAll(sess) - } else { - g.doDropAllNamed(sess, strings.Join(args[1:], " ")) - } - } else { - g.doDrop(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeAttack(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - p := sess.Player - target := g.resolveDefaultMob(p.RoomID) - if target == "" { - sess.WriteLine("Attack what?") - return - } - g.doAttack(sess, target) - } else { - g.doAttack(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeStyle(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - g.doStyle(sess, "") - } else { - g.doStyle(sess, args[0]) - } -} - -func (g *Game) executeLook(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - g.doLook(sess) - } else { - g.doLookTarget(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeMove(sess *net.Session, args []string, rawInput string) { - g.doMove(sess, strings.TrimSpace(rawInput), 1.0) -} - -func (g *Game) executeSay(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - sess.WriteLine("Say what?") - } else { - msgStart := strings.Index(strings.ToLower(rawInput), "say ") + 4 - if msgStart >= 4 && msgStart < len(rawInput) { - g.doSay(sess, rawInput[msgStart:]) - } else { - g.doSay(sess, strings.Join(args, " ")) - } - } -} - -func (g *Game) executeScore(sess *net.Session, args []string, rawInput string) { - g.doScore(sess) -} - -func (g *Game) executeTask(sess *net.Session, args []string, rawInput string) { - g.doTask(sess) -} - -func (g *Game) executeStats(sess *net.Session, args []string, rawInput string) { - g.doStats(sess) -} - -func (g *Game) executeTech(sess *net.Session, args []string, rawInput string) { - g.doTech(sess, strings.Join(args, " ")) -} - -func (g *Game) executeInventory(sess *net.Session, args []string, rawInput string) { - g.doInventory(sess) -} - -func (g *Game) executeQuit(sess *net.Session, args []string, rawInput string) { - g.doQuit(sess) -} - -func (g *Game) executeDescription(sess *net.Session, args []string, rawInput string) { - g.doDescription(sess) -} - -func (g *Game) executeOption(sess *net.Session, args []string, rawInput string) { - g.doOption(sess, strings.Join(args, " ")) -} - -func (g *Game) executeColor(sess *net.Session, args []string, rawInput string) { - g.doColor(sess, strings.Join(args, " ")) -} - -func (g *Game) executeColortable(sess *net.Session, args []string, rawInput string) { - g.doColortable(sess) -} - -func (g *Game) executePrompt(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - g.doPrompt(sess, "") - } else { - msgStart := strings.Index(strings.ToLower(rawInput), "prompt ") + 7 - if msgStart >= 7 && msgStart < len(rawInput) { - g.doPrompt(sess, rawInput[msgStart:]) - } else { - g.doPrompt(sess, strings.Join(args, " ")) - } - } -} - -func (g *Game) executeExits(sess *net.Session, args []string, rawInput string) { - g.doExits(sess) -} - -func (g *Game) executeMap(sess *net.Session, args []string, rawInput string) { - g.doMap(sess) -} - -func (g *Game) executeQueued(sess *net.Session, args []string, rawInput string) { - g.doQueued(sess) -} - -func (g *Game) executeHelp(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - g.doHelp(sess, "") - } else { - g.doHelp(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeGather(sess *net.Session, args []string, rawInput string) { - verb := strings.TrimSpace(rawInput) - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - target := g.resolveDefaultTarget(p.RoomID, verb) - if target == "" { - sess.WriteLine(fmt.Sprintf("%s what?", verb)) - return - } - g.startAction(sess, verb, target) - } else { - g.startAction(sess, verb, strings.Join(args, " ")) - } -} - -func (g *Game) executeUse(sess *net.Session, args []string, rawInput string) { - if g.tryFinishingBlow(sess, strings.Join(args, " ")) { - return - } - g.doUse(sess, strings.Join(args, " ")) -} - -func (g *Game) executeCook(sess *net.Session, args []string, rawInput string) { - g.doCook(sess, strings.Join(args, " ")) -} - -func (g *Game) executeSmelt(sess *net.Session, args []string, rawInput string) { - g.doSmelt(sess, strings.Join(args, " ")) -} - -func (g *Game) executeSmith(sess *net.Session, args []string, rawInput string) { - g.doSmith(sess, strings.Join(args, " ")) -} - -func (g *Game) executeCraft(sess *net.Session, args []string, rawInput string) { - g.doCraft(sess, strings.Join(args, " ")) -} - -func (g *Game) executeEat(sess *net.Session, args []string, rawInput string) { - g.doEat(sess, strings.Join(args, " ")) -} - -func (g *Game) executeDrink(sess *net.Session, args []string, rawInput string) { - g.doDrink(sess, args) -} - -func (g *Game) executeFletch(sess *net.Session, args []string, rawInput string) { - g.doFletch(sess, strings.Join(args, " ")) -} - -func (g *Game) executeClean(sess *net.Session, args []string, rawInput string) { - g.doClean(sess, strings.Join(args, " ")) -} - -func (g *Game) executeMix(sess *net.Session, args []string, rawInput string) { - g.doMix(sess, strings.Join(args, " ")) -} - -func (g *Game) executeIdentify(sess *net.Session, args []string, rawInput string) { - g.doIdentify(sess, strings.Join(args, " ")) -} - -func (g *Game) executeAutotrigger(sess *net.Session, args []string, rawInput string) { - g.doAutotrigger(sess, strings.Join(args, " ")) -} - -func (g *Game) executeMods(sess *net.Session, args []string, rawInput string) { - showAll := len(args) > 0 && args[0] == "all" - g.doMods(sess, showAll) -} - -func (g *Game) executeSneak(sess *net.Session, args []string, rawInput string) { - g.doSneak(sess) -} - -func (g *Game) executeTrigger(sess *net.Session, args []string, rawInput string) { - g.doTrigger(sess, strings.Join(args, " ")) -} - -func (g *Game) executeTalk(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - sess.WriteLine("Talk to whom?") - } else { - g.startAction(sess, "talk", strings.Join(args, " ")) - } -} - -func (g *Game) executeBurn(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - g.doBurn(sess, p, strings.Join(args, " ")) -} - -func (g *Game) executeStoke(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - g.doStoke(sess, p, strings.Join(args, " ")) -} - -func (g *Game) executeAlias(sess *net.Session, args []string, rawInput string) { - g.doAlias(sess, args) -} - -func (g *Game) executeUnalias(sess *net.Session, args []string, rawInput string) { - g.doUnalias(sess, args) -} - -func (g *Game) executeSearch(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - sess.WriteLine("Search what?") - } else { - g.doSearch(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeSteal(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - g.doSteal(sess, "") - } else { - g.doSteal(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executePlant(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - sess.WriteLine("Plant what?") - } else { - g.doPlant(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeHarvest(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - g.doHarvest(sess, "") - } else { - g.doHarvest(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeRake(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - g.doRake(sess, "") - } else { - g.doRake(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeWater(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - g.doWater(sess, "") - } else { - g.doWater(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeCure(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - if len(args) == 0 { - g.doCure(sess, "") - } else { - g.doCure(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeInspect(sess *net.Session, args []string, rawInput string) { - if len(args) == 0 { - g.doInspect(sess, "") - } else { - g.doInspect(sess, strings.Join(args, " ")) - } -} - -func (g *Game) executeWalk(sess *net.Session, args []string, rawInput string) { - g.doWalk(sess, args) -} - -func (g *Game) executeBank(sess *net.Session, args []string, rawInput string) { - g.doBank(sess) -} - -func (g *Game) executeConstruct(sess *net.Session, args []string, rawInput string) { - g.doConstruct(sess, strings.Join(args, " ")) -} - -func (g *Game) executeJack(sess *net.Session, args []string, rawInput string) { - p := sess.Player - g.cancelAction(p) - g.doJack(sess, strings.Join(args, " ")) -} - -func (g *Game) executeWear(sess *net.Session, args []string, rawInput string) { - g.doWear(sess, strings.Join(args, " ")) -} - -func (g *Game) executeRemove(sess *net.Session, args []string, rawInput string) { - g.doRemove(sess, strings.Join(args, " ")) -} diff --git a/internal/game/cmd_id.go b/internal/game/cmd_id.go index f7158e9..cb886dc 100644 --- a/internal/game/cmd_id.go +++ b/internal/game/cmd_id.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" @@ -81,6 +82,10 @@ func junkMultiplier(junkID string, level int) int { return mult } +func (g *Game) executeIdentify(sess *net.Session, args []string, rawInput string) { + g.doIdentify(sess, strings.Join(args, " ")) +} + func (g *Game) doIdentify(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) diff --git a/internal/game/cmd_inventory.go b/internal/game/cmd_inventory.go index ae7b328..5024642 100644 --- a/internal/game/cmd_inventory.go +++ b/internal/game/cmd_inventory.go @@ -6,6 +6,10 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeInventory(sess *net.Session, args []string, rawInput string) { + g.doInventory(sess) +} + func (g *Game) doInventory(sess *net.Session) { p := sess.Player sess.WriteLine("") diff --git a/internal/game/cmd_jack.go b/internal/game/cmd_jack.go index 8cb5062..16f8b2a 100644 --- a/internal/game/cmd_jack.go +++ b/internal/game/cmd_jack.go @@ -10,6 +10,12 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeJack(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + g.doJack(sess, strings.Join(args, " ")) +} + func (g *Game) doJack(sess *net.Session, target string) { p := sess.Player diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 5fa2cf4..6913333 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -768,6 +768,18 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine } } +func (g *Game) executeLook(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + g.doLook(sess) + } else { + g.doLookTarget(sess, strings.Join(args, " ")) + } +} + +func (g *Game) executeExits(sess *net.Session, args []string, rawInput string) { + g.doExits(sess) +} + func (g *Game) showItemStats(sess *net.Session, def *object.ItemDef) { s := def.Stats hasAttack := s.StabAttack != 0 || s.SlashAttack != 0 || s.CrushAttack != 0 || diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go index 7cad008..be35a66 100644 --- a/internal/game/cmd_map.go +++ b/internal/game/cmd_map.go @@ -4,6 +4,10 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeMap(sess *net.Session, args []string, rawInput string) { + g.doMap(sess) +} + func (g *Game) doMap(sess *net.Session) { p := sess.Player mapWidth := p.OptionInt("mapwidth") diff --git a/internal/game/cmd_mix.go b/internal/game/cmd_mix.go index 8fc600b..7bf1f7b 100644 --- a/internal/game/cmd_mix.go +++ b/internal/game/cmd_mix.go @@ -1,11 +1,17 @@ package game import ( + "strings" + "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) +func (g *Game) executeMix(sess *net.Session, args []string, rawInput string) { + g.doMix(sess, strings.Join(args, " ")) +} + func (g *Game) doMix(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go index 65d7ad7..438e11a 100644 --- a/internal/game/cmd_mods.go +++ b/internal/game/cmd_mods.go @@ -9,6 +9,11 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeMods(sess *net.Session, args []string, rawInput string) { + showAll := len(args) > 0 && args[0] == "all" + g.doMods(sess, showAll) +} + func (g *Game) doMods(sess *net.Session, showAll bool) { p := sess.Player sciLevel := p.Level(player.Science) diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index 5d6ea66..e93b14b 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "strings" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" @@ -165,6 +166,10 @@ func (g *Game) seedRoomMobs(roomID int) { g.MobStore.SeedMobs(roomID, room.Mobs) } +func (g *Game) executeMove(sess *net.Session, args []string, rawInput string) { + g.doMove(sess, strings.TrimSpace(rawInput), 1.0) +} + func (g *Game) seedRoomObjects(roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil { diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go index fdf5371..19256ba 100644 --- a/internal/game/cmd_option.go +++ b/internal/game/cmd_option.go @@ -85,6 +85,10 @@ func (g *Game) doOption(sess *net.Session, input string) { sess.WriteLine(fmt.Sprintf("\n%s set to %s.", def.Name, formatOptionValue(p, def))) } +func (g *Game) executeOption(sess *net.Session, args []string, rawInput string) { + g.doOption(sess, strings.Join(args, " ")) +} + func formatOptionValue(p *player.Player, def *player.OptionDef) string { switch def.Type { case player.OptBool: diff --git a/internal/game/cmd_prompt.go b/internal/game/cmd_prompt.go index 8850ea1..d91556a 100644 --- a/internal/game/cmd_prompt.go +++ b/internal/game/cmd_prompt.go @@ -2,10 +2,24 @@ package game import ( "fmt" + "strings" "thehouseoficarus/internal/net" ) +func (g *Game) executePrompt(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + g.doPrompt(sess, "") + } else { + msgStart := strings.Index(strings.ToLower(rawInput), "prompt ") + 7 + if msgStart >= 7 && msgStart < len(rawInput) { + g.doPrompt(sess, rawInput[msgStart:]) + } else { + g.doPrompt(sess, strings.Join(args, " ")) + } + } +} + func (g *Game) doPrompt(sess *net.Session, input string) { p := sess.Player diff --git a/internal/game/cmd_queued.go b/internal/game/cmd_queued.go index fdb1643..58ac74b 100644 --- a/internal/game/cmd_queued.go +++ b/internal/game/cmd_queued.go @@ -7,6 +7,10 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeQueued(sess *net.Session, args []string, rawInput string) { + g.doQueued(sess) +} + func (g *Game) doQueued(sess *net.Session) { p := sess.Player diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go index d52c9ae..46403f4 100644 --- a/internal/game/cmd_quit.go +++ b/internal/game/cmd_quit.go @@ -8,6 +8,10 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeQuit(sess *net.Session, args []string, rawInput string) { + g.doQuit(sess) +} + func (g *Game) doQuit(sess *net.Session) { p := sess.Player diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index e5bf30e..0439cc2 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -168,3 +168,29 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI sess.WriteLine("Unknown command.") } + +func (g *Game) executeGather(sess *net.Session, args []string, rawInput string) { + verb := strings.TrimSpace(rawInput) + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + target := g.resolveDefaultTarget(p.RoomID, verb) + if target == "" { + sess.WriteLine(fmt.Sprintf("%s what?", verb)) + return + } + g.startAction(sess, verb, target) + } else { + g.startAction(sess, verb, strings.Join(args, " ")) + } +} + +func (g *Game) executeTalk(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + sess.WriteLine("Talk to whom?") + } else { + g.startAction(sess, "talk", strings.Join(args, " ")) + } +} diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go index e168fd9..c046537 100644 --- a/internal/game/cmd_remove.go +++ b/internal/game/cmd_remove.go @@ -9,6 +9,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeRemove(sess *net.Session, args []string, rawInput string) { + g.doRemove(sess, strings.Join(args, " ")) +} + func (g *Game) doRemove(sess *net.Session, input string) { p := sess.Player diff --git a/internal/game/cmd_say.go b/internal/game/cmd_say.go index 3d619a0..f21ed88 100644 --- a/internal/game/cmd_say.go +++ b/internal/game/cmd_say.go @@ -2,10 +2,24 @@ package game import ( "fmt" + "strings" "thehouseoficarus/internal/net" ) +func (g *Game) executeSay(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + sess.WriteLine("Say what?") + } else { + msgStart := strings.Index(strings.ToLower(rawInput), "say ") + 4 + if msgStart >= 4 && msgStart < len(rawInput) { + g.doSay(sess, rawInput[msgStart:]) + } else { + g.doSay(sess, strings.Join(args, " ")) + } + } +} + func (g *Game) doSay(sess *net.Session, msg string) { p := sess.Player roomID := p.RoomID diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go index 0b0caf2..b49fd7f 100644 --- a/internal/game/cmd_score.go +++ b/internal/game/cmd_score.go @@ -9,6 +9,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeScore(sess *net.Session, args []string, rawInput string) { + g.doScore(sess) +} + func (g *Game) doScore(sess *net.Session) { p := sess.Player mode := g.colorMode(sess) diff --git a/internal/game/cmd_search.go b/internal/game/cmd_search.go index cc22d49..4122e0a 100644 --- a/internal/game/cmd_search.go +++ b/internal/game/cmd_search.go @@ -7,6 +7,16 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeSearch(sess *net.Session, args []string, rawInput string) { + p := sess.Player + g.cancelAction(p) + if len(args) == 0 { + sess.WriteLine("Search what?") + } else { + g.doSearch(sess, strings.Join(args, " ")) + } +} + func (g *Game) doSearch(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go index 53ff426..a72cf53 100644 --- a/internal/game/cmd_smelt.go +++ b/internal/game/cmd_smelt.go @@ -2,12 +2,17 @@ package game import ( "fmt" + "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) +func (g *Game) executeSmelt(sess *net.Session, args []string, rawInput string) { + g.doSmelt(sess, strings.Join(args, " ")) +} + func (g *Game) doSmelt(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go index 067d251..a2eaad7 100644 --- a/internal/game/cmd_smith.go +++ b/internal/game/cmd_smith.go @@ -10,6 +10,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeSmith(sess *net.Session, args []string, rawInput string) { + g.doSmith(sess, strings.Join(args, " ")) +} + func (g *Game) doSmith(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) diff --git a/internal/game/cmd_sneak.go b/internal/game/cmd_sneak.go index b37862d..325a36b 100644 --- a/internal/game/cmd_sneak.go +++ b/internal/game/cmd_sneak.go @@ -6,6 +6,10 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeSneak(sess *net.Session, args []string, rawInput string) { + g.doSneak(sess) +} + func (g *Game) doSneak(sess *net.Session) { p := sess.Player p.Sneaking = !p.Sneaking diff --git a/internal/game/cmd_stats.go b/internal/game/cmd_stats.go index 51bacf1..906abf2 100644 --- a/internal/game/cmd_stats.go +++ b/internal/game/cmd_stats.go @@ -9,6 +9,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeStats(sess *net.Session, args []string, rawInput string) { + g.doStats(sess) +} + func (g *Game) doStats(sess *net.Session) { p := sess.Player totals := g.playerEquipBonuses(p) diff --git a/internal/game/cmd_style.go b/internal/game/cmd_style.go index 269fa3f..94dd234 100644 --- a/internal/game/cmd_style.go +++ b/internal/game/cmd_style.go @@ -43,3 +43,11 @@ func (g *Game) doStyle(sess *net.Session, input string) { } sess.WriteLine(fmt.Sprintf("\nUnknown style: %s. Choices: accurate, aggressive, defensive, balanced", input)) } + +func (g *Game) executeStyle(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + g.doStyle(sess, "") + } else { + g.doStyle(sess, args[0]) + } +} diff --git a/internal/game/cmd_task.go b/internal/game/cmd_task.go index 5b67421..e54db15 100644 --- a/internal/game/cmd_task.go +++ b/internal/game/cmd_task.go @@ -6,6 +6,10 @@ import ( "thehouseoficarus/internal/net" ) +func (g *Game) executeTask(sess *net.Session, args []string, rawInput string) { + g.doTask(sess) +} + func (g *Game) doTask(sess *net.Session) { p := sess.Player diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go index 89d5c2a..cb4c309 100644 --- a/internal/game/cmd_tech.go +++ b/internal/game/cmd_tech.go @@ -194,3 +194,7 @@ func (g *Game) doTechQuick(sess *net.Session, p *player.Player, input string) { g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("\nQuick tech set to %s.", matches[0].Name)) } + +func (g *Game) executeTech(sess *net.Session, args []string, rawInput string) { + g.doTech(sess, strings.Join(args, " ")) +} diff --git a/internal/game/cmd_trigger.go b/internal/game/cmd_trigger.go index 89ed397..7d637ca 100644 --- a/internal/game/cmd_trigger.go +++ b/internal/game/cmd_trigger.go @@ -7,9 +7,12 @@ import ( "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" - "thehouseoficarus/internal/world" ) +func (g *Game) executeTrigger(sess *net.Session, args []string, rawInput string) { + g.doTrigger(sess, strings.Join(args, " ")) +} + func (g *Game) doTrigger(sess *net.Session, input string) { p := sess.Player input = strings.TrimSpace(input) @@ -106,635 +109,3 @@ func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef p.AutotriggerMod = 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 := mod.BaseXP - g.awardSkillXP(sess, p, player.Science, sciXP) - 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 := mod.BaseXP - g.awardSkillXP(sess, p, player.Science, sciXP) - g.AccountStore.SaveCharacter(p) - - 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 "low_process", "high_process": - g.triggerProcessing(sess, p, mod, targetArg) - 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) - case "plank_make": - g.triggerPlankMake(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 := mod.BaseXP * boneCount - g.awardSkillXP(sess, p, player.Science, sciXP) - 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 := mod.BaseXP - g.awardSkillXP(sess, p, player.Science, sciXP) - 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 := mod.BaseXP - g.awardSkillXP(sess, p, player.Science, sciXP) - if recipe.XP > 0 { - g.awardSkillXP(sess, p, player.SkillName(recipe.Skill), recipe.XP) - } - 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) triggerPlankMake(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 - } - - logTypes := map[string]struct { - plankID string - name string - cost int - }{ - "logs": {"planks", "planks", 3}, - "oak_logs": {"oak_planks", "oak planks", 7}, - "teak_logs": {"teak_planks", "teak planks", 14}, - "mahogany_logs": {"mahogany_planks", "mahogany planks", 28}, - } - - if targetArg != "" { - slot, inv := g.findInventoryItem(p, targetArg) - if slot < 0 { - sess.WriteLine("You don't have that item.") - return - } - info, ok := logTypes[inv.ItemID] - if !ok { - sess.WriteLine("You can't turn that into planks.") - return - } - - if p.Credits < info.cost { - sess.WriteLine(fmt.Sprintf("You need %d credits for Plank Make (70%% of sawmill cost).", info.cost)) - return - } - - if p.Action != nil { - g.cancelAction(p) - } - - g.consumeJunkCost(p, mod) - - p.RemoveItem(inv.ItemID, 1) - p.Credits -= info.cost - - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - sess.WriteLine("Your inventory is full.") - return - } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: info.plankID, Quantity: 1}) - - sciXP := mod.BaseXP - g.awardSkillXP(sess, p, player.Science, sciXP) - g.awardSkillXP(sess, p, player.Construction, 10) - g.AccountStore.SaveCharacter(p) - - sess.WriteLine(fmt.Sprintf("You convert the log into %s.", info.name)) - if p.OptionBool("xp_drops") { - parts := []string{fmt.Sprintf("+%dxp sci", sciXP), "+10xp con"} - sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")")) - } - return - } - - for logID, info := range logTypes { - qty := p.CountItem(logID) - if qty == 0 { - continue - } - - totalCost := info.cost * qty - if p.Credits < totalCost { - continue - } - - if p.Action != nil { - g.cancelAction(p) - } - - g.consumeJunkCost(p, mod) - - p.RemoveItem(logID, qty) - p.Credits -= totalCost - - processed := 0 - for i := 0; i < qty; i++ { - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - g.World.AddGroundItem(p.RoomID, info.plankID, qty-i) - break - } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: info.plankID, Quantity: 1}) - processed++ - } - - sciXP := mod.BaseXP * qty - g.awardSkillXP(sess, p, player.Science, sciXP) - conXP := 10 * qty - g.awardSkillXP(sess, p, player.Construction, conXP) - g.AccountStore.SaveCharacter(p) - - sess.WriteLine(fmt.Sprintf("You convert %d logs into %s for %d credits.", processed, info.name, totalCost)) - if processed < qty { - sess.WriteLine(fmt.Sprintf("Your inventory is full. The remaining %d planks fall to the ground.", qty-processed)) - } - if p.OptionBool("xp_drops") { - parts := []string{fmt.Sprintf("+%dxp sci", sciXP), fmt.Sprintf("+%dxp con", conXP)} - sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")")) - } - return - } - - sess.WriteLine("You don't have any logs to convert.") -} - -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 := mod.BaseXP - g.awardSkillXP(sess, p, player.Science, sciXP) - 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 := mod.BaseXP - g.awardSkillXP(sess, p, player.Science, sciXP) - 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) + g.buffLevelBonus(p, "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 := mod.BaseXP - hpXP := mod.BaseXP / 3 - if hpXP < 1 { - hpXP = 1 - } - var gains []xpGain - - g.awardSkillXP(sess, p, player.Science, sciXP) - gains = append(gains, xpGain{string(player.Science), sciXP}) - - g.awardSkillXP(sess, p, player.Hitpoints, hpXP) - gains = append(gains, xpGain{string(player.Hitpoints), hpXP}) - - g.AccountStore.SaveCharacter(p) - - 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/cmd_trigger_combat.go b/internal/game/cmd_trigger_combat.go new file mode 100644 index 0000000..545a117 --- /dev/null +++ b/internal/game/cmd_trigger_combat.go @@ -0,0 +1,104 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +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) + g.buffLevelBonus(p, "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 := mod.BaseXP + hpXP := mod.BaseXP / 3 + if hpXP < 1 { + hpXP = 1 + } + var gains []xpGain + + g.awardSkillXP(sess, p, player.Science, sciXP) + gains = append(gains, xpGain{string(player.Science), sciXP}) + + g.awardSkillXP(sess, p, player.Hitpoints, hpXP) + gains = append(gains, xpGain{string(player.Hitpoints), hpXP}) + + g.AccountStore.SaveCharacter(p) + + 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/cmd_trigger_enchant.go b/internal/game/cmd_trigger_enchant.go new file mode 100644 index 0000000..535616b --- /dev/null +++ b/internal/game/cmd_trigger_enchant.go @@ -0,0 +1,116 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +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 := mod.BaseXP + g.awardSkillXP(sess, p, player.Science, sciXP) + 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 := mod.BaseXP + g.awardSkillXP(sess, p, player.Science, sciXP) + 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))) + } +} diff --git a/internal/game/cmd_trigger_transport.go b/internal/game/cmd_trigger_transport.go new file mode 100644 index 0000000..2ca8db2 --- /dev/null +++ b/internal/game/cmd_trigger_transport.go @@ -0,0 +1,54 @@ +package game + +import ( + "fmt" + + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +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 := mod.BaseXP + g.awardSkillXP(sess, p, player.Science, sciXP) + 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) +} diff --git a/internal/game/cmd_trigger_utility.go b/internal/game/cmd_trigger_utility.go new file mode 100644 index 0000000..efb23e7 --- /dev/null +++ b/internal/game/cmd_trigger_utility.go @@ -0,0 +1,397 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) triggerUtility(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { + switch mod.ID { + case "low_process", "high_process": + g.triggerProcessing(sess, p, mod, targetArg) + 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) + case "plank_make": + g.triggerPlankMake(sess, p, mod, targetArg) + } +} + +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 := mod.BaseXP + g.awardSkillXP(sess, p, player.Science, sciXP) + g.AccountStore.SaveCharacter(p) + + 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) 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 := mod.BaseXP * boneCount + g.awardSkillXP(sess, p, player.Science, sciXP) + 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 := mod.BaseXP + g.awardSkillXP(sess, p, player.Science, sciXP) + 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.Quantity + 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.Quantity + 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 := mod.BaseXP + g.awardSkillXP(sess, p, player.Science, sciXP) + if recipe.XP > 0 { + g.awardSkillXP(sess, p, player.SkillName(recipe.Skill), recipe.XP) + } + 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) triggerPlankMake(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 + } + + logTypes := map[string]struct { + plankID string + name string + cost int + }{ + "logs": {"planks", "planks", 3}, + "oak_logs": {"oak_planks", "oak planks", 7}, + "teak_logs": {"teak_planks", "teak planks", 14}, + "mahogany_logs": {"mahogany_planks", "mahogany planks", 28}, + } + + if targetArg != "" { + slot, inv := g.findInventoryItem(p, targetArg) + if slot < 0 { + sess.WriteLine("You don't have that item.") + return + } + info, ok := logTypes[inv.ItemID] + if !ok { + sess.WriteLine("You can't turn that into planks.") + return + } + + if p.Credits < info.cost { + sess.WriteLine(fmt.Sprintf("You need %d credits for Plank Make (70%% of sawmill cost).", info.cost)) + return + } + + if p.Action != nil { + g.cancelAction(p) + } + + g.consumeJunkCost(p, mod) + + p.RemoveItem(inv.ItemID, 1) + p.Credits -= info.cost + + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + sess.WriteLine("Your inventory is full.") + return + } + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: info.plankID, Quantity: 1}) + + sciXP := mod.BaseXP + g.awardSkillXP(sess, p, player.Science, sciXP) + g.awardSkillXP(sess, p, player.Construction, 10) + g.AccountStore.SaveCharacter(p) + + sess.WriteLine(fmt.Sprintf("You convert the log into %s.", info.name)) + if p.OptionBool("xp_drops") { + parts := []string{fmt.Sprintf("+%dxp sci", sciXP), "+10xp con"} + sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")")) + } + return + } + + for logID, info := range logTypes { + qty := p.CountItem(logID) + if qty == 0 { + continue + } + + totalCost := info.cost * qty + if p.Credits < totalCost { + continue + } + + if p.Action != nil { + g.cancelAction(p) + } + + g.consumeJunkCost(p, mod) + + p.RemoveItem(logID, qty) + p.Credits -= totalCost + + processed := 0 + for i := 0; i < qty; i++ { + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + g.World.AddGroundItem(p.RoomID, info.plankID, qty-i) + break + } + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: info.plankID, Quantity: 1}) + processed++ + } + + sciXP := mod.BaseXP * qty + g.awardSkillXP(sess, p, player.Science, sciXP) + conXP := 10 * qty + g.awardSkillXP(sess, p, player.Construction, conXP) + g.AccountStore.SaveCharacter(p) + + sess.WriteLine(fmt.Sprintf("You convert %d logs into %s for %d credits.", processed, info.name, totalCost)) + if processed < qty { + sess.WriteLine(fmt.Sprintf("Your inventory is full. The remaining %d planks fall to the ground.", qty-processed)) + } + if p.OptionBool("xp_drops") { + parts := []string{fmt.Sprintf("+%dxp sci", sciXP), fmt.Sprintf("+%dxp con", conXP)} + sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")")) + } + return + } + + sess.WriteLine("You don't have any logs to convert.") +} diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go index fbe5945..4f830a8 100644 --- a/internal/game/cmd_use.go +++ b/internal/game/cmd_use.go @@ -10,6 +10,13 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeUse(sess *net.Session, args []string, rawInput string) { + if g.tryFinishingBlow(sess, strings.Join(args, " ")) { + return + } + g.doUse(sess, strings.Join(args, " ")) +} + func (g *Game) doUse(sess *net.Session, input string) { if strings.TrimSpace(input) == "" { g.doHelp(sess, "use") diff --git a/internal/game/cmd_walk.go b/internal/game/cmd_walk.go index eca608f..9485af5 100644 --- a/internal/game/cmd_walk.go +++ b/internal/game/cmd_walk.go @@ -10,6 +10,10 @@ import ( "thehouseoficarus/internal/world" ) +func (g *Game) executeWalk(sess *net.Session, args []string, rawInput string) { + g.doWalk(sess, args) +} + func (g *Game) doWalk(sess *net.Session, args []string) { p := sess.Player if len(args) == 0 { diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go index 36765e8..f2fd84c 100644 --- a/internal/game/cmd_wear.go +++ b/internal/game/cmd_wear.go @@ -9,6 +9,10 @@ import ( "thehouseoficarus/internal/player" ) +func (g *Game) executeWear(sess *net.Session, args []string, rawInput string) { + g.doWear(sess, strings.Join(args, " ")) +} + func (g *Game) checkRequirements(p *player.Player, def *object.ItemDef) string { if len(def.Requirements) == 0 { return "" diff --git a/internal/game/course.go b/internal/game/core_course.go index 335c97f..908466d 100644 --- a/internal/game/course.go +++ b/internal/game/core_course.go @@ -1,10 +1,10 @@ package game import ( - "os" "path/filepath" "sync" + "thehouseoficarus/internal/action" "gopkg.in/yaml.v3" ) @@ -91,17 +91,10 @@ func (cs *CourseStore) loadAllLocked() { cs.roomToObstacle = make(map[int]*ObstacleInfo) localVerbs := make(map[string]bool) - pattern := filepath.Join(cs.dataDir, "courses", "*.yaml") - files, _ := filepath.Glob(pattern) - - for _, f := range files { - data, err := os.ReadFile(f) - if err != nil { - continue - } + action.WalkYAMLDir(filepath.Join(cs.dataDir, "courses"), func(path, id string, data []byte) error { var cfg CourseConfig if err := yaml.Unmarshal(data, &cfg); err != nil { - continue + return nil } cs.courses[cfg.ID] = &cfg @@ -135,7 +128,8 @@ func (cs *CourseStore) loadAllLocked() { cs.roomToObstacle[obs.RoomID] = info localVerbs[obs.Verb] = true } - } + return nil + }) obstacleVerbs = localVerbs } diff --git a/internal/game/equip_stats.go b/internal/game/core_equip.go index 02b8348..02b8348 100644 --- a/internal/game/equip_stats.go +++ b/internal/game/core_equip.go diff --git a/internal/game/player_flags.go b/internal/game/core_flags.go index 6595cc4..6595cc4 100644 --- a/internal/game/player_flags.go +++ b/internal/game/core_flags.go diff --git a/internal/game/hacking.go b/internal/game/core_hacking.go index 3457728..3457728 100644 --- a/internal/game/hacking.go +++ b/internal/game/core_hacking.go diff --git a/internal/game/login_account.go b/internal/game/core_login_account.go index 87f8c62..87f8c62 100644 --- a/internal/game/login_account.go +++ b/internal/game/core_login_account.go diff --git a/internal/game/login_char.go b/internal/game/core_login_char.go index 84f146b..84f146b 100644 --- a/internal/game/login_char.go +++ b/internal/game/core_login_char.go diff --git a/internal/game/messages.go b/internal/game/core_messages.go index 1c9e85a..1c9e85a 100644 --- a/internal/game/messages.go +++ b/internal/game/core_messages.go diff --git a/internal/game/production_core.go b/internal/game/core_production.go index 42f64e1..227c4ad 100644 --- a/internal/game/production_core.go +++ b/internal/game/core_production.go @@ -423,8 +423,8 @@ func (g *Game) formatRecipeMaterials(r *action.RecipeDef) string { if def, err := g.ItemStore.Load(e.Items[0]); err == nil { itemName = def.Name } - if e.Qty > 1 { - parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Qty)) + if e.Quantity > 1 { + parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Quantity)) } else { parts = append(parts, itemName) } @@ -604,3 +604,200 @@ func (g *Game) hasToolType(p *player.Player, toolType string) bool { } return false } +type recipeEntry struct { + ItemName string + Recipe action.RecipeDef +} + +type productionTypeInfo struct { + ActionType string + DisplayVerb string +} + +var productionTypes = map[string]productionTypeInfo{ + "cooking": {"cook", "cooking"}, + "smelting": {"smelt", "smelting"}, + "smithing": {"smith", "smithing"}, + "crafting": {"craft", "crafting"}, + "combine": {"combine", "combining"}, + "fletching": {"fletch", "fletching"}, + "pharmacy": {"mix", "mixing"}, + "construction": {"construct", "constructing"}, +} + +var productionActionTypes map[string]bool + +func init() { + productionActionTypes = make(map[string]bool) + for _, pt := range productionTypes { + productionActionTypes[pt.ActionType] = true + } +} + +func recipeMenuData(recipeIDs []string) []map[string]string { + out := make([]map[string]string, len(recipeIDs)) + for i, id := range recipeIDs { + out[i] = map[string]string{"recipe_id": id} + } + return out +} + +func recipeDefIDs(defs []action.RecipeDef) []string { + ids := make([]string, len(defs)) + for i, d := range defs { + ids[i] = d.ID + } + return ids +} + +func entryMenuData(entries []recipeEntry) []map[string]string { + out := make([]map[string]string, len(entries)) + for i, e := range entries { + out[i] = map[string]string{"recipe_id": e.Recipe.ID} + } + return out +} + +func (g *Game) promptHowMany(sess *net.Session, recipeID string) { + sess.PendingRecipeID = recipeID + sess.State = net.StateHowMany + sess.Write("How many (return for all)?: ") +} + +type recipeSelection struct { + Recipe *action.RecipeDef + Count int +} + +func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, lastRecipeID string) (sel recipeSelection, errMsg string) { + if input == "" { + if lastRecipeID == "" { + return sel, "never_mind" + } + for i := range recipes { + if recipes[i].ID == lastRecipeID { + sel.Recipe = &recipes[i] + return sel, "" + } + } + return sel, "never_mind" + } + + qty, productName := parseQty(input) + productName = strings.TrimSpace(productName) + + if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(recipes) { + sel.Recipe = &recipes[idx-1] + sel.Count = qty + return sel, "" + } + + matchCount := 0 + for i := range recipes { + outDef, _ := g.ItemStore.Load(recipes[i].Output) + name := recipes[i].Output + if outDef != nil { + name = outDef.Name + } + if action.WordPrefixMatch(productName, name) { + if matchCount == 0 { + sel.Recipe = &recipes[i] + sel.Count = qty + } + matchCount++ + } + } + if matchCount > 1 { + return sel, "ambiguous" + } + if sel.Recipe == nil { + return sel, "never_mind" + } + return sel, "" +} + +func (g *Game) showMenuTable(sess *net.Session, title string, names []string) { + p := sess.Player + tbl := &Table{Title: title} + for i, name := range names { + tbl.Rows = append(tbl.Rows, []string{fmt.Sprintf("%d", i+1), name}) + } + unicode := p.OptionBool("unicode") + sess.WriteLine("") + for _, line := range tbl.Render(unicode) { + sess.WriteLine(line) + } +} + +func (g *Game) handleHowMany(sess *net.Session, input string) { + p := sess.Player + recipeID := sess.PendingRecipeID + sess.PendingRecipeID = "" + sess.State = net.StateGame + + input = strings.TrimSpace(input) + + var count int + if input == "" { + count = 0 + } else if n, err := strconv.Atoi(input); err == nil && n > 0 { + count = n + } else { + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + + var recipe *action.RecipeDef + if strings.HasPrefix(recipeID, "combine_") { + itemID := strings.TrimPrefix(recipeID, "combine_") + itemDef, err := g.ItemStore.Load(itemID) + if err != nil || len(itemDef.MadeFrom) == 0 { + g.reprompt(sess) + return + } + recipe = g.buildCombineRecipe(itemDef) + } else { + all, err := g.RecipeStore.LoadAll() + if err != nil { + g.reprompt(sess) + return + } + for _, r := range all { + if r.ID == recipeID { + recipe = &r + break + } + } + } + + if recipe == nil { + g.reprompt(sess) + return + } + + g.startProductionFromRecipe(sess, p, recipe, count) +} + +func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef { + var consume []action.ConsumeEntry + for _, mf := range def.MadeFrom { + consume = append(consume, action.ConsumeEntry{ + Items: mf.Items, + Quantity: mf.Quantity, + Byproducts: mf.Byproducts, + }) + } + wait := def.Ticks + if wait <= 0 { + wait = 2 + } + return &action.RecipeDef{ + ID: "combine_" + def.ID, + Type: "combine", + Wait: wait, + Consume: consume, + Output: def.ID, + Message: fmt.Sprintf("You create %s.", def.Name), + } +} diff --git a/internal/game/core_skill.go b/internal/game/core_skill.go new file mode 100644 index 0000000..c02cdf3 --- /dev/null +++ b/internal/game/core_skill.go @@ -0,0 +1,15 @@ +package game + +import ( + "fmt" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) awardSkillXP(sess *net.Session, p *player.Player, skill player.SkillName, xp int) { + newLevel := p.AddSkillXP(skill, xp) + if newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) + } +} diff --git a/internal/game/core_startup.go b/internal/game/core_startup.go new file mode 100644 index 0000000..ab52762 --- /dev/null +++ b/internal/game/core_startup.go @@ -0,0 +1,770 @@ +package game + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "thehouseoficarus/internal/action" +) + +type StartupIssue struct { + Level string + Type string + Message string +} + +func (g *Game) ValidateStartup() []StartupIssue { + var issues []StartupIssue + + issues = append(issues, validateDuplicateIDs(g.DataDir, "items", "item")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "rooms", "room")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "mobs", "mob")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "objects", "object")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "drops", "drop table")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "recipes", "recipe")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "courses", "course")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "modules", "module")...) + issues = append(issues, validateDuplicateIDs(g.DataDir, "help", "help topic")...) + + issues = append(issues, validateRoomReferences(g)...) + issues = append(issues, validateMobReferences(g)...) + issues = append(issues, validateObjectReferences(g)...) + issues = append(issues, validateItemReferences(g)...) + issues = append(issues, validateRecipeReferences(g)...) + issues = append(issues, validateDropTableReferences(g)...) + issues = append(issues, validateCourseReferences(g)...) + issues = append(issues, validateRoomWiring(g)...) + + return issues +} + +func validateDuplicateIDs(dataDir, subDir, label string) []StartupIssue { + dir := filepath.Join(dataDir, subDir) + dups := action.CheckDuplicateIDs(dir) + var issues []StartupIssue + for _, d := range dups { + short := make([]string, len(d.Paths)) + for i, p := range d.Paths { + short[i] = strings.TrimPrefix(p, dir+"/") + } + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "duplicate", + Message: fmt.Sprintf("Duplicate %s ID %q found in: %s", + label, d.ID, strings.Join(short, ", ")), + }) + } + return issues +} + +func validateRoomReferences(g *Game) []StartupIssue { + var issues []StartupIssue + roomIndex := g.World.RoomIndex() + itemIDs := g.ItemStore.IDSet() + mobIDs := g.MobStore.AllDefIDs() + objIDs := g.ObjectStore.IDSet() + + for id := range roomIndex { + room, err := g.World.LoadRoom(id) + if err != nil { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: failed to load: %v", id, err), + }) + continue + } + if room.Name == "" { + issues = append(issues, StartupIssue{ + Level: "WARN", + Type: "reference", + Message: fmt.Sprintf("Room %d: has no name", id), + }) + } + + for dir, exit := range room.Exits { + if exit.Room <= 0 { + continue + } + if _, ok := roomIndex[exit.Room]; !ok { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: exit %q targets nonexistent room %d", + id, dir, exit.Room), + }) + } + } + + for _, rm := range room.Mobs { + if rm.ID == "" { + continue + } + if _, ok := mobIDs[rm.ID]; !ok { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: references nonexistent mob %q", + id, rm.ID), + }) + } + for _, wr := range rm.WanderRooms { + if _, ok := roomIndex[wr]; !ok { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: mob %q wander_rooms references nonexistent room %d", + id, rm.ID, wr), + }) + } + } + } + + for _, s := range room.ItemSpawns { + if s.ID != "" && !itemIDs[s.ID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: spawns nonexistent item %q", + id, s.ID), + }) + } + } + + for _, robj := range room.Objects { + if robj.ID != "" && !objIDs[robj.ID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: references nonexistent object %q", + id, robj.ID), + }) + } + for _, wr := range robj.WanderRooms { + if _, ok := roomIndex[wr]; !ok { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: object %q wander_rooms references nonexistent room %d", + id, robj.ID, wr), + }) + } + } + } + } + + return issues +} + +func validateMobReferences(g *Game) []StartupIssue { + var issues []StartupIssue + itemIDs := g.ItemStore.IDSet() + dropIDs := dropTableIDSet(g.DataDir) + mobIDs := g.MobStore.AllDefIDs() + + for id := range mobIDs { + def, err := g.MobStore.LoadDef(id) + if err != nil { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: failed to load: %v", id, err), + }) + continue + } + if def.Name == "" { + issues = append(issues, StartupIssue{ + Level: "WARN", + Type: "reference", + Message: fmt.Sprintf("Mob %q: has no name", id), + }) + } + + if def.Drops.Remains != "" && !itemIDs[def.Drops.Remains] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: drops remains (item %q) does not exist", + id, def.Drops.Remains), + }) + } + + for _, d := range def.Drops.Loot { + if d.ItemID != "" && !itemIDs[d.ItemID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: loot item %q does not exist", + id, d.ItemID), + }) + } + if d.Table != "" && !dropIDs[d.Table] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: loot table %q does not exist", + id, d.Table), + }) + } + } + + if def.StealTable != "" && !dropIDs[def.StealTable] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: steal_table %q does not exist", + id, def.StealTable), + }) + } + + if def.FinishingBlow != "" && !itemIDs[def.FinishingBlow] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: finishing_blow item %q does not exist", + id, def.FinishingBlow), + }) + } + } + + return issues +} + +func validateObjectReferences(g *Game) []StartupIssue { + var issues []StartupIssue + objIDs := g.ObjectStore.IDSet() + itemIDs := g.ItemStore.IDSet() + mobIDs := g.MobStore.AllDefIDs() + dropIDs := dropTableIDSet(g.DataDir) + roomIndex := g.World.RoomIndex() + + for id := range objIDs { + obj, err := g.ObjectStore.Load(id) + if err != nil { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Object %q: failed to load: %v", id, err), + }) + continue + } + if obj.Name == "" { + issues = append(issues, StartupIssue{ + Level: "WARN", + Type: "reference", + Message: fmt.Sprintf("Object %q: has no name", id), + }) + } + + if obj.RemovalItem != "" && !itemIDs[obj.RemovalItem] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Object %q: removal_item %q does not exist", + id, obj.RemovalItem), + }) + } + + if obj.StealTable != "" && !dropIDs[obj.StealTable] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Object %q: steal_table %q does not exist", + id, obj.StealTable), + }) + } + + if obj.GuardMob != "" && !mobIDs[obj.GuardMob] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Object %q: guard_mob %q does not exist", + id, obj.GuardMob), + }) + } + + for _, ui := range obj.UseInteractions { + if ui.Item != "" && !itemIDs[ui.Item] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Object %q: use_interaction item %q does not exist", + id, ui.Item), + }) + } + if ui.Action != nil { + issues = append(issues, validateNodeAction(fmt.Sprintf("Object %q: use_interaction", id), ui.Action, itemIDs, roomIndex)...) + } + } + + if obj.Gather != nil { + issues = append(issues, validateGatherConfig(fmt.Sprintf("Object %q: gather", id), obj.Gather, itemIDs, dropIDs)...) + } + + if obj.Talk != nil { + issues = append(issues, validateTalkConfig(fmt.Sprintf("Object %q: talk", id), obj.Talk, itemIDs, roomIndex)...) + } + + if obj.Use != nil { + issues = append(issues, validateUseConfig(fmt.Sprintf("Object %q: use", id), obj.Use, itemIDs)...) + } + } + + return issues +} + +func validateItemReferences(g *Game) []StartupIssue { + var issues []StartupIssue + itemIDs := g.ItemStore.IDSet() + dropIDs := dropTableIDSet(g.DataDir) + + for id := range itemIDs { + def, err := g.ItemStore.Load(id) + if err != nil { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q: failed to load: %v", id, err), + }) + continue + } + if def.Name == "" { + issues = append(issues, StartupIssue{ + Level: "WARN", + Type: "reference", + Message: fmt.Sprintf("Item %q: has no name", id), + }) + } + + if def.SearchTable != "" && !dropIDs[def.SearchTable] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q: search_table %q does not exist", + id, def.SearchTable), + }) + } + + if def.SearchMiscTable != "" && !dropIDs[def.SearchMiscTable] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q: search_misc_table %q does not exist", + id, def.SearchMiscTable), + }) + } + + for _, mf := range def.MadeFrom { + for _, item := range mf.Items { + if item != "" && !itemIDs[item] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q: made_from item %q does not exist", + id, item), + }) + } + } + for _, bp := range mf.Byproducts { + if bp != "" && !itemIDs[bp] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q: made_from byproduct %q does not exist", + id, bp), + }) + } + } + } + + if def.FarmProduct != "" && !itemIDs[def.FarmProduct] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q (seed): farm_product %q does not exist", + id, def.FarmProduct), + }) + } + } + + return issues +} + +func validateRecipeReferences(g *Game) []StartupIssue { + var issues []StartupIssue + itemIDs := g.ItemStore.IDSet() + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Failed to load recipes: %v", err), + }) + return issues + } + + recipeIDs := make(map[string]bool) + for _, r := range allRecipes { + if r.ID != "" { + if recipeIDs[r.ID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "duplicate", + Message: fmt.Sprintf("Recipe %q: duplicate ID field (two recipe files with same ID value)", + r.ID), + }) + } + recipeIDs[r.ID] = true + } + + for _, e := range r.Consume { + for _, item := range e.Items { + if item != "" && !itemIDs[item] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Recipe %q (%s): consumes nonexistent item %q", + r.ID, r.Type, item), + }) + } + } + } + + if r.Output != "" && !itemIDs[r.Output] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Recipe %q (%s): output item %q does not exist", + r.ID, r.Type, r.Output), + }) + } + + if r.Fail != "" && !itemIDs[r.Fail] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Recipe %q (%s): fail product %q does not exist", + r.ID, r.Type, r.Fail), + }) + } + } + + return issues +} + +func validateDropTableReferences(g *Game) []StartupIssue { + var issues []StartupIssue + dropIDs := dropTableIDSet(g.DataDir) + itemIDs := g.ItemStore.IDSet() + + for id := range dropIDs { + dt, err := action.LoadDropTable(g.DataDir, id) + if err != nil { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Drop table %q: failed to load: %v", id, err), + }) + continue + } + for _, d := range dt.Drops { + if d.ItemID != "" && !itemIDs[d.ItemID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Drop table %q: drops nonexistent item %q", + id, d.ItemID), + }) + } + if d.Table != "" && !dropIDs[d.Table] && d.Table != id { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Drop table %q: references nonexistent sub-table %q", + id, d.Table), + }) + } + } + } + + return issues +} + +func validateCourseReferences(g *Game) []StartupIssue { + var issues []StartupIssue + roomIndex := g.World.RoomIndex() + + courses := g.CourseStore.AllCourses() + for id, cfg := range courses { + if cfg.StartRoom > 0 { + if _, ok := roomIndex[cfg.StartRoom]; !ok { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Course %q: start_room %d does not exist", + id, cfg.StartRoom), + }) + } + } + for _, obs := range cfg.Obstacles { + if _, ok := roomIndex[obs.RoomID]; !ok { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Course %q: obstacle room %d does not exist", + id, obs.RoomID), + }) + } + } + } + + return issues +} + +func validateRoomWiring(g *Game) []StartupIssue { + var issues []StartupIssue + roomIndex := g.World.RoomIndex() + cfg := g.ValidationConfig + + sourceSet := make(map[int]bool) + for _, s := range cfg.CheckSources { + sourceSet[s] = true + } + + ignoreTo := make(map[int]bool) + for _, r := range cfg.IgnoreUnreachable { + ignoreTo[r] = true + } + + hasExitTo := make(map[int]bool) + for id := range roomIndex { + room, err := g.World.LoadRoom(id) + if err != nil { + continue + } + for _, exit := range room.Exits { + if exit.Room > 0 { + hasExitTo[exit.Room] = true + } + } + } + + var orphans []int + for id := range roomIndex { + if ignoreTo[id] { + continue + } + if sourceSet[id] { + continue + } + if !hasExitTo[id] { + orphans = append(orphans, id) + } + } + if len(orphans) > 0 { + sort.Ints(orphans) + var strs []string + for _, id := range orphans { + strs = append(strs, fmt.Sprintf("%d", id)) + } + issues = append(issues, StartupIssue{ + Level: "WARN", + Type: "integrity", + Message: fmt.Sprintf("Orphan rooms (no exit leads to them): %s", + strings.Join(strs, ", ")), + }) + } + + return issues +} + +func validateGatherConfig(prefix string, cfg *action.GatherConfig, itemIDs map[string]bool, dropIDs map[string]bool) []StartupIssue { + var issues []StartupIssue + + if cfg.Bait != "" && !itemIDs[cfg.Bait] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: bait item %q does not exist", + prefix, cfg.Bait), + }) + } + + for _, d := range cfg.Drops { + if d.ItemID != "" && !itemIDs[d.ItemID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: drop item %q does not exist", + prefix, d.ItemID), + }) + } + if d.Table != "" && !dropIDs[d.Table] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: drop table %q does not exist", + prefix, d.Table), + }) + } + } + + return issues +} + +func validateTalkConfig(prefix string, cfg *action.TalkConfig, itemIDs map[string]bool, roomIndex map[int]bool) []StartupIssue { + var issues []StartupIssue + if cfg == nil { + return issues + } + for nodeID, node := range cfg.Nodes { + nodePrefix := fmt.Sprintf("%s: node %q", prefix, nodeID) + if node.Action != nil { + issues = append(issues, validateNodeAction(nodePrefix, node.Action, itemIDs, roomIndex)...) + } + } + return issues +} + +func validateUseConfig(prefix string, cfg *action.UseConfig, itemIDs map[string]bool) []StartupIssue { + var issues []StartupIssue + if cfg == nil { + return issues + } + + for itemID := range cfg.Consume { + if itemID != "" && !itemIDs[itemID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: consumes nonexistent item %q", + prefix, itemID), + }) + } + } + + if cfg.Reward.ItemID != "" && !itemIDs[cfg.Reward.ItemID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: reward item %q does not exist", + prefix, cfg.Reward.ItemID), + }) + } + + return issues +} + +func validateNodeAction(prefix string, na *action.NodeAction, itemIDs map[string]bool, roomIndex map[int]bool) []StartupIssue { + var issues []StartupIssue + if na == nil { + return issues + } + + if na.GiveItem != "" && !itemIDs[na.GiveItem] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: give_item %q does not exist", + prefix, na.GiveItem), + }) + } + + if na.TakeItem != "" && !itemIDs[na.TakeItem] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: take_item %q does not exist", + prefix, na.TakeItem), + }) + } + + if na.Teleport > 0 { + if _, ok := roomIndex[na.Teleport]; !ok { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: teleport to nonexistent room %d", + prefix, na.Teleport), + }) + } + } + + if na.Shop != nil { + for _, si := range na.Shop.Items { + if si.ItemID != "" && !itemIDs[si.ItemID] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: shop item %q does not exist", + prefix, si.ItemID), + }) + } + } + } + + return issues +} + +func dropTableIDSet(dataDir string) map[string]bool { + dir := filepath.Join(dataDir, "drops") + ids := make(map[string]bool) + action.WalkYAMLDir(dir, func(path, id string, data []byte) error { + ids[id] = true + return nil + }) + return ids +} + +func (cs *CourseStore) AllCourses() map[string]*CourseConfig { + cs.mu.Lock() + defer cs.mu.Unlock() + if !cs.loaded { + cs.loadAllLocked() + } + return cs.courses +} + +func (g *Game) ValidateAndLog() { + LogStartupIssues(g.ValidateStartup()) +} + +func LogStartupIssues(issues []StartupIssue) { + if len(issues) == 0 { + fmt.Fprintf(os.Stderr, "Startup validation: OK\n") + return + } + + errors := 0 + warns := 0 + for _, issue := range issues { + if issue.Level == "ERROR" { + errors++ + } else { + warns++ + } + } + + fmt.Fprintf(os.Stderr, "\n=== Startup Validation: %d error(s), %d warning(s) ===\n\n", errors, warns) + + sort.Slice(issues, func(i, j int) bool { + if issues[i].Level != issues[j].Level { + return issues[i].Level == "ERROR" + } + return issues[i].Message < issues[j].Message + }) + + for _, issue := range issues { + tag := fmt.Sprintf("[%s]", issue.Level) + fmt.Fprintf(os.Stderr, " %-7s %s\n", tag, issue.Message) + } + fmt.Fprintln(os.Stderr) + + if errors > 0 { + fmt.Fprintf(os.Stderr, "*** %d startup errors detected. The server may behave unexpectedly. ***\n", errors) + } +}
\ No newline at end of file diff --git a/internal/game/stations.go b/internal/game/core_stations.go index d30a35c..d30a35c 100644 --- a/internal/game/stations.go +++ b/internal/game/core_stations.go diff --git a/internal/game/types.go b/internal/game/core_types.go index d41b18a..d41b18a 100644 --- a/internal/game/types.go +++ b/internal/game/core_types.go diff --git a/internal/game/utils.go b/internal/game/core_utils.go index 9148ea3..409422e 100644 --- a/internal/game/utils.go +++ b/internal/game/core_utils.go @@ -113,9 +113,4 @@ func (g *Game) newInventorySlot(itemID string, qty int) *player.InventorySlot { return slot } -func (g *Game) awardSkillXP(sess *net.Session, p *player.Player, skill player.SkillName, xp int) { - newLevel := p.AddSkillXP(skill, xp) - if newLevel > 0 { - sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) - } -} + diff --git a/internal/game/game.go b/internal/game/game.go index b8eadce..5d6e21e 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -35,18 +35,19 @@ type QueuedCommand struct { } type Game struct { - World *world.World - ObjectStore *object.ObjectStore - ItemStore *object.ItemStore - AccountStore *player.AccountStore - MobStore *world.MobStore - RecipeStore *action.RecipeStore - CourseStore *CourseStore - Hub *net.Hub - Ticks *engine.Engine - WorldFlags map[string]any - ColorConfig *config.ColorsConfig - DataDir string + World *world.World + ObjectStore *object.ObjectStore + ItemStore *object.ItemStore + AccountStore *player.AccountStore + MobStore *world.MobStore + RecipeStore *action.RecipeStore + CourseStore *CourseStore + Hub *net.Hub + Ticks *engine.Engine + WorldFlags map[string]any + ColorConfig *config.ColorsConfig + DataDir string + ValidationConfig config.StartupValidationConfig restTimers map[string]uint64 charsMu sync.Mutex loggedInChars map[string]*net.Session @@ -59,7 +60,7 @@ type Game struct { hackingStates map[string]*hacking.Session } -func New(dataDir string, colorConfig *config.ColorsConfig) *Game { +func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.StartupValidationConfig) *Game { g := &Game{ World: world.New(dataDir), ObjectStore: object.NewObjectStore(dataDir), @@ -72,6 +73,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game { WorldFlags: make(map[string]any), ColorConfig: colorConfig, DataDir: dataDir, + ValidationConfig: valConfig, restTimers: make(map[string]uint64), loggedInChars: make(map[string]*net.Session), freeQueue: make(map[string][]QueuedCommand), @@ -83,6 +85,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game { } g.CourseStore.LoadAll() g.LoadMods() + g.ValidateAndLog() return g } diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go deleted file mode 100644 index 329110b..0000000 --- a/internal/game/production_menu.go +++ /dev/null @@ -1,209 +0,0 @@ -package game - -import ( - "fmt" - "strconv" - "strings" - - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" -) - -type recipeEntry struct { - ItemName string - Recipe action.RecipeDef -} - -type productionTypeInfo struct { - ActionType string - DisplayVerb string -} - -var productionTypes = map[string]productionTypeInfo{ - "cooking": {"cook", "cooking"}, - "smelting": {"smelt", "smelting"}, - "smithing": {"smith", "smithing"}, - "crafting": {"craft", "crafting"}, - "combine": {"combine", "combining"}, - "fletching": {"fletch", "fletching"}, - "pharmacy": {"mix", "mixing"}, - "construction": {"construct", "constructing"}, -} - -var productionActionTypes map[string]bool - -func init() { - productionActionTypes = make(map[string]bool) - for _, pt := range productionTypes { - productionActionTypes[pt.ActionType] = true - } -} - -func recipeMenuData(recipeIDs []string) []map[string]string { - out := make([]map[string]string, len(recipeIDs)) - for i, id := range recipeIDs { - out[i] = map[string]string{"recipe_id": id} - } - return out -} - -func recipeDefIDs(defs []action.RecipeDef) []string { - ids := make([]string, len(defs)) - for i, d := range defs { - ids[i] = d.ID - } - return ids -} - -func entryMenuData(entries []recipeEntry) []map[string]string { - out := make([]map[string]string, len(entries)) - for i, e := range entries { - out[i] = map[string]string{"recipe_id": e.Recipe.ID} - } - return out -} - -func (g *Game) promptHowMany(sess *net.Session, recipeID string) { - sess.PendingRecipeID = recipeID - sess.State = net.StateHowMany - sess.Write("How many (return for all)?: ") -} - -type recipeSelection struct { - Recipe *action.RecipeDef - Count int -} - -func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, lastRecipeID string) (sel recipeSelection, errMsg string) { - if input == "" { - if lastRecipeID == "" { - return sel, "never_mind" - } - for i := range recipes { - if recipes[i].ID == lastRecipeID { - sel.Recipe = &recipes[i] - return sel, "" - } - } - return sel, "never_mind" - } - - qty, productName := parseQty(input) - productName = strings.TrimSpace(productName) - - if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(recipes) { - sel.Recipe = &recipes[idx-1] - sel.Count = qty - return sel, "" - } - - matchCount := 0 - for i := range recipes { - outDef, _ := g.ItemStore.Load(recipes[i].Output) - name := recipes[i].Output - if outDef != nil { - name = outDef.Name - } - if action.WordPrefixMatch(productName, name) { - if matchCount == 0 { - sel.Recipe = &recipes[i] - sel.Count = qty - } - matchCount++ - } - } - if matchCount > 1 { - return sel, "ambiguous" - } - if sel.Recipe == nil { - return sel, "never_mind" - } - return sel, "" -} - -func (g *Game) showMenuTable(sess *net.Session, title string, names []string) { - p := sess.Player - tbl := &Table{Title: title} - for i, name := range names { - tbl.Rows = append(tbl.Rows, []string{fmt.Sprintf("%d", i+1), name}) - } - unicode := p.OptionBool("unicode") - sess.WriteLine("") - for _, line := range tbl.Render(unicode) { - sess.WriteLine(line) - } -} - -func (g *Game) handleHowMany(sess *net.Session, input string) { - p := sess.Player - recipeID := sess.PendingRecipeID - sess.PendingRecipeID = "" - sess.State = net.StateGame - - input = strings.TrimSpace(input) - - var count int - if input == "" { - count = 0 - } else if n, err := strconv.Atoi(input); err == nil && n > 0 { - count = n - } else { - sess.WriteLine("Never mind.") - g.reprompt(sess) - return - } - - var recipe *action.RecipeDef - if strings.HasPrefix(recipeID, "combine_") { - itemID := strings.TrimPrefix(recipeID, "combine_") - itemDef, err := g.ItemStore.Load(itemID) - if err != nil || len(itemDef.MadeFrom) == 0 { - g.reprompt(sess) - return - } - recipe = g.buildCombineRecipe(itemDef) - } else { - all, err := g.RecipeStore.LoadAll() - if err != nil { - g.reprompt(sess) - return - } - for _, r := range all { - if r.ID == recipeID { - recipe = &r - break - } - } - } - - if recipe == nil { - g.reprompt(sess) - return - } - - g.startProductionFromRecipe(sess, p, recipe, count) -} - -func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef { - var consume []action.ConsumeEntry - for _, mf := range def.MadeFrom { - consume = append(consume, action.ConsumeEntry{ - Items: mf.Items, - Qty: mf.Qty, - Byproducts: mf.Byproducts, - }) - } - wait := def.Ticks - if wait <= 0 { - wait = 2 - } - return &action.RecipeDef{ - ID: "combine_" + def.ID, - Type: "combine", - Wait: wait, - Consume: consume, - Output: def.ID, - Message: fmt.Sprintf("You create %s.", def.Name), - } -} diff --git a/internal/game/assassin.go b/internal/game/sys_assassin.go index 1927399..1927399 100644 --- a/internal/game/assassin.go +++ b/internal/game/sys_assassin.go diff --git a/internal/game/buff.go b/internal/game/sys_buff.go index 9449731..9449731 100644 --- a/internal/game/buff.go +++ b/internal/game/sys_buff.go diff --git a/internal/game/death.go b/internal/game/sys_combat.go index d2c1b73..030bed1 100644 --- a/internal/game/death.go +++ b/internal/game/sys_combat.go @@ -1,8 +1,12 @@ package game import ( + "fmt" "sort" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) @@ -64,3 +68,47 @@ func (g *Game) dropItemsOnDeath(p *player.Player) { g.World.AddGroundItem(roomID, it.itemID, it.quantity) } } + +func (g *Game) checkAggro(sess *net.Session) { + p := sess.Player + + if combat.GetCombat(p.Name) != nil { + return + } + + playerLevel := p.CombatLevel() + mobs := g.MobStore.MobsInRoom(p.RoomID) + + for _, mob := range mobs { + if !mob.Aggressive || mob.HP <= 0 || mob.Protected { + continue + } + if combat.IsMobInCombat(mob.InstanceID) { + continue + } + mobLevel := mobCombatLevel(mob) + if playerLevel > mobLevel*2 { + continue + } + aggMob := mob + g.Ticks.Subscribe(engine.ToTicks(1), func() bool { + if combat.GetCombat(p.Name) != nil { + return false + } + if aggMob.HP <= 0 || aggMob.RoomID != p.RoomID { + return false + } + if combat.IsMobInCombat(aggMob.InstanceID) { + return false + } + attacker := aggMob.Name + if !aggMob.Unique { + attacker = "The " + aggMob.Name + } + sess.WriteLine(fmt.Sprintf("\n%s attacks you!", g.colorize(sess, "mob_name", attacker))) + g.startCombat(sess, p, aggMob) + return false + }) + break + } +} diff --git a/internal/game/construction.go b/internal/game/sys_construction.go index 06713cd..06713cd 100644 --- a/internal/game/construction.go +++ b/internal/game/sys_construction.go diff --git a/internal/game/science.go b/internal/game/sys_science.go index 8c7f0af..23aeef4 100644 --- a/internal/game/science.go +++ b/internal/game/sys_science.go @@ -2,13 +2,13 @@ package game import ( "fmt" - "os" "path/filepath" "sort" "strings" "gopkg.in/yaml.v3" + "thehouseoficarus/internal/action" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -41,27 +41,16 @@ var modByID map[string]*ModDef func (g *Game) LoadMods() error { dir := filepath.Join(g.DataDir, "modules") - entries, err := os.ReadDir(dir) - if err != nil { - return err - } AllMods = nil modByID = make(map[string]*ModDef) - for _, e := range entries { - if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { - continue - } - data, err := os.ReadFile(filepath.Join(dir, e.Name())) - if err != nil { - continue - } + action.WalkYAMLDir(dir, func(path, id string, data []byte) error { var m ModDef if err := yaml.Unmarshal(data, &m); err != nil { - continue + return nil } AllMods = append(AllMods, &m) - modByID[m.ID] = AllMods[len(AllMods)-1] - } + return nil + }) modByID = make(map[string]*ModDef, len(AllMods)) for _, m := range AllMods { modByID[m.ID] = m diff --git a/internal/game/tech.go b/internal/game/sys_technology.go index a24c66d..a24c66d 100644 --- a/internal/game/tech.go +++ b/internal/game/sys_technology.go diff --git a/internal/game/color.go b/internal/game/ui_color.go index 962728a..962728a 100644 --- a/internal/game/color.go +++ b/internal/game/ui_color.go diff --git a/internal/game/help.go b/internal/game/ui_help.go index 03e2b8c..151d1ba 100644 --- a/internal/game/help.go +++ b/internal/game/ui_help.go @@ -2,10 +2,10 @@ package game import ( "fmt" - "os" "path/filepath" "strings" + "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "gopkg.in/yaml.v3" ) @@ -81,26 +81,15 @@ var commandList = []cmdEntry{ func LoadHelp(dataDir string) ([]HelpDef, error) { dir := filepath.Join(dataDir, "help") - entries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - var helps []HelpDef - for _, entry := range entries { - if entry.IsDir() || filepath.Ext(entry.Name()) != ".yaml" { - continue - } - data, err := os.ReadFile(filepath.Join(dir, entry.Name())) - if err != nil { - continue - } + action.WalkYAMLDir(dir, func(path, id string, data []byte) error { var h HelpDef if err := yaml.Unmarshal(data, &h); err != nil { - continue + return nil } helps = append(helps, h) - } + return nil + }) return helps, nil } @@ -153,3 +142,11 @@ func firstLine(s string) string { } return s } + +func (g *Game) executeHelp(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + g.doHelp(sess, "") + } else { + g.doHelp(sess, strings.Join(args, " ")) + } +} diff --git a/internal/game/map.go b/internal/game/ui_map.go index 32d3d7c..32d3d7c 100644 --- a/internal/game/map.go +++ b/internal/game/ui_map.go diff --git a/internal/game/prompt.go b/internal/game/ui_prompt.go index 2ecf367..2ecf367 100644 --- a/internal/game/prompt.go +++ b/internal/game/ui_prompt.go diff --git a/internal/game/table.go b/internal/game/ui_table.go index b96dca1..b96dca1 100644 --- a/internal/game/table.go +++ b/internal/game/ui_table.go |
