aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/action/recipe.go39
-rw-r--r--internal/action/store.go89
-rw-r--r--internal/config/config.go12
-rw-r--r--internal/game/action_burn.go13
-rw-r--r--internal/game/action_steal.go10
-rw-r--r--internal/game/aggro.go53
-rw-r--r--internal/game/cmd_alias.go8
-rw-r--r--internal/game/cmd_attack.go14
-rw-r--r--internal/game/cmd_autotrigger.go4
-rw-r--r--internal/game/cmd_bank.go4
-rw-r--r--internal/game/cmd_clean.go4
-rw-r--r--internal/game/cmd_color.go4
-rw-r--r--internal/game/cmd_colortable.go4
-rw-r--r--internal/game/cmd_consume.go (renamed from internal/game/cmd_drink.go)143
-rw-r--r--internal/game/cmd_cook.go5
-rw-r--r--internal/game/cmd_craft.go8
-rw-r--r--internal/game/cmd_description.go4
-rw-r--r--internal/game/cmd_drop.go14
-rw-r--r--internal/game/cmd_eat.go145
-rw-r--r--internal/game/cmd_estate_directory.go2
-rw-r--r--internal/game/cmd_farm.go58
-rw-r--r--internal/game/cmd_fletch.go4
-rw-r--r--internal/game/cmd_get.go15
-rw-r--r--internal/game/cmd_handlers.go370
-rw-r--r--internal/game/cmd_id.go5
-rw-r--r--internal/game/cmd_inventory.go4
-rw-r--r--internal/game/cmd_jack.go6
-rw-r--r--internal/game/cmd_look.go12
-rw-r--r--internal/game/cmd_map.go4
-rw-r--r--internal/game/cmd_mix.go6
-rw-r--r--internal/game/cmd_mods.go5
-rw-r--r--internal/game/cmd_move.go5
-rw-r--r--internal/game/cmd_option.go4
-rw-r--r--internal/game/cmd_prompt.go14
-rw-r--r--internal/game/cmd_queued.go4
-rw-r--r--internal/game/cmd_quit.go4
-rw-r--r--internal/game/cmd_registry.go26
-rw-r--r--internal/game/cmd_remove.go4
-rw-r--r--internal/game/cmd_say.go14
-rw-r--r--internal/game/cmd_score.go4
-rw-r--r--internal/game/cmd_search.go10
-rw-r--r--internal/game/cmd_smelt.go5
-rw-r--r--internal/game/cmd_smith.go4
-rw-r--r--internal/game/cmd_sneak.go4
-rw-r--r--internal/game/cmd_stats.go4
-rw-r--r--internal/game/cmd_style.go8
-rw-r--r--internal/game/cmd_task.go4
-rw-r--r--internal/game/cmd_tech.go4
-rw-r--r--internal/game/cmd_trigger.go637
-rw-r--r--internal/game/cmd_trigger_combat.go104
-rw-r--r--internal/game/cmd_trigger_enchant.go116
-rw-r--r--internal/game/cmd_trigger_transport.go54
-rw-r--r--internal/game/cmd_trigger_utility.go397
-rw-r--r--internal/game/cmd_use.go7
-rw-r--r--internal/game/cmd_walk.go4
-rw-r--r--internal/game/cmd_wear.go4
-rw-r--r--internal/game/core_course.go (renamed from internal/game/course.go)16
-rw-r--r--internal/game/core_equip.go (renamed from internal/game/equip_stats.go)0
-rw-r--r--internal/game/core_flags.go (renamed from internal/game/player_flags.go)0
-rw-r--r--internal/game/core_hacking.go (renamed from internal/game/hacking.go)0
-rw-r--r--internal/game/core_login_account.go (renamed from internal/game/login_account.go)0
-rw-r--r--internal/game/core_login_char.go (renamed from internal/game/login_char.go)0
-rw-r--r--internal/game/core_messages.go (renamed from internal/game/messages.go)0
-rw-r--r--internal/game/core_production.go (renamed from internal/game/production_core.go)201
-rw-r--r--internal/game/core_skill.go15
-rw-r--r--internal/game/core_startup.go770
-rw-r--r--internal/game/core_stations.go (renamed from internal/game/stations.go)0
-rw-r--r--internal/game/core_types.go (renamed from internal/game/types.go)0
-rw-r--r--internal/game/core_utils.go (renamed from internal/game/utils.go)7
-rw-r--r--internal/game/game.go29
-rw-r--r--internal/game/production_menu.go209
-rw-r--r--internal/game/sys_assassin.go (renamed from internal/game/assassin.go)0
-rw-r--r--internal/game/sys_buff.go (renamed from internal/game/buff.go)0
-rw-r--r--internal/game/sys_combat.go (renamed from internal/game/death.go)48
-rw-r--r--internal/game/sys_construction.go (renamed from internal/game/construction.go)0
-rw-r--r--internal/game/sys_science.go (renamed from internal/game/science.go)21
-rw-r--r--internal/game/sys_technology.go (renamed from internal/game/tech.go)0
-rw-r--r--internal/game/ui_color.go (renamed from internal/game/color.go)0
-rw-r--r--internal/game/ui_help.go (renamed from internal/game/help.go)29
-rw-r--r--internal/game/ui_map.go (renamed from internal/game/map.go)0
-rw-r--r--internal/game/ui_prompt.go (renamed from internal/game/prompt.go)0
-rw-r--r--internal/game/ui_table.go (renamed from internal/game/table.go)0
-rw-r--r--internal/object/item.go2
-rw-r--r--internal/object/item_store.go56
-rw-r--r--internal/object/object.go2
-rw-r--r--internal/object/store.go28
-rw-r--r--internal/world/mob.go15
-rw-r--r--internal/world/room.go6
-rw-r--r--internal/world/world.go52
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()