diff options
Diffstat (limited to 'internal')
89 files changed, 2465 insertions, 1554 deletions
diff --git a/internal/action/recipe.go b/internal/action/recipe.go index c1f30fb..5a69195 100644 --- a/internal/action/recipe.go +++ b/internal/action/recipe.go @@ -1,7 +1,6 @@ package action import ( - "os" "path/filepath" "gopkg.in/yaml.v3" @@ -9,7 +8,7 @@ import ( type ConsumeEntry struct { Items []string `yaml:"items"` - Qty int `yaml:"qty"` + Quantity int `yaml:"quantity"` Byproducts []string `yaml:"byproducts"` } @@ -80,25 +79,15 @@ func (s *RecipeStore) LoadByType(recipeType string) ([]RecipeDef, error) { func (s *RecipeStore) loadAllRaw() ([]RecipeDef, error) { dir := filepath.Join(s.dataDir, "recipes") - entries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } var recipes []RecipeDef - 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 - } + WalkYAMLDir(dir, func(path, id string, data []byte) error { var r RecipeDef if err := yaml.Unmarshal(data, &r); err != nil { - continue + return nil } recipes = append(recipes, r) - } + return nil + }) return recipes, nil } @@ -190,23 +179,23 @@ func (r *RecipeDef) ConsumeAll(hasItem func(string) bool, removeItem func(string found := false for _, id := range e.Items { if hasItem(id) { - removeItem(id, e.Qty) - found = true - break - } - } - if !found { - return false + removeItem(id, e.Quantity) + found = true + break } } - return true + if !found { + return false + } +} +return true } func (r *RecipeDef) HasAllItemsQty(countItem func(string) int) bool { for _, e := range r.Consume { found := false for _, id := range e.Items { - qty := e.Qty + qty := e.Quantity if qty <= 0 { qty = 1 } diff --git a/internal/action/store.go b/internal/action/store.go index a8db9df..6cb2e39 100644 --- a/internal/action/store.go +++ b/internal/action/store.go @@ -5,10 +5,79 @@ import ( "math/rand" "os" "path/filepath" + "strings" + "sync" "gopkg.in/yaml.v3" ) +func BuildPathIndex(dir string) map[string]string { + index := make(map[string]string) + filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + index[id] = path + return nil + }) + return index +} + +func BuildRoomIndex(dir string) map[int]string { + index := make(map[int]string) + filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + var n int + if _, scanErr := fmt.Sscanf(id, "%d", &n); scanErr == nil { + index[n] = path + } + return nil + }) + return index +} + +func WalkYAMLDir(dir string, fn func(path, id string, data []byte) error) error { + return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + data, readErr := os.ReadFile(path) + if readErr != nil { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + return fn(path, id, data) + }) +} + +type Duplicate struct { + ID string + Paths []string +} + +func CheckDuplicateIDs(dir string) []Duplicate { + seen := make(map[string][]string) + filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + seen[id] = append(seen[id], path) + return nil + }) + var dups []Duplicate + for id, paths := range seen { + if len(paths) > 1 { + dups = append(dups, Duplicate{ID: id, Paths: paths}) + } + } + return dups +} + func SuccessChance(cfg SuccessFormula, level int, requiredLevel int) float64 { chance := cfg.Base + float64(level-requiredLevel)*cfg.PerLevel if chance > cfg.Cap { @@ -20,8 +89,26 @@ func SuccessChance(cfg SuccessFormula, level int, requiredLevel int) float64 { return chance } +var ( + dropIndex map[string]string + dropIndexMu sync.Mutex +) + +func loadDropIndex(dataDir string) map[string]string { + dropIndexMu.Lock() + defer dropIndexMu.Unlock() + if dropIndex == nil { + dropIndex = BuildPathIndex(filepath.Join(dataDir, "drops")) + } + return dropIndex +} + func LoadDropTable(dataDir, id string) (*DropTableDef, error) { - path := filepath.Join(dataDir, "drops", id+".yaml") + index := loadDropIndex(dataDir) + path, ok := index[id] + if !ok { + path = filepath.Join(dataDir, "drops", id+".yaml") + } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read drop table %s: %w", id, err) diff --git a/internal/config/config.go b/internal/config/config.go index 122c311..124d45d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,7 +60,13 @@ func DefaultColors() ColorsConfig { } type GameConfig struct { - TickLength int `yaml:"tick_length"` + TickLength int `yaml:"tick_length"` + StartupValidation StartupValidationConfig `yaml:"startup_validation"` +} + +type StartupValidationConfig struct { + CheckSources []int `yaml:"check_sources"` + IgnoreUnreachable []int `yaml:"ignore_unreachable"` } type TelnetConfig struct { @@ -91,6 +97,10 @@ func Default() *Config { return &Config{ Game: GameConfig{ TickLength: 600, + StartupValidation: StartupValidationConfig{ + CheckSources: []int{1}, + IgnoreUnreachable: []int{}, + }, }, Colors: DefaultColors(), Telnet: TelnetConfig{ 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 diff --git a/internal/object/item.go b/internal/object/item.go index 7c682a4..8ec1565 100644 --- a/internal/object/item.go +++ b/internal/object/item.go @@ -74,7 +74,7 @@ type ItemDef struct { type MadeFromEntry struct { Items []string `yaml:"items"` - Qty int `yaml:"qty"` + Quantity int `yaml:"quantity"` Byproducts []string `yaml:"byproducts"` } diff --git a/internal/object/item_store.go b/internal/object/item_store.go index 9e932ba..d75179f 100644 --- a/internal/object/item_store.go +++ b/internal/object/item_store.go @@ -4,28 +4,34 @@ import ( "fmt" "os" "path/filepath" - "strings" + "thehouseoficarus/internal/action" "gopkg.in/yaml.v3" ) type ItemStore struct { - dataDir string - cache map[string]*ItemDef + dataDir string + pathIndex map[string]string + cache map[string]*ItemDef } func NewItemStore(dataDir string) *ItemStore { - return &ItemStore{ - dataDir: dataDir, - cache: make(map[string]*ItemDef), + s := &ItemStore{ + dataDir: dataDir, + pathIndex: action.BuildPathIndex(filepath.Join(dataDir, "items")), + cache: make(map[string]*ItemDef), } + return s } func (s *ItemStore) Load(id string) (*ItemDef, error) { if def, ok := s.cache[id]; ok { return def, nil } - path := filepath.Join(s.dataDir, "items", id+".yaml") + path, ok := s.pathIndex[id] + if !ok { + return nil, fmt.Errorf("read item %s: no such item", id) + } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read item %s: %w", id, err) @@ -40,21 +46,31 @@ func (s *ItemStore) Load(id string) (*ItemDef, error) { func (s *ItemStore) LoadAll() ([]*ItemDef, error) { dir := filepath.Join(s.dataDir, "items") - entries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } var defs []*ItemDef - for _, e := range entries { - if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { - continue + err := action.WalkYAMLDir(dir, func(path, id string, data []byte) error { + if def, ok := s.cache[id]; ok { + defs = append(defs, def) + return nil } - id := strings.TrimSuffix(e.Name(), ".yaml") - def, err := s.Load(id) - if err != nil { - continue + var def ItemDef + if err := yaml.Unmarshal(data, &def); err != nil { + return nil } - defs = append(defs, def) + s.cache[id] = &def + defs = append(defs, &def) + return nil + }) + return defs, err +} + +func (s *ItemStore) PathIndex() map[string]string { + return s.pathIndex +} + +func (s *ItemStore) IDSet() map[string]bool { + ids := make(map[string]bool) + for id := range s.pathIndex { + ids[id] = true } - return defs, nil + return ids } diff --git a/internal/object/object.go b/internal/object/object.go index 4d8db69..826a315 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -3,7 +3,7 @@ package object import "thehouseoficarus/internal/action" type UseInteraction struct { - Item string `yaml:"item"` + Item string `yaml:"item_id"` Condition *action.Condition `yaml:"condition"` Message string `yaml:"message"` Action *action.NodeAction `yaml:"action"` diff --git a/internal/object/store.go b/internal/object/store.go index a884601..a11ff7a 100644 --- a/internal/object/store.go +++ b/internal/object/store.go @@ -5,18 +5,21 @@ import ( "os" "path/filepath" + "thehouseoficarus/internal/action" "gopkg.in/yaml.v3" ) type ObjectStore struct { - dataDir string - cache map[string]*ObjectDef + dataDir string + pathIndex map[string]string + cache map[string]*ObjectDef } func NewObjectStore(dataDir string) *ObjectStore { return &ObjectStore{ - dataDir: dataDir, - cache: make(map[string]*ObjectDef), + dataDir: dataDir, + pathIndex: action.BuildPathIndex(filepath.Join(dataDir, "objects")), + cache: make(map[string]*ObjectDef), } } @@ -24,7 +27,10 @@ func (s *ObjectStore) Load(id string) (*ObjectDef, error) { if def, ok := s.cache[id]; ok { return def, nil } - path := filepath.Join(s.dataDir, "objects", fmt.Sprintf("%s.yaml", id)) + path, ok := s.pathIndex[id] + if !ok { + return nil, fmt.Errorf("read object %s: no such object", id) + } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read object %s: %w", id, err) @@ -36,3 +42,15 @@ func (s *ObjectStore) Load(id string) (*ObjectDef, error) { s.cache[id] = &def return &def, nil } + +func (s *ObjectStore) PathIndex() map[string]string { + return s.pathIndex +} + +func (s *ObjectStore) IDSet() map[string]bool { + ids := make(map[string]bool) + for id := range s.pathIndex { + ids[id] = true + } + return ids +} diff --git a/internal/world/mob.go b/internal/world/mob.go index f7988ce..9d07532 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -144,6 +144,7 @@ func (m *MobInstance) StartRegen() { type MobStore struct { dataDir string mu sync.Mutex + pathIndex map[string]string defs map[string]*MobDef instances map[string]*MobInstance } @@ -151,6 +152,7 @@ type MobStore struct { func NewMobStore(dataDir string) *MobStore { return &MobStore{ dataDir: dataDir, + pathIndex: action.BuildPathIndex(filepath.Join(dataDir, "mobs")), defs: make(map[string]*MobDef), instances: make(map[string]*MobInstance), } @@ -164,7 +166,10 @@ func (s *MobStore) LoadDef(id string) (*MobDef, error) { } s.mu.Unlock() - path := filepath.Join(s.dataDir, "mobs", id+".yaml") + path, ok := s.pathIndex[id] + if !ok { + return nil, fmt.Errorf("read mob %s: no such mob", id) + } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read mob %s: %w", id, err) @@ -180,6 +185,14 @@ func (s *MobStore) LoadDef(id string) (*MobDef, error) { return &def, nil } +func (s *MobStore) AllDefIDs() map[string]bool { + ids := make(map[string]bool) + for id := range s.pathIndex { + ids[id] = true + } + return ids +} + func (s *MobStore) GetInstance(id string) *MobInstance { s.mu.Lock() defer s.mu.Unlock() diff --git a/internal/world/room.go b/internal/world/room.go index c0f531e..f006acc 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -41,8 +41,8 @@ var ExitOrder = []ExitDir{ } type SpawnDef struct { - ItemID string `yaml:"item_id"` - Quantity int `yaml:"quantity"` + ID string `yaml:"id"` + Quantity int `yaml:"quantity"` RespawnTicks float64 `yaml:"respawn_ticks"` } @@ -72,7 +72,7 @@ type Room struct { MapSymbol string `yaml:"map_symbol"` Exits map[ExitDir]ExitDef `yaml:"exits"` Objects []RoomObject `yaml:"objects"` - Spawns []SpawnDef `yaml:"spawns"` + ItemSpawns []SpawnDef `yaml:"item_spawns"` Mobs []RoomMob `yaml:"mobs"` OnEnter []EnterStep `yaml:"on_enter"` } diff --git a/internal/world/world.go b/internal/world/world.go index 3094c54..0b40918 100644 --- a/internal/world/world.go +++ b/internal/world/world.go @@ -7,36 +7,38 @@ import ( "strings" "sync" + "thehouseoficarus/internal/action" "gopkg.in/yaml.v3" ) type World struct { - dataDir string - mu sync.Mutex - groundItems map[int][]*groundEntry - seeded map[int]bool - objStates map[string]*ObjState - objMoves []ObjMove + dataDir string + mu sync.Mutex + roomPathIndex map[int]string + groundItems map[int][]*groundEntry + seeded map[int]bool + objStates map[string]*ObjState + objMoves []ObjMove } func New(dataDir string) *World { return &World{ - dataDir: dataDir, - groundItems: make(map[int][]*groundEntry), - seeded: make(map[int]bool), - objStates: make(map[string]*ObjState), + dataDir: dataDir, + roomPathIndex: action.BuildRoomIndex(filepath.Join(dataDir, "rooms")), + groundItems: make(map[int][]*groundEntry), + seeded: make(map[int]bool), + objStates: make(map[string]*ObjState), } } func (w *World) LoadRoom(id int) (*Room, error) { - path := filepath.Join(w.dataDir, "rooms", fmt.Sprintf("%d.yaml", id)) + path, ok := w.roomPathIndex[id] + if !ok { + return nil, fmt.Errorf("read room %d: no such room", id) + } data, err := os.ReadFile(path) if err != nil { - path = filepath.Join(w.dataDir, "rooms", "player_housing", fmt.Sprintf("%d.yaml", id)) - data, err = os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read room %d: %w", id, err) - } + return nil, fmt.Errorf("read room %d: %w", id, err) } var room Room if err := yaml.Unmarshal(data, &room); err != nil { @@ -46,8 +48,8 @@ func (w *World) LoadRoom(id int) (*Room, error) { if room.Exits == nil { room.Exits = make(map[ExitDir]ExitDef) } - if room.Spawns == nil { - room.Spawns = make([]SpawnDef, 0) + if room.ItemSpawns == nil { + room.ItemSpawns = make([]SpawnDef, 0) } if room.Mobs == nil { room.Mobs = make([]RoomMob, 0) @@ -84,10 +86,10 @@ func (w *World) SeedGroundItems(roomID int) { w.mu.Lock() defer w.mu.Unlock() - for _, s := range room.Spawns { + for _, s := range room.ItemSpawns { merged := false for _, e := range w.groundItems[roomID] { - if e.quantity > 0 && strings.EqualFold(e.itemID, s.ItemID) && + if e.quantity > 0 && strings.EqualFold(e.itemID, s.ID) && (e.reserveTimer <= 0 || e.reservedFor == "") && e.despawnTimer <= 0 { e.quantity += s.Quantity @@ -100,7 +102,7 @@ func (w *World) SeedGroundItems(roomID int) { } if !merged { e := &groundEntry{ - itemID: s.ItemID, + itemID: s.ID, quantity: s.Quantity, isSpawn: true, respawnDelay: int(s.RespawnTicks), @@ -111,6 +113,14 @@ func (w *World) SeedGroundItems(roomID int) { } } +func (w *World) RoomIndex() map[int]bool { + ids := make(map[int]bool) + for id := range w.roomPathIndex { + ids[id] = true + } + return ids +} + func (w *World) Tick() { w.mu.Lock() defer w.mu.Unlock() |
