diff options
Diffstat (limited to 'internal/game')
85 files changed, 3113 insertions, 4114 deletions
diff --git a/internal/game/action.go b/internal/game/act.go index 19e79cf..8920d0c 100644 --- a/internal/game/action.go +++ b/internal/game/act.go @@ -6,8 +6,7 @@ import ( "strconv" "strings" - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" @@ -15,25 +14,25 @@ import ( ) var verbAliases = map[string]string{ - "mine": "gather", - "chop": "gather", - "fish": "gather", - "cut": "gather", - "shear": "gather", - "talk": "talk", - "speak": "talk", - "ask": "talk", + "mine": "gather", + "chop": "gather", + "fish": "gather", + "cut": "gather", + "shear": "gather", + "talk": "talk", + "speak": "talk", + "ask": "talk", "steal": "steal", "thieve": "steal", "hide": "safespot", } var verbSkill = map[string]string{ - "mine": "mining", - "chop": "woodcutting", - "cut": "woodcutting", - "fish": "fishing", - "shear": "crafting", + "mine": "mining", + "chop": "woodcutting", + "cut": "woodcutting", + "fish": "fishing", + "shear": "crafting", "steal": "thieving", "thieve": "thieving", } @@ -50,7 +49,7 @@ func (g *Game) startAction(sess *net.Session, verb, target string) { verb = normalizeVerb(verb) - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } @@ -261,7 +260,7 @@ func (g *Game) AdvanceActions() { } } -func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { +func (g *Game) checkCondition(sess *net.Session, c *behavior.Condition) bool { p := sess.Player if c.AllOf != nil { diff --git a/internal/game/action_agility.go b/internal/game/act_agility.go index 116e888..2136699 100644 --- a/internal/game/action_agility.go +++ b/internal/game/act_agility.go @@ -4,7 +4,7 @@ import ( "fmt" "math/rand" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" @@ -23,12 +23,12 @@ func (g *Game) startObstacle(sess *net.Session, p *player.Player, info *Obstacle gerund = g } - p.Action = &action.Action{ - Type: action.TypeObstacle, + p.Action = &behavior.Action{ + Type: behavior.TypeObstacle, TargetID: info.CourseID, TargetName: info.CourseName, WaitLeft: 0, - Data: &action.ObstacleData{ + Data: &behavior.ObstacleData{ CourseID: info.CourseID, Phase: 0, ObstacleIndex: info.ObstacleIndex, @@ -50,7 +50,7 @@ func (g *Game) startObstacle(sess *net.Session, p *player.Player, info *Obstacle } func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) { - d := p.Action.Data.(*action.ObstacleData) + d := p.Action.Data.(*behavior.ObstacleData) phase := d.Phase messages := d.Messages ticksPerPhase := d.TicksPerPhase @@ -124,7 +124,7 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) { p.Action.WaitLeft = engine.ToTicks(ticksPerPhase) } -func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *action.ObstacleData) { +func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.ObstacleData) { startRoom := d.StartRoom failMin := d.FailDamageMin failMax := d.FailDamageMax diff --git a/internal/game/action_burn.go b/internal/game/act_burn.go index e1872ed..861ff93 100644 --- a/internal/game/action_burn.go +++ b/internal/game/act_burn.go @@ -5,10 +5,10 @@ import ( "math/rand" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -120,12 +120,12 @@ func (g *Game) startBurn(sess *net.Session, p *player.Player, itemID string, fro g.broadcastAction(sess, "\n%s starts building a fire.", p.Name) - p.Action = &action.Action{ - Type: action.TypeBurn, + p.Action = &behavior.Action{ + Type: behavior.TypeBurn, TargetID: itemID, TargetName: logDef.Name, WaitLeft: 0, - Data: &action.BurnData{ + Data: &behavior.BurnData{ ItemID: itemID, ToolSpeed: toolSpeed, Level: logDef.FireLevel, @@ -158,12 +158,12 @@ func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) { g.broadcastAction(sess, "\n%s tends to the fire.", p.Name) - p.Action = &action.Action{ - Type: action.TypeStoke, + p.Action = &behavior.Action{ + Type: behavior.TypeStoke, TargetID: itemID, TargetName: logDef.Name, WaitLeft: engine.ToTicks(10), - Data: &action.BurnData{ + Data: &behavior.BurnData{ ItemID: itemID, BurnTicks: logDef.BurnTicks, XP: logDef.FireXP, @@ -172,7 +172,7 @@ func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) { } func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { - d := p.Action.Data.(*action.BurnData) + d := p.Action.Data.(*behavior.BurnData) itemID := d.ItemID toolSpeed := d.ToolSpeed phase := d.Phase @@ -230,20 +230,20 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { g.World.AddObjInstance(p.RoomID, "fire", logDef.BurnTicks) xp := logDef.FireXP - if xp > 0 { - g.awardSkillXP(sess, p, player.Firemaking, xp) - } + if xp > 0 { + g.awardSkillXP(sess, p, player.Firemaking, xp) + } - g.AccountStore.SaveCharacter(p) - g.cancelAction(p) + g.AccountStore.SaveCharacter(p) + g.cancelAction(p) - msg := g.colorize(sess, "fire", "You manage to get a fire going!") - if xp > 0 && p.OptionBool("xp_drops") { - msg += g.formatXpDropSingle(sess, p, player.Firemaking, xp) - } - sess.WriteLine(msg) + msg := g.colorize(sess, "fire", "You manage to get a fire going!") + if xp > 0 && p.OptionBool("xp_drops") { + msg += g.formatXpDropSingle(sess, p, player.Firemaking, xp) + } + sess.WriteLine(msg) - g.broadcastAction(sess, "\n%s started a fire!", p.Name) + g.broadcastAction(sess, "\n%s started a fire!", p.Name) return } @@ -254,7 +254,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { } func (g *Game) advanceStoke(sess *net.Session, p *player.Player) { - d := p.Action.Data.(*action.BurnData) + d := p.Action.Data.(*behavior.BurnData) itemID := d.ItemID fireKey := g.findFireObjectKey(p.RoomID) burnTicks := d.BurnTicks @@ -307,7 +307,7 @@ func (g *Game) findFireTool(sess *net.Session, p *player.Player) (toolSpeed floa toolSpeed = -1 var toolItemID string - if itemID, equipped := p.Equipment[object.SlotMainHand]; equipped { + if itemID, equipped := p.Equipment[item.SlotMainHand]; equipped { def, err := g.ItemStore.Load(itemID) if err == nil && def.ToolType == "fire" { toolSpeed = def.ToolSpeed @@ -432,7 +432,7 @@ func (g *Game) FireTick() { func (g *Game) cancelStokers(roomID int) { for _, other := range g.Hub.PlayersInRoom(roomID) { op := other.Player - if op != nil && op.Action != nil && op.Action.Type == action.TypeStoke { + if op != nil && op.Action != nil && op.Action.Type == behavior.TypeStoke { other.WriteLine("The fire went out!") g.cancelAction(op) g.writePrompt(other) diff --git a/internal/game/action_clean.go b/internal/game/act_clean.go index d41746e..0c128a1 100644 --- a/internal/game/action_clean.go +++ b/internal/game/act_clean.go @@ -3,15 +3,15 @@ package game import ( "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) func (g *Game) advanceClean(sess *net.Session, p *player.Player) { - data, ok := p.BackgroundAction.Data.(*action.CleanData) + data, ok := p.BackgroundAction.Data.(*behavior.CleanData) if !ok { g.cancelBgAction(p) return @@ -27,7 +27,7 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) { items := g.CraftIndex.ByType("clean") - var matchItem *object.ItemDef + var matchItem *item.ItemDef for _, item := range items { c := item.FirstCraft() if filter != "" { diff --git a/internal/game/action_default_target.go b/internal/game/act_default_target.go index a184f32..a184f32 100644 --- a/internal/game/action_default_target.go +++ b/internal/game/act_default_target.go diff --git a/internal/game/action_farm.go b/internal/game/act_farm.go index 61a66cc..62e08fd 100644 --- a/internal/game/action_farm.go +++ b/internal/game/act_farm.go @@ -6,9 +6,8 @@ import ( "sort" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) @@ -173,7 +172,7 @@ func (g *Game) advancePlant(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } - data, ok := p.Action.Data.(*action.FarmData) + data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } @@ -215,7 +214,7 @@ func (g *Game) advanceHarvest(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } - data, ok := p.Action.Data.(*action.FarmData) + data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } @@ -309,7 +308,7 @@ func (g *Game) advanceRake(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } - data, ok := p.Action.Data.(*action.FarmData) + data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } @@ -335,7 +334,7 @@ func (g *Game) advanceWater(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } - data, ok := p.Action.Data.(*action.FarmData) + data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } @@ -355,7 +354,7 @@ func (g *Game) advanceCure(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } - data, ok := p.Action.Data.(*action.FarmData) + data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } @@ -498,7 +497,7 @@ func (g *Game) farmMatchPrefix(p *player.Player, input string) (prefix string, d lower := strings.ToLower(input) for _, f := range farms { - if strings.Contains(lower, strings.ToLower(f.name)) || object.WordPrefixMatch(lower, f.name) { + if strings.Contains(lower, strings.ToLower(f.name)) || behavior.WordPrefixMatch(lower, f.name) { return f.prefix, f.defID, f.index } } diff --git a/internal/game/action_finishing_blow.go b/internal/game/act_finishing_blow.go index d404fb3..fbd9781 100644 --- a/internal/game/action_finishing_blow.go +++ b/internal/game/act_finishing_blow.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" @@ -12,7 +11,7 @@ import ( func (g *Game) tryFinishingBlow(sess *net.Session, input string) bool { p := sess.Player - cs := combat.GetCombat(p.Name) + cs := g.Combat.Get(p.Name) if cs == nil { return false } diff --git a/internal/game/action_fletch.go b/internal/game/act_fletch.go index 53346a2..78a19ad 100644 --- a/internal/game/action_fletch.go +++ b/internal/game/act_fletch.go @@ -3,28 +3,28 @@ package game import ( "fmt" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) -func (g *Game) startFletchAction(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { +func (g *Game) startFletchAction(sess *net.Session, p *player.Player, def *item.ItemDef, count int) { p.EnsureFlags() - p.Flags["last_fletch"] = item.ID + p.Flags["last_fletch"] = def.ID g.AccountStore.SaveCharacter(p) - if item.FirstCraft().Wait <= 0 { - g.doInstantFletch(sess, p, item, count) + if def.FirstCraft().Wait <= 0 { + g.doInstantFletch(sess, p, def, count) return } - g.startTimedFletch(sess, p, item, count) + g.startTimedFletch(sess, p, def, count) } -func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { - c := item.FirstCraft() +func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.ItemDef, count int) { + c := def.FirstCraft() outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 @@ -33,17 +33,17 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, item *object batches := 0 totalOutput := 0 skill := player.SkillName(c.EffectiveSkill()) - tmpItem := &object.ItemDef{Craft: object.CraftList{*c}} + tmpItem := &item.ItemDef{Craft: item.CraftList{*c}} for { if !g.canDoFletchItem(p, tmpItem) { break } placed := false - if item.Stackable { + if def.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) - if slot != nil && slot.ItemID == item.ID { + if slot != nil && slot.ItemID == def.ID { craftConsumeAll(c, p.HasItem, p.RemoveItem) slot.Quantity += outputQty placed = true @@ -57,7 +57,7 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, item *object if freeSlot == -1 { break } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: def.ID, Quantity: outputQty}) } if c.XP > 0 { @@ -82,8 +82,8 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, item *object g.AccountStore.SaveCharacter(p) - outDef, _ := g.ItemStore.Load(item.ID) - outputName := item.Name + outDef, _ := g.ItemStore.Load(def.ID) + outputName := def.Name if outDef != nil { outputName = g.itemColorize(sess, outDef, outDef.Name) } @@ -94,26 +94,26 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, item *object sess.WriteLine(msg) } -func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { - craft := item.FirstCraft() +func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.ItemDef, count int) { + craft := def.FirstCraft() - startMsg := g.resolveCraftMessage(sess, craft.StartMessage, "You begin fletching %i1.", craft, item.ID, nil, p.HasItem) + startMsg := g.resolveCraftMessage(sess, craft.StartMessage, "You begin fletching %i1.", craft, def.ID, nil, p.HasItem) sess.WriteLine(startMsg) - broadcastName := item.Name - if def, _ := g.ItemStore.Load(item.ID); def != nil { - broadcastName = def.Name + broadcastName := def.Name + if dd, _ := g.ItemStore.Load(def.ID); dd != nil { + broadcastName = dd.Name } g.broadcastAction(sess, "%s starts fletching.", p.Name) _ = broadcastName - p.BackgroundAction = &action.Action{ + p.BackgroundAction = &behavior.Action{ Type: "fletch", - TargetID: item.ID, - TargetName: item.Name, - Data: &action.FletchData{ - ItemID: item.ID, + TargetID: def.ID, + TargetName: def.Name, + Data: &behavior.FletchData{ + ItemID: def.ID, Phase: 0, Wait: craft.Wait, Remaining: count, @@ -123,7 +123,7 @@ func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, item *objec } func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { - data, ok := p.BackgroundAction.Data.(*action.FletchData) + data, ok := p.BackgroundAction.Data.(*behavior.FletchData) if !ok { g.cancelBgAction(p) return @@ -133,12 +133,12 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { wait := data.Wait remaining := data.Remaining - item := g.loadCraftItem(itemID) - if item == nil { + def := g.loadCraftItem(itemID) + if def == nil { g.cancelBgAction(p) return } - c := item.FirstCraft() + c := def.FirstCraft() if phase == 0 { data.Phase = 1 @@ -146,7 +146,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { return } - tmpItem := &object.ItemDef{Craft: object.CraftList{*c}} + tmpItem := &item.ItemDef{Craft: item.CraftList{*c}} if !g.canDoFletchItem(p, tmpItem) { sess.WriteLine("You've run out of fletching materials.") g.cancelBgAction(p) @@ -161,10 +161,10 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { skill := player.SkillName(c.EffectiveSkill()) placed := false - if item.Stackable { + if def.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) - if slot != nil && slot.ItemID == item.ID { + if slot != nil && slot.ItemID == def.ID { craftConsumeAll(c, p.HasItem, p.RemoveItem) slot.Quantity += outputQty placed = true @@ -180,7 +180,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { g.cancelBgAction(p) return } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: def.ID, Quantity: outputQty}) } if c.XP > 0 { @@ -188,7 +188,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { } g.AccountStore.SaveCharacter(p) - msg := g.resolveCraftMessage(sess, c.Message, "You fletch a %n.", c, item.ID, nil, p.HasItem) + msg := g.resolveCraftMessage(sess, c.Message, "You fletch a %n.", c, def.ID, nil, p.HasItem) if p.OptionBool("xp_drops") && c.XP > 0 { msg += g.formatXpDropSingle(sess, p, skill, c.XP) } @@ -204,7 +204,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { } } - if !g.canContinueProduction(p, item) { + if !g.canContinueProduction(p, def) { sess.WriteLine("You've run out of fletching materials.") g.cancelBgAction(p) return diff --git a/internal/game/action_gather.go b/internal/game/act_gather.go index 1d685be..0c743e7 100644 --- a/internal/game/action_gather.go +++ b/internal/game/act_gather.go @@ -5,7 +5,7 @@ import ( "math/rand" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/color" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" @@ -16,7 +16,7 @@ import ( const birdsNestItemID = "birds_nest" -func loadGatherConfig(g *Game, objDefID string) *action.GatherConfig { +func loadGatherConfig(g *Game, objDefID string) *behavior.GatherConfig { def, err := g.ObjectStore.Load(objDefID) if err != nil { return nil @@ -96,7 +96,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje g.broadcastAction(sess, "\n%s starts %s the %s.", p.Name, verb, obj.Name) - d := &action.GatherData{ + d := &behavior.GatherData{ ObjDefID: obj.ID, InstanceKey: g.World.ObjStateKey(st.RoomID, st.DefID, st.Index), InstanceIdx: st.Index + 1, @@ -107,8 +107,8 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje if cfg.DepleteTimer > 0 { d.DepleteTimer = true } - p.Action = &action.Action{ - Type: action.TypeGather, + p.Action = &behavior.Action{ + Type: behavior.TypeGather, TargetID: st.DefID, TargetName: obj.Name, WaitLeft: 0, @@ -117,7 +117,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje } func (g *Game) advanceGather(sess *net.Session, p *player.Player) { - d := p.Action.Data.(*action.GatherData) + d := p.Action.Data.(*behavior.GatherData) cfg := loadGatherConfig(g, d.ObjDefID) if cfg == nil { g.cancelAction(p) @@ -177,11 +177,11 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { } skillLevel := p.Level(player.SkillName(cfg.Skill)) - chance := action.SuccessChance(cfg.Success, skillLevel, cfg.Level) + chance := behavior.SuccessChance(cfg.Success, skillLevel, cfg.Level) if rand.Float64() < chance { eligible := filterDropsByLevel(cfg.Drops, skillLevel) - drop := action.ResolveDrop(g.DataDir, eligible) + drop := behavior.ResolveDrop(g.DataDir, eligible) if drop != nil { freeSlot := p.FirstFreeSlot() if freeSlot == -1 { @@ -255,10 +255,10 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { continue } op := other.Player - if op == nil || op.Action == nil || op.Action.Type != action.TypeGather { + if op == nil || op.Action == nil || op.Action.Type != behavior.TypeGather { continue } - if gd, ok := op.Action.Data.(*action.GatherData); ok && gd.InstanceKey == instanceKey { + if gd, ok := op.Action.Data.(*behavior.GatherData); ok && gd.InstanceKey == instanceKey { other.WriteLine(fmt.Sprintf("\nThe %s was depleted by %s!", g.colorize(sess, "item", p.Action.TargetName), g.colorize(sess, "player_name", p.Name))) g.cancelAction(op) g.writePrompt(other) @@ -291,8 +291,8 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { p.Action.WaitLeft = engine.ToTicks(wait) } -func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry { - var eligible []action.DropEntry +func filterDropsByLevel(drops []behavior.DropEntry, level int) []behavior.DropEntry { + var eligible []behavior.DropEntry for _, d := range drops { if level >= d.Level { eligible = append(eligible, d) @@ -304,7 +304,7 @@ func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry return eligible } -func (g *Game) depleteSharedTree(st *world.ObjState, cfg *action.GatherConfig, targetName string, playerNames []string) { +func (g *Game) depleteSharedTree(st *world.ObjState, cfg *behavior.GatherConfig, targetName string, playerNames []string) { st.Depleted = true st.DepleteTimer = float64(engine.ToTicks(cfg.RespawnTimer)) st.SharedTimer = 0 @@ -316,10 +316,10 @@ func (g *Game) depleteSharedTree(st *world.ObjState, cfg *action.GatherConfig, t for _, sess := range g.Hub.PlayersInRoom(st.RoomID) { op := sess.Player - if op == nil || op.Action == nil || op.Action.Type != action.TypeGather { + if op == nil || op.Action == nil || op.Action.Type != behavior.TypeGather { continue } - if gd, ok := op.Action.Data.(*action.GatherData); !ok || gd.InstanceKey != g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) { + if gd, ok := op.Action.Data.(*behavior.GatherData); !ok || gd.InstanceKey != g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) { continue } isDepleter := false @@ -369,7 +369,7 @@ func (g *Game) flushPendingDepletions() { g.pendingDepletions = nil } -func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *action.GatherConfig, currentDropSlot int) { +func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *behavior.GatherConfig, currentDropSlot int) { if cfg.NestChance <= 0 { return } diff --git a/internal/game/action_identify.go b/internal/game/act_identify.go index 401ab94..b905494 100644 --- a/internal/game/action_identify.go +++ b/internal/game/act_identify.go @@ -3,13 +3,13 @@ package game import ( "fmt" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) { - data, ok := p.Action.Data.(*action.IdentifyData) + data, ok := p.Action.Data.(*behavior.IdentifyData) if !ok { g.cancelAction(p) return @@ -44,7 +44,7 @@ func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) { for i := 0; i < 28; i++ { slot := p.InvSlot(i) - if slot != nil && slot.ItemID == "scrap" { + if slot != nil && slot.ItemID == "scrap" { p.SetInvSlot(i, nil) } } diff --git a/internal/game/action_room.go b/internal/game/act_room.go index 86c1737..86c1737 100644 --- a/internal/game/action_room.go +++ b/internal/game/act_room.go diff --git a/internal/game/action_search.go b/internal/game/act_search.go index 0f755a5..6af15ca 100644 --- a/internal/game/action_search.go +++ b/internal/game/act_search.go @@ -3,7 +3,7 @@ package game import ( "fmt" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" @@ -21,12 +21,12 @@ func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, s ticks = 3 } - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "search", TargetID: itemID, TargetName: def.Name, WaitLeft: engine.ToTicks(ticks), - Data: &action.SearchData{ + Data: &behavior.SearchData{ ItemID: itemID, SlotIdx: slotIdx, Started: false, @@ -37,7 +37,7 @@ func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, s } func (g *Game) advanceSearch(sess *net.Session, p *player.Player) { - d, ok := p.Action.Data.(*action.SearchData) + d, ok := p.Action.Data.(*behavior.SearchData) if !ok || d == nil { sess.WriteLine("Something went wrong.") g.cancelAction(p) @@ -74,18 +74,18 @@ func (g *Game) advanceSearch(sess *net.Session, p *player.Player) { p.SetInvSlot(d.SlotIdx, nil) } - dt, err := action.LoadDropTable(g.DataDir, def.SearchTable) + dt, err := behavior.LoadDropTable(g.DataDir, def.SearchTable) if err == nil && len(dt.Drops) > 0 { - drop := action.ResolveDrop(g.DataDir, dt.Drops) + drop := behavior.ResolveDrop(g.DataDir, dt.Drops) if drop != nil && drop.ItemID != "" { g.giveSearchLoot(sess, p, drop) } } if def.SearchMiscTable != "" { - dt2, err := action.LoadDropTable(g.DataDir, def.SearchMiscTable) + dt2, err := behavior.LoadDropTable(g.DataDir, def.SearchMiscTable) if err == nil && len(dt2.Drops) > 0 { - drop := action.ResolveDrop(g.DataDir, dt2.Drops) + drop := behavior.ResolveDrop(g.DataDir, dt2.Drops) if drop != nil && drop.ItemID != "" { g.giveSearchLoot(sess, p, drop) } @@ -96,7 +96,7 @@ func (g *Game) advanceSearch(sess *net.Session, p *player.Player) { g.cancelAction(p) } -func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *action.DropEntry) { +func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *behavior.DropEntry) { qty := drop.Quantity if qty <= 0 { qty = 1 diff --git a/internal/game/action_state.go b/internal/game/act_state.go index fc4b461..a219875 100644 --- a/internal/game/action_state.go +++ b/internal/game/act_state.go @@ -1,13 +1,12 @@ package game import ( - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/player" ) func (g *Game) playerActionDisplay(p *player.Player) string { - if cs := combat.GetCombat(p.Name); cs != nil { + if cs := g.Combat.Get(p.Name); cs != nil { mob := g.MobStore.GetInstance(cs.MobID) if mob != nil { if mob.IsTask() { @@ -34,62 +33,62 @@ func (g *Game) playerActionDisplay(p *player.Player) string { if a := p.Action; a != nil { switch a.Type { - case action.TypeGather: - if d, ok := a.Data.(*action.GatherData); ok { + case behavior.TypeGather: + if d, ok := a.Data.(*behavior.GatherData); ok { desc := d.Verb + " a " + a.TargetName if d.ToolName != "" { desc += " with a " + d.ToolName } return desc } - case action.TypeSteal: + case behavior.TypeSteal: return "stealing from " + a.TargetName - case action.TypeTalk: + case behavior.TypeTalk: return "talking to " + a.TargetName - case action.TypeUse: + case behavior.TypeUse: return "using a " + a.TargetName - case action.TypeBurn: + case behavior.TypeBurn: return "trying to start a fire" - case action.TypeStoke: + case behavior.TypeStoke: return "tending to a fire" - case action.TypeSearch: + case behavior.TypeSearch: return "digging through a " + a.TargetName - case action.TypeIdentify: + case behavior.TypeIdentify: return "identifying scrap at " + a.TargetName - case action.TypePlant: + case behavior.TypePlant: return "planting " + a.TargetName - case action.TypeHarvest: + case behavior.TypeHarvest: return "harvesting " + a.TargetName - case action.TypeRake: + case behavior.TypeRake: return "raking a " + a.TargetName - case action.TypeWater: + case behavior.TypeWater: return "watering a " + a.TargetName - case action.TypeCure: + case behavior.TypeCure: return "curing a " + a.TargetName - case action.TypeObstacle: + case behavior.TypeObstacle: return "traversing across " + a.TargetName - case action.TypeCook: + case behavior.TypeCook: return "cooking some " + a.TargetName - case action.TypeSmelt: + case behavior.TypeSmelt: return "smelting some " + a.TargetName - case action.TypeSmith: + case behavior.TypeSmith: return "smithing some " + a.TargetName - case action.TypeCraft: + case behavior.TypeCraft: return "crafting some " + a.TargetName - case action.TypeCombine: + case behavior.TypeCombine: return "combining some " + a.TargetName - case action.TypeMix: + case behavior.TypeMix: return "mixing some " + a.TargetName - case action.TypeConstruct: + case behavior.TypeConstruct: return "constructing some " + a.TargetName } } if a := p.BackgroundAction; a != nil { switch a.Type { - case action.TypeFletch: + case behavior.TypeFletch: return "fletching " + a.TargetName - case action.TypeClean: + case behavior.TypeClean: return "cleaning herbs" } } diff --git a/internal/game/action_steal.go b/internal/game/act_steal.go index 65c684c..6f67944 100644 --- a/internal/game/action_steal.go +++ b/internal/game/act_steal.go @@ -7,8 +7,7 @@ import ( "strconv" "strings" - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" @@ -26,35 +25,35 @@ func (g *Game) executeSteal(sess *net.Session, args []string, rawInput string) { } } -var stallGuardTalk = &action.TalkConfig{ - Nodes: map[string]action.TalkNode{ +var stallGuardTalk = &behavior.TalkConfig{ + Nodes: map[string]behavior.TalkNode{ "start": { Message: "Caught you red-handed! You have three options, thief.", - Options: []action.TalkOption{ - {Text: "\"I'll pay a fine. (500 credits)\"", Goto: "bribe", Condition: &action.Condition{MinCredits: 500}}, + Options: []behavior.TalkOption{ + {Text: "\"I'll pay a fine. (500 credits)\"", Goto: "bribe", Condition: &behavior.Condition{MinCredits: 500}}, {Text: "\"Take me to jail.\"", Goto: "jail"}, {Text: "\"You'll have to catch me first!\"", Goto: "fight"}, }, }, "bribe": { Message: "Smart choice. Hand over 500 credits and we'll forget this happened.", - Action: &action.NodeAction{Cost: 500}, - Options: []action.TalkOption{{Text: "\"Fine, take it.\"", End: true}}, + Action: &behavior.NodeAction{Cost: 500}, + Options: []behavior.TalkOption{{Text: "\"Fine, take it.\"", End: true}}, }, "jail": { Message: "Off to the detention cell with you!", - Action: &action.NodeAction{Teleport: 162}, - Options: []action.TalkOption{{Text: "(You are dragged away)", End: true}}, + Action: &behavior.NodeAction{Teleport: 162}, + Options: []behavior.TalkOption{{Text: "(You are dragged away)", End: true}}, }, "fight": { Message: "Then defend yourself!", - Action: &action.NodeAction{SetFlags: map[string]any{"guard_hostile": true}}, - Options: []action.TalkOption{{Text: "(The guard attacks!)", End: true}}, + Action: &behavior.NodeAction{SetFlags: map[string]any{"guard_hostile": true}}, + Options: []behavior.TalkOption{{Text: "(The guard attacks!)", End: true}}, }, }, } -var stealSuccess = action.SuccessFormula{ +var stealSuccess = behavior.SuccessFormula{ Base: 0.5, PerLevel: 0.03, Cap: 0.95, @@ -63,7 +62,7 @@ var stealSuccess = action.SuccessFormula{ func (g *Game) doSteal(sess *net.Session, input string) { p := sess.Player - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } @@ -169,7 +168,7 @@ func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input str var matchingObjs []*object.ObjectDef for _, o := range stealObjs { - if object.WordPrefixMatch(searchName, o.Name) { + if behavior.WordPrefixMatch(searchName, o.Name) { matchingObjs = append(matchingObjs, o) } } @@ -216,7 +215,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string sess.WriteLine("That is already dead.") return } - if combat.IsMobInCombat(mob.InstanceID) { + if g.Combat.IsMobInCombat(mob.InstanceID) { sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName)) return } @@ -260,12 +259,12 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string g.broadcastAction(sess, "\n%s glances around the room.", p.Name) - p.Action = &action.Action{ - Type: action.TypeSteal, + p.Action = &behavior.Action{ + Type: behavior.TypeSteal, TargetID: targetID, TargetName: targetName, WaitLeft: engine.ToTicks(stealSpeed), - Data: &action.StealData{ + Data: &behavior.StealData{ TargetType: targetType, StealTable: stealTable, StealLevel: stealLevel, @@ -281,7 +280,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string } func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { - d := p.Action.Data.(*action.StealData) + d := p.Action.Data.(*behavior.StealData) targetType := d.TargetType stealTable := d.StealTable stealLevel := d.StealLevel @@ -299,7 +298,7 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { g.cancelAction(p) return } - if combat.IsMobInCombat(mob.InstanceID) { + if g.Combat.IsMobInCombat(mob.InstanceID) { sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName)) g.cancelAction(p) return @@ -350,7 +349,7 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { } level := p.Level(player.Thieving) - chance := action.SuccessChance(stealSuccess, level, stealLevel) + chance := behavior.SuccessChance(stealSuccess, level, stealLevel) if guardWatching { chance *= 0.5 if chance < 0.05 { @@ -359,11 +358,11 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { } if rand.Float64() < chance { - dt, err := action.LoadDropTable(g.DataDir, stealTable) + dt, err := behavior.LoadDropTable(g.DataDir, stealTable) if err != nil || len(dt.Drops) == 0 { sess.WriteLine("You steal nothing of value.") } else { - drop := action.ResolveDrop(g.DataDir, dt.Drops) + drop := behavior.ResolveDrop(g.DataDir, dt.Drops) if drop == nil || drop.ItemID == "" { sess.WriteLine("You steal nothing of value.") } else { @@ -409,7 +408,7 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { } } -func (g *Game) giveStealLoot(sess *net.Session, p *player.Player, drop *action.DropEntry) { +func (g *Game) giveStealLoot(sess *net.Session, p *player.Player, drop *behavior.DropEntry) { qty := drop.Quantity if qty <= 0 { qty = 1 @@ -466,11 +465,11 @@ func (g *Game) startStealGuardTalk(sess *net.Session, p *player.Player, guardMob return } - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "talk", TargetID: guardMob.DefID, TargetName: guardMob.Name, - Data: &action.TalkData{Node: "start", Cfg: cfg, StealGuard: true}, + Data: &behavior.TalkData{Node: "start", Cfg: cfg, StealGuard: true}, } g.showTalkNode(sess, node) diff --git a/internal/game/action_talk.go b/internal/game/act_talk.go index 5b3a095..e9047b2 100644 --- a/internal/game/action_talk.go +++ b/internal/game/act_talk.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" @@ -20,11 +20,11 @@ func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobI } if node, ok := cfg.Nodes["start"]; ok { - p.Action = &action.Action{ - Type: action.TypeTalk, + p.Action = &behavior.Action{ + Type: behavior.TypeTalk, TargetID: mob.DefID, TargetName: mob.Name, - Data: &action.TalkData{Node: "start", Cfg: cfg}, + Data: &behavior.TalkData{Node: "start", Cfg: cfg}, } g.showTalkNode(sess, node) } else { @@ -40,11 +40,11 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object } if node, ok := cfg.Nodes["start"]; ok { - p.Action = &action.Action{ - Type: action.TypeTalk, + p.Action = &behavior.Action{ + Type: behavior.TypeTalk, TargetID: obj.ID, TargetName: obj.Name, - Data: &action.TalkData{Node: "start", Cfg: cfg}, + Data: &behavior.TalkData{Node: "start", Cfg: cfg}, } g.showTalkNode(sess, node) } else { @@ -52,7 +52,7 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object } } -func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) { +func (g *Game) showTalkNode(sess *net.Session, node behavior.TalkNode) { sess.WriteLine(fmt.Sprintf("%s", g.colorize(sess, "dialog", node.Message))) if node.Action != nil { @@ -67,7 +67,7 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) { g.Flags.Delete("guard_hostile") p := sess.Player if p != nil && p.Action != nil { - if td, ok := p.Action.Data.(*action.TalkData); ok && td.StealGuard { + if td, ok := p.Action.Data.(*behavior.TalkData); ok && td.StealGuard { guardMob := g.findGuardInRoom(p.RoomID, "guard") if guardMob != nil { sess.State = net.StateGame @@ -101,7 +101,7 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) { func (g *Game) handleTalkInput(sess *net.Session, input string) { p := sess.Player - if p == nil || p.Action == nil || p.Action.Type != action.TypeTalk { + if p == nil || p.Action == nil || p.Action.Type != behavior.TypeTalk { sess.State = net.StateGame g.writePrompt(sess) return @@ -116,12 +116,12 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) { return } - if td, ok := p.Action.Data.(*action.TalkData); ok && td.EstateDirectory { + if td, ok := p.Action.Data.(*behavior.TalkData); ok && td.EstateDirectory { g.handleEstateDirectoryTalk(sess, input) return } - td, ok := p.Action.Data.(*action.TalkData) + td, ok := p.Action.Data.(*behavior.TalkData) if !ok || td.Cfg == nil { g.cancelAction(p) sess.State = net.StateGame @@ -148,7 +148,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) { } validIdx := 0 - var chosen *action.TalkOption + var chosen *behavior.TalkOption for i := range node.Options { opt := &node.Options[i] if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { @@ -188,14 +188,14 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) { g.writePrompt(sess) } -func (g *Game) enterShop(sess *net.Session, cfg *action.ShopConfig) { +func (g *Game) enterShop(sess *net.Session, cfg *behavior.ShopConfig) { sess.Shop = cfg sess.State = net.StateShop g.showShopBrowse(sess) g.writeShopPrompt(sess) } -func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) { +func (g *Game) applyNodeAction(sess *net.Session, na *behavior.NodeAction) { p := sess.Player if p == nil { return diff --git a/internal/game/action_use.go b/internal/game/act_use.go index 4bac96d..34f28c3 100644 --- a/internal/game/action_use.go +++ b/internal/game/act_use.go @@ -4,7 +4,7 @@ import ( "fmt" "math/rand" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" @@ -36,19 +36,19 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD g.broadcastAction(sess, "\n%s uses the %s.", p.Name, obj.Name) - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "use", TargetID: obj.ID, TargetName: obj.Name, WaitLeft: engine.ToTicks(1), - Data: &action.UseData{Cfg: cfg, Step: 0}, + Data: &behavior.UseData{Cfg: cfg, Step: 0}, } sess.WriteLine(fmt.Sprintf("%s", cfg.Message)) } func (g *Game) advanceUse(sess *net.Session, p *player.Player) { - d, ok := p.Action.Data.(*action.UseData) + d, ok := p.Action.Data.(*behavior.UseData) if !ok || d == nil || d.Cfg == nil { g.cancelAction(p) return @@ -74,7 +74,7 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) { if cfg.Success != nil { skillLevel := p.Level(player.SkillName(cfg.Skill)) - chance := action.SuccessChance(*cfg.Success, skillLevel, cfg.Level) + chance := behavior.SuccessChance(*cfg.Success, skillLevel, cfg.Level) if rand.Float64() >= chance { if cfg.FailMsg != "" { sess.WriteLine(cfg.FailMsg) diff --git a/internal/game/cmd_agility.go b/internal/game/cmd_agility.go index e347322..4a6f733 100644 --- a/internal/game/cmd_agility.go +++ b/internal/game/cmd_agility.go @@ -3,7 +3,6 @@ package game import ( "fmt" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) @@ -11,7 +10,7 @@ import ( func (g *Game) doObstacle(sess *net.Session, verb string) { p := sess.Player - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } diff --git a/internal/game/cmd_aps.go b/internal/game/cmd_aps.go index 8e20c6c..265277a 100644 --- a/internal/game/cmd_aps.go +++ b/internal/game/cmd_aps.go @@ -9,6 +9,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" ) func (g *Game) executeAps(sess *net.Session, args []string, rawInput string) { @@ -137,7 +138,7 @@ func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int) } } - t := &Table{ + t := &ui.Table{ Title: "APS Datapad", Columns: []string{ color.Render(mode, color.Parse("245"), "Room"), diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index abaefbb..5635fd0 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -2,740 +2,11 @@ package game import ( "fmt" - "math/rand" - "sort" - "strconv" "strings" - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/color" - "thehouseoficarus/internal/combat" - "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" - "thehouseoficarus/internal/player" - "thehouseoficarus/internal/world" ) -func (g *Game) doAttack(sess *net.Session, input string) { - p := sess.Player - - if combat.GetCombat(p.Name) != nil { - sess.WriteLine("You are already in combat!") - return - } - - if p.Action != nil { - g.cancelAction(p) - } - - mob := g.findMob(sess, input, p.RoomID) - if mob == nil { - return - } - - if mob.HP <= 0 { - if mob.IsTask() { - sess.WriteLine(fmt.Sprintf("%s is already complete.", mobDisplayName(mob, true))) - } else { - sess.WriteLine("That is already dead.") - } - return - } - - if mob.Protected { - sess.WriteLine(fmt.Sprintf("You can't attack %s!", mobDisplayName(mob, true))) - return - } - - if mob.AssassinLevel > 0 && p.Level(player.Assassin) < mob.AssassinLevel { - sess.WriteLine(fmt.Sprintf("You need Assassin level %d to attack %s.", mob.AssassinLevel, mobDisplayName(mob, true))) - return - } - - if combat.IsMobInCombat(mob.InstanceID) { - sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false))) - return - } - - if itemID, ok := p.Equipment[object.SlotMainHand]; ok { - if def, err := g.ItemStore.Load(itemID); err == nil && def.WeaponType == object.WeaponRanged { - if _, hasAmmo := p.Equipment[object.SlotAmmo]; !hasAmmo || p.AmmoQty <= 0 { - sess.WriteLine("You don't have any ammo equipped.") - return - } - } - } - - g.startCombat(sess, p, mob) -} - -func (g *Game) findMob(sess *net.Session, input string, roomID int) *world.MobInstance { - lower := strings.ToLower(input) - mobs := g.MobStore.MobsInRoom(roomID) - - idx := -1 - name := lower - if dotPos := strings.Index(lower, "."); dotPos > 0 { - if n, err := strconv.Atoi(lower[:dotPos]); err == nil && n > 0 { - idx = n - name = lower[dotPos+1:] - } - } - - var exact []*world.MobInstance - var prefix []*world.MobInstance - for _, m := range mobs { - q := m.MatchQuality(name) - if q == world.MatchExact { - exact = append(exact, m) - } else if q == world.MatchPrefix { - prefix = append(prefix, m) - } - } - - candidates := exact - if len(candidates) == 0 { - candidates = prefix - } - - if len(candidates) == 0 { - sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input)) - return nil - } - - sort.Slice(candidates, func(i, j int) bool { - return candidates[i].InstanceID < candidates[j].InstanceID - }) - - if idx > 0 { - if idx-1 < len(candidates) { - return candidates[idx-1] - } - return nil - } - - if len(candidates) == 1 { - return candidates[0] - } - - seen := make(map[string]bool) - for _, m := range candidates { - seen[mobDisplayName(m, false)] = true - } - if len(seen) > 1 { - sess.WriteLine("Which one?") - return nil - } - return candidates[0] -} - -func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { - g.cancelRest(p.Name) - g.cancelBgAction(p) - - combat.EnterCombat(p.Name, mob.InstanceID) - p.AttackTimer = 0 - - isTask := mob.IsTask() - autotriggerActive := p.AutotriggerMod != "" - - if !autotriggerActive { - attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle)) - var styleParts []string - if attBonus > 0 { - styleParts = append(styleParts, fmt.Sprintf("+%d atk", attBonus)) - } - if strBonus > 0 { - styleParts = append(styleParts, fmt.Sprintf("+%d str", strBonus)) - } - if defBonus > 0 { - styleParts = append(styleParts, fmt.Sprintf("+%d def", defBonus)) - } - styleStr := "" - if len(styleParts) > 0 { - styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")" - } - if isTask { - sess.WriteLine(fmt.Sprintf("You begin to %s %s.%s", taskWorkVerb(mob), g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) - } else { - sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) - } - } else { - mod := GetMod(p.AutotriggerMod) - modName := p.AutotriggerMod - if mod != nil { - modName = mod.Name - } - if isTask { - sess.WriteLine(fmt.Sprintf("You begin to %s %s using %s!", - taskWorkVerb(mob), - g.colorize(sess, "mob", mobDisplayName(mob, true)), - g.colorize(sess, "science_mod", modName))) - } else { - sess.WriteLine(fmt.Sprintf("You attack %s with %s!", - g.colorize(sess, "mob", mobDisplayName(mob, true)), - g.colorize(sess, "science_mod", modName))) - } - } - if isTask { - g.broadcastAction(sess, "\n%s begins working on %s.", p.Name, mobDisplayName(mob, true)) - } else { - g.broadcastAction(sess, "\n%s attacks %s!", p.Name, mobDisplayName(mob, true)) - } - - g.Ticks.Subscribe(1, func() bool { - cs := combat.GetCombat(p.Name) - if cs == nil || !cs.Active { - return false - } - currentMob := g.MobStore.GetInstance(cs.MobID) - if currentMob == nil || currentMob.HP <= 0 { - g.endCombat(sess, p, currentMob) - return false - } - - p.AttackTimer++ - if float64(p.AttackTimer) >= g.playerWeaponSpeed(p) { - p.AttackTimer = 0 - - if g.hasDeckEquipped(p) { - if p.AutotriggerMod != "" { - mod := GetMod(p.AutotriggerMod) - if mod != nil && g.hasJunkCost(p, mod) { - g.scienceAttack(sess, p, currentMob, mod) - } else if mod != nil { - sess.WriteLine(g.colorize(sess, "error", - fmt.Sprintf("\nYou're out of junk for %s!! You flail with your fists in desperation!", mod.Name))) - g.playerAttackUnarmed(sess, p, currentMob) - } else { - p.AutotriggerMod = "" - sess.WriteLine(g.colorize(sess, "error", - "\nNo autotrigger set! Set a module with the 'autotrigger' command.")) - g.playerAttackUnarmed(sess, p, currentMob) - } - } else { - sess.WriteLine(g.colorize(sess, "error", - "\nNo autotrigger set! Set a module with the 'autotrigger' command.")) - g.playerAttackUnarmed(sess, p, currentMob) - } - } else { - if p.QueuedTrigger != "" { - mod := GetMod(p.QueuedTrigger) - p.QueuedTrigger = "" - if mod != nil && g.hasJunkCost(p, mod) { - g.scienceAttack(sess, p, currentMob, mod) - } else { - g.playerAttack(sess, p, currentMob) - } - } else { - g.playerAttack(sess, p, currentMob) - } - } - } - - if currentMob.HP <= 0 { - g.endCombat(sess, p, currentMob) - return false - } - return true - }) - - if !isTask { - g.Ticks.Subscribe(engine.ToTicks(mob.Speed), func() bool { - cs := combat.GetCombat(p.Name) - if cs == nil || !cs.Active { - return false - } - currentMob := g.MobStore.GetInstance(cs.MobID) - if currentMob == nil || currentMob.HP <= 0 { - return false - } - g.mobAttack(sess, p, currentMob) - if p.HP <= 0 { - g.endCombat(sess, p, currentMob) - return false - } - return true - }) - } -} - -func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { - if g.processConsumeQueue(p, sess) { - return - } - - attackType, weaponType, attRoll, defRoll, maxHit := g.calculatePlayerAttackRoll(p, mob) - - if g.isSafespotted(p.Name) && attackType != "ranged" && attackType != "science" { - if ss, ok := g.safespot.Get(p.Name); ok { - g.forceLeaveSafespot(sess, p, &ss, "You leave your cover to close in for melee!") - } - } - - if combat.HitCheck(attRoll, defRoll) { - g.applyPlayerHit(sess, p, mob, attackType, weaponType, maxHit) - } else { - g.applyPlayerMiss(sess, p, mob, weaponType) - } -} - -func (g *Game) calculatePlayerAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType object.WeaponType, attRoll int, defRoll int, maxHit int) { - attackType = "crush" - if itemID, ok := p.Equipment[object.SlotMainHand]; ok { - if def, err := g.ItemStore.Load(itemID); err == nil { - if def.AttackType != "" { - attackType = def.AttackType - } else if def.WeaponType == object.WeaponRanged { - attackType = "ranged" - } else if def.WeaponType == object.WeaponScience { - attackType = "science" - } - weaponType = def.WeaponType - } - } - - totals := g.playerEquipBonuses(p) - isRanged := weaponType == object.WeaponRanged - - if isRanged { - rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle)) - equipAttack := totals.RangedAttack - effectiveRanged := p.Level(player.Ranged) + g.techLevelBonus(p, "ranged") + g.buffLevelBonus(p, "ranged") - attRoll = combat.EffectiveRoll(effectiveRanged, rangedBonus, equipAttack) - - mobDefBonus := combat.SelectBonus(attackType, - mob.StabDefense, mob.SlashDefense, mob.CrushDefense, - mob.ScienceDefense, mob.RangedDefense) - defRoll = combat.EffectiveRoll(mob.Defense, 0, mobDefBonus) - maxHit = combat.MaxHit(effectiveRanged, rangedBonus, totals.RangedStrength) - } else { - attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) - equipAttack := combat.SelectBonus(attackType, - totals.StabAttack, totals.SlashAttack, totals.CrushAttack, - totals.ScienceAttack, totals.RangedAttack) - effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") - attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, equipAttack) - - mobDefBonus := combat.SelectBonus(attackType, - mob.StabDefense, mob.SlashDefense, mob.CrushDefense, - mob.ScienceDefense, mob.RangedDefense) - defRoll = combat.EffectiveRoll(mob.Defense, 0, mobDefBonus) - maxHit = combat.MaxHit(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus, totals.StrengthBonus) - } - return -} - -func (g *Game) playerAttackUnarmed(sess *net.Session, p *player.Player, mob *world.MobInstance) { - if g.processConsumeQueue(p, sess) { - return - } - - attackType, _, attRoll, defRoll, maxHit := g.calculateUnarmedAttackRoll(p, mob) - - if combat.HitCheck(attRoll, defRoll) { - g.applyPlayerHit(sess, p, mob, attackType, object.WeaponMelee, maxHit) - } else { - g.applyPlayerMiss(sess, p, mob, object.WeaponMelee) - } -} - -func (g *Game) calculateUnarmedAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType object.WeaponType, attRoll int, defRoll int, maxHit int) { - attackType = "crush" - weaponType = object.WeaponMelee - - attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) - effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") - attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, 0) - - mobDefBonus := combat.SelectBonus(attackType, - mob.StabDefense, mob.SlashDefense, mob.CrushDefense, - mob.ScienceDefense, mob.RangedDefense) - defRoll = combat.EffectiveRoll(mob.Defense, 0, mobDefBonus) - maxHit = combat.MaxHit(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus, 0) - return -} - -func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.MobInstance, attackType string, weaponType object.WeaponType, maxHit int) { - dmg := combat.RollDamage(maxHit) - - mob.HP -= dmg - if mob.HP < 0 { - mob.HP = 0 - } - if mob.FinishingBlow != "" && mob.HP <= 0 { - mob.HP = 1 - } - if mob.HP < mob.MaxHP && mob.HP > 0 { - mob.StartRegen() - } - - if mob.FinishingBlow != "" && mob.HP == 1 { - fbDef, _ := g.ItemStore.Load(mob.FinishingBlow) - fbName := mob.FinishingBlow - if fbDef != nil { - fbName = fbDef.Name - } - sess.WriteLine(g.colorize(sess, "warning", - fmt.Sprintf("%s resists death! Use %s on it to finish it off.", - mobDisplayName(mob, false), fbName))) - } - - isRanged := weaponType == object.WeaponRanged - gains := g.awardCombatXP(sess, p, dmg, isRanged) - - if isRanged { - g.consumeAmmo(sess, p) - } - - if mob.IsTask() { - g.writeTaskProgress(sess, p, mob, dmg, gains) - } else { - mobName := mobDisplayName(mob, true) - attacker := mob.Name - if !mob.Unique { - attacker = "The " + mob.Name - } - w := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)) - if w2 := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)); w2 > w { - w = w2 - } - prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", fmt.Sprint(dmg))) - visLen := color.VisibleLen(prefix) - if visLen < w { - prefix += strings.Repeat(" ", w-visLen+1) - } - hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP) - line := prefix + hpSuffix + g.formatXpDrop(sess, p, gains) - sess.WriteLine(line) - } - - if isRanged && p.AmmoQty <= 0 { - sess.WriteLine("You've run out of ammo!") - combat.LeaveCombat(p.Name) - } -} - -func (g *Game) applyPlayerMiss(sess *net.Session, p *player.Player, mob *world.MobInstance, weaponType object.WeaponType) { - sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("You miss %s.", mobDisplayName(mob, true)))) - - if weaponType == object.WeaponRanged { - g.consumeAmmo(sess, p) - if p.AmmoQty <= 0 { - sess.WriteLine("You've run out of ammo!") - combat.LeaveCombat(p.Name) - } - } -} - -func (g *Game) consumeAmmo(sess *net.Session, p *player.Player) { - ammoID := p.Equipment[object.SlotAmmo] - - p.AmmoQty-- - if p.AmmoQty <= 0 { - delete(p.Equipment, object.SlotAmmo) - p.AmmoQty = 0 - } - - if ammoID != "" { - if def, err := g.ItemStore.Load(ammoID); err == nil && def.Recoverable { - if rand.Float64() < 0.80 { - g.World.AddGroundItem(p.RoomID, def.ID, 1) - } - } - } - - g.AccountStore.SaveCharacter(p) -} - -func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { - if g.isSafespotted(p.Name) && g.safespotBlocksMob(p, mob) { - return - } - - attRoll, defRoll := g.calculateMobAttack(p, mob) - - if combat.HitCheck(attRoll, defRoll) { - g.applyMobHit(sess, p, mob) - } else { - g.applyMobMiss(sess, mob) - } -} - -func (g *Game) calculateMobAttack(p *player.Player, mob *world.MobInstance) (attRoll int, defRoll int) { - _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle)) - - mobAttackType := mob.AttackType - if mobAttackType == "" { - mobAttackType = "crush" - } - - attRoll = combat.EffectiveRoll(mob.Attack, 0, mob.AttackBonus) - - totals := g.playerEquipBonuses(p) - equipDef := combat.SelectBonus(mobAttackType, - totals.StabDefense, totals.SlashDefense, totals.CrushDefense, - totals.ScienceDefense, totals.RangedDefense) - defRoll = combat.EffectiveRoll(p.Level(player.Defense)+g.techLevelBonus(p, "defense")+g.buffLevelBonus(p, "defense"), defStyleBonus, equipDef) - return -} - -func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobInstance) { - maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus) - dmg := combat.RollDamage(maxHit) - dmg = g.applyTechProtection(p, mob, dmg) - - if mob.DamageWithout != "" { - hasProtection := false - for _, itemID := range p.Equipment { - if itemID == mob.DamageWithout { - hasProtection = true - break - } - } - if !hasProtection { - dmg = dmg * 3 / 2 - if dmg < 1 { - dmg = 1 - } - cs := combat.GetCombat(p.Name) - if cs != nil && !cs.DamageWarningShown { - cs.DamageWarningShown = true - fbDef, _ := g.ItemStore.Load(mob.DamageWithout) - fbName := mob.DamageWithout - if fbDef != nil { - fbName = fbDef.Name - } - attacker := mob.Name - if !mob.Unique { - attacker = "The " + mob.Name - } - sess.WriteLine(g.colorize(sess, "warning", - fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.", - attacker, fbName))) - } - } - } - - p.HP -= dmg - if p.HP < 0 { - p.HP = 0 - } - p.StartRegen() - g.AccountStore.SaveCharacter(p) - - attacker := mob.Name - if !mob.Unique { - attacker = "The " + mob.Name - } - mobName := mobDisplayName(mob, true) - w := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)) - if w2 := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)); w2 > w { - w = w2 - } - prefix := fmt.Sprintf("%s hits you for %s damage.", g.colorize(sess, "mob", attacker), g.colorize(sess, "damage_taken", fmt.Sprint(dmg))) - visLen := color.VisibleLen(prefix) - if visLen < w { - prefix += strings.Repeat(" ", w-visLen+1) - } - hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, p.HP, p.MaxHP()), p.HP, p.MaxHP()) - sess.WriteLine(prefix + hpSuffix) - - if ss, hasSS := g.safespot.Get(p.Name); hasSS && ss.HideCountdown > 0 { - g.safespot.Delete(p.Name) - sess.WriteLine(g.colorize(sess, "error", "You're hit while repositioning! You failed to hide.")) - } - - if p.MoveTicks > 0 { - p.ClearMoveState() - sess.WriteLine("Can't escape!") - } -} - -func (g *Game) applyMobMiss(sess *net.Session, mob *world.MobInstance) { - attacker := mob.Name - if !mob.Unique { - attacker = "The " + mob.Name - } - sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s misses you.", attacker))) -} - -func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { - combat.LeaveCombat(p.Name) - - if p.HP <= 0 { - g.killPlayer(sess, p, mob) - return - } - - if mob != nil && mob.HP <= 0 { - isTask := mob.IsTask() - p.Stats.RecordMobKill(mob.DefID) - - if isTask { - complete := mob.CompleteMessage - if complete == "" { - complete = fmt.Sprintf("You finish your work on %s!", mobDisplayName(mob, true)) - } - sess.WriteLine(g.colorize(sess, "victory", "\n"+complete)) - } else { - sess.WriteLine(g.colorize(sess, "victory", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true)))) - g.onAssassinKill(sess, p, mob) - } - - if g.Hub != nil { - for _, other := range g.Hub.PlayersInRoom(p.RoomID) { - if other != sess && other.Player != nil { - if isTask { - other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s finishes working on %s.", p.Name, mobDisplayName(mob, false)))) - } else { - op := other.Player - mobLvl := mobCombatLevel(mob) - levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl)) - other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr))) - } - } - } - } - - dropLabel := func() string { - if isTask { - return "You receive:" - } - dropper := mob.Name - if !mob.Unique { - dropper = "The " + mob.Name - } - return dropper + " drops:" - }() - - if mob.Drops.Remains != "" { - g.World.AddReservedItem(p.RoomID, mob.Drops.Remains, 1, p.Name) - def, _ := g.ItemStore.Load(mob.Drops.Remains) - name := mob.Drops.Remains - if def != nil { - name = def.Name - } - coloredName := g.itemColorize(sess, def, name) - sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName)) - } - - if len(mob.Drops.Loot) > 0 { - entry := action.ResolveDrop(g.DataDir, mob.Drops.Loot) - if entry != nil && entry.ItemID != "" { - qty := entry.Quantity - if qty <= 0 { - qty = 1 - } - g.World.AddReservedItem(p.RoomID, entry.ItemID, qty, p.Name) - def, _ := g.ItemStore.Load(entry.ItemID) - name := entry.ItemID - if def != nil { - name = def.Name - } - coloredName := g.itemColorize(sess, def, name) - if qty > 1 { - sess.WriteLine(fmt.Sprintf("%s %d x %s", g.colorize(sess, "drop_message", dropLabel), qty, coloredName)) - } else { - sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName)) - } - } - } - - respawnTicks := engine.ToTicks(mob.RespawnTicks) - if respawnTicks <= 0 { - respawnTicks = engine.ToTicks(30) - } - instanceID := mob.InstanceID - g.Ticks.Subscribe(respawnTicks, func() bool { - g.respawnMob(instanceID) - return false - }) - g.writePrompt(sess) - } -} - -// killPlayer handles a player death from any source (combat or a room hazard). -// mob may be nil (e.g. a hazard kill); it is only used for Dead Man's Switch -// retribution. -func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobInstance) { - combat.LeaveCombat(p.Name) - - if p.HasActiveTech("retribution") { - retDef := GetTechDef("retribution") - if retDef != nil && mob != nil && mob.HP > 0 { - retDmg := int(float64(p.MaxHP()) * retDef.Effects.RetributionPct) - if retDmg > 0 { - mob.HP -= retDmg - if mob.HP < 0 { - mob.HP = 0 - } - sess.WriteLine(fmt.Sprintf("\nDead Man's Switch activates! %s takes %d damage!", - mobDisplayName(mob, true), retDmg)) - } - } - } - p.DeactivateAllTechs() - p.Stats.RecordDeath() - sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!")) - p.ClearMoveState() - p.HazardTimer = 0 - if ss, ok := g.safespot.Get(p.Name); ok { - g.forceLeaveSafespot(sess, p, &ss, "") - } - g.dropItemsOnDeath(p) - p.HP = p.MaxHP() - p.RoomID = 1001 - g.AccountStore.SaveCharacter(p) - if g.Hub != nil { - g.Hub.EnterRoom(sess, p.RoomID) - } - g.doLook(sess) - g.writePrompt(sess) -} - -func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRanged bool) []xpGain { - baseXP := dmg * 4 - var gains []xpGain - - if isRanged { - gains = []xpGain{ - {string(player.Ranged), baseXP * 3 / 4}, - {string(player.Hitpoints), baseXP / 4}, - } - } else { - switch p.AttackStyle { - case player.Accurate: - gains = []xpGain{{string(player.Accuracy), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} - case player.Aggressive: - gains = []xpGain{{string(player.Strength), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} - case player.Defensive: - gains = []xpGain{{string(player.Defense), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} - case player.Balanced: - quarter := baseXP / 4 - gains = []xpGain{ - {string(player.Accuracy), quarter}, - {string(player.Strength), quarter}, - {string(player.Defense), quarter}, - {string(player.Hitpoints), quarter}, - } - } - } - - for _, gain := range gains { - g.awardSkillXP(sess, p, player.SkillName(gain.Skill), gain.XP) - } - g.AccountStore.SaveCharacter(p) - return gains -} - -func (g *Game) stopCombat(playerName string) { - if cs := combat.GetCombat(playerName); cs != nil { - combat.LeaveCombat(playerName) - } -} - func (g *Game) executeAttack(sess *net.Session, args []string, rawInput string) { if len(args) == 0 { p := sess.Player @@ -786,4 +57,3 @@ func (g *Game) respawnMob(instanceID string) { } } } - diff --git a/internal/game/cmd_bank.go b/internal/game/cmd_bank.go index b31dd16..8d96c5f 100644 --- a/internal/game/cmd_bank.go +++ b/internal/game/cmd_bank.go @@ -5,9 +5,10 @@ import ( "sort" "strings" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" ) func (g *Game) executeBank(sess *net.Session, args []string, rawInput string) { @@ -65,7 +66,7 @@ func (g *Game) showBankBrowse(sess *net.Session) { unicode := p.OptionBool("unicode") - t := Table{ + t := ui.Table{ Title: "Bank Contents", Columns: []string{"#", "Item", "Quantity"}, } @@ -240,7 +241,7 @@ func (g *Game) doBankWithdraw(sess *net.Session, input string) { } lower := strings.ToLower(input) for _, e := range entries { - if object.WordPrefixMatch(e.name, lower) { + if behavior.WordPrefixMatch(e.name, lower) { matches = append(matches, e) } } diff --git a/internal/game/cmd_clean.go b/internal/game/cmd_clean.go index 63bea24..a43295a 100644 --- a/internal/game/cmd_clean.go +++ b/internal/game/cmd_clean.go @@ -3,11 +3,10 @@ package game import ( "strings" - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -18,7 +17,7 @@ func (g *Game) executeClean(sess *net.Session, args []string, rawInput string) { func (g *Game) doClean(sess *net.Session, input string) { p := sess.Player - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } @@ -63,10 +62,10 @@ func (g *Game) doClean(sess *net.Session, input string) { g.cancelBgAction(p) - p.BackgroundAction = &action.Action{ + p.BackgroundAction = &behavior.Action{ Type: "clean", TargetID: "clean_herbs", - Data: &action.CleanData{ + Data: &behavior.CleanData{ Phase: 0, Filter: filter, }, @@ -78,7 +77,7 @@ func (g *Game) doClean(sess *net.Session, input string) { sess.WriteLine("You begin cleaning herbs.") } -func matchesCleanFilter(item *object.ItemDef, filter string) bool { +func matchesCleanFilter(item *item.ItemDef, filter string) bool { if len(item.Craft) > 0 && len(item.FirstCraft().Consume) > 0 && len(item.FirstCraft().Consume[0].Items) > 0 { if strings.Contains(item.FirstCraft().Consume[0].Items[0], filter) { return true diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index 7bf073d..666a622 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -7,6 +7,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/config" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/ui" ) func (g *Game) doColor(sess *net.Session, input string) { @@ -166,7 +167,7 @@ var colorCategoryOrder = []string{ func showColorTable(g *Game, sess *net.Session) { p := sess.Player mode := g.colorMode(sess) - table := &Table{ + table := &ui.Table{ Columns: []string{ color.Render(mode, color.Parse("75"), "Target"), "Color", diff --git a/internal/game/cmd_consume.go b/internal/game/cmd_consume.go index 249bc9a..133bf57 100644 --- a/internal/game/cmd_consume.go +++ b/internal/game/cmd_consume.go @@ -6,8 +6,8 @@ import ( "time" "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -76,7 +76,7 @@ func (g *Game) doEat(sess *net.Session, input string) { } } -func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def *object.ItemDef) { +func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def *item.ItemDef) { if def == nil { var err error def, err = g.ItemStore.Load(itemID) @@ -211,7 +211,7 @@ func (g *Game) doDrink(sess *net.Session, args []string) { } } -func (g *Game) applyPotion(sess *net.Session, p *player.Player, itemID string, def *object.ItemDef) { +func (g *Game) applyPotion(sess *net.Session, p *player.Player, itemID string, def *item.ItemDef) { if def == nil { var err error def, err = g.ItemStore.Load(itemID) diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go index 6dfbcce..0e4f194 100644 --- a/internal/game/cmd_cook.go +++ b/internal/game/cmd_cook.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -47,7 +47,7 @@ func (g *Game) doCook(sess *net.Session, input string) { itemID := matches[0].ID - var recipes []*object.ItemDef + var recipes []*item.ItemDef for _, item := range items { c := item.FirstCraft() if stationMatch(stationDefID, c.Station) && craftMatchesEntry(c, itemID) && craftHasAllItems(c, p.HasItem) { @@ -59,9 +59,9 @@ func (g *Game) doCook(sess *net.Session, input string) { g.showRecipeChoice(sess, p, recipes, "You can't cook that.", title, "") } -func (g *Game) showCookMenu(sess *net.Session, p *player.Player, items []*object.ItemDef, stationDefID, stationName string) { +func (g *Game) showCookMenu(sess *net.Session, p *player.Player, items []*item.ItemDef, stationDefID, stationName string) { seen := make(map[string]bool) - var recipes []*object.ItemDef + var recipes []*item.ItemDef for _, item := range items { c := item.FirstCraft() diff --git a/internal/game/cmd_craft.go b/internal/game/cmd_craft.go index 3290daa..254b938 100644 --- a/internal/game/cmd_craft.go +++ b/internal/game/cmd_craft.go @@ -3,8 +3,9 @@ package game import ( "strings" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -30,7 +31,7 @@ func (g *Game) doStationSkill(sess *net.Session, input string, recipeType string items := g.CraftIndex.ByType(recipeType) - var filtered []*object.ItemDef + var filtered []*item.ItemDef for _, item := range items { if !g.itemVisible(p, item) { continue @@ -75,13 +76,13 @@ func (g *Game) doStationSkill(sess *net.Session, input string, recipeType string g.showProductionTable(sess, p, available, label, recipeType, flagKey, verbCap, false) } -func (g *Game) doWithInput(sess *net.Session, p *player.Player, items []*object.ItemDef, input string, skill player.SkillName, flagKey, label, verbCap string) { +func (g *Game) doWithInput(sess *net.Session, p *player.Player, items []*item.ItemDef, input string, skill player.SkillName, flagKey, label, verbCap string) { qty, productName := parseQty(input) productName = strings.TrimSpace(productName) - var matched []*object.ItemDef + var matched []*item.ItemDef for _, item := range items { - if object.WordPrefixMatch(productName, item.Name) { + if behavior.WordPrefixMatch(productName, item.Name) { matched = append(matched, item) } } @@ -91,7 +92,7 @@ func (g *Game) doWithInput(sess *net.Session, p *player.Player, items []*object. return } - var available []*object.ItemDef + var available []*item.ItemDef for _, item := range matched { if g.canDoItem(p, item, skill) { available = append(available, item) @@ -111,8 +112,8 @@ func (g *Game) doWithInput(sess *net.Session, p *player.Player, items []*object. g.showProductionTable(sess, p, available, label, label, flagKey, verbCap, false) } -func (g *Game) itemVisible(p *player.Player, item *object.ItemDef) bool { - return item.FindCraft(func(c object.CraftDef) bool { +func (g *Game) itemVisible(p *player.Player, def *item.ItemDef) bool { + return def.FindCraft(func(c item.CraftDef) bool { if len(c.Station) > 0 { stID, _ := g.findStation(p.RoomID, c.Station) if stID == "" { @@ -126,8 +127,8 @@ func (g *Game) itemVisible(p *player.Player, item *object.ItemDef) bool { }) != nil } -func (g *Game) canDoItem(p *player.Player, item *object.ItemDef, skill player.SkillName) bool { - m := item.FindCraft(func(c object.CraftDef) bool { +func (g *Game) canDoItem(p *player.Player, def *item.ItemDef, skill player.SkillName) bool { + m := def.FindCraft(func(c item.CraftDef) bool { if p.Level(skill) < c.Level { return false } @@ -148,31 +149,31 @@ func (g *Game) canDoItem(p *player.Player, item *object.ItemDef, skill player.Sk return m != nil } -func (g *Game) availableItems(p *player.Player, allItems []*object.ItemDef) []*object.ItemDef { - var result []*object.ItemDef - for _, item := range allItems { - m := item.FindCraft(func(c object.CraftDef) bool { +func (g *Game) availableItems(p *player.Player, allItems []*item.ItemDef) []*item.ItemDef { + var result []*item.ItemDef + for _, def := range allItems { + m := def.FindCraft(func(c item.CraftDef) bool { return craftHasAllItemsQty(&c, p.CountItem) }) if m != nil { - result = append(result, item) + result = append(result, def) } } return result } -func (g *Game) startItem(sess *net.Session, p *player.Player, item *object.ItemDef, count int, flagKey string) { +func (g *Game) startItem(sess *net.Session, p *player.Player, def *item.ItemDef, count int, flagKey string) { p.EnsureFlags() - p.Flags[flagKey] = item.ID + p.Flags[flagKey] = def.ID g.AccountStore.SaveCharacter(p) - g.startProductionFromItem(sess, p, item, count) + g.startProductionFromItem(sess, p, def, count) } func (g *Game) hasGrimyHerbs(p *player.Player) bool { items := g.CraftIndex.ByType("clean") - for _, item := range items { - if item.FindCraft(func(c object.CraftDef) bool { + for _, def := range items { + if def.FindCraft(func(c item.CraftDef) bool { return craftHasAllItems(&c, p.HasItem) }) != nil { return true @@ -181,7 +182,7 @@ func (g *Game) hasGrimyHerbs(p *player.Player) bool { return false } -func (g *Game) findCraftItems(p *player.Player, itemAID, itemBID string) []*object.ItemDef { +func (g *Game) findCraftItems(p *player.Player, itemAID, itemBID string) []*item.ItemDef { items := g.CraftIndex.ByType("crafting") toolTypeA := "" @@ -193,9 +194,9 @@ func (g *Game) findCraftItems(p *player.Player, itemAID, itemBID string) []*obje toolTypeB = defB.ToolType } - var matched []*object.ItemDef - for _, item := range items { - match := item.FindCraft(func(c object.CraftDef) bool { + var matched []*item.ItemDef + for _, def := range items { + match := def.FindCraft(func(c item.CraftDef) bool { if c.Tool == "" { return false } @@ -208,7 +209,7 @@ func (g *Game) findCraftItems(p *player.Player, itemAID, itemBID string) []*obje return false }) if match != nil { - matched = append(matched, item) + matched = append(matched, def) } } return matched diff --git a/internal/game/cmd_equipment.go b/internal/game/cmd_equipment.go index f5a9a11..88bc7f3 100644 --- a/internal/game/cmd_equipment.go +++ b/internal/game/cmd_equipment.go @@ -3,8 +3,8 @@ package game import ( "fmt" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" ) func (g *Game) doEquipment(sess *net.Session) { @@ -23,7 +23,7 @@ func (g *Game) doEquipment(sess *net.Session) { if err == nil { name = def.Name } - if slot == object.SlotAmmo && p.AmmoQty > 0 { + if slot == item.SlotAmmo && p.AmmoQty > 0 { sess.WriteLine(fmt.Sprintf(" %-12s %s (x%d)", slot, g.itemColorize(sess, def, name), p.AmmoQty)) } else { sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, def, name))) diff --git a/internal/game/cmd_estate_directory.go b/internal/game/cmd_estate_directory.go index ca28018..5a4423b 100644 --- a/internal/game/cmd_estate_directory.go +++ b/internal/game/cmd_estate_directory.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" @@ -72,11 +72,11 @@ func (g *Game) showEstateDirectoryTalk(sess *net.Session, p *player.Player, home sess.WriteLine(" You are a registered homeowner in this neighborhood.") sess.WriteLine(" Type 'enter' to go to your house, or 'leave' to step away.") sess.State = net.StateTalk - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "talk", TargetID: "estate_directory", TargetName: "Estate Directory", - Data: &action.TalkData{Node: "start", EstateDirectory: true}, + Data: &behavior.TalkData{Node: "start", EstateDirectory: true}, } sess.Write("\nChoice: ") return diff --git a/internal/game/cmd_farm.go b/internal/game/cmd_farm.go index eca0429..b601e49 100644 --- a/internal/game/cmd_farm.go +++ b/internal/game/cmd_farm.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) @@ -134,12 +134,12 @@ func (g *Game) doPlant(sess *net.Session, input string) { p.RemoveItem(seedID, 1) - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "plant", TargetID: seedID, TargetName: seedDef.Name, WaitLeft: 2, - Data: &action.FarmData{ + Data: &behavior.FarmData{ SeedID: seedID, Prefix: prefix, XP: float64(seedDef.FarmPlantXP), @@ -216,12 +216,12 @@ func (g *Game) doHarvest(sess *net.Session, input string) { } harvestXP := seedDef.FarmHarvestXP - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "harvest", TargetID: prefix, TargetName: productName, WaitLeft: 3, - Data: &action.FarmData{ + Data: &behavior.FarmData{ Prefix: prefix, SeedID: seedID, Product: product, @@ -266,12 +266,12 @@ func (g *Game) doRake(sess *net.Session, input string) { patchName := patchDisplayName(prefix) - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "rake", TargetID: prefix, TargetName: patchName, WaitLeft: 4, - Data: &action.FarmData{ + Data: &behavior.FarmData{ Prefix: prefix, }, } @@ -325,12 +325,12 @@ func (g *Game) doWater(sess *net.Session, input string) { patchName := patchDisplayName(prefix) - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "water", TargetID: prefix, TargetName: patchName, WaitLeft: 2, - Data: &action.FarmData{ + Data: &behavior.FarmData{ Prefix: prefix, }, } @@ -368,12 +368,12 @@ func (g *Game) doCure(sess *net.Session, input string) { patchName := patchDisplayName(prefix) - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "cure", TargetID: prefix, TargetName: patchName, WaitLeft: 2, - Data: &action.FarmData{ + Data: &behavior.FarmData{ Prefix: prefix, }, } diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go index 9cf50bc..0da8129 100644 --- a/internal/game/cmd_fletch.go +++ b/internal/game/cmd_fletch.go @@ -3,9 +3,9 @@ package game import ( "strings" - "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -23,15 +23,15 @@ var fletchGemItems = map[string]bool{ "uncut_ruby": true, "uncut_diamond": true, } -func fletchNeedsKnife(item *object.ItemDef) bool { - return item.FindCraft(func(c object.CraftDef) bool { +func fletchNeedsKnife(def *item.ItemDef) bool { + return def.FindCraft(func(c item.CraftDef) bool { if c.Tool == "knife" { return true } for _, e := range c.Consume { for _, id := range e.Items { if fletchLogItems[id] { - if strings.Contains(item.ID, "shaft") || strings.Contains(item.ID, "bow") { + if strings.Contains(def.ID, "shaft") || strings.Contains(def.ID, "bow") { return true } } @@ -41,8 +41,8 @@ func fletchNeedsKnife(item *object.ItemDef) bool { }) != nil } -func fletchNeedsChisel(item *object.ItemDef) bool { - return item.FindCraft(func(c object.CraftDef) bool { +func fletchNeedsChisel(def *item.ItemDef) bool { + return def.FindCraft(func(c item.CraftDef) bool { if c.Tool == "chisel" { return true } @@ -60,7 +60,7 @@ func fletchNeedsChisel(item *object.ItemDef) bool { func (g *Game) doFletch(sess *net.Session, input string) { p := sess.Player - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } @@ -103,13 +103,13 @@ func (g *Game) doFletch(sess *net.Session, input string) { g.showProductionTable(sess, p, available, "Fletching", "fletching", "last_fletch", "Fletch", true) } -func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, items []*object.ItemDef, input string) { +func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, items []*item.ItemDef, input string) { qty, productName := parseQty(input) productName = strings.TrimSpace(productName) - var matched []*object.ItemDef + var matched []*item.ItemDef for _, item := range items { - if object.WordPrefixMatch(productName, item.Name) { + if behavior.WordPrefixMatch(productName, item.Name) { matched = append(matched, item) } } @@ -119,7 +119,7 @@ func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, items []*o return } - var available []*object.ItemDef + var available []*item.ItemDef for _, item := range matched { if g.canDoFletchItem(p, item) { available = append(available, item) @@ -139,7 +139,7 @@ func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, items []*o g.showProductionTable(sess, p, available, "Fletching", "fletching", "last_fletch", "Fletch", true) } -func (g *Game) hasFletchTools(p *player.Player, item *object.ItemDef) bool { +func (g *Game) hasFletchTools(p *player.Player, item *item.ItemDef) bool { c := item.FirstCraft() if c.Tool != "" { return g.hasToolType(p, c.Tool) @@ -153,27 +153,27 @@ func (g *Game) hasFletchTools(p *player.Player, item *object.ItemDef) bool { return true } -func (g *Game) canDoFletchItem(p *player.Player, item *object.ItemDef) bool { - return item.FindCraft(func(c object.CraftDef) bool { +func (g *Game) canDoFletchItem(p *player.Player, def *item.ItemDef) bool { + return def.FindCraft(func(c item.CraftDef) bool { return p.Level(player.Fletching) >= c.Level && craftHasAllItemsQty(&c, p.CountItem) - }) != nil && g.hasFletchTools(p, item) + }) != nil && g.hasFletchTools(p, def) } -func (g *Game) availableFletchItems(p *player.Player, allItems []*object.ItemDef) []*object.ItemDef { - var result []*object.ItemDef - for _, item := range allItems { - match := item.FindCraft(func(c object.CraftDef) bool { +func (g *Game) availableFletchItems(p *player.Player, allItems []*item.ItemDef) []*item.ItemDef { + var result []*item.ItemDef + for _, def := range allItems { + match := def.FindCraft(func(c item.CraftDef) bool { return craftHasAllItemsQty(&c, p.CountItem) }) - if match != nil && g.hasFletchTools(p, item) { - result = append(result, item) + if match != nil && g.hasFletchTools(p, def) { + result = append(result, def) } } return result } -func (g *Game) findFletchItems(p *player.Player, itemAID, itemBID string) []*object.ItemDef { +func (g *Game) findFletchItems(p *player.Player, itemAID, itemBID string) []*item.ItemDef { items := g.CraftIndex.ByType("fletching") toolTypeA := "" @@ -187,7 +187,7 @@ func (g *Game) findFletchItems(p *player.Player, itemAID, itemBID string) []*obj isToolA := toolTypeA == "knife" || toolTypeA == "chisel" isToolB := toolTypeB == "knife" || toolTypeB == "chisel" - var matched []*object.ItemDef + var matched []*item.ItemDef for _, item := range items { c := item.FirstCraft() diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go index fe5d322..ea1c667 100644 --- a/internal/game/cmd_get.go +++ b/internal/game/cmd_get.go @@ -97,9 +97,9 @@ func (g *Game) doGet(sess *net.Session, input string) { p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty)) g.AccountStore.SaveCharacter(p) if qty == 1 { - sess.WriteLine(fmt.Sprintf("You pick up a %s.", g.itemColorize(sess, def, def.Name))) - } else { - sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", qty, g.itemColorize(sess, def, def.Name))) + sess.WriteLine(fmt.Sprintf("You pick up a %s.", g.itemColorize(sess, def, def.Name))) + } else { + sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", qty, g.itemColorize(sess, def, def.Name))) } return } diff --git a/internal/game/cmd_hide.go b/internal/game/cmd_hide.go index d22f3fa..b5d38fe 100644 --- a/internal/game/cmd_hide.go +++ b/internal/game/cmd_hide.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/world" ) @@ -91,7 +90,7 @@ func (g *Game) executeHide(sess *net.Session, args []string, rawInput string) { st.SafespotTicksAtLevel = 0 } - inCombat := combat.GetCombat(p.Name) != nil + inCombat := g.Combat.Get(p.Name) != nil hideCountdown := 1 if inCombat { hideCountdown = 4 diff --git a/internal/game/cmd_id.go b/internal/game/cmd_id.go index 3ddfe33..87adce7 100644 --- a/internal/game/cmd_id.go +++ b/internal/game/cmd_id.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) @@ -134,12 +134,12 @@ func (g *Game) doIdentify(sess *net.Session, input string) { altarName = def.Name } - p.Action = &action.Action{ + p.Action = &behavior.Action{ Type: "identify", TargetID: found.AltarID, TargetName: altarName, WaitLeft: 1, - Data: &action.IdentifyData{ + Data: &behavior.IdentifyData{ JunkID: found.JunkID, XPPer: found.XPPer, }, diff --git a/internal/game/cmd_jack.go b/internal/game/cmd_jack.go index ea31b7d..a0f102e 100644 --- a/internal/game/cmd_jack.go +++ b/internal/game/cmd_jack.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/game/hacking" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" @@ -19,7 +18,7 @@ func (g *Game) executeJack(sess *net.Session, args []string, rawInput string) { func (g *Game) doJack(sess *net.Session, target string) { p := sess.Player - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 5cdc15d..a1ad708 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -7,9 +7,7 @@ import ( "strings" "thehouseoficarus/internal/color" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) @@ -50,721 +48,6 @@ func (g *Game) doLook(sess *net.Session) { g.showRoomPlayers(sess, p, room) } -func (g *Game) showRoomName(sess *net.Session, p *player.Player, room *world.Room) { - sess.WriteLines( - "", - fmt.Sprintf("%s (%s)", g.colorize(sess, "room_name", room.Name), g.colorize(sess, "room_number", fmt.Sprintf("#%d", room.ID))), - ) -} - -func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) []string { - descWidth := p.OptionInt("room_desc_width") - if descWidth <= 0 { - descWidth = 70 - } - rawLines := wrapText(g.roomDescription(sess, room), descWidth) - mode := g.colorMode(sess) - roomDescSpec := g.resolveColor(sess, "room_desc") - lines := make([]string, len(rawLines)) - for i, l := range rawLines { - lines[i] = color.ExpandTagsDefault(mode, roomDescSpec, l) - } - return lines -} - -// roomDescription resolves the room's effective description for this player, -// picking the first conditional variant whose condition passes, else the base -// Description. -func (g *Game) roomDescription(sess *net.Session, room *world.Room) string { - for _, d := range room.Descriptions { - if d.Condition == nil || g.checkCondition(sess, d.Condition) { - return d.Text - } - } - return room.Description -} - -// resolveObjDesc resolves the description an object shows to this player. For an -// object with conditional Descriptions, the first variant whose condition -// passes wins; if none pass the object is reported as not present (false), so -// look falls through as if it were not there. Objects without Descriptions use -// their plain Description and are always present. -func (g *Game) resolveObjDesc(sess *net.Session, def *object.ObjectDef) (string, bool) { - if len(def.Descriptions) == 0 { - return def.Description, true - } - for _, d := range def.Descriptions { - if d.Condition == nil || g.checkCondition(sess, d.Condition) { - return d.Text, true - } - } - return "", false -} - -func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) []string { - mobs := g.MobStore.MobsInRoom(p.RoomID) - if len(mobs) == 0 { - return nil - } - sort.Slice(mobs, func(i, j int) bool { - iDamaged := mobs[i].HP < mobs[i].MaxHP - jDamaged := mobs[j].HP < mobs[j].MaxHP - if iDamaged != jDamaged { - return iDamaged - } - return mobs[i].InstanceID < mobs[j].InstanceID - }) - var lines []string - lines = append(lines, "") - playerLevel := p.CombatLevel() - for _, m := range mobs { - hp := "" - if m.HP < m.MaxHP { - if m.IsTask() { - pct := 0 - if m.MaxHP > 0 { - pct = (m.MaxHP - m.HP) * 100 / m.MaxHP - } - hp = fmt.Sprintf(" [%d%% complete]", pct) - } else { - hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) - } - } - var desc string - if combat.IsMobInCombat(m.InstanceID) { - def, err := g.MobStore.LoadDef(m.DefID) - if err == nil && len(def.CombatDescriptions) > 0 { - target := combat.GetMobTarget(m.InstanceID) - pattern := def.CombatDescriptions[randInt(len(def.CombatDescriptions))] - desc = " " + fmt.Sprintf(pattern, target) - } - } else if m.IdleDescription != "" { - desc = fmt.Sprintf(" %s", m.IdleDescription) - } - displayName := m.Name - if !m.Unique { - displayName = "A " + m.Name - } - mobColor := "mob" - if m.Protected { - mobColor = "protected_mob" - } - mobLevel := mobCombatLevel(m) - levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel)) - lines = append(lines, fmt.Sprintf("%s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc)) - } - return lines -} - -func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) []string { - objs := g.World.AllObjInstances(p.RoomID) - if len(objs) == 0 { - return g.showFarmPatches(sess, p) - } - var lines []string - lines = append(lines, "") - grouped := make(map[string]int) - var order []string - for _, o := range objs { - if _, ok := grouped[o.DefID]; !ok { - order = append(order, o.DefID) - } - grouped[o.DefID]++ - } - for _, objID := range order { - count := grouped[objID] - def, err := g.ObjectStore.Load(objID) - if err != nil { - continue - } - - if def.Hidden { - continue - } - - if farmPatchDefIDs[objID] { - continue - } - - type instInfo struct { - idx int - depleted bool - sharedMax int - sharedCur int - respawnIn int - quality int - } - var instances []instInfo - for i := 0; i < count; i++ { - st := g.World.GetObjState(p.RoomID, objID, i) - if st == nil { - continue - } - instances = append(instances, instInfo{ - idx: i + 1, - depleted: st.Depleted, - sharedMax: int(st.SharedMax), - sharedCur: st.SharedTimer, - respawnIn: int(st.DepleteTimer), - quality: int(st.Quality), - }) - } - multi := len(instances) > 1 - showTimers := p.OptionBool("depletion") - - var freshIdxs []int - var timed, depleted []instInfo - for _, ins := range instances { - if ins.depleted { - depleted = append(depleted, ins) - } else if ins.sharedMax > 0 && ins.sharedCur < ins.sharedMax { - timed = append(timed, ins) - } else { - freshIdxs = append(freshIdxs, ins.idx) - } - } - - roomDesc := def.InRoomDescription - if roomDesc != "" { - roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc) - } - coloredName := g.objColorize(sess, def, def.Name) - coloredPlural := g.objColorize(sess, def, def.Name+"s") - - if len(freshIdxs) > 0 { - var line string - if roomDesc != "" { - line = roomDesc - } else if len(freshIdxs) == 1 { - line = fmt.Sprintf("A %s is here.", coloredName) - } else { - line = fmt.Sprintf("%d %s are here.", len(freshIdxs), coloredPlural) - } - var suffix string - if multi && (len(timed) > 0 || len(depleted) > 0) { - suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs)) - } - qualityTimer := "" - if showTimers && len(instances) > 0 && instances[0].quality > 0 { - qualityTimer = fmt.Sprintf(" (Burning for %d more ticks)", instances[0].quality) - } - lines = append(lines, fmt.Sprintf("%s%s%s", line, suffix, qualityTimer)) - } - - for _, ins := range timed { - var line string - if roomDesc != "" { - line = roomDesc - } else { - line = fmt.Sprintf("A %s is here.", coloredName) - } - tag := "" - if multi { - tag = fmt.Sprintf(" [%d]", ins.idx) - } - timer := "" - if showTimers { - timer = fmt.Sprintf(" (despawn: %d/%d)", ins.sharedCur, ins.sharedMax) - } - lines = append(lines, fmt.Sprintf("%s%s%s", line, tag, timer)) - } - - for _, ins := range depleted { - var line string - if roomDesc != "" { - line = roomDesc - } else { - line = fmt.Sprintf("A %s is here.", coloredName) - } - tag := "" - if multi { - tag = fmt.Sprintf(" [%d]", ins.idx) - } - timer := "" - if showTimers { - timer = fmt.Sprintf(" (respawns in %d ticks)", ins.respawnIn) - } - lines = append(lines, fmt.Sprintf("%s (depleted)%s%s", line, tag, timer)) - } - } - - lines = append(lines, g.showFarmPatches(sess, p)...) - return lines -} - -func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) []string { - ground := g.World.GroundItemsDetailed(p.RoomID) - if len(ground) == 0 { - return nil - } - showDespawn := p.OptionBool("despawn") - showReserve := p.OptionBool("reserve") - - type displayLine struct { - name string - quantity int - colorName string - annotation string - } - - type groupKey struct { - itemID string - despawnTimer int - } - - var lines []displayLine - groups := make(map[groupKey]*displayLine) - var groupOrder []groupKey - - for _, info := range ground { - def, err := g.ItemStore.Load(info.ItemID) - name := info.ItemID - if err == nil { - name = def.Name - } - coloredName := g.itemColorize(sess, def, name) - - if info.ReservedFor != "" { - var parts []string - if showReserve { - parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) - } else { - parts = append(parts, "reserved") - } - if showDespawn && !info.IsSpawn { - parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) - } - annotation := "" - if len(parts) > 0 { - annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) - } - lines = append(lines, displayLine{ - name: name, - quantity: info.Quantity, - colorName: coloredName, - annotation: annotation, - }) - continue - } - - timerKey := -1 - if showDespawn && !info.IsSpawn { - timerKey = info.DespawnTimer - } - key := groupKey{itemID: info.ItemID, despawnTimer: timerKey} - - if existing, ok := groups[key]; ok { - existing.quantity += info.Quantity - } else { - annotation := "" - if showDespawn && !info.IsSpawn { - annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer) - } - dl := &displayLine{ - name: name, - quantity: info.Quantity, - colorName: coloredName, - annotation: annotation, - } - groups[key] = dl - groupOrder = append(groupOrder, key) - } - } - - for _, key := range groupOrder { - lines = append(lines, *groups[key]) - } - - sort.Slice(lines, func(i, j int) bool { - return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) - }) - - var out []string - out = append(out, "") - out = append(out, "On the ground:") - - type fmtLine struct { - prefix string - annotation string - } - var fmtLines []fmtLine - maxPrefix := 0 - - for _, dl := range lines { - prefix := "" - if dl.quantity > 1 { - prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName) - } else { - prefix = fmt.Sprintf(" %s", dl.colorName) - } - if visibleLen(prefix) > maxPrefix { - maxPrefix = visibleLen(prefix) - } - fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation}) - } - - for _, l := range fmtLines { - if l.annotation != "" { - pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix)) - out = append(out, fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation)) - } else { - out = append(out, l.prefix) - } - } - return out -} - -func (g *Game) showRoomExits(sess *net.Session, p *player.Player, room *world.Room) { - if len(room.Exits) > 0 { - sess.WriteLine("") - if p.OptionBool("exits") { - sess.WriteLine("Exits:") - type exitLine struct { - dir string - targetName string - } - var lines []exitLine - maxDirLen := 0 - for _, dir := range world.ExitOrder { - exitDef, ok := room.Exits[dir] - if !ok { - continue - } - coloredDir := g.colorize(sess, "exit_direction", string(dir)) - targetRoom, err := g.World.LoadRoom(exitDef.Room) - targetName := g.colorize(sess, "room_number", fmt.Sprintf("#%d", exitDef.Room)) - if err == nil { - targetName = g.colorize(sess, "exit_name", targetRoom.Name) - } - if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { - targetName += " (blocked)" - } - lines = append(lines, exitLine{coloredDir, targetName}) - if visibleLen(coloredDir) > maxDirLen { - maxDirLen = visibleLen(coloredDir) - } - } - for _, l := range lines { - pad := maxDirLen + (len(l.dir) - visibleLen(l.dir)) - sess.WriteLine(fmt.Sprintf(" %-*s - %s", pad, l.dir, l.targetName)) - } - } else { - sess.Write("Exits: ") - first := true - for _, dir := range world.ExitOrder { - if _, ok := room.Exits[dir]; ok { - if !first { - sess.Write(", ") - } - sess.Write(g.colorize(sess, "exit_direction", string(dir))) - first = false - } - } - sess.WriteLine("") - } - } -} - -func (g *Game) showRoomPlayers(sess *net.Session, p *player.Player, room *world.Room) { - others := g.Hub.PlayersInRoom(p.RoomID) - for _, other := range others { - if other != sess && other.Player != nil { - op := other.Player - line := fmt.Sprintf("\n%s is here", g.colorize(sess, "player_name", op.Name)) - if cs := combat.GetCombat(op.Name); cs != nil { - if mob := g.MobStore.GetInstance(cs.MobID); mob != nil && mob.HP > 0 { - name := mobDisplayName(mob, false) - if idx := mobInstanceIdx(mob, g.MobStore.MobsInRoom(p.RoomID)); idx > 0 { - name += fmt.Sprintf(" [%d]", idx) - } - line += fmt.Sprintf(" (fighting %s)", name) - } - } else if desc := g.playerActionDisplay(op); desc != "" { - line += ", " + desc - } - sess.WriteLine(line + ".") - } - } -} - -func (g *Game) doLookTarget(sess *net.Session, input string) { - p := sess.Player - lower := strings.ToLower(input) - - if exitDir := g.World.ResolveExit(lower); exitDir != "" { - room, err := g.World.LoadRoom(p.RoomID) - if err != nil { - sess.WriteLine("You can't see anything that way.") - return - } - exitDef, ok := room.Exits[exitDir] - if !ok { - sess.WriteLine("You can't see anything that way.") - return - } - if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { - msg := exitDef.BlockedMessage - if msg == "" { - msg = fmt.Sprintf("The way %s is blocked.", exitDir) - } - sess.WriteLine(msg) - return - } - g.World.SeedGroundItems(exitDef.Room) - g.seedRoomMobs(exitDef.Room) - origRoom := p.RoomID - p.RoomID = exitDef.Room - g.doLook(sess) - p.RoomID = origRoom - return - } - - var best *world.MobInstance - bestQ := world.MatchNone - for _, m := range g.MobStore.MobsInRoom(p.RoomID) { - q := m.MatchQuality(lower) - if q > bestQ { - bestQ = q - best = m - } - } - if best != nil { - mobColor := "mob" - if best.Protected { - mobColor = "protected_mob" - } - mobLevel := mobCombatLevel(best) - levelStr := g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel)) - sess.WriteLines( - "", - fmt.Sprintf("%s %s", g.colorize(sess, mobColor, best.Name), levelStr), - ) - if best.IdleDescription != "" { - sess.WriteLine(best.IdleDescription) - } - sess.WriteLines( - "", - fmt.Sprintf("Attack: %d", best.Attack), - fmt.Sprintf("Strength: %d", best.Strength), - fmt.Sprintf("Defense: %d", best.Defense), - fmt.Sprintf("HP: %d/%d", best.HP, best.MaxHP), - ) - if best.StabDefense != 0 || best.SlashDefense != 0 || best.CrushDefense != 0 || - best.ScienceDefense != 0 || best.RangedDefense != 0 { - sess.WriteLines( - "", - "Defense bonuses:", - fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense), - fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense), - ) - } - if best.Weakness != "" { - sess.WriteLine(fmt.Sprintf("Weakness: %s", best.Weakness)) - } - return - } - - rawInstances := g.World.FindObjInstances(p.RoomID, lower) - - // Group matched object instances by definition, keeping only those visible - // to this player (an object whose conditional descriptions all fail is - // absent). If more than one distinct object matches, disambiguate. - var presentDefs []string - byDef := map[string][]world.ObjState{} - descByDef := map[string]string{} - for _, ist := range rawInstances { - def, err := g.ObjectStore.Load(ist.DefID) - if err != nil { - continue - } - text, present := g.resolveObjDesc(sess, def) - if !present { - continue - } - if _, seen := byDef[ist.DefID]; !seen { - presentDefs = append(presentDefs, ist.DefID) - descByDef[ist.DefID] = text - } - byDef[ist.DefID] = append(byDef[ist.DefID], ist) - } - - if len(presentDefs) > 1 { - sess.WriteLine("That's ambiguous, which one?") - for _, defID := range presentDefs { - if def, err := g.ObjectStore.Load(defID); err == nil { - sess.WriteLine(fmt.Sprintf(" %s", def.Name)) - } - } - return - } - - if len(presentDefs) == 1 { - instances := byDef[presentDefs[0]] - objDescText := descByDef[presentDefs[0]] - st := &instances[0] - def, _ := g.ObjectStore.Load(st.DefID) - - if st.DefID == "estate_directory" { - g.lookEstateDirectory(sess) - return - } - - sess.WriteLine("") - if len(instances) > 1 { - sess.WriteLine(fmt.Sprintf("%d %ss:", len(instances), def.Name)) - } - - if objDescText != "" && !strings.Contains(objDescText, "{quality}") { - sess.WriteLine(color.ExpandTags(g.colorMode(sess), objDescText)) - } - - if def.Safespot != nil { - realSt := g.World.GetObjState(p.RoomID, st.DefID, st.Index) - if realSt != nil { - levelStr := "intact" - if realSt.SafespotLevel <= 0 { - levelStr = fmt.Sprintf("intact (level %d/%d)", len(def.Safespot.Levels), len(def.Safespot.Levels)) - } else if realSt.SafespotLevel < len(def.Safespot.Levels) { - levelStr = fmt.Sprintf("degraded (level %d/%d)", realSt.SafespotLevel, len(def.Safespot.Levels)) - } else { - levelStr = fmt.Sprintf("intact (level %d/%d)", realSt.SafespotLevel, len(def.Safespot.Levels)) - } - blockInfo := "anything" - if def.Safespot.MaxBlockSize != "" { - blockInfo = fmt.Sprintf("up to %s mobs", def.Safespot.MaxBlockSize) - } - sess.WriteLine(fmt.Sprintf("Safespot: %s — blocks %s", levelStr, blockInfo)) - if len(realSt.SafespotOccupants) > 0 { - sess.WriteLine(fmt.Sprintf("Occupied by: %s", strings.Join(realSt.SafespotOccupants, ", "))) - } - } - } - - if p.OptionBool("depletion") { - for _, ist := range instances { - if ist.Depleted { - if len(instances) > 1 { - sess.WriteLine(fmt.Sprintf("%s %d: depleted, respawns in %d ticks.", def.Name, ist.Index+1, int(ist.DepleteTimer))) - } else { - sess.WriteLine(fmt.Sprintf("Depleted, respawns in %d ticks.", int(ist.DepleteTimer))) - } - } else if ist.SharedMax > 0 && float64(ist.SharedTimer) < ist.SharedMax { - if len(instances) > 1 { - sess.WriteLine(fmt.Sprintf("%s %d: despawn timer %d/%.0f.", def.Name, ist.Index+1, ist.SharedTimer, ist.SharedMax)) - } else { - sess.WriteLine(fmt.Sprintf("Despawn timer: %d/%.0f ticks.", ist.SharedTimer, ist.SharedMax)) - } - } - } - } - for _, ist := range instances { - if ist.Quality > 0 && strings.Contains(def.Description, "{quality}") { - desc := strings.ReplaceAll(def.Description, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) - sess.WriteLine(color.ExpandTags(g.colorMode(sess), desc)) - } - } - - farmSuffix := g.farmObjSuffix(p, st.DefID, st.Index) - if farmSuffix != "" { - sess.WriteLine(farmSuffix) - } - return - } - - ground := g.World.GroundItems(p.RoomID) - for itemID := range ground { - def, err := g.ItemStore.Load(itemID) - if err != nil || !def.MatchesName(input) { - continue - } - sess.WriteLines( - "", - g.itemColorize(sess, def, def.Name), - color.ExpandTags(g.colorMode(sess), def.Description), - fmt.Sprintf("Value: %d credits", def.Value), - ) - g.showItemStats(sess, def) - return - } - - for i := 0; i < 28; i++ { - slot := p.InvSlot(i) - if slot == nil { - continue - } - def, err := g.ItemStore.Load(slot.ItemID) - if err != nil || !def.MatchesName(input) { - continue - } - lines := []string{ - "", - g.itemColorize(sess, def, def.Name), - color.ExpandTags(g.colorMode(sess), def.Description), - fmt.Sprintf("Value: %d credits", def.Value), - } - if slot.MaxQuality > 0 { - lines = append(lines, fmt.Sprintf("Has %d units of butane left.", slot.Quality)) - } - sess.WriteLines(lines...) - g.showItemStats(sess, def) - return - } - - others := g.Hub.PlayersInRoom(p.RoomID) - for _, other := range others { - if other == sess || other.Player == nil { - continue - } - op := other.Player - if strings.ToLower(op.Name) != lower { - continue - } - showPlayerInfo(g, sess, op) - return - } - - sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input)) -} - -func showPlayerInfo(g *Game, sess *net.Session, p *player.Player) { - myP := sess.Player - theirLevel := p.CombatLevel() - levelStr := g.levelColorize(sess, myP.CombatLevel(), theirLevel, fmt.Sprint(theirLevel)) - sess.WriteLines( - "", - p.Name, - fmt.Sprintf("Combat Level: %s", levelStr), - fmt.Sprintf("HP: %d/%d", p.HP, p.MaxHP()), - "", - ) - - for _, s := range player.AllSkills { - level := p.Level(s) - sess.WriteLine(fmt.Sprintf("%-12s Level: %d", s, level)) - } - - sess.WriteLine("") - sess.WriteLine("Equipment:") - for _, slot := range EquipSlots { - itemID, ok := p.Equipment[slot] - if !ok { - continue - } - name := itemID - var itemDef *object.ItemDef - if def, err := g.ItemStore.Load(itemID); err == nil { - name = def.Name - itemDef = def - } - sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, itemDef, name))) - } - - if p.Description != "" { - sess.WriteLine("") - sess.WriteLine(p.Description) - } -} - func (g *Game) doExits(sess *net.Session) { p := sess.Player room, err := g.World.LoadRoom(p.RoomID) @@ -898,56 +181,3 @@ func (g *Game) executeLook(sess *net.Session, args []string, rawInput string) { 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 || - s.ScienceAttack != 0 || s.RangedAttack != 0 - hasDefense := s.StabDefense != 0 || s.SlashDefense != 0 || s.CrushDefense != 0 || - s.ScienceDefense != 0 || s.RangedDefense != 0 - hasOther := s.StrengthBonus != 0 || s.RangedStrength != 0 || - s.ScienceDamage != 0 || s.TechnologyBonus != 0 - - if !hasAttack && !hasDefense && !hasOther { - return - } - - sess.WriteLine("") - if hasAttack || hasDefense { - sess.WriteLine("Attack bonuses: Defense bonuses:") - sess.WriteLine(fmt.Sprintf(" Stab: %+4d Stab: %+4d", s.StabAttack, s.StabDefense)) - sess.WriteLine(fmt.Sprintf(" Slash: %+4d Slash: %+4d", s.SlashAttack, s.SlashDefense)) - sess.WriteLine(fmt.Sprintf(" Crush: %+4d Crush: %+4d", s.CrushAttack, s.CrushDefense)) - sess.WriteLine(fmt.Sprintf(" Science:%+4d Science:%+4d", s.ScienceAttack, s.ScienceDefense)) - sess.WriteLine(fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", s.RangedAttack, s.RangedDefense)) - } - if hasOther { - sess.WriteLine("") - sess.WriteLine("Other bonuses:") - if s.StrengthBonus != 0 { - sess.WriteLine(fmt.Sprintf(" Melee strength: %+d", s.StrengthBonus)) - } - if s.RangedStrength != 0 { - sess.WriteLine(fmt.Sprintf(" Ranged strength: %+d", s.RangedStrength)) - } - if s.ScienceDamage != 0 { - sess.WriteLine(fmt.Sprintf(" Science damage: %+d", s.ScienceDamage)) - } - if s.TechnologyBonus != 0 { - sess.WriteLine(fmt.Sprintf(" Technology: %+d", s.TechnologyBonus)) - } - } - if def.AttackType != "" { - sess.WriteLine(fmt.Sprintf("Attack type: %s", def.AttackType)) - } - if def.Speed > 0 { - sess.WriteLine(fmt.Sprintf("Speed: %.0f", def.Speed)) - } - if len(def.Requirements) > 0 { - sess.WriteLine("") - sess.WriteLine("Requirements:") - for skill, level := range def.Requirements { - sess.WriteLine(fmt.Sprintf(" %s: %d", skill, level)) - } - } -} diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go index 438e11a..54c8067 100644 --- a/internal/game/cmd_mods.go +++ b/internal/game/cmd_mods.go @@ -7,6 +7,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" ) func (g *Game) executeMods(sess *net.Session, args []string, rawInput string) { @@ -51,7 +52,7 @@ func (g *Game) doMods(sess *net.Session, showAll bool) { return catMods[i].Level < catMods[j].Level }) - t := &Table{ + t := &ui.Table{ Title: cat.Name, Columns: []string{ color.Render(mode, color.Parse("75"), "Module"), diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index 2939702..63f9bcd 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -4,10 +4,9 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -70,7 +69,7 @@ func (g *Game) doMove(sess *net.Session, dir string, multiplier float64) { p.MovePendingFlags = exitDef.SetFlags p.MovePendingPlayerFlags = exitDef.SetPlayerFlags - inCombat := combat.GetCombat(p.Name) != nil + inCombat := g.Combat.Get(p.Name) != nil if !inCombat && p.Action != nil { g.cancelAction(p) @@ -106,7 +105,7 @@ var gracefulItems = map[string]bool{ } func (g *Game) moveTicks(p *player.Player, multiplier float64) int { - if id, ok := p.Equipment[object.SlotBack]; ok && id == "cape_of_agility" { + if id, ok := p.Equipment[item.SlotBack]; ok && id == "cape_of_agility" { return 0 } @@ -134,7 +133,7 @@ func (g *Game) completeMove(sess *net.Session, p *player.Player) { p.ClearMoveState() - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { g.stopCombat(p.Name) } diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go index a5bf11c..05a6ece 100644 --- a/internal/game/cmd_option.go +++ b/internal/game/cmd_option.go @@ -8,6 +8,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" ) func (g *Game) doOption(sess *net.Session, input string) { @@ -16,7 +17,7 @@ func (g *Game) doOption(sess *net.Session, input string) { if input == "" { sess.WriteLine("") - table := &Table{ + table := &ui.Table{ Columns: []string{ color.Render(mode, color.Parse("75"), "Option"), color.Render(mode, color.Parse("230"), "Value"), diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go index 4599614..2740275 100644 --- a/internal/game/cmd_quit.go +++ b/internal/game/cmd_quit.go @@ -1,7 +1,6 @@ package game import ( - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" ) @@ -13,7 +12,7 @@ func (g *Game) executeQuit(sess *net.Session, args []string, rawInput string) { func (g *Game) doQuit(sess *net.Session) { p := sess.Player - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't rest during combat!") return } @@ -36,8 +35,8 @@ func (g *Game) doQuit(sess *net.Session) { g.cancelRest(p.Name) g.AccountStore.SaveCharacter(p) g.charsMu.Lock() - delete(g.loggedInChars, p.Name) - g.charsMu.Unlock() + delete(g.loggedInChars, p.Name) + g.charsMu.Unlock() if g.Hub != nil { g.Hub.LeaveRoom(sess) } diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index bc910ed..7ac9d0b 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -15,124 +15,124 @@ type commandDef struct { } var commandRegistry = map[string]commandDef{ - "get": {(*Game).executeGet, ClassActive}, - "take": {(*Game).executeGet, ClassActive}, - "grab": {(*Game).executeGet, ClassActive}, - "pick": {(*Game).executeGet, ClassActive}, - "drop": {(*Game).executeDrop, ClassActive}, - "attack": {(*Game).executeAttack, ClassActive}, - "kill": {(*Game).executeAttack, ClassActive}, - "work": {(*Game).executeWork, ClassActive}, - "style": {(*Game).executeStyle, ClassInstant}, - "look": {(*Game).executeLook, ClassInstant}, - "l": {(*Game).executeLook, ClassInstant}, - "north": {(*Game).executeMove, ClassActive}, - "n": {(*Game).executeMove, ClassActive}, - "south": {(*Game).executeMove, ClassActive}, - "s": {(*Game).executeMove, ClassActive}, - "east": {(*Game).executeMove, ClassActive}, - "e": {(*Game).executeMove, ClassActive}, - "west": {(*Game).executeMove, ClassActive}, - "w": {(*Game).executeMove, ClassActive}, - "up": {(*Game).executeMove, ClassActive}, - "u": {(*Game).executeMove, ClassActive}, - "down": {(*Game).executeMove, ClassActive}, - "d": {(*Game).executeMove, ClassActive}, - "say": {(*Game).executeSay, ClassInstant}, - "global": {(*Game).executeGlobal, ClassInstant}, - "who": {(*Game).executeWho, ClassInstant}, - "sc": {(*Game).executeScore, ClassInstant}, - "score": {(*Game).executeScore, ClassInstant}, - "skills": {(*Game).executeSkills, ClassInstant}, - "sk": {(*Game).executeSkills, ClassInstant}, - "task": {(*Game).executeTask, ClassInstant}, - "stats": {(*Game).executeStats, ClassInstant}, - "tech": {(*Game).executeTech, ClassInstant}, - "t": {(*Game).executeTech, ClassInstant}, - "i": {(*Game).executeInventory, ClassInstant}, - "inv": {(*Game).executeInventory, ClassInstant}, - "inventory": {(*Game).executeInventory, ClassInstant}, - "quit": {(*Game).executeQuit, ClassActive}, + "get": {(*Game).executeGet, ClassActive}, + "take": {(*Game).executeGet, ClassActive}, + "grab": {(*Game).executeGet, ClassActive}, + "pick": {(*Game).executeGet, ClassActive}, + "drop": {(*Game).executeDrop, ClassActive}, + "attack": {(*Game).executeAttack, ClassActive}, + "kill": {(*Game).executeAttack, ClassActive}, + "work": {(*Game).executeWork, ClassActive}, + "style": {(*Game).executeStyle, ClassInstant}, + "look": {(*Game).executeLook, ClassInstant}, + "l": {(*Game).executeLook, ClassInstant}, + "north": {(*Game).executeMove, ClassActive}, + "n": {(*Game).executeMove, ClassActive}, + "south": {(*Game).executeMove, ClassActive}, + "s": {(*Game).executeMove, ClassActive}, + "east": {(*Game).executeMove, ClassActive}, + "e": {(*Game).executeMove, ClassActive}, + "west": {(*Game).executeMove, ClassActive}, + "w": {(*Game).executeMove, ClassActive}, + "up": {(*Game).executeMove, ClassActive}, + "u": {(*Game).executeMove, ClassActive}, + "down": {(*Game).executeMove, ClassActive}, + "d": {(*Game).executeMove, ClassActive}, + "say": {(*Game).executeSay, ClassInstant}, + "global": {(*Game).executeGlobal, ClassInstant}, + "who": {(*Game).executeWho, ClassInstant}, + "sc": {(*Game).executeScore, ClassInstant}, + "score": {(*Game).executeScore, ClassInstant}, + "skills": {(*Game).executeSkills, ClassInstant}, + "sk": {(*Game).executeSkills, ClassInstant}, + "task": {(*Game).executeTask, ClassInstant}, + "stats": {(*Game).executeStats, ClassInstant}, + "tech": {(*Game).executeTech, ClassInstant}, + "t": {(*Game).executeTech, ClassInstant}, + "i": {(*Game).executeInventory, ClassInstant}, + "inv": {(*Game).executeInventory, ClassInstant}, + "inventory": {(*Game).executeInventory, ClassInstant}, + "quit": {(*Game).executeQuit, ClassActive}, "description": {(*Game).executeDescription, ClassInstant}, - "desc": {(*Game).executeDescription, ClassInstant}, - "option": {(*Game).executeOption, ClassInstant}, - "options": {(*Game).executeOption, ClassInstant}, - "color": {(*Game).executeColor, ClassInstant}, - "colors": {(*Game).executeColor, ClassInstant}, - "colortable": {(*Game).executeColortable, ClassInstant}, - "prompt": {(*Game).executePrompt, ClassInstant}, - "exits": {(*Game).executeExits, ClassInstant}, - "map": {(*Game).executeMap, ClassInstant}, - "symbol": {(*Game).executeSymbol, ClassInstant}, - "queued": {(*Game).executeQueued, ClassInstant}, - "help": {(*Game).executeHelp, ClassInstant}, - "mine": {(*Game).executeGather, ClassActive}, - "chop": {(*Game).executeGather, ClassActive}, - "fish": {(*Game).executeGather, ClassActive}, - "cut": {(*Game).executeGather, ClassActive}, - "pull": {(*Game).executeUse, ClassActive}, - "push": {(*Game).executeUse, ClassActive}, - "use": {(*Game).executeUse, ClassActive}, - "cook": {(*Game).executeCook, ClassActive}, - "smelt": {(*Game).executeSmelt, ClassActive}, - "smith": {(*Game).executeSmith, ClassActive}, - "craft": {(*Game).executeCraft, ClassActive}, - "eat": {(*Game).executeEat, ClassFree}, - "drink": {(*Game).executeDrink, ClassFree}, - "fletch": {(*Game).executeFletch, ClassFree}, - "clean": {(*Game).executeClean, ClassFree}, - "mix": {(*Game).executeMix, ClassActive}, - "id": {(*Game).executeIdentify, ClassActive}, - "identify": {(*Game).executeIdentify, ClassActive}, + "desc": {(*Game).executeDescription, ClassInstant}, + "option": {(*Game).executeOption, ClassInstant}, + "options": {(*Game).executeOption, ClassInstant}, + "color": {(*Game).executeColor, ClassInstant}, + "colors": {(*Game).executeColor, ClassInstant}, + "colortable": {(*Game).executeColortable, ClassInstant}, + "prompt": {(*Game).executePrompt, ClassInstant}, + "exits": {(*Game).executeExits, ClassInstant}, + "map": {(*Game).executeMap, ClassInstant}, + "symbol": {(*Game).executeSymbol, ClassInstant}, + "queued": {(*Game).executeQueued, ClassInstant}, + "help": {(*Game).executeHelp, ClassInstant}, + "mine": {(*Game).executeGather, ClassActive}, + "chop": {(*Game).executeGather, ClassActive}, + "fish": {(*Game).executeGather, ClassActive}, + "cut": {(*Game).executeGather, ClassActive}, + "pull": {(*Game).executeUse, ClassActive}, + "push": {(*Game).executeUse, ClassActive}, + "use": {(*Game).executeUse, ClassActive}, + "cook": {(*Game).executeCook, ClassActive}, + "smelt": {(*Game).executeSmelt, ClassActive}, + "smith": {(*Game).executeSmith, ClassActive}, + "craft": {(*Game).executeCraft, ClassActive}, + "eat": {(*Game).executeEat, ClassFree}, + "drink": {(*Game).executeDrink, ClassFree}, + "fletch": {(*Game).executeFletch, ClassFree}, + "clean": {(*Game).executeClean, ClassFree}, + "mix": {(*Game).executeMix, ClassActive}, + "id": {(*Game).executeIdentify, ClassActive}, + "identify": {(*Game).executeIdentify, ClassActive}, "autotrigger": {(*Game).executeAutotrigger, ClassInstant}, - "auto": {(*Game).executeAutotrigger, ClassInstant}, - "mods": {(*Game).executeMods, ClassInstant}, - "modlist": {(*Game).executeMods, ClassInstant}, - "mod": {(*Game).executeMods, ClassInstant}, - "modules": {(*Game).executeMods, ClassInstant}, - "module": {(*Game).executeMods, ClassInstant}, - "sneak": {(*Game).executeSneak, ClassInstant}, - "trigger": {(*Game).executeTrigger, ClassActive}, - "talk": {(*Game).executeTalk, ClassActive}, - "speak": {(*Game).executeTalk, ClassActive}, - "ask": {(*Game).executeTalk, ClassActive}, - "burn": {(*Game).executeBurn, ClassActive}, - "stoke": {(*Game).executeStoke, ClassActive}, - "alias": {(*Game).executeAlias, ClassInstant}, - "unalias": {(*Game).executeUnalias, ClassInstant}, - "search": {(*Game).executeSearch, ClassActive}, - "steal": {(*Game).executeSteal, ClassActive}, - "thieve": {(*Game).executeSteal, ClassActive}, - "plant": {(*Game).executePlant, ClassActive}, - "harvest": {(*Game).executeHarvest, ClassActive}, - "rake": {(*Game).executeRake, ClassActive}, - "water": {(*Game).executeWater, ClassActive}, - "cure": {(*Game).executeCure, ClassActive}, - "inspect": {(*Game).executeInspect, ClassInstant}, - "walk": {(*Game).executeWalk, ClassActive}, - "bank": {(*Game).executeBank, ClassActive}, - "construct": {(*Game).executeConstruct, ClassActive}, - "make": {(*Game).executeConstruct, ClassActive}, - "jack": {(*Game).executeJack, ClassActive}, - "jackin": {(*Game).executeJack, ClassActive}, - "eq": {(*Game).executeWear, ClassFree}, - "equipment": {(*Game).executeWear, ClassFree}, - "equip": {(*Game).executeWear, ClassFree}, - "wear": {(*Game).executeWear, ClassFree}, - "wield": {(*Game).executeWear, ClassFree}, - "remove": {(*Game).executeRemove, ClassFree}, - "unwear": {(*Game).executeRemove, ClassFree}, - "unwield": {(*Game).executeRemove, ClassFree}, - "aps": {(*Game).executeAps, ClassActive}, - "verbs": {(*Game).executeVerbs, ClassInstant}, - "what": {(*Game).executeVerbs, ClassInstant}, - "syntax": {(*Game).executeVerbs, ClassInstant}, - "commands": {(*Game).executeVerbs, ClassInstant}, - "stop": {(*Game).executeStop, ClassInstant}, - "hide": {(*Game).executeHide, ClassActive}, - "safespot": {(*Game).executeHide, ClassActive}, - "unhide": {(*Game).executeUnhide, ClassActive}, - "unsafespot": {(*Game).executeUnhide, ClassActive}, + "auto": {(*Game).executeAutotrigger, ClassInstant}, + "mods": {(*Game).executeMods, ClassInstant}, + "modlist": {(*Game).executeMods, ClassInstant}, + "mod": {(*Game).executeMods, ClassInstant}, + "modules": {(*Game).executeMods, ClassInstant}, + "module": {(*Game).executeMods, ClassInstant}, + "sneak": {(*Game).executeSneak, ClassInstant}, + "trigger": {(*Game).executeTrigger, ClassActive}, + "talk": {(*Game).executeTalk, ClassActive}, + "speak": {(*Game).executeTalk, ClassActive}, + "ask": {(*Game).executeTalk, ClassActive}, + "burn": {(*Game).executeBurn, ClassActive}, + "stoke": {(*Game).executeStoke, ClassActive}, + "alias": {(*Game).executeAlias, ClassInstant}, + "unalias": {(*Game).executeUnalias, ClassInstant}, + "search": {(*Game).executeSearch, ClassActive}, + "steal": {(*Game).executeSteal, ClassActive}, + "thieve": {(*Game).executeSteal, ClassActive}, + "plant": {(*Game).executePlant, ClassActive}, + "harvest": {(*Game).executeHarvest, ClassActive}, + "rake": {(*Game).executeRake, ClassActive}, + "water": {(*Game).executeWater, ClassActive}, + "cure": {(*Game).executeCure, ClassActive}, + "inspect": {(*Game).executeInspect, ClassInstant}, + "walk": {(*Game).executeWalk, ClassActive}, + "bank": {(*Game).executeBank, ClassActive}, + "construct": {(*Game).executeConstruct, ClassActive}, + "make": {(*Game).executeConstruct, ClassActive}, + "jack": {(*Game).executeJack, ClassActive}, + "jackin": {(*Game).executeJack, ClassActive}, + "eq": {(*Game).executeWear, ClassFree}, + "equipment": {(*Game).executeWear, ClassFree}, + "equip": {(*Game).executeWear, ClassFree}, + "wear": {(*Game).executeWear, ClassFree}, + "wield": {(*Game).executeWear, ClassFree}, + "remove": {(*Game).executeRemove, ClassFree}, + "unwear": {(*Game).executeRemove, ClassFree}, + "unwield": {(*Game).executeRemove, ClassFree}, + "aps": {(*Game).executeAps, ClassActive}, + "verbs": {(*Game).executeVerbs, ClassInstant}, + "what": {(*Game).executeVerbs, ClassInstant}, + "syntax": {(*Game).executeVerbs, ClassInstant}, + "commands": {(*Game).executeVerbs, ClassInstant}, + "stop": {(*Game).executeStop, ClassInstant}, + "hide": {(*Game).executeHide, ClassActive}, + "safespot": {(*Game).executeHide, ClassActive}, + "unhide": {(*Game).executeUnhide, ClassActive}, + "unsafespot": {(*Game).executeUnhide, ClassActive}, } func classifyCommand(cmd string) CommandClass { diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go index c046537..3a451af 100644 --- a/internal/game/cmd_remove.go +++ b/internal/game/cmd_remove.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -30,7 +30,7 @@ func (g *Game) doRemove(sess *net.Session, input string) { qty, itemName := parseQty(input) - var foundSlot object.EquipSlot + var foundSlot item.EquipSlot var foundItemID string for _, slot := range EquipSlots { @@ -59,7 +59,7 @@ func (g *Game) doRemove(sess *net.Session, input string) { return } - if foundSlot == object.SlotAmmo && p.AmmoQty > 0 { + if foundSlot == item.SlotAmmo && p.AmmoQty > 0 { g.removeAmmo(sess, p, qty) return } @@ -91,7 +91,7 @@ func (g *Game) doRemove(sess *net.Session, input string) { } func (g *Game) removeAmmo(sess *net.Session, p *player.Player, qty int) { - ammoID := p.Equipment[object.SlotAmmo] + ammoID := p.Equipment[item.SlotAmmo] removeQty := p.AmmoQty if qty > 0 && qty < removeQty { removeQty = qty @@ -103,7 +103,7 @@ func (g *Game) removeAmmo(sess *net.Session, p *player.Player, qty int) { s.Quantity += removeQty p.AmmoQty -= removeQty if p.AmmoQty <= 0 { - delete(p.Equipment, object.SlotAmmo) + delete(p.Equipment, item.SlotAmmo) p.AmmoQty = 0 } g.AccountStore.SaveCharacter(p) @@ -126,7 +126,7 @@ func (g *Game) removeAmmo(sess *net.Session, p *player.Player, qty int) { p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: ammoID, Quantity: removeQty}) p.AmmoQty -= removeQty if p.AmmoQty <= 0 { - delete(p.Equipment, object.SlotAmmo) + delete(p.Equipment, item.SlotAmmo) p.AmmoQty = 0 } g.AccountStore.SaveCharacter(p) @@ -148,7 +148,7 @@ func (g *Game) doRemoveAll(sess *net.Session) { } needed := len(p.Equipment) - if _, hasAmmo := p.Equipment[object.SlotAmmo]; hasAmmo { + if _, hasAmmo := p.Equipment[item.SlotAmmo]; hasAmmo { needed-- needed++ } @@ -165,7 +165,7 @@ func (g *Game) doRemoveAll(sess *net.Session) { continue } - if slot == object.SlotAmmo && p.AmmoQty > 0 { + if slot == item.SlotAmmo && p.AmmoQty > 0 { g.removeAllAmmo(sess, p, itemID) continue } @@ -198,7 +198,7 @@ func (g *Game) removeAllAmmo(sess *net.Session, p *player.Player, itemID string) s := p.InvSlot(i) if s != nil && s.ItemID == itemID { s.Quantity += p.AmmoQty - delete(p.Equipment, object.SlotAmmo) + delete(p.Equipment, item.SlotAmmo) p.AmmoQty = 0 def, _ := g.ItemStore.Load(itemID) name := itemID @@ -213,7 +213,7 @@ func (g *Game) removeAllAmmo(sess *net.Session, p *player.Player, itemID string) if freeSlot >= 0 { p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: itemID, Quantity: p.AmmoQty}) } - delete(p.Equipment, object.SlotAmmo) + delete(p.Equipment, item.SlotAmmo) p.AmmoQty = 0 def, _ := g.ItemStore.Load(itemID) name := itemID diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go index d4dd823..20c61d9 100644 --- a/internal/game/cmd_score.go +++ b/internal/game/cmd_score.go @@ -8,6 +8,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" ) func (g *Game) executeScore(sess *net.Session, args []string, rawInput string) { @@ -49,10 +50,10 @@ func (g *Game) doScore(sess *net.Session) { player.Fishing: "Fishing", player.Woodcutting: "Woodcutting", player.Mining: "Mining", player.Hacking: "Hacking", player.Scavenging: "Scavenging", player.Farming: "Farming", player.Thieving: "Thieving", - player.Cooking: "Cooking", player.Smithing: "Smithing", player.Crafting: "Crafting", + player.Cooking: "Cooking", player.Smithing: "Smithing", player.Crafting: "Crafting", player.Fletching: "Fletching", player.Pharmacy: "Pharmacy", player.Construction: "Construction", player.Firemaking: "Firemaking", - player.Agility: "Agility", + player.Agility: "Agility", } type skillCat struct { @@ -105,7 +106,7 @@ func (g *Game) doScore(sess *net.Session) { gap() hpStyle := hpBarStyle(p.HP, p.MaxHP()) - hpBarStr := RenderColoredBar(p.HP, p.MaxHP(), 26, unicode, hpStyle, mode) + hpBarStr := ui.RenderColoredBar(p.HP, p.MaxHP(), 26, unicode, hpStyle, mode) hpFg := hpBarFg(p.HP, p.MaxHP()) hpLine := fmt.Sprintf("HP [%s] %s / %s", hpBarStr, @@ -113,7 +114,7 @@ func (g *Game) doScore(sess *net.Session) { color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP()))) content(hpLine) - batBarRaw := RenderBarF(p.Battery, p.MaxBattery(), 26, unicode) + batBarRaw := ui.RenderBarF(p.Battery, p.MaxBattery(), 26, unicode) batBarStr := color.Render(mode, color.ColorSpec{Fg: 45}, batBarRaw) batLine := fmt.Sprintf("BAT [%s] %s / %s", batBarStr, @@ -242,7 +243,7 @@ func formatInt(n int) string { return strings.Join(parts, ",") } -func hpBarStyle(current, max int) *BarStyle { +func hpBarStyle(current, max int) *ui.BarStyle { if max <= 0 { max = 1 } @@ -256,7 +257,7 @@ func hpBarStyle(current, max int) *BarStyle { default: fg = 196 } - return &BarStyle{FilledColor: fg, EmptyColor: 238, EmptyDim: true} + return &ui.BarStyle{FilledColor: fg, EmptyColor: 238, EmptyDim: true} } func hpBarFg(current, max int) int { @@ -292,7 +293,7 @@ func skillLevelColor(level int) int { func xpProgressBar(xp int, width int, unicode bool) string { currentLevel := player.LevelForXP(xp) if currentLevel >= 99 { - fillRune, _ := barRunes(unicode) + fillRune, _ := ui.BarRunes(unicode) return strings.Repeat(fillRune, width) } xpForLevel := player.XPForLevel(currentLevel) @@ -302,7 +303,7 @@ func xpProgressBar(xp int, width int, unicode bool) string { if needed <= 0 { needed = 1 } - fillRune, emptyRune := barRunes(unicode) + fillRune, emptyRune := ui.BarRunes(unicode) filled := progress * width / needed if filled > width { filled = width diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go index b293ff9..51d0ab1 100644 --- a/internal/game/cmd_shop.go +++ b/internal/game/cmd_shop.go @@ -5,9 +5,10 @@ import ( "strconv" "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" + "thehouseoficarus/internal/ui" ) func (g *Game) handleShopInput(sess *net.Session, input string) { @@ -90,15 +91,15 @@ func (g *Game) writeShopPrompt(sess *net.Session) { sess.Write(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", "> "))) } -func (g *Game) shopConfig(sess *net.Session) *action.ShopConfig { +func (g *Game) shopConfig(sess *net.Session) *behavior.ShopConfig { cfg := sess.Shop return cfg } type shopMatch struct { - si *action.ShopItem + si *behavior.ShopItem name string - def *object.ItemDef + def *item.ItemDef } func (g *Game) showShopBrowse(sess *net.Session) { @@ -121,7 +122,7 @@ func (g *Game) showShopBrowse(sess *net.Session) { unicode = p.OptionBool("unicode") } - t := Table{ + t := ui.Table{ Title: "Shop Inventory", Columns: []string{"Item", "Buy Price", "Sell Price"}, } @@ -149,7 +150,7 @@ func (g *Game) showShopBrowse(sess *net.Session) { } } -func (g *Game) findShopMatches(input string, cfg *action.ShopConfig) []shopMatch { +func (g *Game) findShopMatches(input string, cfg *behavior.ShopConfig) []shopMatch { var matches []shopMatch for i := range cfg.Items { si := &cfg.Items[i] @@ -158,7 +159,7 @@ func (g *Game) findShopMatches(input string, cfg *action.ShopConfig) []shopMatch if def != nil { itemName = def.Name } - if object.WordPrefixMatch(input, itemName) { + if behavior.WordPrefixMatch(input, itemName) { matches = append(matches, shopMatch{si: si, name: itemName, def: def}) } } @@ -216,7 +217,7 @@ func (g *Game) doShopBuy(sess *net.Session, input string, qty int, all bool) { g.buyShopItem(sess, *si, actualQty) } -func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem, qty int) { +func (g *Game) buyShopItem(sess *net.Session, si behavior.ShopItem, qty int) { p := sess.Player totalCost := si.BuyPrice * qty @@ -334,7 +335,7 @@ func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) { match := matches[0] - var shopItem *action.ShopItem + var shopItem *behavior.ShopItem for i := range cfg.Items { si := &cfg.Items[i] if si.ItemID == match.ID && si.SellPrice > 0 { @@ -393,7 +394,7 @@ func (g *Game) leaveShop(sess *net.Session) { return } - td, ok := p.Action.Data.(*action.TalkData) + td, ok := p.Action.Data.(*behavior.TalkData) if !ok || td.Cfg == nil { sess.State = net.StateGame g.cancelAction(p) diff --git a/internal/game/cmd_skills.go b/internal/game/cmd_skills.go index 9d2e247..9dba5b7 100644 --- a/internal/game/cmd_skills.go +++ b/internal/game/cmd_skills.go @@ -6,6 +6,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" ) func (g *Game) executeSkills(sess *net.Session, args []string, rawInput string) { @@ -15,7 +16,7 @@ func (g *Game) executeSkills(sess *net.Session, args []string, rawInput string) func (g *Game) doSkills(sess *net.Session) { p := sess.Player mode := g.colorMode(sess) - t := &Table{Title: "Skills", Columns: []string{ + t := &ui.Table{Title: "Skills", Columns: []string{ color.Render(mode, color.Parse("75"), "Skill"), color.Render(mode, color.Parse("245"), "Abbr"), color.Render(mode, color.Parse("230"), "Level"), diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go index 8ad2391..59e52db 100644 --- a/internal/game/cmd_smelt.go +++ b/internal/game/cmd_smelt.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -44,7 +44,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) { itemID := matches[0].ID - var recipes []*object.ItemDef + var recipes []*item.ItemDef for _, item := range items { c := item.FirstCraft() if !stationMatch(stationDefID, c.Station) || !craftMatchesEntry(c, itemID) { @@ -59,9 +59,9 @@ func (g *Game) doSmelt(sess *net.Session, input string) { g.showRecipeChoice(sess, p, recipes, "You can't smelt that.", "What would you like to smelt?", "") } -func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, items []*object.ItemDef, stationDefID, stationName string) { +func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, items []*item.ItemDef, stationDefID, stationName string) { seen := make(map[string]bool) - var recipes []*object.ItemDef + var recipes []*item.ItemDef for _, item := range items { c := item.FirstCraft() diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go index 712c03c..9722896 100644 --- a/internal/game/cmd_smith.go +++ b/internal/game/cmd_smith.go @@ -5,8 +5,8 @@ import ( "sort" "strings" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -90,7 +90,7 @@ func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string) } allItems := g.CraftIndex.ByType("smithing") - var recipes []*object.ItemDef + var recipes []*item.ItemDef for _, item := range allItems { if craftMatchesEntry(item.FirstCraft(), barID) { recipes = append(recipes, item) @@ -106,7 +106,7 @@ func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string) g.showProductionTable(sess, p, recipes, titleCase(barName)+" Smithing", "smithing", lastFlag, "Produce", false) } -func hasSmithingItems(items []*object.ItemDef, barID string) bool { +func hasSmithingItems(items []*item.ItemDef, barID string) bool { for _, item := range items { if craftMatchesEntry(item.FirstCraft(), barID) { return true @@ -115,7 +115,7 @@ func hasSmithingItems(items []*object.ItemDef, barID string) bool { return false } -func findSmithableBars(items []*object.ItemDef, p *player.Player) []string { +func findSmithableBars(items []*item.ItemDef, p *player.Player) []string { barSet := make(map[string]bool) for _, item := range items { for _, e := range item.FirstCraft().Consume { @@ -142,8 +142,8 @@ func barMenuData(barIDs []string) []map[string]string { return out } -func (g *Game) findSmithItem(p *player.Player, barID string, items []*object.ItemDef) *object.ItemDef { - var makeable []*object.ItemDef +func (g *Game) findSmithItem(p *player.Player, barID string, items []*item.ItemDef) *item.ItemDef { + var makeable []*item.ItemDef skillLevel := p.Level(player.Smithing) for _, item := range items { c := item.FirstCraft() diff --git a/internal/game/cmd_stats.go b/internal/game/cmd_stats.go index 2466a5f..ef5e2ba 100644 --- a/internal/game/cmd_stats.go +++ b/internal/game/cmd_stats.go @@ -4,8 +4,8 @@ import ( "fmt" "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -19,18 +19,18 @@ func (g *Game) doStats(sess *net.Session) { attackType := "crush" weaponName := "unarmed" - var weaponType object.WeaponType - var weaponDef *object.ItemDef - if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + var weaponType item.WeaponType + var weaponDef *item.ItemDef + if itemID, ok := p.Equipment[item.SlotMainHand]; ok { if def, err := g.ItemStore.Load(itemID); err == nil { weaponDef = def weaponName = def.Name weaponType = def.WeaponType if def.AttackType != "" { attackType = def.AttackType - } else if def.WeaponType == object.WeaponRanged { + } else if def.WeaponType == item.WeaponRanged { attackType = "ranged" - } else if def.WeaponType == object.WeaponScience { + } else if def.WeaponType == item.WeaponScience { attackType = "science" } } @@ -58,7 +58,7 @@ func (g *Game) doStats(sess *net.Session) { ) var attRoll, maxHitVal int - if weaponType == object.WeaponRanged { + if weaponType == item.WeaponRanged { rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle)) attRoll = combat.EffectiveRoll(p.Level(player.Ranged), rangedBonus, totals.RangedAttack) maxHitVal = combat.MaxHit(p.Level(player.Ranged), rangedBonus, totals.RangedStrength) diff --git a/internal/game/cmd_stop.go b/internal/game/cmd_stop.go index bfd8fd4..d1b8faa 100644 --- a/internal/game/cmd_stop.go +++ b/internal/game/cmd_stop.go @@ -1,14 +1,13 @@ package game import ( - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" ) func (g *Game) executeStop(sess *net.Session, args []string, rawInput string) { p := sess.Player ss, _ := g.safespot.Get(p.Name) - if p.Action == nil && p.BackgroundAction == nil && combat.GetCombat(p.Name) == nil && p.MoveTicks == 0 && len(p.WalkSequence) == 0 && !ss.Active { + if p.Action == nil && p.BackgroundAction == nil && g.Combat.Get(p.Name) == nil && p.MoveTicks == 0 && len(p.WalkSequence) == 0 && !ss.Active { sess.WriteLine("You're not doing anything.") return } diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go index d4a6adc..cb4c9f7 100644 --- a/internal/game/cmd_tech.go +++ b/internal/game/cmd_tech.go @@ -7,6 +7,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" ) func (g *Game) doTech(sess *net.Session, input string) { @@ -121,7 +122,7 @@ func (g *Game) doTechList(sess *net.Session) { ), ) - t := &Table{Title: "Technology", Columns: []string{"Tech", "Level", "Drain/tick", "Effect", "Status"}} + t := &ui.Table{Title: "Technology", Columns: []string{"Tech", "Level", "Drain/tick", "Effect", "Status"}} for _, tech := range AllTechs { status := "" diff --git a/internal/game/cmd_trigger.go b/internal/game/cmd_trigger.go index 0acbeff..34e438a 100644 --- a/internal/game/cmd_trigger.go +++ b/internal/game/cmd_trigger.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) @@ -65,7 +64,7 @@ func (g *Game) parseTriggerArgs(input string, sciLevel int) (*ModDef, string) { } func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { - if cs := combat.GetCombat(p.Name); cs != nil { + if cs := g.Combat.Get(p.Name); cs != nil { p.QueuedTrigger = mod.ID sess.WriteLine(fmt.Sprintf("You queue %s for your next attack.", mod.Name)) return @@ -101,7 +100,7 @@ func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef return } - if combat.IsMobInCombat(mob.InstanceID) { + if g.Combat.IsMobInCombat(mob.InstanceID) { sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false))) return } diff --git a/internal/game/cmd_trigger_transport.go b/internal/game/cmd_trigger_transport.go index d3f7368..4272a07 100644 --- a/internal/game/cmd_trigger_transport.go +++ b/internal/game/cmd_trigger_transport.go @@ -3,13 +3,12 @@ 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 { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't teleport during combat!") return } diff --git a/internal/game/cmd_trigger_utility.go b/internal/game/cmd_trigger_utility.go index 5733722..0dae505 100644 --- a/internal/game/cmd_trigger_utility.go +++ b/internal/game/cmd_trigger_utility.go @@ -4,9 +4,8 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -26,7 +25,7 @@ func (g *Game) triggerUtility(sess *net.Session, p *player.Player, mod *ModDef, } func (g *Game) triggerProcessing(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } @@ -75,7 +74,7 @@ func (g *Game) triggerProcessing(sess *net.Session, p *player.Player, mod *ModDe } func (g *Game) triggerBonesToNuts(sess *net.Session, p *player.Player, mod *ModDef) { - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } @@ -154,7 +153,7 @@ func (g *Game) triggerEmGrab(sess *net.Session, p *player.Player, mod *ModDef, t } func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } @@ -170,7 +169,7 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef return } - var match *object.ItemDef + var match *item.ItemDef for _, item := range g.CraftIndex.ByType("smelting") { if craftMatchesEntry(item.FirstCraft(), inv.ItemID) { match = item @@ -190,18 +189,18 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef for _, e := range c.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 + qty := e.Quantity + if qty <= 0 { + qty = 1 } - sess.WriteLine(fmt.Sprintf("You need %d %s.", qty, name)) - return + 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 } } } @@ -247,7 +246,7 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef } func (g *Game) triggerPlankMake(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go index a2cddaf..64f35c0 100644 --- a/internal/game/cmd_use.go +++ b/internal/game/cmd_use.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -114,9 +114,9 @@ func (g *Game) useRoomObject(sess *net.Session, p *player.Player, input string) return true } -func (g *Game) stationRecipes(p *player.Player, stationDefID string) []*object.ItemDef { +func (g *Game) stationRecipes(p *player.Player, stationDefID string) []*item.ItemDef { items := g.CraftIndex.ByStation(stationDefID) - var results []*object.ItemDef + var results []*item.ItemDef for _, item := range items { c := item.FirstCraft() if !craftHasAllItemsQty(c, p.CountItem) { @@ -137,7 +137,7 @@ func (g *Game) stationRecipes(p *player.Player, stationDefID string) []*object.I return results } -func (g *Game) showRecipeMenu(sess *net.Session, p *player.Player, items []*object.ItemDef, stationName string) { +func (g *Game) showRecipeMenu(sess *net.Session, p *player.Player, items []*item.ItemDef, stationName string) { if len(items) == 0 { sess.WriteLine(fmt.Sprintf("You don't have anything you can make at the %s.", stationName)) return @@ -214,7 +214,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, cType = recipes[0].FirstCraft().Type } if cType == "smelting" { - var filtered []*object.ItemDef + var filtered []*item.ItemDef for _, item := range recipes { c := item.FirstCraft() if c.Type != "smelting" { @@ -237,7 +237,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, g.promptHowMany(sess, filtered[0].ID) return } - sess.PendingMenu = itemMenuData(itemIDs(filtered)) + sess.PendingMenu = itemMenuData(itemIDs(filtered)) sess.State = net.StateRecipeChoice var names []string for _, item := range filtered { @@ -258,7 +258,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, g.promptHowMany(sess, recipes[0].ID) return } - sess.PendingMenu = itemMenuData(itemIDs(recipes)) + sess.PendingMenu = itemMenuData(itemIDs(recipes)) sess.State = net.StateRecipeChoice var names []string for _, item := range recipes { @@ -328,7 +328,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, } if len(craftItems) > 0 { - var available []*object.ItemDef + var available []*item.ItemDef for _, item := range craftItems { if g.canDoItem(p, item, player.Crafting) { available = append(available, item) @@ -345,7 +345,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, } if len(fletchItems) > 0 { - var available []*object.ItemDef + var available []*item.ItemDef for _, item := range fletchItems { if g.canDoFletchItem(p, item) { available = append(available, item) @@ -362,7 +362,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, } matched := g.CraftIndex.FindByTwoInputs(itemAID, itemBID) - var matchable []*object.ItemDef + var matchable []*item.ItemDef for _, item := range matched { if craftHasAllItemsQty(item.FirstCraft(), p.CountItem) { matchable = append(matchable, item) @@ -373,7 +373,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, return } if len(matchable) > 1 { - sess.PendingMenu = itemMenuData(itemIDs(matchable)) + sess.PendingMenu = itemMenuData(itemIDs(matchable)) sess.State = net.StateRecipeChoice var names []string for _, item := range matchable { diff --git a/internal/game/cmd_verbs.go b/internal/game/cmd_verbs.go index f76ee6c..2ba1a7e 100644 --- a/internal/game/cmd_verbs.go +++ b/internal/game/cmd_verbs.go @@ -3,7 +3,7 @@ package game import ( "strings" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/world" ) @@ -92,7 +92,7 @@ func (g *Game) executeVerbs(sess *net.Session, args []string, rawInput string) { addUnique(§ionObjs, &seen, "use "+name) for _, r := range recipes { if info, ok := productionTypes[r.FirstCraft().Type]; ok { - if info.ActionType != action.TypeCombine && info.ActionType != action.TypeFletch && info.ActionType != action.TypeMix { + if info.ActionType != behavior.TypeCombine && info.ActionType != behavior.TypeFletch && info.ActionType != behavior.TypeMix { addUnique(§ionObjs, &seen, string(info.ActionType)) } } diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go index 8f8117f..64069bc 100644 --- a/internal/game/cmd_wear.go +++ b/internal/game/cmd_wear.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -13,7 +13,7 @@ func (g *Game) executeWear(sess *net.Session, args []string, rawInput string) { g.doWear(sess, strings.Join(args, " ")) } -func (g *Game) checkRequirements(sess *net.Session, p *player.Player, def *object.ItemDef) string { +func (g *Game) checkRequirements(sess *net.Session, p *player.Player, def *item.ItemDef) string { if len(def.Requirements) == 0 { return "" } @@ -85,7 +85,7 @@ func (g *Game) doWear(sess *net.Session, input string) { return } - if def.EquipSlot == object.SlotAmmo && def.Stackable { + if def.EquipSlot == item.SlotAmmo && def.Stackable { g.equipAmmo(sess, p, match.Slot, slot, def, qty) return } @@ -126,13 +126,13 @@ func (g *Game) doWear(sess *net.Session, input string) { sess.WriteLine(fmt.Sprintf("You %s %s.", verb, g.itemColorize(sess, def, match.Name))) } -func (g *Game) equipAmmo(sess *net.Session, p *player.Player, invIdx int, slot *player.InventorySlot, def *object.ItemDef, qty int) { +func (g *Game) equipAmmo(sess *net.Session, p *player.Player, invIdx int, slot *player.InventorySlot, def *item.ItemDef, qty int) { transferQty := slot.Quantity if qty > 0 && qty < transferQty { transferQty = qty } - existingAmmoID, hasAmmo := p.Equipment[object.SlotAmmo] + existingAmmoID, hasAmmo := p.Equipment[item.SlotAmmo] if hasAmmo && existingAmmoID != slot.ItemID { g.unequipAmmo(p) } @@ -146,7 +146,7 @@ func (g *Game) equipAmmo(sess *net.Session, p *player.Player, invIdx int, slot * if hasAmmo && existingAmmoID == slot.ItemID { p.AmmoQty += transferQty } else { - p.Equipment[object.SlotAmmo] = slot.ItemID + p.Equipment[item.SlotAmmo] = slot.ItemID p.AmmoQty = transferQty } @@ -156,7 +156,7 @@ func (g *Game) equipAmmo(sess *net.Session, p *player.Player, invIdx int, slot * } func (g *Game) unequipAmmo(p *player.Player) { - ammoID, ok := p.Equipment[object.SlotAmmo] + ammoID, ok := p.Equipment[item.SlotAmmo] if !ok || p.AmmoQty <= 0 { return } @@ -164,7 +164,7 @@ func (g *Game) unequipAmmo(p *player.Player) { s := p.InvSlot(i) if s != nil && s.ItemID == ammoID { s.Quantity += p.AmmoQty - delete(p.Equipment, object.SlotAmmo) + delete(p.Equipment, item.SlotAmmo) p.AmmoQty = 0 return } @@ -173,7 +173,7 @@ func (g *Game) unequipAmmo(p *player.Player) { if freeSlot >= 0 { p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: ammoID, Quantity: p.AmmoQty}) } - delete(p.Equipment, object.SlotAmmo) + delete(p.Equipment, item.SlotAmmo) p.AmmoQty = 0 } @@ -181,7 +181,7 @@ func (g *Game) doWearAll(sess *net.Session) { p := sess.Player g.cancelAction(p) - bestPerSlot := make(map[object.EquipSlot]struct { + bestPerSlot := make(map[item.EquipSlot]struct { itemID string value int invSlot int @@ -201,7 +201,7 @@ func (g *Game) doWearAll(sess *net.Session) { } current, exists := bestPerSlot[def.EquipSlot] val := def.Value - if def.EquipSlot == object.SlotAmmo && def.Stackable { + if def.EquipSlot == item.SlotAmmo && def.Stackable { val = def.Value * slot.Quantity } if !exists || val > current.value { @@ -221,19 +221,19 @@ func (g *Game) doWearAll(sess *net.Session) { var equipped []string for eqSlot, best := range bestPerSlot { if existing, ok := p.Equipment[eqSlot]; ok && existing == best.itemID { - if eqSlot != object.SlotAmmo { + if eqSlot != item.SlotAmmo { continue } } - if eqSlot == object.SlotAmmo { + if eqSlot == item.SlotAmmo { bestDef, _ := g.ItemStore.Load(best.itemID) if bestDef != nil && bestDef.Stackable { invSlot := p.InvSlot(best.invSlot) if invSlot == nil { continue } - existingID, hasAmmo := p.Equipment[object.SlotAmmo] + existingID, hasAmmo := p.Equipment[item.SlotAmmo] if hasAmmo && existingID != best.itemID { g.unequipAmmo(p) } @@ -242,7 +242,7 @@ func (g *Game) doWearAll(sess *net.Session) { if hasAmmo && existingID == best.itemID { p.AmmoQty += transferQty } else { - p.Equipment[object.SlotAmmo] = best.itemID + p.Equipment[item.SlotAmmo] = best.itemID p.AmmoQty = transferQty } equipped = append(equipped, g.itemColorize(sess, bestDef, bestDef.Name)) diff --git a/internal/game/combat_attack.go b/internal/game/combat_attack.go new file mode 100644 index 0000000..ee7dee8 --- /dev/null +++ b/internal/game/combat_attack.go @@ -0,0 +1,449 @@ +package game + +import ( + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) doAttack(sess *net.Session, input string) { + p := sess.Player + + if g.Combat.Get(p.Name) != nil { + sess.WriteLine("You are already in combat!") + return + } + + if p.Action != nil { + g.cancelAction(p) + } + + mob := g.findMob(sess, input, p.RoomID) + if mob == nil { + return + } + + if mob.HP <= 0 { + if mob.IsTask() { + sess.WriteLine(fmt.Sprintf("%s is already complete.", mobDisplayName(mob, true))) + } else { + sess.WriteLine("That is already dead.") + } + return + } + + if mob.Protected { + sess.WriteLine(fmt.Sprintf("You can't attack %s!", mobDisplayName(mob, true))) + return + } + + if mob.AssassinLevel > 0 && p.Level(player.Assassin) < mob.AssassinLevel { + sess.WriteLine(fmt.Sprintf("You need Assassin level %d to attack %s.", mob.AssassinLevel, mobDisplayName(mob, true))) + return + } + + if g.Combat.IsMobInCombat(mob.InstanceID) { + sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false))) + return + } + + if itemID, ok := p.Equipment[item.SlotMainHand]; ok { + if def, err := g.ItemStore.Load(itemID); err == nil && def.WeaponType == item.WeaponRanged { + if _, hasAmmo := p.Equipment[item.SlotAmmo]; !hasAmmo || p.AmmoQty <= 0 { + sess.WriteLine("You don't have any ammo equipped.") + return + } + } + } + + g.startCombat(sess, p, mob) +} + +func (g *Game) findMob(sess *net.Session, input string, roomID int) *world.MobInstance { + lower := strings.ToLower(input) + mobs := g.MobStore.MobsInRoom(roomID) + + idx := -1 + name := lower + if dotPos := strings.Index(lower, "."); dotPos > 0 { + if n, err := strconv.Atoi(lower[:dotPos]); err == nil && n > 0 { + idx = n + name = lower[dotPos+1:] + } + } + + var exact []*world.MobInstance + var prefix []*world.MobInstance + for _, m := range mobs { + q := m.MatchQuality(name) + if q == world.MatchExact { + exact = append(exact, m) + } else if q == world.MatchPrefix { + prefix = append(prefix, m) + } + } + + candidates := exact + if len(candidates) == 0 { + candidates = prefix + } + + if len(candidates) == 0 { + sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input)) + return nil + } + + sort.Slice(candidates, func(i, j int) bool { + return candidates[i].InstanceID < candidates[j].InstanceID + }) + + if idx > 0 { + if idx-1 < len(candidates) { + return candidates[idx-1] + } + return nil + } + + if len(candidates) == 1 { + return candidates[0] + } + + seen := make(map[string]bool) + for _, m := range candidates { + seen[mobDisplayName(m, false)] = true + } + if len(seen) > 1 { + sess.WriteLine("Which one?") + return nil + } + return candidates[0] +} + +func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { + g.cancelRest(p.Name) + g.cancelBgAction(p) + + g.Combat.Enter(p.Name, mob.InstanceID) + p.AttackTimer = 0 + + isTask := mob.IsTask() + autotriggerActive := p.AutotriggerMod != "" + + if !autotriggerActive { + attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle)) + var styleParts []string + if attBonus > 0 { + styleParts = append(styleParts, fmt.Sprintf("+%d atk", attBonus)) + } + if strBonus > 0 { + styleParts = append(styleParts, fmt.Sprintf("+%d str", strBonus)) + } + if defBonus > 0 { + styleParts = append(styleParts, fmt.Sprintf("+%d def", defBonus)) + } + styleStr := "" + if len(styleParts) > 0 { + styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")" + } + if isTask { + sess.WriteLine(fmt.Sprintf("You begin to %s %s.%s", taskWorkVerb(mob), g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) + } else { + sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) + } + } else { + mod := GetMod(p.AutotriggerMod) + modName := p.AutotriggerMod + if mod != nil { + modName = mod.Name + } + if isTask { + sess.WriteLine(fmt.Sprintf("You begin to %s %s using %s!", + taskWorkVerb(mob), + g.colorize(sess, "mob", mobDisplayName(mob, true)), + g.colorize(sess, "science_mod", modName))) + } else { + sess.WriteLine(fmt.Sprintf("You attack %s with %s!", + g.colorize(sess, "mob", mobDisplayName(mob, true)), + g.colorize(sess, "science_mod", modName))) + } + } + if isTask { + g.broadcastAction(sess, "\n%s begins working on %s.", p.Name, mobDisplayName(mob, true)) + } else { + g.broadcastAction(sess, "\n%s attacks %s!", p.Name, mobDisplayName(mob, true)) + } + + g.Ticks.Subscribe(1, func() bool { + cs := g.Combat.Get(p.Name) + if cs == nil || !cs.Active { + return false + } + currentMob := g.MobStore.GetInstance(cs.MobID) + if currentMob == nil || currentMob.HP <= 0 { + g.endCombat(sess, p, currentMob) + return false + } + + p.AttackTimer++ + if float64(p.AttackTimer) >= g.playerWeaponSpeed(p) { + p.AttackTimer = 0 + + if g.hasDeckEquipped(p) { + if p.AutotriggerMod != "" { + mod := GetMod(p.AutotriggerMod) + if mod != nil && g.hasJunkCost(p, mod) { + g.scienceAttack(sess, p, currentMob, mod) + } else if mod != nil { + sess.WriteLine(g.colorize(sess, "error", + fmt.Sprintf("\nYou're out of junk for %s!! You flail with your fists in desperation!", mod.Name))) + g.playerAttackUnarmed(sess, p, currentMob) + } else { + p.AutotriggerMod = "" + sess.WriteLine(g.colorize(sess, "error", + "\nNo autotrigger set! Set a module with the 'autotrigger' command.")) + g.playerAttackUnarmed(sess, p, currentMob) + } + } else { + sess.WriteLine(g.colorize(sess, "error", + "\nNo autotrigger set! Set a module with the 'autotrigger' command.")) + g.playerAttackUnarmed(sess, p, currentMob) + } + } else { + if p.QueuedTrigger != "" { + mod := GetMod(p.QueuedTrigger) + p.QueuedTrigger = "" + if mod != nil && g.hasJunkCost(p, mod) { + g.scienceAttack(sess, p, currentMob, mod) + } else { + g.playerAttack(sess, p, currentMob) + } + } else { + g.playerAttack(sess, p, currentMob) + } + } + } + + if currentMob.HP <= 0 { + g.endCombat(sess, p, currentMob) + return false + } + return true + }) + + if !isTask { + g.Ticks.Subscribe(engine.ToTicks(mob.Speed), func() bool { + cs := g.Combat.Get(p.Name) + if cs == nil || !cs.Active { + return false + } + currentMob := g.MobStore.GetInstance(cs.MobID) + if currentMob == nil || currentMob.HP <= 0 { + return false + } + g.mobAttack(sess, p, currentMob) + if p.HP <= 0 { + g.endCombat(sess, p, currentMob) + return false + } + return true + }) + } +} + +func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { + if g.processConsumeQueue(p, sess) { + return + } + + attackType, weaponType, attRoll, defRoll, maxHit := g.calculatePlayerAttackRoll(p, mob) + + if g.isSafespotted(p.Name) && attackType != "ranged" && attackType != "science" { + if ss, ok := g.safespot.Get(p.Name); ok { + g.forceLeaveSafespot(sess, p, &ss, "You leave your cover to close in for melee!") + } + } + + if combat.HitCheck(attRoll, defRoll) { + g.applyPlayerHit(sess, p, mob, attackType, weaponType, maxHit) + } else { + g.applyPlayerMiss(sess, p, mob, weaponType) + } +} + +func (g *Game) calculatePlayerAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType item.WeaponType, attRoll int, defRoll int, maxHit int) { + attackType = "crush" + if itemID, ok := p.Equipment[item.SlotMainHand]; ok { + if def, err := g.ItemStore.Load(itemID); err == nil { + if def.AttackType != "" { + attackType = def.AttackType + } else if def.WeaponType == item.WeaponRanged { + attackType = "ranged" + } else if def.WeaponType == item.WeaponScience { + attackType = "science" + } + weaponType = def.WeaponType + } + } + + totals := g.playerEquipBonuses(p) + isRanged := weaponType == item.WeaponRanged + + if isRanged { + rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle)) + equipAttack := totals.RangedAttack + effectiveRanged := p.Level(player.Ranged) + g.techLevelBonus(p, "ranged") + g.buffLevelBonus(p, "ranged") + attRoll = combat.EffectiveRoll(effectiveRanged, rangedBonus, equipAttack) + + mobDefBonus := combat.SelectBonus(attackType, + mob.StabDefense, mob.SlashDefense, mob.CrushDefense, + mob.ScienceDefense, mob.RangedDefense) + defRoll = combat.EffectiveRoll(mob.Defense, 0, mobDefBonus) + maxHit = combat.MaxHit(effectiveRanged, rangedBonus, totals.RangedStrength) + } else { + attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) + equipAttack := combat.SelectBonus(attackType, + totals.StabAttack, totals.SlashAttack, totals.CrushAttack, + totals.ScienceAttack, totals.RangedAttack) + effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") + attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, equipAttack) + + mobDefBonus := combat.SelectBonus(attackType, + mob.StabDefense, mob.SlashDefense, mob.CrushDefense, + mob.ScienceDefense, mob.RangedDefense) + defRoll = combat.EffectiveRoll(mob.Defense, 0, mobDefBonus) + maxHit = combat.MaxHit(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus, totals.StrengthBonus) + } + return +} + +func (g *Game) playerAttackUnarmed(sess *net.Session, p *player.Player, mob *world.MobInstance) { + if g.processConsumeQueue(p, sess) { + return + } + + attackType, _, attRoll, defRoll, maxHit := g.calculateUnarmedAttackRoll(p, mob) + + if combat.HitCheck(attRoll, defRoll) { + g.applyPlayerHit(sess, p, mob, attackType, item.WeaponMelee, maxHit) + } else { + g.applyPlayerMiss(sess, p, mob, item.WeaponMelee) + } +} + +func (g *Game) calculateUnarmedAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType item.WeaponType, attRoll int, defRoll int, maxHit int) { + attackType = "crush" + weaponType = item.WeaponMelee + + attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) + effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") + attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, 0) + + mobDefBonus := combat.SelectBonus(attackType, + mob.StabDefense, mob.SlashDefense, mob.CrushDefense, + mob.ScienceDefense, mob.RangedDefense) + defRoll = combat.EffectiveRoll(mob.Defense, 0, mobDefBonus) + maxHit = combat.MaxHit(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus, 0) + return +} + +func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.MobInstance, attackType string, weaponType item.WeaponType, maxHit int) { + dmg := combat.RollDamage(maxHit) + + mob.HP -= dmg + if mob.HP < 0 { + mob.HP = 0 + } + if mob.FinishingBlow != "" && mob.HP <= 0 { + mob.HP = 1 + } + if mob.HP < mob.MaxHP && mob.HP > 0 { + mob.StartRegen() + } + + if mob.FinishingBlow != "" && mob.HP == 1 { + fbDef, _ := g.ItemStore.Load(mob.FinishingBlow) + fbName := mob.FinishingBlow + if fbDef != nil { + fbName = fbDef.Name + } + sess.WriteLine(g.colorize(sess, "warning", + fmt.Sprintf("%s resists death! Use %s on it to finish it off.", + mobDisplayName(mob, false), fbName))) + } + + isRanged := weaponType == item.WeaponRanged + gains := g.awardCombatXP(sess, p, dmg, isRanged) + + if isRanged { + g.consumeAmmo(sess, p) + } + + if mob.IsTask() { + g.writeTaskProgress(sess, p, mob, dmg, gains) + } else { + mobName := mobDisplayName(mob, true) + attacker := mob.Name + if !mob.Unique { + attacker = "The " + mob.Name + } + w := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)) + if w2 := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)); w2 > w { + w = w2 + } + prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", fmt.Sprint(dmg))) + visLen := color.VisibleLen(prefix) + if visLen < w { + prefix += strings.Repeat(" ", w-visLen+1) + } + hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP) + line := prefix + hpSuffix + g.formatXpDrop(sess, p, gains) + sess.WriteLine(line) + } + + if isRanged && p.AmmoQty <= 0 { + sess.WriteLine("You've run out of ammo!") + g.Combat.Leave(p.Name) + } +} + +func (g *Game) applyPlayerMiss(sess *net.Session, p *player.Player, mob *world.MobInstance, weaponType item.WeaponType) { + sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("You miss %s.", mobDisplayName(mob, true)))) + + if weaponType == item.WeaponRanged { + g.consumeAmmo(sess, p) + if p.AmmoQty <= 0 { + sess.WriteLine("You've run out of ammo!") + g.Combat.Leave(p.Name) + } + } +} + +func (g *Game) consumeAmmo(sess *net.Session, p *player.Player) { + ammoID := p.Equipment[item.SlotAmmo] + + p.AmmoQty-- + if p.AmmoQty <= 0 { + delete(p.Equipment, item.SlotAmmo) + p.AmmoQty = 0 + } + + if ammoID != "" { + if def, err := g.ItemStore.Load(ammoID); err == nil && def.Recoverable { + if rand.Float64() < 0.80 { + g.World.AddGroundItem(p.RoomID, def.ID, 1) + } + } + } + + g.AccountStore.SaveCharacter(p) +} diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go new file mode 100644 index 0000000..e06e307 --- /dev/null +++ b/internal/game/combat_mob.go @@ -0,0 +1,301 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { + if g.isSafespotted(p.Name) && g.safespotBlocksMob(p, mob) { + return + } + + attRoll, defRoll := g.calculateMobAttack(p, mob) + + if combat.HitCheck(attRoll, defRoll) { + g.applyMobHit(sess, p, mob) + } else { + g.applyMobMiss(sess, mob) + } +} + +func (g *Game) calculateMobAttack(p *player.Player, mob *world.MobInstance) (attRoll int, defRoll int) { + _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle)) + + mobAttackType := mob.AttackType + if mobAttackType == "" { + mobAttackType = "crush" + } + + attRoll = combat.EffectiveRoll(mob.Attack, 0, mob.AttackBonus) + + totals := g.playerEquipBonuses(p) + equipDef := combat.SelectBonus(mobAttackType, + totals.StabDefense, totals.SlashDefense, totals.CrushDefense, + totals.ScienceDefense, totals.RangedDefense) + defRoll = combat.EffectiveRoll(p.Level(player.Defense)+g.techLevelBonus(p, "defense")+g.buffLevelBonus(p, "defense"), defStyleBonus, equipDef) + return +} + +func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobInstance) { + maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus) + dmg := combat.RollDamage(maxHit) + dmg = g.applyTechProtection(p, mob, dmg) + + if mob.DamageWithout != "" { + hasProtection := false + for _, itemID := range p.Equipment { + if itemID == mob.DamageWithout { + hasProtection = true + break + } + } + if !hasProtection { + dmg = dmg * 3 / 2 + if dmg < 1 { + dmg = 1 + } + cs := g.Combat.Get(p.Name) + if cs != nil && !cs.DamageWarningShown { + cs.DamageWarningShown = true + fbDef, _ := g.ItemStore.Load(mob.DamageWithout) + fbName := mob.DamageWithout + if fbDef != nil { + fbName = fbDef.Name + } + attacker := mob.Name + if !mob.Unique { + attacker = "The " + mob.Name + } + sess.WriteLine(g.colorize(sess, "warning", + fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.", + attacker, fbName))) + } + } + } + + p.HP -= dmg + if p.HP < 0 { + p.HP = 0 + } + p.StartRegen() + g.AccountStore.SaveCharacter(p) + + attacker := mob.Name + if !mob.Unique { + attacker = "The " + mob.Name + } + mobName := mobDisplayName(mob, true) + w := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)) + if w2 := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)); w2 > w { + w = w2 + } + prefix := fmt.Sprintf("%s hits you for %s damage.", g.colorize(sess, "mob", attacker), g.colorize(sess, "damage_taken", fmt.Sprint(dmg))) + visLen := color.VisibleLen(prefix) + if visLen < w { + prefix += strings.Repeat(" ", w-visLen+1) + } + hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, p.HP, p.MaxHP()), p.HP, p.MaxHP()) + sess.WriteLine(prefix + hpSuffix) + + if ss, hasSS := g.safespot.Get(p.Name); hasSS && ss.HideCountdown > 0 { + g.safespot.Delete(p.Name) + sess.WriteLine(g.colorize(sess, "error", "You're hit while repositioning! You failed to hide.")) + } + + if p.MoveTicks > 0 { + p.ClearMoveState() + sess.WriteLine("Can't escape!") + } +} + +func (g *Game) applyMobMiss(sess *net.Session, mob *world.MobInstance) { + attacker := mob.Name + if !mob.Unique { + attacker = "The " + mob.Name + } + sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s misses you.", attacker))) +} + +func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { + g.Combat.Leave(p.Name) + + if p.HP <= 0 { + g.killPlayer(sess, p, mob) + return + } + + if mob != nil && mob.HP <= 0 { + isTask := mob.IsTask() + p.Stats.RecordMobKill(mob.DefID) + + if isTask { + complete := mob.CompleteMessage + if complete == "" { + complete = fmt.Sprintf("You finish your work on %s!", mobDisplayName(mob, true)) + } + sess.WriteLine(g.colorize(sess, "victory", "\n"+complete)) + } else { + sess.WriteLine(g.colorize(sess, "victory", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true)))) + g.onAssassinKill(sess, p, mob) + } + + if g.Hub != nil { + for _, other := range g.Hub.PlayersInRoom(p.RoomID) { + if other != sess && other.Player != nil { + if isTask { + other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s finishes working on %s.", p.Name, mobDisplayName(mob, false)))) + } else { + op := other.Player + mobLvl := mobCombatLevel(mob) + levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl)) + other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr))) + } + } + } + } + + dropLabel := func() string { + if isTask { + return "You receive:" + } + dropper := mob.Name + if !mob.Unique { + dropper = "The " + mob.Name + } + return dropper + " drops:" + }() + + if mob.Drops.Remains != "" { + g.World.AddReservedItem(p.RoomID, mob.Drops.Remains, 1, p.Name) + def, _ := g.ItemStore.Load(mob.Drops.Remains) + name := mob.Drops.Remains + if def != nil { + name = def.Name + } + coloredName := g.itemColorize(sess, def, name) + sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName)) + } + + if len(mob.Drops.Loot) > 0 { + entry := behavior.ResolveDrop(g.DataDir, mob.Drops.Loot) + if entry != nil && entry.ItemID != "" { + qty := entry.Quantity + if qty <= 0 { + qty = 1 + } + g.World.AddReservedItem(p.RoomID, entry.ItemID, qty, p.Name) + def, _ := g.ItemStore.Load(entry.ItemID) + name := entry.ItemID + if def != nil { + name = def.Name + } + coloredName := g.itemColorize(sess, def, name) + if qty > 1 { + sess.WriteLine(fmt.Sprintf("%s %d x %s", g.colorize(sess, "drop_message", dropLabel), qty, coloredName)) + } else { + sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName)) + } + } + } + + respawnTicks := engine.ToTicks(mob.RespawnTicks) + if respawnTicks <= 0 { + respawnTicks = engine.ToTicks(30) + } + instanceID := mob.InstanceID + g.Ticks.Subscribe(respawnTicks, func() bool { + g.respawnMob(instanceID) + return false + }) + g.writePrompt(sess) + } +} + +// killPlayer handles a player death from any source (combat or a room hazard). +// mob may be nil (e.g. a hazard kill); it is only used for Dead Man's Switch +// retribution. +func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobInstance) { + g.Combat.Leave(p.Name) + + if p.HasActiveTech("retribution") { + retDef := GetTechDef("retribution") + if retDef != nil && mob != nil && mob.HP > 0 { + retDmg := int(float64(p.MaxHP()) * retDef.Effects.RetributionPct) + if retDmg > 0 { + mob.HP -= retDmg + if mob.HP < 0 { + mob.HP = 0 + } + sess.WriteLine(fmt.Sprintf("\nDead Man's Switch activates! %s takes %d damage!", + mobDisplayName(mob, true), retDmg)) + } + } + } + p.DeactivateAllTechs() + p.Stats.RecordDeath() + sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!")) + p.ClearMoveState() + p.HazardTimer = 0 + if ss, ok := g.safespot.Get(p.Name); ok { + g.forceLeaveSafespot(sess, p, &ss, "") + } + g.dropItemsOnDeath(p) + p.HP = p.MaxHP() + p.RoomID = 1001 + g.AccountStore.SaveCharacter(p) + if g.Hub != nil { + g.Hub.EnterRoom(sess, p.RoomID) + } + g.doLook(sess) + g.writePrompt(sess) +} + +func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRanged bool) []xpGain { + baseXP := dmg * 4 + var gains []xpGain + + if isRanged { + gains = []xpGain{ + {string(player.Ranged), baseXP * 3 / 4}, + {string(player.Hitpoints), baseXP / 4}, + } + } else { + switch p.AttackStyle { + case player.Accurate: + gains = []xpGain{{string(player.Accuracy), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} + case player.Aggressive: + gains = []xpGain{{string(player.Strength), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} + case player.Defensive: + gains = []xpGain{{string(player.Defense), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} + case player.Balanced: + quarter := baseXP / 4 + gains = []xpGain{ + {string(player.Accuracy), quarter}, + {string(player.Strength), quarter}, + {string(player.Defense), quarter}, + {string(player.Hitpoints), quarter}, + } + } + } + + for _, gain := range gains { + g.awardSkillXP(sess, p, player.SkillName(gain.Skill), gain.XP) + } + g.AccountStore.SaveCharacter(p) + return gains +} + +func (g *Game) stopCombat(playerName string) { + if cs := g.Combat.Get(playerName); cs != nil { + g.Combat.Leave(playerName) + } +} diff --git a/internal/game/condition_test.go b/internal/game/condition_test.go index d932d18..f365f2e 100644 --- a/internal/game/condition_test.go +++ b/internal/game/condition_test.go @@ -3,7 +3,7 @@ package game import ( "testing" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" @@ -45,7 +45,7 @@ func TestCheckConditionPlayerFlagTruthy(t *testing.T) { unset := sessWithFlags(map[string]any{}) // {player_flag: x} -> set means present+truthy - posCond := &action.Condition{PlayerFlag: "x"} + posCond := &behavior.Condition{PlayerFlag: "x"} if !g.checkCondition(set, posCond) { t.Error("expected positive flag check to pass when flag is true") } @@ -54,7 +54,7 @@ func TestCheckConditionPlayerFlagTruthy(t *testing.T) { } // {player_flag: x, not: true} -> negative - negCond := &action.Condition{PlayerFlag: "x", Not: true} + negCond := &behavior.Condition{PlayerFlag: "x", Not: true} if g.checkCondition(set, negCond) { t.Error("expected negative flag check to fail when flag is true") } @@ -76,13 +76,13 @@ func TestCheckConditionValueComparisonPreserved(t *testing.T) { g := &Game{Flags: NewFlagStore()} sess := sessWithFlags(map[string]any{"n": 1}) - if !g.checkCondition(sess, &action.Condition{PlayerFlag: "n", Value: 1}) { + if !g.checkCondition(sess, &behavior.Condition{PlayerFlag: "n", Value: 1}) { t.Error("value match should pass for equal int") } - if g.checkCondition(sess, &action.Condition{PlayerFlag: "n", Value: 2}) { + if g.checkCondition(sess, &behavior.Condition{PlayerFlag: "n", Value: 2}) { t.Error("value match should fail for unequal int") } - if !g.checkCondition(sess, &action.Condition{PlayerFlag: "n", Value: 2, Not: true}) { + if !g.checkCondition(sess, &behavior.Condition{PlayerFlag: "n", Value: 2, Not: true}) { t.Error("negated value match should pass for unequal int") } } @@ -92,13 +92,13 @@ func TestCheckConditionWorldFlag(t *testing.T) { g.Flags.Set("gate_open", true) sess := sessWithFlags(map[string]any{}) - if !g.checkCondition(sess, &action.Condition{Flag: "gate_open"}) { + if !g.checkCondition(sess, &behavior.Condition{Flag: "gate_open"}) { t.Error("world flag truthy check should pass") } - if g.checkCondition(sess, &action.Condition{Flag: "gate_open", Not: true}) { + if g.checkCondition(sess, &behavior.Condition{Flag: "gate_open", Not: true}) { t.Error("negated world flag should fail when set") } - if g.checkCondition(sess, &action.Condition{Flag: "missing"}) { + if g.checkCondition(sess, &behavior.Condition{Flag: "missing"}) { t.Error("missing world flag should not pass") } } @@ -107,9 +107,9 @@ func TestResolveObjDescPresence(t *testing.T) { g := &Game{Flags: NewFlagStore()} def := &object.ObjectDef{ - Descriptions: []object.ConditionalDesc{ - {Text: "lined", Condition: &action.Condition{PlayerFlag: "lined_up"}}, - {Text: "milling", Condition: &action.Condition{PlayerFlag: "lined_up", Not: true}}, + Description: behavior.DescList{ + {Text: "lined", Condition: &behavior.Condition{PlayerFlag: "lined_up"}}, + {Text: "milling", Condition: &behavior.Condition{PlayerFlag: "lined_up", Not: true}}, }, } @@ -124,16 +124,16 @@ func TestResolveObjDescPresence(t *testing.T) { // gated entirely off -> absent gated := &object.ObjectDef{ - Descriptions: []object.ConditionalDesc{ - {Text: "x", Condition: &action.Condition{PlayerFlag: "done", Not: true}}, + Description: behavior.DescList{ + {Text: "x", Condition: &behavior.Condition{PlayerFlag: "done", Not: true}}, }, } if _, present := g.resolveObjDesc(sessWithFlags(map[string]any{"done": true}), gated); present { t.Error("expected object to be absent when no variant matches") } - // plain object (no Descriptions) always present - plain := &object.ObjectDef{Description: "plain"} + // plain object always present + plain := &object.ObjectDef{Description: behavior.DescList{{Text: "plain"}}} if text, present := g.resolveObjDesc(sessWithFlags(nil), plain); !present || text != "plain" { t.Errorf("expected plain/present, got %q/%v", text, present) } @@ -142,9 +142,9 @@ func TestResolveObjDescPresence(t *testing.T) { func TestRoomDescriptionSelection(t *testing.T) { g := &Game{Flags: NewFlagStore()} room := &world.Room{ - Description: "empty pad", - Descriptions: []world.RoomDesc{ - {Text: "crowd", Condition: &action.Condition{PlayerFlag: "done", Not: true}}, + Description: behavior.DescList{ + {Text: "crowd", Condition: &behavior.Condition{PlayerFlag: "done", Not: true}}, + {Text: "empty pad"}, }, } if got := g.roomDescription(sessWithFlags(map[string]any{}), room); got != "crowd" { diff --git a/internal/game/core_course.go b/internal/game/core_course.go index 7d7fe08..2b6b28f 100644 --- a/internal/game/core_course.go +++ b/internal/game/core_course.go @@ -4,8 +4,8 @@ import ( "path/filepath" "sync" - "thehouseoficarus/internal/action" "gopkg.in/yaml.v3" + "thehouseoficarus/internal/behavior" ) type ObstacleDef struct { @@ -77,6 +77,16 @@ func (cs *CourseStore) LoadAll() { cs.loadAllLocked() } +// AllCourses returns the full course config map, loading on first access. +func (cs *CourseStore) AllCourses() map[string]*CourseConfig { + cs.mu.Lock() + defer cs.mu.Unlock() + if !cs.loaded { + cs.loadAllLocked() + } + return cs.courses +} + func (cs *CourseStore) GetObstacle(roomID int) *ObstacleInfo { cs.mu.Lock() defer cs.mu.Unlock() @@ -91,7 +101,7 @@ func (cs *CourseStore) loadAllLocked() { cs.roomToObstacle = make(map[int]*ObstacleInfo) localVerbs := make(map[string]bool) - action.WalkYAMLDir(filepath.Join(cs.dataDir, "courses"), func(path, id string, data []byte) error { + behavior.WalkYAMLDir(filepath.Join(cs.dataDir, "courses"), func(path, id string, data []byte) error { var cfg CourseConfig if err := yaml.Unmarshal(data, &cfg); err != nil { return nil diff --git a/internal/game/core_equip.go b/internal/game/core_equip.go index 02b8348..cc8016d 100644 --- a/internal/game/core_equip.go +++ b/internal/game/core_equip.go @@ -1,12 +1,12 @@ package game import ( - "thehouseoficarus/internal/object" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/player" ) -func (g *Game) playerEquipBonuses(p *player.Player) object.ItemStats { - var totals object.ItemStats +func (g *Game) playerEquipBonuses(p *player.Player) item.ItemStats { + var totals item.ItemStats for _, itemID := range p.Equipment { def, err := g.ItemStore.Load(itemID) if err != nil { @@ -31,7 +31,7 @@ func (g *Game) playerEquipBonuses(p *player.Player) object.ItemStats { } func (g *Game) playerWeaponSpeed(p *player.Player) float64 { - if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + if itemID, ok := p.Equipment[item.SlotMainHand]; ok { def, err := g.ItemStore.Load(itemID) if err == nil && def.Speed > 0 { return def.Speed diff --git a/internal/game/core_production.go b/internal/game/core_production.go index 56d1ccc..8a38f36 100644 --- a/internal/game/core_production.go +++ b/internal/game/core_production.go @@ -1,622 +1,14 @@ package game import ( - "fmt" - "math/rand" - "sort" - "strconv" - "strings" - - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/color" - "thehouseoficarus/internal/engine" - "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/player" ) -func (g *Game) resolveCraftVar(sess *net.Session, varSpec string, craft *object.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { - if !strings.Contains(varSpec, "%") { - return varSpec - } - - outDef, _ := g.ItemStore.Load(outputID) - outputName := outputID - if outDef != nil { - outputName = g.itemColorize(sess, outDef, outDef.Name) - } - varSpec = strings.ReplaceAll(varSpec, "%n", outputName) - - for i, e := range craft.Consume { - key := fmt.Sprintf("%%i%d", i+1) - if !strings.Contains(varSpec, key) { - continue - } - var matchedID string - for _, id := range e.Items { - if hasItem(id) { - matchedID = id - break - } - } - if matchedID == "" && len(e.Items) > 0 { - matchedID = e.Items[0] - } - if def, err := g.ItemStore.Load(matchedID); err == nil { - varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) - } else { - varSpec = strings.ReplaceAll(varSpec, key, matchedID) - } - } - - for i, bpID := range byproducts { - key := fmt.Sprintf("%%b%d", i+1) - if !strings.Contains(varSpec, key) { - continue - } - if def, err := g.ItemStore.Load(bpID); err == nil { - varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) - } else { - varSpec = strings.ReplaceAll(varSpec, key, bpID) - } - } - - return color.ExpandTags(g.colorMode(sess), varSpec) -} - -func (g *Game) resolveCraftMessage(sess *net.Session, itemMsg, defaultMsg string, craft *object.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { - msg := itemMsg - if msg == "" { - msg = defaultMsg - } - if msg == "" { - return "" - } - return g.resolveCraftVar(sess, msg, craft, outputID, byproducts, hasItem) -} - -func (g *Game) startProduction(sess *net.Session, p *player.Player, item *object.ItemDef, craft *object.CraftDef, actionType action.ActionType, displayVerb, startMsg, endMsg string, count int) { - g.cancelAction(p) - g.cancelBgAction(p) - - skill := craft.EffectiveSkill() - if skill != "" { - skillLevel := p.Level(player.SkillName(skill)) - if skillLevel < craft.Level { - sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", craft.Level, skill)) - g.reprompt(sess) - return - } - } - - if !craftHasAllItemsQty(craft, p.CountItem) { - sess.WriteLine("You don't have the required materials.") - g.reprompt(sess) - return - } - - outDef, _ := g.ItemStore.Load(item.ID) - - wait := craft.Wait - if wait <= 0 { - wait = 4 - } - - outputName := item.ID - if outDef != nil { - outputName = outDef.Name - } - - p.Action = &action.Action{ - Type: actionType, - TargetID: item.ID, - TargetName: outputName, - Data: &action.ProductionData{ - ItemID: item.ID, - Phase: 0, - Wait: wait, - StartMsg: startMsg, - EndMsg: endMsg, - Remaining: count, - }, - WaitLeft: engine.ToTicks(1), - } - - g.broadcastAction(sess, "%s starts %s.", p.Name, displayVerb) -} - -func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { - craft := item.FirstCraft() - if craft == nil { - return - } - - _, stationName := g.findStation(p.RoomID, craft.Station) - if stationName == "" { - stationName = "inventory" - } - - info, ok := productionTypes[craft.Type] - if !ok { - info = productionTypeInfo{ - ActionType: action.ActionType(craft.Type), - DisplayVerb: craft.Type, - StartMessage: "You start " + craft.Type + " %i1.", - SuccessMessage: "You produce %n.", - EndMessage: "You've finished " + craft.Type + ".", - } - } - - startMsg := g.resolveCraftMessage(sess, craft.StartMessage, info.StartMessage, craft, item.ID, nil, p.HasItem) - if stationName != "inventory" { - startMsg += " on the " + stationName + "." - } - - endMsg := g.resolveCraftMessage(sess, craft.EndMessage, info.EndMessage, craft, item.ID, nil, p.HasItem) - - g.startProduction(sess, p, item, craft, info.ActionType, info.DisplayVerb, startMsg, endMsg, count) -} - -func (g *Game) loadCraftItem(itemID string) *object.ItemDef { - item, err := g.ItemStore.Load(itemID) - if err != nil || len(item.Craft) == 0 { - return nil - } - return item -} - -func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { - d, ok := p.Action.Data.(*action.ProductionData) - if !ok { - g.cancelAction(p) - return false - } - itemID := d.ItemID - phase := d.Phase - wait := d.Wait - startMsg := d.StartMsg - endMsg := d.EndMsg - remaining := d.Remaining - - item := g.loadCraftItem(itemID) - if item == nil { - g.cancelAction(p) - return false - } - craft := item.FirstCraft() - - if phase == 0 { - if startMsg != "" { - sess.WriteLine(startMsg) - } - if len(craft.Steps) > 0 { - d.NextStepIndex = 0 - d.StepsAccumulatedTicks = 0 - } - d.Phase = 1 - p.Action.WaitLeft = engine.ToTicks(wait) - return true - } - - if stepsIdx := d.NextStepIndex; stepsIdx < len(craft.Steps) { - accTicks := d.StepsAccumulatedTicks - accTicks += wait - d.StepsAccumulatedTicks = accTicks - for i := stepsIdx; i < len(craft.Steps); i++ { - if accTicks >= craft.Steps[i].Tick { - sess.WriteLine(g.resolveCraftVar(sess, craft.Steps[i].Message, craft, item.ID, nil, p.HasItem)) - d.NextStepIndex = i + 1 - } - } - } - - skill := craft.EffectiveSkill() - skillLevel := p.Level(player.SkillName(skill)) - chance := 1.0 - if craft.Success != nil { - chance = action.SuccessChance(action.SuccessFormula(*craft.Success), skillLevel, craft.Level) - } - - info, _ := productionTypes[craft.Type] - - if rand.Float64() < chance { - outputQty := craft.OutputQty - if outputQty <= 0 { - outputQty = 1 - } - - byproducts := g.collectByproducts(p, item) - - placed := false - outDef, _ := g.ItemStore.Load(item.ID) - if outDef != nil && outDef.Stackable { - for i := 0; i < 28; i++ { - slot := p.InvSlot(i) - if slot != nil && slot.ItemID == item.ID { - craftConsumeAll(craft, p.HasItem, p.RemoveItem) - slot.Quantity += outputQty - placed = true - break - } - } - } - if !placed { - craftConsumeAll(craft, p.HasItem, p.RemoveItem) - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - sess.WriteLine("Your inventory is too full!") - g.cancelAction(p) - return false - } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) - } - - for _, bp := range byproducts { - if slot := p.FirstFreeSlot(); slot >= 0 { - p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, Quantity: 1}) - } - } - - if craft.XP > 0 { - if newLevel := p.AddSkillXP(player.SkillName(skill), craft.XP); newLevel > 0 { - sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) - } - } - g.AccountStore.SaveCharacter(p) - - var defaultSuccess string - if info.SuccessMessage != "" { - defaultSuccess = info.SuccessMessage - } else { - defaultSuccess = "You produce %n." - } - msg := g.resolveCraftMessage(sess, craft.Message, defaultSuccess, craft, item.ID, byproducts, p.HasItem) - if p.OptionBool("xp_drops") && craft.XP > 0 { - msg += g.formatXpDropSingle(sess, p, player.SkillName(skill), craft.XP) - } - sess.WriteLine(msg) - } else { - craftConsumeAll(craft, p.HasItem, p.RemoveItem) - - if craft.Fail != "" { - freeSlot := p.FirstFreeSlot() - if freeSlot >= 0 { - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: craft.Fail, Quantity: 1}) - } - } - g.AccountStore.SaveCharacter(p) - - msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, p.HasItem) - if msg != "" { - sess.WriteLine(g.colorize(sess, "damage_taken", msg)) - } - } - - if remaining > 0 { - remaining-- - d.Remaining = remaining - if remaining <= 0 { - if endMsg != "" { - sess.WriteLine(endMsg) - } - g.cancelAction(p) - return false - } - } - - if g.canContinueProduction(p, item) { - p.Action.WaitLeft = engine.ToTicks(wait) - return true - } - - if endMsg != "" { - sess.WriteLine(endMsg) - } - g.cancelAction(p) - return false -} - -func (g *Game) collectByproducts(p *player.Player, item *object.ItemDef) []string { - craft := item.FirstCraft() - if craft == nil { - return nil - } - var byproducts []string - for _, e := range craft.Consume { - if len(e.Byproducts) == 0 { - continue - } - for i, id := range e.Items { - if p.HasItem(id) && i < len(e.Byproducts) && e.Byproducts[i] != "" { - byproducts = append(byproducts, e.Byproducts[i]) - break - } - } - } - return byproducts -} - -func (g *Game) canContinueProduction(p *player.Player, item *object.ItemDef) bool { - craft := item.FirstCraft() - if craft == nil { - return false - } - if !craftHasAllItemsQty(craft, p.CountItem) { - return false - } - outputQty := craft.OutputQty - if outputQty <= 0 { - outputQty = 1 - } - outDef, _ := g.ItemStore.Load(item.ID) - if outDef != nil && outDef.Stackable { - for i := 0; i < 28; i++ { - slot := p.InvSlot(i) - if slot != nil && slot.ItemID == item.ID { - return true - } - } - } - return p.FirstFreeSlot() >= 0 -} - -func (g *Game) handleRecipeChoice(sess *net.Session, input string) { - input = strings.TrimSpace(input) - - menuData := sess.PendingMenu - if len(menuData) == 0 { - sess.State = net.StateGame - g.reprompt(sess) - return - } - - if input == "" { - sess.PendingMenu = nil - sess.State = net.StateGame - sess.WriteLine("Never mind.") - g.reprompt(sess) - return - } - - if choice, err := strconv.Atoi(input); err == nil { - sess.PendingMenu = nil - sess.State = net.StateGame - if choice <= 0 || choice > len(menuData) { - sess.WriteLine("Never mind.") - g.reprompt(sess) - return - } - g.dispatchMenuEntry(sess, menuData[choice-1]) - return - } - - lower := strings.ToLower(input) - matchIdx := -1 - for i, entry := range menuData { - name := g.menuEntryName(entry) - if name == "" { - continue - } - if strings.ToLower(name) == lower || object.WordPrefixMatch(input, name) { - if matchIdx >= 0 { - sess.WriteLine("That's ambiguous.") - return - } - matchIdx = i - } - } - if matchIdx >= 0 { - sess.PendingMenu = nil - sess.State = net.StateGame - g.dispatchMenuEntry(sess, menuData[matchIdx]) - return - } - - sess.PendingMenu = nil - sess.State = net.StateGame - sess.WriteLine("Never mind.") - g.reprompt(sess) -} - -func (g *Game) menuEntryName(entry map[string]string) string { - if iid, ok := entry["item_id"]; ok { - if def, _ := g.ItemStore.Load(iid); def != nil { - return def.Name - } - return iid - } - if barID, ok := entry["bar_id"]; ok { - if def, _ := g.ItemStore.Load(barID); def != nil { - return def.Name - } - return barID - } - return "" -} - -func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) { - if iid, ok := entry["item_id"]; ok { - g.promptHowMany(sess, iid) - return - } - if barID, ok := entry["bar_id"]; ok { - g.showSmithTable(sess, sess.Player, barID) - return - } - g.reprompt(sess) -} - -func (g *Game) formatMaterials(sess *net.Session, item *object.ItemDef) string { - craft := item.FirstCraft() - if craft == nil { - return "" - } - var parts []string - for _, e := range craft.Consume { - itemName := e.Items[0] - if def, err := g.ItemStore.Load(e.Items[0]); err == nil { - itemName = g.itemColorize(sess, def, def.Name) - } - if e.Quantity > 1 { - parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Quantity)) - } else { - parts = append(parts, itemName) - } - } - return strings.Join(parts, ", ") -} - -func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []*object.ItemDef, title, skill, lastFlagKey, promptVerb string, background bool) { - sort.Slice(items, func(i, j int) bool { - return items[i].FirstCraft().Level < items[j].FirstCraft().Level - }) - - mode := g.colorMode(sess) - skillLevel := p.Level(player.SkillName(skill)) - dimSpec := color.Parse("240") - - tbl := &Table{ - Title: title, - Columns: []string{"#", "Product", "Materials", "Level"}, - } - - for i, item := range items { - craft := item.FirstCraft() - outDef, _ := g.ItemStore.Load(item.ID) - productName := item.ID - if outDef != nil { - productName = outDef.Name - } - - outputQty := craft.OutputQty - if outputQty <= 0 { - outputQty = 1 - } - if outDef != nil && outDef.Stackable && outputQty > 1 { - productName = fmt.Sprintf("%s x%d", productName, outputQty) - } - - matStr := g.formatMaterials(sess, item) - levelStr := fmt.Sprint(craft.Level) - numStr := fmt.Sprintf("%d", i+1) - - canMake := skillLevel >= craft.Level && craftHasAllItemsQty(craft, p.CountItem) - if canMake { - productName = g.itemColorize(sess, outDef, productName) - } else { - productName = color.Render(mode, dimSpec, productName) - matStr = color.Render(mode, dimSpec, matStr) - levelStr = color.Render(mode, dimSpec, levelStr) - numStr = color.Render(mode, dimSpec, numStr) - } - - tbl.Rows = append(tbl.Rows, []string{numStr, productName, matStr, levelStr}) - } - - unicode := p.OptionBool("unicode") - sess.WriteLine("") - for _, line := range tbl.Render(unicode) { - sess.WriteLine(line) - } - - lastItemID, _ := p.Flags[lastFlagKey].(string) - hint := "" - if lastItemID != "" { - for _, item := range items { - if item.ID == lastItemID { - if outDef, err := g.ItemStore.Load(item.ID); err == nil { - hint = outDef.Name - } - break - } - } - } - - if hint != "" { - sess.Write(fmt.Sprintf("%s what (enter for all %s): ", promptVerb, hint)) - } else { - sess.Write(fmt.Sprintf("%s what: ", promptVerb)) - } - - sess.PendingMenu = itemMenuData(itemIDs(items)) - sess.PendingSkill = skill - sess.PendingLastFlag = lastFlagKey - sess.PendingBackground = background - sess.State = net.StateProductChoice -} - -func (g *Game) handleProductChoice(sess *net.Session, input string) { - p := sess.Player - menuData := sess.PendingMenu - skill := sess.PendingSkill - lastFlagKey := sess.PendingLastFlag - background := sess.PendingBackground - sess.PendingMenu = nil - sess.PendingSkill = "" - sess.PendingLastFlag = "" - sess.PendingBackground = false - sess.State = net.StateGame - - input = strings.TrimSpace(input) - - var items []*object.ItemDef - for _, entry := range menuData { - iid := entry["item_id"] - if item := g.loadCraftItem(iid); item != nil { - items = append(items, item) - } - } - - if len(items) == 0 { - g.reprompt(sess) - return - } - - sort.Slice(items, func(i, j int) bool { - return items[i].FirstCraft().Level < items[j].FirstCraft().Level - }) - - lastItemID, _ := p.Flags[lastFlagKey].(string) - - sel, errMsg := g.resolveCraftByInput(input, items, lastItemID) - switch errMsg { - case "ambiguous": - sess.WriteLine("That's ambiguous.") - g.reprompt(sess) - return - case "never_mind": - sess.WriteLine("Never mind.") - g.reprompt(sess) - return - } - - craft := sel.Item.FirstCraft() - skillLevel := p.Level(player.SkillName(skill)) - if skillLevel < craft.Level { - sess.WriteLine(fmt.Sprintf("You need level %d %s to make that.", craft.Level, skill)) - g.reprompt(sess) - return - } - if !craftHasAllItemsQty(craft, p.CountItem) { - sess.WriteLine("You don't have the materials for that.") - g.reprompt(sess) - return - } - - p.EnsureFlags() - p.Flags[lastFlagKey] = sel.Item.ID - g.AccountStore.SaveCharacter(p) - - if background { - g.startFletchAction(sess, p, sel.Item, sel.Count) - } else { - g.startProductionFromItem(sess, p, sel.Item, sel.Count) - } -} - func (g *Game) findTool(p *player.Player, toolTypes []string) (toolSpeed float64, toolName string, found bool) { toolSpeed = -1 - if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + if itemID, ok := p.Equipment[item.SlotMainHand]; ok { if def, err := g.ItemStore.Load(itemID); err == nil { for _, t := range toolTypes { if def.ToolType == t { @@ -649,7 +41,7 @@ func (g *Game) hasToolType(p *player.Player, toolType string) bool { } type productionTypeInfo struct { - ActionType action.ActionType + ActionType behavior.ActionType DisplayVerb string StartMessage string SuccessMessage string @@ -659,7 +51,7 @@ type productionTypeInfo struct { var productionTypes = map[string]productionTypeInfo{ "cooking": { - ActionType: action.TypeCook, + ActionType: behavior.TypeCook, DisplayVerb: "cooking", StartMessage: "You start cooking %i1.", SuccessMessage: "Cooked to perfection. %n looks great!", @@ -667,7 +59,7 @@ var productionTypes = map[string]productionTypeInfo{ FailMessage: "You accidentally burn the %i1.", }, "smelting": { - ActionType: action.TypeSmelt, + ActionType: behavior.TypeSmelt, DisplayVerb: "smelting", StartMessage: "You place the %i1 into the furnace.", SuccessMessage: "You remove a white hot %n!", @@ -675,42 +67,42 @@ var productionTypes = map[string]productionTypeInfo{ FailMessage: "The %i1 is consumed by the flames.", }, "smithing": { - ActionType: action.TypeSmith, + ActionType: behavior.TypeSmith, DisplayVerb: "smithing", StartMessage: "You begin smithing %i1.", SuccessMessage: "You smith a %n.", EndMessage: "You've finished smithing.", }, "crafting": { - ActionType: action.TypeCraft, + ActionType: behavior.TypeCraft, DisplayVerb: "crafting", StartMessage: "You begin crafting %i1.", SuccessMessage: "You craft a %n.", EndMessage: "You've finished crafting.", }, "combine": { - ActionType: action.TypeCombine, + ActionType: behavior.TypeCombine, DisplayVerb: "combining", StartMessage: "You start combining %i1.", SuccessMessage: "You produce %n.", EndMessage: "You've finished combining.", }, "fletching": { - ActionType: action.TypeFletch, + ActionType: behavior.TypeFletch, DisplayVerb: "fletching", StartMessage: "You begin fletching %i1.", SuccessMessage: "You fletch a %n.", EndMessage: "You've finished fletching.", }, "pharmacy": { - ActionType: action.TypeMix, + ActionType: behavior.TypeMix, DisplayVerb: "mixing", StartMessage: "You start mixing %i1.", SuccessMessage: "You mix a %n.", EndMessage: "You've finished mixing.", }, "construction": { - ActionType: action.TypeConstruct, + ActionType: behavior.TypeConstruct, DisplayVerb: "constructing", StartMessage: "You begin constructing %i1.", SuccessMessage: "You construct a %n.", @@ -726,150 +118,3 @@ func init() { productionActionTypes[string(pt.ActionType)] = true } } - -func itemMenuData(itemIDs []string) []map[string]string { - out := make([]map[string]string, len(itemIDs)) - for i, id := range itemIDs { - out[i] = map[string]string{"item_id": id} - } - return out -} - -func itemIDs(defs []*object.ItemDef) []string { - ids := make([]string, len(defs)) - for i, d := range defs { - ids[i] = d.ID - } - return ids -} - -func (g *Game) promptHowMany(sess *net.Session, itemID string) { - sess.PendingItemID = itemID - sess.State = net.StateHowMany - sess.Write("How many (return for all)?: ") -} - -type craftSelection struct { - Item *object.ItemDef - Count int -} - -func (g *Game) resolveCraftByInput(input string, items []*object.ItemDef, lastItemID string) (sel craftSelection, errMsg string) { - if input == "" { - if lastItemID == "" { - return sel, "never_mind" - } - for i := range items { - if items[i].ID == lastItemID { - sel.Item = items[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(items) { - sel.Item = items[idx-1] - sel.Count = qty - return sel, "" - } - - matchCount := 0 - for i := range items { - outDef, _ := g.ItemStore.Load(items[i].ID) - name := items[i].ID - if outDef != nil { - name = outDef.Name - } - if object.WordPrefixMatch(productName, name) { - if matchCount == 0 { - sel.Item = items[i] - sel.Count = qty - } - matchCount++ - } - } - if matchCount > 1 { - return sel, "ambiguous" - } - if sel.Item == 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) itemName(sess *net.Session, item *object.ItemDef) string { - if item != nil { - if def, err := g.ItemStore.Load(item.ID); err == nil { - return g.itemColorize(sess, def, def.Name) - } - return item.ID - } - return "" -} - -func (g *Game) showRecipeChoice(sess *net.Session, p *player.Player, items []*object.ItemDef, emptyMsg, title string, autoOption string) { - if len(items) == 0 { - sess.WriteLine(emptyMsg) - return - } - if len(items) == 1 { - if autoOption != "" && p.OptionBool(autoOption) { - g.startProductionFromItem(sess, p, items[0], 0) - return - } - g.promptHowMany(sess, items[0].ID) - return - } - sess.State = net.StateRecipeChoice - sess.PendingMenu = itemMenuData(itemIDs(items)) - var names []string - for _, item := range items { - names = append(names, g.itemName(sess, item)) - } - g.showMenuTable(sess, title, names) -} - -func (g *Game) handleHowMany(sess *net.Session, input string) { - p := sess.Player - itemID := sess.PendingItemID - sess.PendingItemID = "" - 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 - } - - item := g.loadCraftItem(itemID) - if item == nil { - g.reprompt(sess) - return - } - - g.startProductionFromItem(sess, p, item, count) -} diff --git a/internal/game/core_startup.go b/internal/game/core_startup.go deleted file mode 100644 index 2e81eac..0000000 --- a/internal/game/core_startup.go +++ /dev/null @@ -1,887 +0,0 @@ -package game - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/world" -) - -type StartupIssue struct { - Level string - Type string - Message string -} - -func (g *Game) ValidateStartup() []StartupIssue { - var issues []StartupIssue - - issues = append(issues, validateIDs(g.DataDir, "items", "item")...) - issues = append(issues, validateIDs(g.DataDir, "rooms", "room")...) - issues = append(issues, validateIDs(g.DataDir, "mobs", "mob")...) - issues = append(issues, validateIDs(g.DataDir, "objects", "object")...) - issues = append(issues, validateIDs(g.DataDir, "drops", "drop table")...) - issues = append(issues, validateIDs(g.DataDir, "courses", "course")...) - issues = append(issues, validateIDs(g.DataDir, "modules", "module")...) - issues = append(issues, validateIDs(g.DataDir, "techs", "tech")...) - issues = append(issues, validateIDs(g.DataDir, "help", "help topic")...) - - issues = append(issues, validateRooms(g)...) - issues = append(issues, validateMobs(g)...) - issues = append(issues, validateHazards(g)...) - issues = append(issues, validateObjects(g)...) - issues = append(issues, validateItems(g)...) - issues = append(issues, validateCraftBlocks(g)...) - issues = append(issues, validateDropTables(g)...) - issues = append(issues, validateCourses(g)...) - issues = append(issues, validateTechs(g)...) - issues = append(issues, validateRoomWiring(g)...) - - return issues -} - -func validateIDs(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 validateRooms(g *Game) []StartupIssue { - var issues []StartupIssue - roomIndex := g.World.RoomIndex() - itemIDs := g.ItemStore.IDSet() - mobIDs := g.MobStore.AllDefIDs() - objIDs := g.ObjectStore.IDSet() - hazardIDs := g.World.HazardIndex() - - 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 { - switch dir { - case world.North, world.South, world.East, world.West, world.Up, world.Down: - default: - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Room %d: invalid exit direction %q (only north/south/east/west/up/down supported)", id, dir), - }) - continue - } - 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), - }) - } - } - } - - if room.Hazard != "" && !hazardIDs[room.Hazard] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Room %d: references nonexistent hazard %q", - id, room.Hazard), - }) - } - } - - return issues -} - -func validateMobs(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.Kind != "" && def.Kind != "combat" && def.Kind != "task" { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Mob %q: invalid kind %q (must be \"combat\" or \"task\")", id, def.Kind), - }) - } - - 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 validateHazards(g *Game) []StartupIssue { - var issues []StartupIssue - itemIDs := g.ItemStore.IDSet() - validTypes := map[string]bool{"stab": true, "slash": true, "crush": true, "ranged": true, "science": true} - - for id := range g.World.HazardIndex() { - def, err := g.World.LoadHazard(id) - if err != nil { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Hazard %q: failed to load: %v", id, err), - }) - continue - } - if def.AttackType != "" && !validTypes[def.AttackType] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Hazard %q: invalid attack_type %q (must be stab/slash/crush/ranged/science)", id, def.AttackType), - }) - } - if def.RequiredItem != "" && !itemIDs[def.RequiredItem] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Hazard %q: required_item %q does not exist", id, def.RequiredItem), - }) - } - } - - return issues -} - -func validateObjects(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, validateGather(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 validateItems(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), - }) - } - - 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 validateCraftBlocks(g *Game) []StartupIssue { - var issues []StartupIssue - itemIDs := g.ItemStore.IDSet() - objIDs := g.ObjectStore.IDSet() - - allItems, err := g.ItemStore.LoadAll() - if err != nil { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Failed to load items: %v", err), - }) - return issues - } - - for _, item := range allItems { - if len(item.Craft) == 0 { - continue - } - for ci := range item.Craft { - c := &item.Craft[ci] - - for _, e := range c.Consume { - for _, consumeItem := range e.Items { - if consumeItem != "" && !itemIDs[consumeItem] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Item %q (craft: %s): consumes nonexistent item %q", - item.ID, c.Type, consumeItem), - }) - } - } - } - - if c.Fail != "" && !itemIDs[c.Fail] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Item %q (craft: %s): fail product %q does not exist", - item.ID, c.Type, c.Fail), - }) - } - - for _, sid := range c.Station { - if sid != "" && !objIDs[sid] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Item %q (craft: %s): station object %q does not exist", - item.ID, c.Type, sid), - }) - } - } - } - } - - return issues -} - -func validateDropTables(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 validateCourses(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 validateGather(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 -} - -var knownTechCategories = map[string]bool{ - "accuracy": true, - "strength": true, - "defense": true, - "ranged": true, - "science": true, - "protection": true, - "utility": true, - "combo": true, -} - -// Reserved tech IDs whose semantics are coupled to code logic — -// damageAfterTechProtection maps attack types to protect_melee/ranged/science -// and killPlayer checks for retribution. These IDs must exist in YAML. -var reservedTechIDs = map[string]bool{ - "protect_melee": true, - "protect_ranged": true, - "protect_science": true, - "retribution": true, -} - -func validateTechs(g *Game) []StartupIssue { - var issues []StartupIssue - seen := make(map[string]bool) - for _, def := range AllTechs { - if def.ID == "" { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: "Tech has empty id", - }) - continue - } - if seen[def.ID] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "duplicate", - Message: fmt.Sprintf("Duplicate tech %q", def.ID), - }) - } - seen[def.ID] = true - - if def.Name == "" { - issues = append(issues, StartupIssue{ - Level: "WARN", - Type: "reference", - Message: fmt.Sprintf("Tech %q: has no name", def.ID), - }) - } - if def.Category != "" && !knownTechCategories[def.Category] { - issues = append(issues, StartupIssue{ - Level: "WARN", - Type: "reference", - Message: fmt.Sprintf("Tech %q: unknown category %q", def.ID, def.Category), - }) - } - if def.DrainRate < 0 { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Tech %q: negative drain_rate %.2f", def.ID, def.DrainRate), - }) - } - } - - for id := range reservedTechIDs { - if _, ok := techByID[id]; !ok { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Reserved tech %q does not exist — required by code (damage protection / retribution)", id), - }) - } - } - - 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) - } -} diff --git a/internal/game/core_types.go b/internal/game/core_types.go index 898078b..08866cc 100644 --- a/internal/game/core_types.go +++ b/internal/game/core_types.go @@ -1,11 +1,11 @@ package game -import "thehouseoficarus/internal/object" +import "thehouseoficarus/internal/item" -var EquipSlots = []object.EquipSlot{ - object.SlotHead, object.SlotNeck, object.SlotTorso, object.SlotLegs, - object.SlotHands, object.SlotFeet, object.SlotBack, object.SlotAmmo, - object.SlotMainHand, object.SlotOffHand, object.SlotRing, +var EquipSlots = []item.EquipSlot{ + item.SlotHead, item.SlotNeck, item.SlotTorso, item.SlotLegs, + item.SlotHands, item.SlotFeet, item.SlotBack, item.SlotAmmo, + item.SlotMainHand, item.SlotOffHand, item.SlotRing, } type itemMatch struct { @@ -20,10 +20,10 @@ type xpGain struct { } type deathDrop struct { - itemID string - quantity int + itemID string + quantity int totalValue int - isEquip bool - equipSlot object.EquipSlot - invSlot int + isEquip bool + equipSlot item.EquipSlot + invSlot int } diff --git a/internal/game/core_utils.go b/internal/game/core_utils.go index c169580..4c087e8 100644 --- a/internal/game/core_utils.go +++ b/internal/game/core_utils.go @@ -128,5 +128,3 @@ func (g *Game) newInventorySlot(itemID string, qty int) *player.InventorySlot { } return slot } - - diff --git a/internal/game/craft_index.go b/internal/game/craft_index.go index 078ec3c..648ccc8 100644 --- a/internal/game/craft_index.go +++ b/internal/game/craft_index.go @@ -3,31 +3,31 @@ package game import ( "sync" - "thehouseoficarus/internal/object" + "thehouseoficarus/internal/item" ) type CraftIndex struct { mu sync.RWMutex - byType map[string][]*object.ItemDef - byInput map[string][]*object.ItemDef - byStation map[string][]*object.ItemDef + byType map[string][]*item.ItemDef + byInput map[string][]*item.ItemDef + byStation map[string][]*item.ItemDef } func NewCraftIndex() *CraftIndex { return &CraftIndex{ - byType: make(map[string][]*object.ItemDef), - byInput: make(map[string][]*object.ItemDef), - byStation: make(map[string][]*object.ItemDef), + byType: make(map[string][]*item.ItemDef), + byInput: make(map[string][]*item.ItemDef), + byStation: make(map[string][]*item.ItemDef), } } -func (idx *CraftIndex) Build(items []*object.ItemDef) { +func (idx *CraftIndex) Build(items []*item.ItemDef) { idx.mu.Lock() defer idx.mu.Unlock() - idx.byType = make(map[string][]*object.ItemDef) - idx.byInput = make(map[string][]*object.ItemDef) - idx.byStation = make(map[string][]*object.ItemDef) + idx.byType = make(map[string][]*item.ItemDef) + idx.byInput = make(map[string][]*item.ItemDef) + idx.byStation = make(map[string][]*item.ItemDef) for _, def := range items { if len(def.Craft) == 0 { @@ -54,7 +54,7 @@ func (idx *CraftIndex) Build(items []*object.ItemDef) { } } -func appendUnique(items []*object.ItemDef, item *object.ItemDef) []*object.ItemDef { +func appendUnique(items []*item.ItemDef, item *item.ItemDef) []*item.ItemDef { for _, existing := range items { if existing == item { return items @@ -63,34 +63,34 @@ func appendUnique(items []*object.ItemDef, item *object.ItemDef) []*object.ItemD return append(items, item) } -func (idx *CraftIndex) ByType(craftType string) []*object.ItemDef { +func (idx *CraftIndex) ByType(craftType string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() items := idx.byType[craftType] - out := make([]*object.ItemDef, len(items)) + out := make([]*item.ItemDef, len(items)) copy(out, items) return out } -func (idx *CraftIndex) ByStation(stationDefID string) []*object.ItemDef { +func (idx *CraftIndex) ByStation(stationDefID string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() items := idx.byStation[stationDefID] - out := make([]*object.ItemDef, len(items)) + out := make([]*item.ItemDef, len(items)) copy(out, items) return out } -func (idx *CraftIndex) ByInput(itemID string) []*object.ItemDef { +func (idx *CraftIndex) ByInput(itemID string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() items := idx.byInput[itemID] - out := make([]*object.ItemDef, len(items)) + out := make([]*item.ItemDef, len(items)) copy(out, items) return out } -func (idx *CraftIndex) FindByTwoInputs(itemA, itemB string) []*object.ItemDef { +func (idx *CraftIndex) FindByTwoInputs(itemA, itemB string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() @@ -104,7 +104,7 @@ func (idx *CraftIndex) FindByTwoInputs(itemA, itemB string) []*object.ItemDef { candidateSetB[def.ID] = true } - var results []*object.ItemDef + var results []*item.ItemDef for _, def := range candidatesA { if !candidateSetB[def.ID] { continue @@ -121,11 +121,11 @@ func (idx *CraftIndex) FindByTwoInputs(itemA, itemB string) []*object.ItemDef { return results } -func (idx *CraftIndex) FindByStationInput(stationDefID, itemID string) []*object.ItemDef { +func (idx *CraftIndex) FindByStationInput(stationDefID, itemID string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() - var results []*object.ItemDef + var results []*item.ItemDef for _, def := range idx.byStation[stationDefID] { for _, craft := range def.Craft { if craftMatchesEntry(&craft, itemID) { @@ -137,7 +137,7 @@ func (idx *CraftIndex) FindByStationInput(stationDefID, itemID string) []*object return results } -func craftEntryFor(def *object.ItemDef, itemID string) int { +func craftEntryFor(def *item.ItemDef, itemID string) int { for _, craft := range def.Craft { if ei := craftEntry(craft, itemID); ei >= 0 { return ei @@ -146,7 +146,7 @@ func craftEntryFor(def *object.ItemDef, itemID string) int { return -1 } -func craftEntry(craft object.CraftDef, itemID string) int { +func craftEntry(craft item.CraftDef, itemID string) int { for ei, e := range craft.Consume { for _, id := range e.Items { if id == itemID { @@ -157,7 +157,7 @@ func craftEntry(craft object.CraftDef, itemID string) int { return -1 } -func craftMatchesEntry(c *object.CraftDef, itemID string) bool { +func craftMatchesEntry(c *item.CraftDef, itemID string) bool { if c == nil { return false } @@ -171,7 +171,7 @@ func craftMatchesEntry(c *object.CraftDef, itemID string) bool { return false } -func craftHasAllItems(c *object.CraftDef, hasItem func(string) bool) bool { +func craftHasAllItems(c *item.CraftDef, hasItem func(string) bool) bool { if c == nil { return false } @@ -190,7 +190,7 @@ func craftHasAllItems(c *object.CraftDef, hasItem func(string) bool) bool { return true } -func craftHasAllItemsQty(c *object.CraftDef, countItem func(string) int) bool { +func craftHasAllItemsQty(c *item.CraftDef, countItem func(string) int) bool { if c == nil { return false } @@ -213,7 +213,7 @@ func craftHasAllItemsQty(c *object.CraftDef, countItem func(string) int) bool { return true } -func craftConsumeAll(c *object.CraftDef, hasItem func(string) bool, removeItem func(string, int) bool) bool { +func craftConsumeAll(c *item.CraftDef, hasItem func(string) bool, removeItem func(string, int) bool) bool { if c == nil { return false } diff --git a/internal/game/game.go b/internal/game/game.go index dfa5143..320304f 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -11,6 +11,7 @@ import ( "thehouseoficarus/internal/config" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/game/hacking" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" @@ -26,55 +27,71 @@ const ( ClassUnknown ) +// Deps holds the long-lived data stores and engines a Game depends on. They are +// constructed once at startup and never replaced. Deps is embedded in Game, so +// game code accesses them directly (e.g. g.World, g.ItemStore). +type Deps struct { + World *world.World + ObjectStore *object.ObjectStore + ItemStore *item.ItemStore + AccountStore *player.AccountStore + MobStore *world.MobStore + CraftIndex *CraftIndex + CourseStore *CourseStore + Ticks *engine.Engine + ColorConfig *config.ColorsConfig + DataDir string +} + +// Game is the central orchestrator: it owns the data stores (via the embedded +// Deps), the shared mutable game state (flags, combat tracker, command queue, +// safespots), and the per-session runtime bookkeeping. type Game struct { - World *world.World - ObjectStore *object.ObjectStore - ItemStore *object.ItemStore - AccountStore *player.AccountStore - MobStore *world.MobStore - CraftIndex *CraftIndex - CourseStore *CourseStore - Hub *net.Hub - Ticks *engine.Engine - Flags *FlagStore - ColorConfig *config.ColorsConfig - DataDir string - ValidationConfig config.ValidationConfig - restTimers map[string]uint64 - charsMu sync.Mutex - loggedInChars map[string]*net.Session - queue *CommandQueue - pendingDepletions []pendingDepletion + Deps + + Hub *net.Hub + Flags *FlagStore + Combat *combat.Tracker + queue *CommandQueue + safespot *SafespotManager + ValidationConfig config.ValidationConfig + + // Per-tick / per-session runtime bookkeeping. + charsMu sync.Mutex + loggedInChars map[string]*net.Session + restTimers map[string]uint64 guardWatchTimers map[string]int - farmTickCounter int hackingStates map[string]*hacking.Session - safespot *SafespotManager - enterMu sync.Mutex - enterSeqs map[string]*enterSeq + pendingDepletions []pendingDepletion + farmTickCounter int + enterMu sync.Mutex + enterSeqs map[string]*enterSeq } func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.ValidationConfig) *Game { g := &Game{ - World: world.New(dataDir), - ObjectStore: object.NewObjectStore(dataDir), - ItemStore: object.NewItemStore(dataDir), - AccountStore: player.NewAccountStore(dataDir), - MobStore: world.NewMobStore(dataDir), - CraftIndex: NewCraftIndex(), - CourseStore: NewCourseStore(dataDir), - Ticks: engine.New(), - Flags: NewFlagStore(), - ColorConfig: colorConfig, - DataDir: dataDir, + Deps: Deps{ + World: world.New(dataDir), + ObjectStore: object.NewObjectStore(dataDir), + ItemStore: item.NewItemStore(dataDir), + AccountStore: player.NewAccountStore(dataDir), + MobStore: world.NewMobStore(dataDir), + CraftIndex: NewCraftIndex(), + CourseStore: NewCourseStore(dataDir), + Ticks: engine.New(), + ColorConfig: colorConfig, + DataDir: dataDir, + }, + Flags: NewFlagStore(), + Combat: combat.NewTracker(), + queue: NewCommandQueue(), + safespot: NewSafespotManager(), ValidationConfig: valConfig, - restTimers: make(map[string]uint64), loggedInChars: make(map[string]*net.Session), - queue: NewCommandQueue(), - pendingDepletions: nil, - guardWatchTimers: make(map[string]int), - hackingStates: make(map[string]*hacking.Session), - safespot: NewSafespotManager(), - enterSeqs: make(map[string]*enterSeq), + restTimers: make(map[string]uint64), + guardWatchTimers: make(map[string]int), + hackingStates: make(map[string]*hacking.Session), + enterSeqs: make(map[string]*enterSeq), } g.CourseStore.LoadAll() g.LoadMods() @@ -299,7 +316,7 @@ func (g *Game) ProcessQueuedCommands() { } ss, _ := g.safespot.Get(p.Name) isHiding := ss.Active - isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil || p.MoveTicks > 0 || isHiding + isBusy := p.Action != nil || len(p.WalkSequence) > 0 || g.Combat.Get(p.Name) != nil || p.MoveTicks > 0 || isHiding _, isResting := g.restTimers[p.Name] if !isResting && !isBusy && qc.Session.State == net.StateGame { g.writePrompt(qc.Session) diff --git a/internal/game/hacking/liars_dice.go b/internal/game/hacking/liars_dice.go index bcfbd40..1dd27b6 100644 --- a/internal/game/hacking/liars_dice.go +++ b/internal/game/hacking/liars_dice.go @@ -8,16 +8,16 @@ import ( ) type LiarsDiceGame struct { - playerDice []int - aiDice []int - currentBid LiarsBid - hasBid bool - playerTurn bool - round int - gameOver bool - won bool - playerLost int - completed bool + playerDice []int + aiDice []int + currentBid LiarsBid + hasBid bool + playerTurn bool + round int + gameOver bool + won bool + playerLost int + completed bool } type LiarsBid struct { diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go new file mode 100644 index 0000000..f9412d4 --- /dev/null +++ b/internal/game/look_entities.go @@ -0,0 +1,347 @@ +package game + +import ( + "fmt" + "sort" + "strings" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) []string { + mobs := g.MobStore.MobsInRoom(p.RoomID) + if len(mobs) == 0 { + return nil + } + sort.Slice(mobs, func(i, j int) bool { + iDamaged := mobs[i].HP < mobs[i].MaxHP + jDamaged := mobs[j].HP < mobs[j].MaxHP + if iDamaged != jDamaged { + return iDamaged + } + return mobs[i].InstanceID < mobs[j].InstanceID + }) + var lines []string + lines = append(lines, "") + playerLevel := p.CombatLevel() + for _, m := range mobs { + hp := "" + if m.HP < m.MaxHP { + if m.IsTask() { + pct := 0 + if m.MaxHP > 0 { + pct = (m.MaxHP - m.HP) * 100 / m.MaxHP + } + hp = fmt.Sprintf(" [%d%% complete]", pct) + } else { + hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) + } + } + var desc string + if g.Combat.IsMobInCombat(m.InstanceID) { + def, err := g.MobStore.LoadDef(m.DefID) + if err == nil && len(def.CombatDescriptions) > 0 { + target := g.Combat.MobTarget(m.InstanceID) + pattern := def.CombatDescriptions[randInt(len(def.CombatDescriptions))] + desc = " " + fmt.Sprintf(pattern, target) + } + } else if m.IdleDescription != "" { + desc = fmt.Sprintf(" %s", m.IdleDescription) + } + displayName := m.Name + if !m.Unique { + displayName = "A " + m.Name + } + mobColor := "mob" + if m.Protected { + mobColor = "protected_mob" + } + mobLevel := mobCombatLevel(m) + levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel)) + lines = append(lines, fmt.Sprintf("%s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc)) + } + return lines +} + +func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) []string { + objs := g.World.AllObjInstances(p.RoomID) + if len(objs) == 0 { + return g.showFarmPatches(sess, p) + } + var lines []string + lines = append(lines, "") + grouped := make(map[string]int) + var order []string + for _, o := range objs { + if _, ok := grouped[o.DefID]; !ok { + order = append(order, o.DefID) + } + grouped[o.DefID]++ + } + for _, objID := range order { + count := grouped[objID] + def, err := g.ObjectStore.Load(objID) + if err != nil { + continue + } + + if def.Hidden { + continue + } + + if farmPatchDefIDs[objID] { + continue + } + + type instInfo struct { + idx int + depleted bool + sharedMax int + sharedCur int + respawnIn int + quality int + } + var instances []instInfo + for i := 0; i < count; i++ { + st := g.World.GetObjState(p.RoomID, objID, i) + if st == nil { + continue + } + instances = append(instances, instInfo{ + idx: i + 1, + depleted: st.Depleted, + sharedMax: int(st.SharedMax), + sharedCur: st.SharedTimer, + respawnIn: int(st.DepleteTimer), + quality: int(st.Quality), + }) + } + multi := len(instances) > 1 + showTimers := p.OptionBool("depletion") + + var freshIdxs []int + var timed, depleted []instInfo + for _, ins := range instances { + if ins.depleted { + depleted = append(depleted, ins) + } else if ins.sharedMax > 0 && ins.sharedCur < ins.sharedMax { + timed = append(timed, ins) + } else { + freshIdxs = append(freshIdxs, ins.idx) + } + } + + roomDesc := def.InRoomDescription + if roomDesc != "" { + roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc) + } + coloredName := g.objColorize(sess, def, def.Name) + coloredPlural := g.objColorize(sess, def, def.Name+"s") + + if len(freshIdxs) > 0 { + var line string + if roomDesc != "" { + line = roomDesc + } else if len(freshIdxs) == 1 { + line = fmt.Sprintf("A %s is here.", coloredName) + } else { + line = fmt.Sprintf("%d %s are here.", len(freshIdxs), coloredPlural) + } + var suffix string + if multi && (len(timed) > 0 || len(depleted) > 0) { + suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs)) + } + qualityTimer := "" + if showTimers && len(instances) > 0 && instances[0].quality > 0 { + qualityTimer = fmt.Sprintf(" (Burning for %d more ticks)", instances[0].quality) + } + lines = append(lines, fmt.Sprintf("%s%s%s", line, suffix, qualityTimer)) + } + + for _, ins := range timed { + var line string + if roomDesc != "" { + line = roomDesc + } else { + line = fmt.Sprintf("A %s is here.", coloredName) + } + tag := "" + if multi { + tag = fmt.Sprintf(" [%d]", ins.idx) + } + timer := "" + if showTimers { + timer = fmt.Sprintf(" (despawn: %d/%d)", ins.sharedCur, ins.sharedMax) + } + lines = append(lines, fmt.Sprintf("%s%s%s", line, tag, timer)) + } + + for _, ins := range depleted { + var line string + if roomDesc != "" { + line = roomDesc + } else { + line = fmt.Sprintf("A %s is here.", coloredName) + } + tag := "" + if multi { + tag = fmt.Sprintf(" [%d]", ins.idx) + } + timer := "" + if showTimers { + timer = fmt.Sprintf(" (respawns in %d ticks)", ins.respawnIn) + } + lines = append(lines, fmt.Sprintf("%s (depleted)%s%s", line, tag, timer)) + } + } + + lines = append(lines, g.showFarmPatches(sess, p)...) + return lines +} + +func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) []string { + ground := g.World.GroundItemsDetailed(p.RoomID) + if len(ground) == 0 { + return nil + } + showDespawn := p.OptionBool("despawn") + showReserve := p.OptionBool("reserve") + + type displayLine struct { + name string + quantity int + colorName string + annotation string + } + + type groupKey struct { + itemID string + despawnTimer int + } + + var lines []displayLine + groups := make(map[groupKey]*displayLine) + var groupOrder []groupKey + + for _, info := range ground { + def, err := g.ItemStore.Load(info.ItemID) + name := info.ItemID + if err == nil { + name = def.Name + } + coloredName := g.itemColorize(sess, def, name) + + if info.ReservedFor != "" { + var parts []string + if showReserve { + parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) + } else { + parts = append(parts, "reserved") + } + if showDespawn && !info.IsSpawn { + parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) + } + annotation := "" + if len(parts) > 0 { + annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) + } + lines = append(lines, displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, + }) + continue + } + + timerKey := -1 + if showDespawn && !info.IsSpawn { + timerKey = info.DespawnTimer + } + key := groupKey{itemID: info.ItemID, despawnTimer: timerKey} + + if existing, ok := groups[key]; ok { + existing.quantity += info.Quantity + } else { + annotation := "" + if showDespawn && !info.IsSpawn { + annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer) + } + dl := &displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, + } + groups[key] = dl + groupOrder = append(groupOrder, key) + } + } + + for _, key := range groupOrder { + lines = append(lines, *groups[key]) + } + + sort.Slice(lines, func(i, j int) bool { + return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) + }) + + var out []string + out = append(out, "") + out = append(out, "On the ground:") + + type fmtLine struct { + prefix string + annotation string + } + var fmtLines []fmtLine + maxPrefix := 0 + + for _, dl := range lines { + prefix := "" + if dl.quantity > 1 { + prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName) + } else { + prefix = fmt.Sprintf(" %s", dl.colorName) + } + if visibleLen(prefix) > maxPrefix { + maxPrefix = visibleLen(prefix) + } + fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation}) + } + + for _, l := range fmtLines { + if l.annotation != "" { + pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix)) + out = append(out, fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation)) + } else { + out = append(out, l.prefix) + } + } + return out +} + +func (g *Game) showRoomPlayers(sess *net.Session, p *player.Player, room *world.Room) { + others := g.Hub.PlayersInRoom(p.RoomID) + for _, other := range others { + if other != sess && other.Player != nil { + op := other.Player + line := fmt.Sprintf("\n%s is here", g.colorize(sess, "player_name", op.Name)) + if cs := g.Combat.Get(op.Name); cs != nil { + if mob := g.MobStore.GetInstance(cs.MobID); mob != nil && mob.HP > 0 { + name := mobDisplayName(mob, false) + if idx := mobInstanceIdx(mob, g.MobStore.MobsInRoom(p.RoomID)); idx > 0 { + name += fmt.Sprintf(" [%d]", idx) + } + line += fmt.Sprintf(" (fighting %s)", name) + } + } else if desc := g.playerActionDisplay(op); desc != "" { + line += ", " + desc + } + sess.WriteLine(line + ".") + } + } +} diff --git a/internal/game/look_room.go b/internal/game/look_room.go new file mode 100644 index 0000000..8a1da04 --- /dev/null +++ b/internal/game/look_room.go @@ -0,0 +1,107 @@ +package game + +import ( + "fmt" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) showRoomName(sess *net.Session, p *player.Player, room *world.Room) { + sess.WriteLines( + "", + fmt.Sprintf("%s (%s)", g.colorize(sess, "room_name", room.Name), g.colorize(sess, "room_number", fmt.Sprintf("#%d", room.ID))), + ) +} + +func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) []string { + descWidth := p.OptionInt("room_desc_width") + if descWidth <= 0 { + descWidth = 70 + } + rawLines := wrapText(g.roomDescription(sess, room), descWidth) + mode := g.colorMode(sess) + roomDescSpec := g.resolveColor(sess, "room_desc") + lines := make([]string, len(rawLines)) + for i, l := range rawLines { + lines[i] = color.ExpandTagsDefault(mode, roomDescSpec, l) + } + return lines +} + +// roomDescription resolves the room's effective description for this player, +// picking the first conditional variant whose condition passes. +func (g *Game) roomDescription(sess *net.Session, room *world.Room) string { + for _, d := range room.Description { + if d.Condition == nil || g.checkCondition(sess, d.Condition) { + return d.Text + } + } + return "" +} + +// resolveObjDesc resolves the description an object shows to this player. +// The first variant whose condition passes wins; if none pass the object is +// reported as not present (false), so look falls through as if it were not there. +func (g *Game) resolveObjDesc(sess *net.Session, def *object.ObjectDef) (string, bool) { + for _, d := range def.Description { + if d.Condition == nil || g.checkCondition(sess, d.Condition) { + return d.Text, true + } + } + return "", false +} + +func (g *Game) showRoomExits(sess *net.Session, p *player.Player, room *world.Room) { + if len(room.Exits) > 0 { + sess.WriteLine("") + if p.OptionBool("exits") { + sess.WriteLine("Exits:") + type exitLine struct { + dir string + targetName string + } + var lines []exitLine + maxDirLen := 0 + for _, dir := range world.ExitOrder { + exitDef, ok := room.Exits[dir] + if !ok { + continue + } + coloredDir := g.colorize(sess, "exit_direction", string(dir)) + targetRoom, err := g.World.LoadRoom(exitDef.Room) + targetName := g.colorize(sess, "room_number", fmt.Sprintf("#%d", exitDef.Room)) + if err == nil { + targetName = g.colorize(sess, "exit_name", targetRoom.Name) + } + if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { + targetName += " (blocked)" + } + lines = append(lines, exitLine{coloredDir, targetName}) + if visibleLen(coloredDir) > maxDirLen { + maxDirLen = visibleLen(coloredDir) + } + } + for _, l := range lines { + pad := maxDirLen + (len(l.dir) - visibleLen(l.dir)) + sess.WriteLine(fmt.Sprintf(" %-*s - %s", pad, l.dir, l.targetName)) + } + } else { + sess.Write("Exits: ") + first := true + for _, dir := range world.ExitOrder { + if _, ok := room.Exits[dir]; ok { + if !first { + sess.Write(", ") + } + sess.Write(g.colorize(sess, "exit_direction", string(dir))) + first = false + } + } + sess.WriteLine("") + } + } +} diff --git a/internal/game/look_target.go b/internal/game/look_target.go new file mode 100644 index 0000000..4819411 --- /dev/null +++ b/internal/game/look_target.go @@ -0,0 +1,343 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) doLookTarget(sess *net.Session, input string) { + p := sess.Player + lower := strings.ToLower(input) + + if exitDir := g.World.ResolveExit(lower); exitDir != "" { + room, err := g.World.LoadRoom(p.RoomID) + if err != nil { + sess.WriteLine("You can't see anything that way.") + return + } + exitDef, ok := room.Exits[exitDir] + if !ok { + sess.WriteLine("You can't see anything that way.") + return + } + if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { + msg := exitDef.BlockedMessage + if msg == "" { + msg = fmt.Sprintf("The way %s is blocked.", exitDir) + } + sess.WriteLine(msg) + return + } + g.World.SeedGroundItems(exitDef.Room) + g.seedRoomMobs(exitDef.Room) + origRoom := p.RoomID + p.RoomID = exitDef.Room + g.doLook(sess) + p.RoomID = origRoom + return + } + + var best *world.MobInstance + bestQ := world.MatchNone + for _, m := range g.MobStore.MobsInRoom(p.RoomID) { + q := m.MatchQuality(lower) + if q > bestQ { + bestQ = q + best = m + } + } + if best != nil { + mobColor := "mob" + if best.Protected { + mobColor = "protected_mob" + } + mobLevel := mobCombatLevel(best) + levelStr := g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel)) + sess.WriteLines( + "", + fmt.Sprintf("%s %s", g.colorize(sess, mobColor, best.Name), levelStr), + ) + if best.IdleDescription != "" { + sess.WriteLine(best.IdleDescription) + } + sess.WriteLines( + "", + fmt.Sprintf("Attack: %d", best.Attack), + fmt.Sprintf("Strength: %d", best.Strength), + fmt.Sprintf("Defense: %d", best.Defense), + fmt.Sprintf("HP: %d/%d", best.HP, best.MaxHP), + ) + if best.StabDefense != 0 || best.SlashDefense != 0 || best.CrushDefense != 0 || + best.ScienceDefense != 0 || best.RangedDefense != 0 { + sess.WriteLines( + "", + "Defense bonuses:", + fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense), + fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense), + ) + } + if best.Weakness != "" { + sess.WriteLine(fmt.Sprintf("Weakness: %s", best.Weakness)) + } + return + } + + rawInstances := g.World.FindObjInstances(p.RoomID, lower) + + // Group matched object instances by definition, keeping only those visible + // to this player (an object whose conditional descriptions all fail is + // absent). If more than one distinct object matches, disambiguate. + var presentDefs []string + byDef := map[string][]world.ObjState{} + descByDef := map[string]string{} + for _, ist := range rawInstances { + def, err := g.ObjectStore.Load(ist.DefID) + if err != nil { + continue + } + text, present := g.resolveObjDesc(sess, def) + if !present { + continue + } + if _, seen := byDef[ist.DefID]; !seen { + presentDefs = append(presentDefs, ist.DefID) + descByDef[ist.DefID] = text + } + byDef[ist.DefID] = append(byDef[ist.DefID], ist) + } + + if len(presentDefs) > 1 { + sess.WriteLine("That's ambiguous, which one?") + for _, defID := range presentDefs { + if def, err := g.ObjectStore.Load(defID); err == nil { + sess.WriteLine(fmt.Sprintf(" %s", def.Name)) + } + } + return + } + + if len(presentDefs) == 1 { + instances := byDef[presentDefs[0]] + objDescText := descByDef[presentDefs[0]] + st := &instances[0] + def, _ := g.ObjectStore.Load(st.DefID) + + if st.DefID == "estate_directory" { + g.lookEstateDirectory(sess) + return + } + + sess.WriteLine("") + if len(instances) > 1 { + sess.WriteLine(fmt.Sprintf("%d %ss:", len(instances), def.Name)) + } + + if objDescText != "" && !strings.Contains(objDescText, "{quality}") { + sess.WriteLine(color.ExpandTags(g.colorMode(sess), objDescText)) + } + + if def.Safespot != nil { + realSt := g.World.GetObjState(p.RoomID, st.DefID, st.Index) + if realSt != nil { + levelStr := "intact" + if realSt.SafespotLevel <= 0 { + levelStr = fmt.Sprintf("intact (level %d/%d)", len(def.Safespot.Levels), len(def.Safespot.Levels)) + } else if realSt.SafespotLevel < len(def.Safespot.Levels) { + levelStr = fmt.Sprintf("degraded (level %d/%d)", realSt.SafespotLevel, len(def.Safespot.Levels)) + } else { + levelStr = fmt.Sprintf("intact (level %d/%d)", realSt.SafespotLevel, len(def.Safespot.Levels)) + } + blockInfo := "anything" + if def.Safespot.MaxBlockSize != "" { + blockInfo = fmt.Sprintf("up to %s mobs", def.Safespot.MaxBlockSize) + } + sess.WriteLine(fmt.Sprintf("Safespot: %s — blocks %s", levelStr, blockInfo)) + if len(realSt.SafespotOccupants) > 0 { + sess.WriteLine(fmt.Sprintf("Occupied by: %s", strings.Join(realSt.SafespotOccupants, ", "))) + } + } + } + + if p.OptionBool("depletion") { + for _, ist := range instances { + if ist.Depleted { + if len(instances) > 1 { + sess.WriteLine(fmt.Sprintf("%s %d: depleted, respawns in %d ticks.", def.Name, ist.Index+1, int(ist.DepleteTimer))) + } else { + sess.WriteLine(fmt.Sprintf("Depleted, respawns in %d ticks.", int(ist.DepleteTimer))) + } + } else if ist.SharedMax > 0 && float64(ist.SharedTimer) < ist.SharedMax { + if len(instances) > 1 { + sess.WriteLine(fmt.Sprintf("%s %d: despawn timer %d/%.0f.", def.Name, ist.Index+1, ist.SharedTimer, ist.SharedMax)) + } else { + sess.WriteLine(fmt.Sprintf("Despawn timer: %d/%.0f ticks.", ist.SharedTimer, ist.SharedMax)) + } + } + } + } + for _, ist := range instances { + if ist.Quality > 0 && strings.Contains(objDescText, "{quality}") { + desc := strings.ReplaceAll(objDescText, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) + sess.WriteLine(color.ExpandTags(g.colorMode(sess), desc)) + } + } + + farmSuffix := g.farmObjSuffix(p, st.DefID, st.Index) + if farmSuffix != "" { + sess.WriteLine(farmSuffix) + } + return + } + + ground := g.World.GroundItems(p.RoomID) + for itemID := range ground { + def, err := g.ItemStore.Load(itemID) + if err != nil || !def.MatchesName(input) { + continue + } + sess.WriteLines( + "", + g.itemColorize(sess, def, def.Name), + color.ExpandTags(g.colorMode(sess), def.Description), + fmt.Sprintf("Value: %d credits", def.Value), + ) + g.showItemStats(sess, def) + return + } + + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot == nil { + continue + } + def, err := g.ItemStore.Load(slot.ItemID) + if err != nil || !def.MatchesName(input) { + continue + } + lines := []string{ + "", + g.itemColorize(sess, def, def.Name), + color.ExpandTags(g.colorMode(sess), def.Description), + fmt.Sprintf("Value: %d credits", def.Value), + } + if slot.MaxQuality > 0 { + lines = append(lines, fmt.Sprintf("Has %d units of butane left.", slot.Quality)) + } + sess.WriteLines(lines...) + g.showItemStats(sess, def) + return + } + + others := g.Hub.PlayersInRoom(p.RoomID) + for _, other := range others { + if other == sess || other.Player == nil { + continue + } + op := other.Player + if strings.ToLower(op.Name) != lower { + continue + } + showPlayerInfo(g, sess, op) + return + } + + sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input)) +} + +func showPlayerInfo(g *Game, sess *net.Session, p *player.Player) { + myP := sess.Player + theirLevel := p.CombatLevel() + levelStr := g.levelColorize(sess, myP.CombatLevel(), theirLevel, fmt.Sprint(theirLevel)) + sess.WriteLines( + "", + p.Name, + fmt.Sprintf("Combat Level: %s", levelStr), + fmt.Sprintf("HP: %d/%d", p.HP, p.MaxHP()), + "", + ) + + for _, s := range player.AllSkills { + level := p.Level(s) + sess.WriteLine(fmt.Sprintf("%-12s Level: %d", s, level)) + } + + sess.WriteLine("") + sess.WriteLine("Equipment:") + for _, slot := range EquipSlots { + itemID, ok := p.Equipment[slot] + if !ok { + continue + } + name := itemID + var itemDef *item.ItemDef + if def, err := g.ItemStore.Load(itemID); err == nil { + name = def.Name + itemDef = def + } + sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, itemDef, name))) + } + + if p.Description != "" { + sess.WriteLine("") + sess.WriteLine(p.Description) + } +} + +func (g *Game) showItemStats(sess *net.Session, def *item.ItemDef) { + s := def.Stats + hasAttack := s.StabAttack != 0 || s.SlashAttack != 0 || s.CrushAttack != 0 || + s.ScienceAttack != 0 || s.RangedAttack != 0 + hasDefense := s.StabDefense != 0 || s.SlashDefense != 0 || s.CrushDefense != 0 || + s.ScienceDefense != 0 || s.RangedDefense != 0 + hasOther := s.StrengthBonus != 0 || s.RangedStrength != 0 || + s.ScienceDamage != 0 || s.TechnologyBonus != 0 + + if !hasAttack && !hasDefense && !hasOther { + return + } + + sess.WriteLine("") + if hasAttack || hasDefense { + sess.WriteLine("Attack bonuses: Defense bonuses:") + sess.WriteLine(fmt.Sprintf(" Stab: %+4d Stab: %+4d", s.StabAttack, s.StabDefense)) + sess.WriteLine(fmt.Sprintf(" Slash: %+4d Slash: %+4d", s.SlashAttack, s.SlashDefense)) + sess.WriteLine(fmt.Sprintf(" Crush: %+4d Crush: %+4d", s.CrushAttack, s.CrushDefense)) + sess.WriteLine(fmt.Sprintf(" Science:%+4d Science:%+4d", s.ScienceAttack, s.ScienceDefense)) + sess.WriteLine(fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", s.RangedAttack, s.RangedDefense)) + } + if hasOther { + sess.WriteLine("") + sess.WriteLine("Other bonuses:") + if s.StrengthBonus != 0 { + sess.WriteLine(fmt.Sprintf(" Melee strength: %+d", s.StrengthBonus)) + } + if s.RangedStrength != 0 { + sess.WriteLine(fmt.Sprintf(" Ranged strength: %+d", s.RangedStrength)) + } + if s.ScienceDamage != 0 { + sess.WriteLine(fmt.Sprintf(" Science damage: %+d", s.ScienceDamage)) + } + if s.TechnologyBonus != 0 { + sess.WriteLine(fmt.Sprintf(" Technology: %+d", s.TechnologyBonus)) + } + } + if def.AttackType != "" { + sess.WriteLine(fmt.Sprintf("Attack type: %s", def.AttackType)) + } + if def.Speed > 0 { + sess.WriteLine(fmt.Sprintf("Speed: %.0f", def.Speed)) + } + if len(def.Requirements) > 0 { + sess.WriteLine("") + sess.WriteLine("Requirements:") + for skill, level := range def.Requirements { + sess.WriteLine(fmt.Sprintf(" %s: %d", skill, level)) + } + } +} diff --git a/internal/game/map_test.go b/internal/game/map_test.go index dded62e..9a85806 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -10,7 +10,7 @@ import ( func TestBuildTinyMap(t *testing.T) { g := &Game{ - World: world.New("../../data"), + Deps: Deps{World: world.New("../../data")}, } mg := mapGlyphsForPlayer(true) @@ -97,7 +97,7 @@ func TestWrapText(t *testing.T) { func TestBuildFullMap(t *testing.T) { g := &Game{ - World: world.New("../../data"), + Deps: Deps{World: world.New("../../data")}, } mg := mapGlyphsForPlayer(true) diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go new file mode 100644 index 0000000..c30e7bb --- /dev/null +++ b/internal/game/production_engine.go @@ -0,0 +1,359 @@ +package game + +import ( + "fmt" + "math/rand" + "strings" + + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) resolveCraftVar(sess *net.Session, varSpec string, craft *item.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { + if !strings.Contains(varSpec, "%") { + return varSpec + } + + outDef, _ := g.ItemStore.Load(outputID) + outputName := outputID + if outDef != nil { + outputName = g.itemColorize(sess, outDef, outDef.Name) + } + varSpec = strings.ReplaceAll(varSpec, "%n", outputName) + + for i, e := range craft.Consume { + key := fmt.Sprintf("%%i%d", i+1) + if !strings.Contains(varSpec, key) { + continue + } + var matchedID string + for _, id := range e.Items { + if hasItem(id) { + matchedID = id + break + } + } + if matchedID == "" && len(e.Items) > 0 { + matchedID = e.Items[0] + } + if def, err := g.ItemStore.Load(matchedID); err == nil { + varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) + } else { + varSpec = strings.ReplaceAll(varSpec, key, matchedID) + } + } + + for i, bpID := range byproducts { + key := fmt.Sprintf("%%b%d", i+1) + if !strings.Contains(varSpec, key) { + continue + } + if def, err := g.ItemStore.Load(bpID); err == nil { + varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) + } else { + varSpec = strings.ReplaceAll(varSpec, key, bpID) + } + } + + return color.ExpandTags(g.colorMode(sess), varSpec) +} + +func (g *Game) resolveCraftMessage(sess *net.Session, itemMsg, defaultMsg string, craft *item.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { + msg := itemMsg + if msg == "" { + msg = defaultMsg + } + if msg == "" { + return "" + } + return g.resolveCraftVar(sess, msg, craft, outputID, byproducts, hasItem) +} + +func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, actionType behavior.ActionType, displayVerb, startMsg, endMsg string, count int) { + g.cancelAction(p) + g.cancelBgAction(p) + + skill := craft.EffectiveSkill() + if skill != "" { + skillLevel := p.Level(player.SkillName(skill)) + if skillLevel < craft.Level { + sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", craft.Level, skill)) + g.reprompt(sess) + return + } + } + + if !craftHasAllItemsQty(craft, p.CountItem) { + sess.WriteLine("You don't have the required materials.") + g.reprompt(sess) + return + } + + outDef, _ := g.ItemStore.Load(item.ID) + + wait := craft.Wait + if wait <= 0 { + wait = 4 + } + + outputName := item.ID + if outDef != nil { + outputName = outDef.Name + } + + p.Action = &behavior.Action{ + Type: actionType, + TargetID: item.ID, + TargetName: outputName, + Data: &behavior.ProductionData{ + ItemID: item.ID, + Phase: 0, + Wait: wait, + StartMsg: startMsg, + EndMsg: endMsg, + Remaining: count, + }, + WaitLeft: engine.ToTicks(1), + } + + g.broadcastAction(sess, "%s starts %s.", p.Name, displayVerb) +} + +func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item *item.ItemDef, count int) { + craft := item.FirstCraft() + if craft == nil { + return + } + + _, stationName := g.findStation(p.RoomID, craft.Station) + if stationName == "" { + stationName = "inventory" + } + + info, ok := productionTypes[craft.Type] + if !ok { + info = productionTypeInfo{ + ActionType: behavior.ActionType(craft.Type), + DisplayVerb: craft.Type, + StartMessage: "You start " + craft.Type + " %i1.", + SuccessMessage: "You produce %n.", + EndMessage: "You've finished " + craft.Type + ".", + } + } + + startMsg := g.resolveCraftMessage(sess, craft.StartMessage, info.StartMessage, craft, item.ID, nil, p.HasItem) + if stationName != "inventory" { + startMsg += " on the " + stationName + "." + } + + endMsg := g.resolveCraftMessage(sess, craft.EndMessage, info.EndMessage, craft, item.ID, nil, p.HasItem) + + g.startProduction(sess, p, item, craft, info.ActionType, info.DisplayVerb, startMsg, endMsg, count) +} + +func (g *Game) loadCraftItem(itemID string) *item.ItemDef { + item, err := g.ItemStore.Load(itemID) + if err != nil || len(item.Craft) == 0 { + return nil + } + return item +} + +func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { + d, ok := p.Action.Data.(*behavior.ProductionData) + if !ok { + g.cancelAction(p) + return false + } + itemID := d.ItemID + phase := d.Phase + wait := d.Wait + startMsg := d.StartMsg + endMsg := d.EndMsg + remaining := d.Remaining + + item := g.loadCraftItem(itemID) + if item == nil { + g.cancelAction(p) + return false + } + craft := item.FirstCraft() + + if phase == 0 { + if startMsg != "" { + sess.WriteLine(startMsg) + } + if len(craft.Steps) > 0 { + d.NextStepIndex = 0 + d.StepsAccumulatedTicks = 0 + } + d.Phase = 1 + p.Action.WaitLeft = engine.ToTicks(wait) + return true + } + + if stepsIdx := d.NextStepIndex; stepsIdx < len(craft.Steps) { + accTicks := d.StepsAccumulatedTicks + accTicks += wait + d.StepsAccumulatedTicks = accTicks + for i := stepsIdx; i < len(craft.Steps); i++ { + if accTicks >= craft.Steps[i].Tick { + sess.WriteLine(g.resolveCraftVar(sess, craft.Steps[i].Message, craft, item.ID, nil, p.HasItem)) + d.NextStepIndex = i + 1 + } + } + } + + skill := craft.EffectiveSkill() + skillLevel := p.Level(player.SkillName(skill)) + chance := 1.0 + if craft.Success != nil { + chance = behavior.SuccessChance(behavior.SuccessFormula(*craft.Success), skillLevel, craft.Level) + } + + info, _ := productionTypes[craft.Type] + + if rand.Float64() < chance { + outputQty := craft.OutputQty + if outputQty <= 0 { + outputQty = 1 + } + + byproducts := g.collectByproducts(p, item) + + placed := false + outDef, _ := g.ItemStore.Load(item.ID) + if outDef != nil && outDef.Stackable { + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot != nil && slot.ItemID == item.ID { + craftConsumeAll(craft, p.HasItem, p.RemoveItem) + slot.Quantity += outputQty + placed = true + break + } + } + } + if !placed { + craftConsumeAll(craft, p.HasItem, p.RemoveItem) + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + sess.WriteLine("Your inventory is too full!") + g.cancelAction(p) + return false + } + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) + } + + for _, bp := range byproducts { + if slot := p.FirstFreeSlot(); slot >= 0 { + p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, Quantity: 1}) + } + } + + if craft.XP > 0 { + if newLevel := p.AddSkillXP(player.SkillName(skill), craft.XP); newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) + } + } + g.AccountStore.SaveCharacter(p) + + var defaultSuccess string + if info.SuccessMessage != "" { + defaultSuccess = info.SuccessMessage + } else { + defaultSuccess = "You produce %n." + } + msg := g.resolveCraftMessage(sess, craft.Message, defaultSuccess, craft, item.ID, byproducts, p.HasItem) + if p.OptionBool("xp_drops") && craft.XP > 0 { + msg += g.formatXpDropSingle(sess, p, player.SkillName(skill), craft.XP) + } + sess.WriteLine(msg) + } else { + craftConsumeAll(craft, p.HasItem, p.RemoveItem) + + if craft.Fail != "" { + freeSlot := p.FirstFreeSlot() + if freeSlot >= 0 { + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: craft.Fail, Quantity: 1}) + } + } + g.AccountStore.SaveCharacter(p) + + msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, p.HasItem) + if msg != "" { + sess.WriteLine(g.colorize(sess, "damage_taken", msg)) + } + } + + if remaining > 0 { + remaining-- + d.Remaining = remaining + if remaining <= 0 { + if endMsg != "" { + sess.WriteLine(endMsg) + } + g.cancelAction(p) + return false + } + } + + if g.canContinueProduction(p, item) { + p.Action.WaitLeft = engine.ToTicks(wait) + return true + } + + if endMsg != "" { + sess.WriteLine(endMsg) + } + g.cancelAction(p) + return false +} + +func (g *Game) collectByproducts(p *player.Player, item *item.ItemDef) []string { + craft := item.FirstCraft() + if craft == nil { + return nil + } + var byproducts []string + for _, e := range craft.Consume { + if len(e.Byproducts) == 0 { + continue + } + for i, id := range e.Items { + if p.HasItem(id) && i < len(e.Byproducts) && e.Byproducts[i] != "" { + byproducts = append(byproducts, e.Byproducts[i]) + break + } + } + } + return byproducts +} + +func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef) bool { + craft := item.FirstCraft() + if craft == nil { + return false + } + if !craftHasAllItemsQty(craft, p.CountItem) { + return false + } + outputQty := craft.OutputQty + if outputQty <= 0 { + outputQty = 1 + } + outDef, _ := g.ItemStore.Load(item.ID) + if outDef != nil && outDef.Stackable { + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot != nil && slot.ItemID == item.ID { + return true + } + } + } + return p.FirstFreeSlot() >= 0 +} diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go new file mode 100644 index 0000000..7181a0e --- /dev/null +++ b/internal/game/production_menu.go @@ -0,0 +1,416 @@ +package game + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/ui" +) + +func (g *Game) handleRecipeChoice(sess *net.Session, input string) { + input = strings.TrimSpace(input) + + menuData := sess.PendingMenu + if len(menuData) == 0 { + sess.State = net.StateGame + g.reprompt(sess) + return + } + + if input == "" { + sess.PendingMenu = nil + sess.State = net.StateGame + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + + if choice, err := strconv.Atoi(input); err == nil { + sess.PendingMenu = nil + sess.State = net.StateGame + if choice <= 0 || choice > len(menuData) { + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + g.dispatchMenuEntry(sess, menuData[choice-1]) + return + } + + lower := strings.ToLower(input) + matchIdx := -1 + for i, entry := range menuData { + name := g.menuEntryName(entry) + if name == "" { + continue + } + if strings.ToLower(name) == lower || behavior.WordPrefixMatch(input, name) { + if matchIdx >= 0 { + sess.WriteLine("That's ambiguous.") + return + } + matchIdx = i + } + } + if matchIdx >= 0 { + sess.PendingMenu = nil + sess.State = net.StateGame + g.dispatchMenuEntry(sess, menuData[matchIdx]) + return + } + + sess.PendingMenu = nil + sess.State = net.StateGame + sess.WriteLine("Never mind.") + g.reprompt(sess) +} + +func (g *Game) menuEntryName(entry map[string]string) string { + if iid, ok := entry["item_id"]; ok { + if def, _ := g.ItemStore.Load(iid); def != nil { + return def.Name + } + return iid + } + if barID, ok := entry["bar_id"]; ok { + if def, _ := g.ItemStore.Load(barID); def != nil { + return def.Name + } + return barID + } + return "" +} + +func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) { + if iid, ok := entry["item_id"]; ok { + g.promptHowMany(sess, iid) + return + } + if barID, ok := entry["bar_id"]; ok { + g.showSmithTable(sess, sess.Player, barID) + return + } + g.reprompt(sess) +} + +func (g *Game) formatMaterials(sess *net.Session, item *item.ItemDef) string { + craft := item.FirstCraft() + if craft == nil { + return "" + } + var parts []string + for _, e := range craft.Consume { + itemName := e.Items[0] + if def, err := g.ItemStore.Load(e.Items[0]); err == nil { + itemName = g.itemColorize(sess, def, def.Name) + } + if e.Quantity > 1 { + parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Quantity)) + } else { + parts = append(parts, itemName) + } + } + return strings.Join(parts, ", ") +} + +func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []*item.ItemDef, title, skill, lastFlagKey, promptVerb string, background bool) { + sort.Slice(items, func(i, j int) bool { + return items[i].FirstCraft().Level < items[j].FirstCraft().Level + }) + + mode := g.colorMode(sess) + skillLevel := p.Level(player.SkillName(skill)) + dimSpec := color.Parse("240") + + tbl := &ui.Table{ + Title: title, + Columns: []string{"#", "Product", "Materials", "Level"}, + } + + for i, item := range items { + craft := item.FirstCraft() + outDef, _ := g.ItemStore.Load(item.ID) + productName := item.ID + if outDef != nil { + productName = outDef.Name + } + + outputQty := craft.OutputQty + if outputQty <= 0 { + outputQty = 1 + } + if outDef != nil && outDef.Stackable && outputQty > 1 { + productName = fmt.Sprintf("%s x%d", productName, outputQty) + } + + matStr := g.formatMaterials(sess, item) + levelStr := fmt.Sprint(craft.Level) + numStr := fmt.Sprintf("%d", i+1) + + canMake := skillLevel >= craft.Level && craftHasAllItemsQty(craft, p.CountItem) + if canMake { + productName = g.itemColorize(sess, outDef, productName) + } else { + productName = color.Render(mode, dimSpec, productName) + matStr = color.Render(mode, dimSpec, matStr) + levelStr = color.Render(mode, dimSpec, levelStr) + numStr = color.Render(mode, dimSpec, numStr) + } + + tbl.Rows = append(tbl.Rows, []string{numStr, productName, matStr, levelStr}) + } + + unicode := p.OptionBool("unicode") + sess.WriteLine("") + for _, line := range tbl.Render(unicode) { + sess.WriteLine(line) + } + + lastItemID, _ := p.Flags[lastFlagKey].(string) + hint := "" + if lastItemID != "" { + for _, item := range items { + if item.ID == lastItemID { + if outDef, err := g.ItemStore.Load(item.ID); err == nil { + hint = outDef.Name + } + break + } + } + } + + if hint != "" { + sess.Write(fmt.Sprintf("%s what (enter for all %s): ", promptVerb, hint)) + } else { + sess.Write(fmt.Sprintf("%s what: ", promptVerb)) + } + + sess.PendingMenu = itemMenuData(itemIDs(items)) + sess.PendingSkill = skill + sess.PendingLastFlag = lastFlagKey + sess.PendingBackground = background + sess.State = net.StateProductChoice +} + +func (g *Game) handleProductChoice(sess *net.Session, input string) { + p := sess.Player + menuData := sess.PendingMenu + skill := sess.PendingSkill + lastFlagKey := sess.PendingLastFlag + background := sess.PendingBackground + sess.PendingMenu = nil + sess.PendingSkill = "" + sess.PendingLastFlag = "" + sess.PendingBackground = false + sess.State = net.StateGame + + input = strings.TrimSpace(input) + + var items []*item.ItemDef + for _, entry := range menuData { + iid := entry["item_id"] + if item := g.loadCraftItem(iid); item != nil { + items = append(items, item) + } + } + + if len(items) == 0 { + g.reprompt(sess) + return + } + + sort.Slice(items, func(i, j int) bool { + return items[i].FirstCraft().Level < items[j].FirstCraft().Level + }) + + lastItemID, _ := p.Flags[lastFlagKey].(string) + + sel, errMsg := g.resolveCraftByInput(input, items, lastItemID) + switch errMsg { + case "ambiguous": + sess.WriteLine("That's ambiguous.") + g.reprompt(sess) + return + case "never_mind": + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + + craft := sel.Item.FirstCraft() + skillLevel := p.Level(player.SkillName(skill)) + if skillLevel < craft.Level { + sess.WriteLine(fmt.Sprintf("You need level %d %s to make that.", craft.Level, skill)) + g.reprompt(sess) + return + } + if !craftHasAllItemsQty(craft, p.CountItem) { + sess.WriteLine("You don't have the materials for that.") + g.reprompt(sess) + return + } + + p.EnsureFlags() + p.Flags[lastFlagKey] = sel.Item.ID + g.AccountStore.SaveCharacter(p) + + if background { + g.startFletchAction(sess, p, sel.Item, sel.Count) + } else { + g.startProductionFromItem(sess, p, sel.Item, sel.Count) + } +} + +func itemMenuData(itemIDs []string) []map[string]string { + out := make([]map[string]string, len(itemIDs)) + for i, id := range itemIDs { + out[i] = map[string]string{"item_id": id} + } + return out +} + +func itemIDs(defs []*item.ItemDef) []string { + ids := make([]string, len(defs)) + for i, d := range defs { + ids[i] = d.ID + } + return ids +} + +func (g *Game) promptHowMany(sess *net.Session, itemID string) { + sess.PendingItemID = itemID + sess.State = net.StateHowMany + sess.Write("How many (return for all)?: ") +} + +type craftSelection struct { + Item *item.ItemDef + Count int +} + +func (g *Game) resolveCraftByInput(input string, items []*item.ItemDef, lastItemID string) (sel craftSelection, errMsg string) { + if input == "" { + if lastItemID == "" { + return sel, "never_mind" + } + for i := range items { + if items[i].ID == lastItemID { + sel.Item = items[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(items) { + sel.Item = items[idx-1] + sel.Count = qty + return sel, "" + } + + matchCount := 0 + for i := range items { + outDef, _ := g.ItemStore.Load(items[i].ID) + name := items[i].ID + if outDef != nil { + name = outDef.Name + } + if behavior.WordPrefixMatch(productName, name) { + if matchCount == 0 { + sel.Item = items[i] + sel.Count = qty + } + matchCount++ + } + } + if matchCount > 1 { + return sel, "ambiguous" + } + if sel.Item == nil { + return sel, "never_mind" + } + return sel, "" +} + +func (g *Game) showMenuTable(sess *net.Session, title string, names []string) { + p := sess.Player + tbl := &ui.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) itemName(sess *net.Session, item *item.ItemDef) string { + if item != nil { + if def, err := g.ItemStore.Load(item.ID); err == nil { + return g.itemColorize(sess, def, def.Name) + } + return item.ID + } + return "" +} + +func (g *Game) showRecipeChoice(sess *net.Session, p *player.Player, items []*item.ItemDef, emptyMsg, title string, autoOption string) { + if len(items) == 0 { + sess.WriteLine(emptyMsg) + return + } + if len(items) == 1 { + if autoOption != "" && p.OptionBool(autoOption) { + g.startProductionFromItem(sess, p, items[0], 0) + return + } + g.promptHowMany(sess, items[0].ID) + return + } + sess.State = net.StateRecipeChoice + sess.PendingMenu = itemMenuData(itemIDs(items)) + var names []string + for _, item := range items { + names = append(names, g.itemName(sess, item)) + } + g.showMenuTable(sess, title, names) +} + +func (g *Game) handleHowMany(sess *net.Session, input string) { + p := sess.Player + itemID := sess.PendingItemID + sess.PendingItemID = "" + 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 + } + + item := g.loadCraftItem(itemID) + if item == nil { + g.reprompt(sess) + return + } + + g.startProductionFromItem(sess, p, item, count) +} diff --git a/internal/game/ui_color.go b/internal/game/render_color.go index 962728a..c6f154c 100644 --- a/internal/game/ui_color.go +++ b/internal/game/render_color.go @@ -3,6 +3,7 @@ package game import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/config" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" ) @@ -70,7 +71,7 @@ func (g *Game) objColorize(sess *net.Session, objDef *object.ObjectDef, text str return text } -func (g *Game) itemColorize(sess *net.Session, itemDef *object.ItemDef, text string) string { +func (g *Game) itemColorize(sess *net.Session, itemDef *item.ItemDef, text string) string { if itemDef != nil && itemDef.Color != "" { spec := color.Parse(itemDef.Color) return color.Render(g.colorMode(sess), spec, text) diff --git a/internal/game/ui_combat.go b/internal/game/render_combat.go index 77faae8..7285ce7 100644 --- a/internal/game/ui_combat.go +++ b/internal/game/render_combat.go @@ -1,6 +1,9 @@ package game -import "thehouseoficarus/internal/net" +import ( + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/ui" +) // renderBar is the shared bar renderer for both HP bars and task-progress bars. // lowColor is the color used when the value falls below 40% (160 red for HP, @@ -28,8 +31,8 @@ func (g *Game) renderBar(sess *net.Session, current, max, lowColor int) string { } unicode := sess.Player != nil && sess.Player.OptionBool("unicode") - style := &BarStyle{FilledColor: fgColor, EmptyColor: 238, EmptyDim: true} - bar := RenderColoredBar(current, max, 10, unicode, style, g.colorMode(sess)) + style := &ui.BarStyle{FilledColor: fgColor, EmptyColor: 238, EmptyDim: true} + bar := ui.RenderColoredBar(current, max, 10, unicode, style, g.colorMode(sess)) return "[" + bar + "]" } diff --git a/internal/game/ui_help.go b/internal/game/render_help.go index 28f4371..1727153 100644 --- a/internal/game/ui_help.go +++ b/internal/game/render_help.go @@ -5,9 +5,10 @@ import ( "path/filepath" "strings" - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/net" "gopkg.in/yaml.v3" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/ui" ) type HelpDef struct { @@ -85,7 +86,7 @@ var commandList = []cmdEntry{ func LoadHelp(dataDir string) ([]HelpDef, error) { dir := filepath.Join(dataDir, "help") var helps []HelpDef - action.WalkYAMLDir(dir, func(path, id string, data []byte) error { + behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { var h HelpDef if err := yaml.Unmarshal(data, &h); err != nil { return nil @@ -104,7 +105,7 @@ func (g *Game) doHelp(sess *net.Session, topic string) { } sess.WriteLine("") - t := &Table{ + t := &ui.Table{ Title: "Commands", Columns: []string{"Command", "Type", "Description"}, } diff --git a/internal/game/ui_map.go b/internal/game/render_map.go index a07606f..2bac749 100644 --- a/internal/game/ui_map.go +++ b/internal/game/render_map.go @@ -10,12 +10,12 @@ import ( ) type mapGlyphs struct { - topLeft, topRight rune - bottomLeft, bottomRight rune - side rune - topFill rune - connectorH, connectorV rune - upArrow, downArrow rune + topLeft, topRight rune + bottomLeft, bottomRight rune + side rune + topFill rune + connectorH, connectorV rune + upArrow, downArrow rune } func mapGlyphsForPlayer(unicode bool) mapGlyphs { diff --git a/internal/game/ui_prompt.go b/internal/game/render_prompt.go index 6263ac2..7d3fafd 100644 --- a/internal/game/ui_prompt.go +++ b/internal/game/render_prompt.go @@ -5,7 +5,6 @@ import ( "strings" "thehouseoficarus/internal/color" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) @@ -42,7 +41,7 @@ func (g *Game) expandPromptVars(sess *net.Session, text string) string { text = strings.ReplaceAll(text, "%S", attackStyleLong(p.AttackStyle)) mobHP, mobMaxHP := "", "" - if cs := combat.GetCombat(p.Name); cs != nil { + if cs := g.Combat.Get(p.Name); cs != nil { mob := g.MobStore.GetInstance(cs.MobID) if mob != nil && mob.HP > 0 { mobHP = fmt.Sprint(mob.HP) diff --git a/internal/game/ui_xpdrop.go b/internal/game/render_xpdrop.go index a9bff23..a9bff23 100644 --- a/internal/game/ui_xpdrop.go +++ b/internal/game/render_xpdrop.go diff --git a/internal/game/sys_combat.go b/internal/game/sys_combat.go index 91a07cf..e020ccf 100644 --- a/internal/game/sys_combat.go +++ b/internal/game/sys_combat.go @@ -4,7 +4,6 @@ import ( "fmt" "sort" - "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" @@ -29,10 +28,10 @@ func (g *Game) dropItemsOnDeath(p *player.Player) { val = def.Value * inv.Quantity } items = append(items, deathDrop{ - itemID: inv.ItemID, - quantity: inv.Quantity, + itemID: inv.ItemID, + quantity: inv.Quantity, totalValue: val, - invSlot: slot, + invSlot: slot, }) } @@ -42,11 +41,11 @@ func (g *Game) dropItemsOnDeath(p *player.Player) { val = def.Value } items = append(items, deathDrop{ - itemID: itemID, - quantity: 1, + itemID: itemID, + quantity: 1, totalValue: val, - isEquip: true, - equipSlot: eqSlot, + isEquip: true, + equipSlot: eqSlot, }) } @@ -72,7 +71,7 @@ func (g *Game) dropItemsOnDeath(p *player.Player) { func (g *Game) checkAggro(sess *net.Session) { p := sess.Player - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { return } @@ -83,7 +82,7 @@ func (g *Game) checkAggro(sess *net.Session) { if !mob.Aggressive || mob.HP <= 0 || mob.Protected { continue } - if combat.IsMobInCombat(mob.InstanceID) { + if g.Combat.IsMobInCombat(mob.InstanceID) { continue } mobLevel := mobCombatLevel(mob) @@ -95,13 +94,13 @@ func (g *Game) checkAggro(sess *net.Session) { } aggMob := mob g.Ticks.Subscribe(engine.ToTicks(1), func() bool { - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { return false } if aggMob.HP <= 0 || aggMob.RoomID != p.RoomID { return false } - if combat.IsMobInCombat(aggMob.InstanceID) { + if g.Combat.IsMobInCombat(aggMob.InstanceID) { return false } attacker := aggMob.Name diff --git a/internal/game/sys_science.go b/internal/game/sys_science.go index 517e051..0ee446b 100644 --- a/internal/game/sys_science.go +++ b/internal/game/sys_science.go @@ -8,9 +8,9 @@ import ( "gopkg.in/yaml.v3" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -44,7 +44,7 @@ func (g *Game) LoadMods() error { dir := filepath.Join(g.DataDir, "modules") AllMods = nil modByID = make(map[string]*ModDef) - action.WalkYAMLDir(dir, func(path, id string, data []byte) error { + behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { var m ModDef if err := yaml.Unmarshal(data, &m); err != nil { return nil @@ -173,7 +173,7 @@ var chipMap = map[string]chipEntry{ } func (g *Game) hasDeckEquipped(p *player.Player) bool { - itemID, ok := p.Equipment[object.SlotMainHand] + itemID, ok := p.Equipment[item.SlotMainHand] if !ok { return false } @@ -181,11 +181,11 @@ func (g *Game) hasDeckEquipped(p *player.Player) bool { if err != nil { return false } - return def.WeaponType == object.WeaponScience + return def.WeaponType == item.WeaponScience } func (g *Game) providesJunk(p *player.Player) string { - itemID, ok := p.Equipment[object.SlotMainHand] + itemID, ok := p.Equipment[item.SlotMainHand] if !ok { return "" } @@ -296,5 +296,3 @@ func (g *Game) totalScienceAttack(p *player.Player) int { } return total } - - diff --git a/internal/game/sys_technology.go b/internal/game/sys_technology.go index fa0301f..7df68d4 100644 --- a/internal/game/sys_technology.go +++ b/internal/game/sys_technology.go @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v3" - "thehouseoficarus/internal/action" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" @@ -45,7 +45,7 @@ func (g *Game) LoadTechs() error { dir := filepath.Join(g.DataDir, "techs") AllTechs = nil techByID = make(map[string]*TechDef) - action.WalkYAMLDir(dir, func(path, id string, data []byte) error { + behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { var t TechDef if err := yaml.Unmarshal(data, &t); err != nil { return nil @@ -185,14 +185,14 @@ func (g *Game) tryRecharge(sess *net.Session, p *player.Player, input string) bo instances := g.World.FindObjInstances(p.RoomID, lower) for _, st := range instances { if st.DefID == "charging_station" || st.DefID == "power_conduit" { - if p.Battery >= p.MaxBattery() { - sess.WriteLine("Your battery is already full.") - } else { - p.Battery = p.MaxBattery() - g.AccountStore.SaveCharacter(p) - sess.WriteLine(g.colorize(sess, "battery", - "You connect to the charging station. Your battery is fully recharged.")) - } + if p.Battery >= p.MaxBattery() { + sess.WriteLine("Your battery is already full.") + } else { + p.Battery = p.MaxBattery() + g.AccountStore.SaveCharacter(p) + sess.WriteLine(g.colorize(sess, "battery", + "You connect to the charging station. Your battery is fully recharged.")) + } return true } } diff --git a/internal/game/tick.go b/internal/game/tick.go index 6d79964..ee9ecf2 100644 --- a/internal/game/tick.go +++ b/internal/game/tick.go @@ -4,8 +4,7 @@ import ( "fmt" "math/rand" - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) @@ -23,7 +22,7 @@ func (g *Game) DisconnectTick() { g.Hub.Remove(sess) continue } - if combat.GetCombat(p.Name) != nil { + if g.Combat.Get(p.Name) != nil { continue } sess.DisconnectTicks-- @@ -91,7 +90,7 @@ func (g *Game) WanderTick() { if inst.HP <= 0 || inst.WanderInterval <= 0 { continue } - if combat.IsMobInCombat(inst.InstanceID) { + if g.Combat.IsMobInCombat(inst.InstanceID) { continue } inst.WanderTickCounter++ @@ -140,23 +139,23 @@ func (g *Game) WanderTick() { if p == nil { continue } - if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") { - if m.level > 0 { - levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) - sess.WriteLine(fmt.Sprintf("\n%s %s leaves.", g.colorize(sess, "mob", m.name), levelStr)) - } else { - sess.WriteLine(fmt.Sprintf("\n%s moves away.", g.colorize(sess, "mob", m.name))) + if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") { + if m.level > 0 { + levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) + sess.WriteLine(fmt.Sprintf("\n%s %s leaves.", g.colorize(sess, "mob", m.name), levelStr)) + } else { + sess.WriteLine(fmt.Sprintf("\n%s moves away.", g.colorize(sess, "mob", m.name))) + } } - } - if p.RoomID == m.toRoom && p.OptionBool("mob_enter") { - if m.level > 0 { - levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) - sess.WriteLine(fmt.Sprintf("\n%s %s enters.", g.colorize(sess, "mob", m.name), levelStr)) - } else { - sess.WriteLine(fmt.Sprintf("\n%s drifts in.", g.colorize(sess, "mob", m.name))) + if p.RoomID == m.toRoom && p.OptionBool("mob_enter") { + if m.level > 0 { + levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) + sess.WriteLine(fmt.Sprintf("\n%s %s enters.", g.colorize(sess, "mob", m.name), levelStr)) + } else { + sess.WriteLine(fmt.Sprintf("\n%s drifts in.", g.colorize(sess, "mob", m.name))) + } } } - } } } @@ -167,7 +166,7 @@ func (g *Game) SharedDepletionTick() { if p == nil || p.Action == nil || p.Action.Type != "gather" { continue } - if gd, ok := p.Action.Data.(*action.GatherData); ok { + if gd, ok := p.Action.Data.(*behavior.GatherData); ok { activeKeys[gd.InstanceKey] = true } } @@ -331,7 +330,7 @@ func (g *Game) MoveTick() { } p.MoveTicks-- if p.MoveTicks > 0 { - if combat.GetCombat(p.Name) != nil && p.OptionBool("run_countdown") { + if g.Combat.Get(p.Name) != nil && p.OptionBool("run_countdown") { if p.MoveTicks > 1 { sess.WriteLine(fmt.Sprintf("Running in %d ticks...", p.MoveTicks)) } else { diff --git a/internal/game/ui_bar.go b/internal/game/ui_bar.go deleted file mode 100644 index ca0b721..0000000 --- a/internal/game/ui_bar.go +++ /dev/null @@ -1,104 +0,0 @@ -package game - -import ( - "math" - "strings" - - "thehouseoficarus/internal/color" -) - -type BarStyle struct { - FilledColor int - EmptyColor int - EmptyDim bool -} - -func RenderBar(current, max, width int, unicode bool) string { - if max <= 0 { - max = 1 - } - if current < 0 { - current = 0 - } - if current > max { - current = max - } - filled := int(math.Round(float64(current) * float64(width) / float64(max))) - if filled > width { - filled = width - } - if filled < 0 { - filled = 0 - } - - fillRune, emptyRune := barRunes(unicode) - return strings.Repeat(fillRune, filled) + strings.Repeat(emptyRune, width-filled) -} - -func RenderBarF(current, max float64, width int, unicode bool) string { - if max <= 0 { - max = 1 - } - ratio := current / max - if ratio > 1 { - ratio = 1 - } - if ratio < 0 { - ratio = 0 - } - filled := int(math.Round(ratio * float64(width))) - if filled > width { - filled = width - } - if filled < 0 { - filled = 0 - } - - fillRune, emptyRune := barRunes(unicode) - return strings.Repeat(fillRune, filled) + strings.Repeat(emptyRune, width-filled) -} - -func RenderColoredBar(current, max, width int, unicode bool, style *BarStyle, mode string) string { - if max <= 0 { - max = 1 - } - if current < 0 { - current = 0 - } - if current > max { - current = max - } - filled := int(math.Round(float64(current) * float64(width) / float64(max))) - if filled > width { - filled = width - } - if filled < 0 { - filled = 0 - } - - fillRune, emptyRune := barRunes(unicode) - fillStr := strings.Repeat(fillRune, filled) - emptyStr := strings.Repeat(emptyRune, width-filled) - - if style != nil && mode != "none" && mode != "" { - if style.FilledColor >= 0 { - fillStr = color.Render(mode, color.ColorSpec{Fg: style.FilledColor}, fillStr) - } - if style.EmptyColor >= 0 { - spec := color.ColorSpec{Fg: style.EmptyColor} - if style.EmptyDim { - spec.Dim = true - } - emptyStr = color.Render(mode, spec, emptyStr) - } - } - - return fillStr + emptyStr -} - -func barRunes(unicode bool) (fill, empty string) { - if unicode { - return "█", "░" - } - return "#", "." -} diff --git a/internal/game/ui_table.go b/internal/game/ui_table.go deleted file mode 100644 index b96dca1..0000000 --- a/internal/game/ui_table.go +++ /dev/null @@ -1,150 +0,0 @@ -package game - -import ( - "strings" - - "thehouseoficarus/internal/color" -) - -type tableGlyphs struct { - topLeft, topSep, topRight rune - side, colSep rune - sepLeft, sepCross, sepRight rune - botLeft, botSep, botRight rune - fillH, fillHSep rune - titleSep rune -} - -func tableGlyphSet(unicode bool) tableGlyphs { - if unicode { - return tableGlyphs{ - topLeft: '╔', topSep: '╤', topRight: '╗', side: '║', colSep: '│', - sepLeft: '╟', sepCross: '┼', sepRight: '╢', - botLeft: '╚', botSep: '╧', botRight: '╝', fillH: '═', fillHSep: '─', - titleSep: '┬', - } - } - return tableGlyphs{ - topLeft: '+', topSep: '+', topRight: '+', side: '|', colSep: '|', - sepLeft: '+', sepCross: '+', sepRight: '+', - botLeft: '+', botSep: '+', botRight: '+', fillH: '-', fillHSep: '-', - titleSep: '+', - } -} - -type Table struct { - Title string - Columns []string - Rows [][]string -} - -func (t *Table) Render(unicode bool) []string { - g := tableGlyphSet(unicode) - - nCols := len(t.Columns) - if nCols == 0 && len(t.Rows) > 0 { - nCols = len(t.Rows[0]) - } - if nCols == 0 { - return nil - } - - colWidths := make([]int, nCols) - for i, h := range t.Columns { - colWidths[i] = color.VisibleLen(h) - } - for _, row := range t.Rows { - for i, cell := range row { - if i >= nCols { - break - } - vl := color.VisibleLen(cell) - if vl > colWidths[i] { - colWidths[i] = vl - } - } - } - - makeSep := func(left, cross, right rune, fill rune) string { - var b strings.Builder - b.WriteRune(left) - for i := 0; i < nCols; i++ { - if i > 0 { - b.WriteRune(cross) - } - b.WriteString(strings.Repeat(string(fill), colWidths[i]+2)) - } - b.WriteRune(right) - return b.String() - } - - makeRow := func(cells []string) string { - var b strings.Builder - b.WriteRune(g.side) - for i := 0; i < nCols; i++ { - if i > 0 { - b.WriteRune(g.colSep) - } - cell := "" - if i < len(cells) { - cell = cells[i] - } - pad := colWidths[i] - color.VisibleLen(cell) - if pad < 0 { - pad = 0 - } - b.WriteString(" " + cell + strings.Repeat(" ", pad) + " ") - } - b.WriteRune(g.side) - return b.String() - } - - hasTitle := t.Title != "" - hasCols := len(t.Columns) > 0 - - var out []string - - if hasTitle { - innerWidth := 0 - for _, w := range colWidths { - innerWidth += w + 2 - } - innerWidth += nCols - 1 - - var tb strings.Builder - tb.WriteRune(g.topLeft) - tb.WriteString(strings.Repeat(string(g.fillH), innerWidth)) - tb.WriteRune(g.topRight) - out = append(out, tb.String()) - - var tr strings.Builder - tr.WriteRune(g.side) - tr.WriteString(" ") - tr.WriteString(t.Title) - padding := innerWidth - 2 - color.VisibleLen(t.Title) - if padding < 0 { - padding = 0 - } - tr.WriteString(strings.Repeat(" ", padding)) - tr.WriteString(" ") - tr.WriteRune(g.side) - out = append(out, tr.String()) - - out = append(out, makeSep(g.sepLeft, g.titleSep, g.sepRight, g.fillHSep)) - } else { - out = append(out, makeSep(g.topLeft, g.topSep, g.topRight, g.fillH)) - } - - if hasCols { - out = append(out, makeRow(t.Columns)) - out = append(out, makeSep(g.sepLeft, g.sepCross, g.sepRight, g.fillHSep)) - } - - for _, row := range t.Rows { - out = append(out, makeRow(row)) - } - - out = append(out, makeSep(g.botLeft, g.botSep, g.botRight, g.fillH)) - - return out -} diff --git a/internal/game/validate_source.go b/internal/game/validate_source.go new file mode 100644 index 0000000..b93c45a --- /dev/null +++ b/internal/game/validate_source.go @@ -0,0 +1,53 @@ +package game + +import ( + "thehouseoficarus/internal/validate" +) + +// ValidateAndLog runs all startup integrity checks and logs the results. +func (g *Game) ValidateAndLog() { + validate.LogIssues(validate.Run(g.validationSource())) +} + +// validationSource assembles the read-only view of all data stores that the +// validate package needs, including the game-defined tech and course tables. +func (g *Game) validationSource() validate.Source { + courses := g.CourseStore.AllCourses() + courseViews := make([]validate.CourseView, 0, len(courses)) + for id, cfg := range courses { + rooms := make([]int, 0, len(cfg.Obstacles)) + for _, obs := range cfg.Obstacles { + rooms = append(rooms, obs.RoomID) + } + courseViews = append(courseViews, validate.CourseView{ + ID: id, + StartRoom: cfg.StartRoom, + ObstacleRooms: rooms, + }) + } + + techViews := make([]validate.TechView, 0, len(AllTechs)) + techIDs := make(map[string]bool, len(AllTechs)) + for _, t := range AllTechs { + techViews = append(techViews, validate.TechView{ + ID: t.ID, + Name: t.Name, + Category: t.Category, + DrainRate: t.DrainRate, + }) + techIDs[t.ID] = true + } + + return validate.Source{ + DataDir: g.DataDir, + Items: g.ItemStore, + Objects: g.ObjectStore, + Mobs: g.MobStore, + World: g.World, + Courses: courseViews, + Techs: techViews, + TechIDs: techIDs, + CheckSources: g.ValidationConfig.CheckSources, + IgnoreUnreachable: g.ValidationConfig.IgnoreUnreachable, + } +} |
