aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/act.go (renamed from internal/game/action.go)33
-rw-r--r--internal/game/act_agility.go (renamed from internal/game/action_agility.go)12
-rw-r--r--internal/game/act_burn.go (renamed from internal/game/action_burn.go)46
-rw-r--r--internal/game/act_clean.go (renamed from internal/game/action_clean.go)8
-rw-r--r--internal/game/act_default_target.go (renamed from internal/game/action_default_target.go)0
-rw-r--r--internal/game/act_farm.go (renamed from internal/game/action_farm.go)15
-rw-r--r--internal/game/act_finishing_blow.go (renamed from internal/game/action_finishing_blow.go)3
-rw-r--r--internal/game/act_fletch.go (renamed from internal/game/action_fletch.go)72
-rw-r--r--internal/game/act_gather.go (renamed from internal/game/action_gather.go)32
-rw-r--r--internal/game/act_identify.go (renamed from internal/game/action_identify.go)6
-rw-r--r--internal/game/act_room.go (renamed from internal/game/action_room.go)0
-rw-r--r--internal/game/act_search.go (renamed from internal/game/action_search.go)18
-rw-r--r--internal/game/act_state.go (renamed from internal/game/action_state.go)53
-rw-r--r--internal/game/act_steal.go (renamed from internal/game/action_steal.go)53
-rw-r--r--internal/game/act_talk.go (renamed from internal/game/action_talk.go)30
-rw-r--r--internal/game/act_use.go (renamed from internal/game/action_use.go)10
-rw-r--r--internal/game/cmd_agility.go3
-rw-r--r--internal/game/cmd_aps.go3
-rw-r--r--internal/game/cmd_attack.go730
-rw-r--r--internal/game/cmd_bank.go7
-rw-r--r--internal/game/cmd_clean.go13
-rw-r--r--internal/game/cmd_color.go3
-rw-r--r--internal/game/cmd_consume.go6
-rw-r--r--internal/game/cmd_cook.go8
-rw-r--r--internal/game/cmd_craft.go51
-rw-r--r--internal/game/cmd_equipment.go4
-rw-r--r--internal/game/cmd_estate_directory.go6
-rw-r--r--internal/game/cmd_farm.go22
-rw-r--r--internal/game/cmd_fletch.go48
-rw-r--r--internal/game/cmd_get.go6
-rw-r--r--internal/game/cmd_hide.go3
-rw-r--r--internal/game/cmd_id.go6
-rw-r--r--internal/game/cmd_jack.go3
-rw-r--r--internal/game/cmd_look.go770
-rw-r--r--internal/game/cmd_mods.go3
-rw-r--r--internal/game/cmd_move.go9
-rw-r--r--internal/game/cmd_option.go3
-rw-r--r--internal/game/cmd_quit.go7
-rw-r--r--internal/game/cmd_registry.go232
-rw-r--r--internal/game/cmd_remove.go20
-rw-r--r--internal/game/cmd_score.go17
-rw-r--r--internal/game/cmd_shop.go23
-rw-r--r--internal/game/cmd_skills.go3
-rw-r--r--internal/game/cmd_smelt.go8
-rw-r--r--internal/game/cmd_smith.go12
-rw-r--r--internal/game/cmd_stats.go14
-rw-r--r--internal/game/cmd_stop.go3
-rw-r--r--internal/game/cmd_tech.go3
-rw-r--r--internal/game/cmd_trigger.go5
-rw-r--r--internal/game/cmd_trigger_transport.go3
-rw-r--r--internal/game/cmd_trigger_utility.go35
-rw-r--r--internal/game/cmd_use.go22
-rw-r--r--internal/game/cmd_verbs.go4
-rw-r--r--internal/game/cmd_wear.go30
-rw-r--r--internal/game/combat_attack.go449
-rw-r--r--internal/game/combat_mob.go301
-rw-r--r--internal/game/condition_test.go38
-rw-r--r--internal/game/core_course.go14
-rw-r--r--internal/game/core_equip.go8
-rw-r--r--internal/game/core_production.go779
-rw-r--r--internal/game/core_startup.go887
-rw-r--r--internal/game/core_types.go20
-rw-r--r--internal/game/core_utils.go2
-rw-r--r--internal/game/craft_index.go56
-rw-r--r--internal/game/game.go99
-rw-r--r--internal/game/hacking/liars_dice.go20
-rw-r--r--internal/game/look_entities.go347
-rw-r--r--internal/game/look_room.go107
-rw-r--r--internal/game/look_target.go343
-rw-r--r--internal/game/map_test.go4
-rw-r--r--internal/game/production_engine.go359
-rw-r--r--internal/game/production_menu.go416
-rw-r--r--internal/game/render_color.go (renamed from internal/game/ui_color.go)3
-rw-r--r--internal/game/render_combat.go (renamed from internal/game/ui_combat.go)9
-rw-r--r--internal/game/render_help.go (renamed from internal/game/ui_help.go)9
-rw-r--r--internal/game/render_map.go (renamed from internal/game/ui_map.go)12
-rw-r--r--internal/game/render_prompt.go (renamed from internal/game/ui_prompt.go)3
-rw-r--r--internal/game/render_xpdrop.go (renamed from internal/game/ui_xpdrop.go)0
-rw-r--r--internal/game/sys_combat.go23
-rw-r--r--internal/game/sys_science.go14
-rw-r--r--internal/game/sys_technology.go20
-rw-r--r--internal/game/tick.go39
-rw-r--r--internal/game/ui_bar.go104
-rw-r--r--internal/game/ui_table.go150
-rw-r--r--internal/game/validate_source.go53
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(&sectionObjs, &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(&sectionObjs, &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,
+ }
+}