aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-10 04:44:31 -0400
committerhistoria <[not public]>2026-06-10 04:44:31 -0400
commit69d8757ad983697cdc9182fdcc1cdb612a77fb41 (patch)
tree348c26d1f6defe3aef64f5aa9e7cefed0e764214 /internal/game
parenta226d72e51eecb768b13600303f73483118d9104 (diff)
downloadthehouseoficarus-69d8757ad983697cdc9182fdcc1cdb612a77fb41.tar.gz
feat: yaml architecture for complex object interaction
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/action.go77
-rw-r--r--internal/game/cmd_alias.go102
-rw-r--r--internal/game/cmd_attack.go2
-rw-r--r--internal/game/cmd_drop.go61
-rw-r--r--internal/game/cmd_look.go36
-rw-r--r--internal/game/cmd_misc.go18
-rw-r--r--internal/game/cmd_move.go2
-rw-r--r--internal/game/game.go33
-rw-r--r--internal/game/session.go5
9 files changed, 305 insertions, 31 deletions
diff --git a/internal/game/action.go b/internal/game/action.go
index 029fec3..62cdf62 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -427,10 +427,13 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
return
}
- for i, opt := range node.Options {
- if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { continue
+ visible := 0
+ for _, opt := range node.Options {
+ if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) {
+ continue
}
- sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text))
+ visible++
+ sess.WriteLine(fmt.Sprintf(" %d. %s", visible, opt.Text))
}
sess.State = net.StateTalk
sess.Write("\nChoice: ")
@@ -527,6 +530,12 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
for k, v := range na.SetFlags {
g.WorldFlags[k] = v
}
+ for k, v := range na.SetPlayerFlags {
+ if p.Flags == nil {
+ p.Flags = make(map[string]any)
+ }
+ p.Flags[k] = v
+ }
if na.TakeItem != "" {
if p.HasItem(na.TakeItem) {
p.RemoveItem(na.TakeItem, 1)
@@ -541,10 +550,59 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
g.AccountStore.SaveCharacter(p)
}
}
+ if na.Teleport > 0 {
+ p.RoomID = na.Teleport
+ g.AccountStore.SaveCharacter(p)
+ g.World.SeedGroundItems(p.RoomID)
+ g.seedRoomMobs(p.RoomID)
+ g.ensureRoomObjects(p.RoomID)
+ if g.Hub != nil {
+ g.Hub.EnterRoom(sess, p.RoomID)
+ }
+ g.doLook(sess)
+ g.RunEnterSteps(sess, p.RoomID)
+ }
+ if na.Heal > 0 {
+ p.HP += na.Heal
+ if maxHP := p.MaxHP(); p.HP > maxHP {
+ p.HP = maxHP
+ }
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine(fmt.Sprintf("You regain %d hitpoints.", na.Heal))
+ }
}
func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool {
p, _ := sess.Player.(*player.Player)
+
+ if c.AllOf != nil {
+ for _, sub := range c.AllOf {
+ if !g.checkCondition(sess, &sub) {
+ return false
+ }
+ }
+ return true
+ }
+ if c.AnyOf != nil {
+ for _, sub := range c.AnyOf {
+ if g.checkCondition(sess, &sub) {
+ return true
+ }
+ }
+ return false
+ }
+
+ if c.PlayerFlag != "" {
+ if p == nil || p.Flags == nil {
+ return c.Not
+ }
+ val, ok := p.Flags[c.PlayerFlag]
+ if c.Not {
+ return !ok || val != c.Value
+ }
+ return ok && val == c.Value
+ }
+
if c.Flag != "" {
val, ok := g.WorldFlags[c.Flag]
if c.Not {
@@ -714,24 +772,13 @@ func normalizeVerb(v string) string {
return v
}
-func (g *Game) CheckExitCondition(c *world.ExitCondition) bool {
- if c.Flag != "" {
- val, ok := g.WorldFlags[c.Flag]
- if c.Not {
- return !ok || val != c.Value
- }
- return ok && val == c.Value
- }
- return true
-}
-
func (g *Game) RunEnterSteps(sess *net.Session, roomID int) {
room, err := g.World.LoadRoom(roomID)
if err != nil || len(room.OnEnter) == 0 {
return
}
for _, step := range room.OnEnter {
- if step.Condition != nil && !g.CheckExitCondition(step.Condition) {
+ if step.Condition != nil && !g.checkCondition(sess, step.Condition) {
continue
}
if step.Message != "" {
diff --git a/internal/game/cmd_alias.go b/internal/game/cmd_alias.go
new file mode 100644
index 0000000..cb0537e
--- /dev/null
+++ b/internal/game/cmd_alias.go
@@ -0,0 +1,102 @@
+package game
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "thirdcollapse/internal/net"
+)
+
+func (g *Game) doAlias(sess *net.Session, args []string) {
+ if sess.Account == nil {
+ sess.WriteLine("No account loaded.")
+ return
+ }
+
+ if len(args) == 0 {
+ if len(sess.Account.Aliases) == 0 {
+ sess.WriteLine("\nNo aliases defined. Use ALIAS <name> <command> to create one.")
+ return
+ }
+ sess.WriteLine("\nAliases:")
+ names := make([]string, 0, len(sess.Account.Aliases))
+ maxLen := 0
+ for name := range sess.Account.Aliases {
+ names = append(names, name)
+ if len(name) > maxLen {
+ maxLen = len(name)
+ }
+ }
+ sort.Strings(names)
+ for _, name := range names {
+ sess.WriteLine(fmt.Sprintf(" %-*s: %s", maxLen, name, sess.Account.Aliases[name]))
+ }
+ return
+ }
+
+ if len(args) < 2 {
+ sess.WriteLine("Usage: ALIAS <name> <command>")
+ return
+ }
+
+ aliasName := strings.ToLower(args[0])
+ aliasCmd := strings.Join(args[1:], " ")
+
+ if sess.Account.Aliases == nil {
+ sess.Account.Aliases = make(map[string]string)
+ }
+ sess.Account.Aliases[aliasName] = aliasCmd
+
+ acc, err := g.AccountStore.LoadAccount(sess.Account.Name)
+ if err != nil {
+ sess.WriteLine("Error saving alias.")
+ return
+ }
+ acc.Aliases = sess.Account.Aliases
+ if err := g.AccountStore.SaveAccount(acc); err != nil {
+ sess.WriteLine("Error saving alias.")
+ return
+ }
+
+ sess.WriteLine(fmt.Sprintf("\nAlias set: %s -> %s", aliasName, aliasCmd))
+}
+
+func (g *Game) doUnalias(sess *net.Session, args []string) {
+ if sess.Account == nil {
+ sess.WriteLine("No account loaded.")
+ return
+ }
+
+ if len(args) == 0 {
+ sess.WriteLine("\nUsage: UNALIAS <name>")
+ sess.WriteLine("Removes an alias. Use ALIAS with no arguments to see your aliases.")
+ return
+ }
+
+ aliasName := strings.ToLower(args[0])
+
+ if sess.Account.Aliases == nil {
+ sess.Account.Aliases = make(map[string]string)
+ }
+
+ if _, ok := sess.Account.Aliases[aliasName]; !ok {
+ sess.WriteLine(fmt.Sprintf("No alias '%s' found.", aliasName))
+ return
+ }
+
+ delete(sess.Account.Aliases, aliasName)
+
+ acc, err := g.AccountStore.LoadAccount(sess.Account.Name)
+ if err != nil {
+ sess.WriteLine("Error saving alias.")
+ return
+ }
+ acc.Aliases = sess.Account.Aliases
+ if err := g.AccountStore.SaveAccount(acc); err != nil {
+ sess.WriteLine("Error saving alias.")
+ return
+ }
+
+ sess.WriteLine(fmt.Sprintf("\nAlias '%s' removed.", aliasName))
+}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index ebf88ac..4e2d2c0 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -454,7 +454,7 @@ func (g *Game) respawnMob(instanceID string) {
if g.Hub != nil {
for _, sess := range g.Hub.PlayersInRoom(homeRoom) {
if p, ok := sess.Player.(*player.Player); ok && p.Toggles["mobspawn"] {
- sess.WriteLine(fmt.Sprintf("\n%s (level %d) enters the area.", mobDisplayName(inst, false), mobCombatLevel(inst)))
+ sess.WriteLine(fmt.Sprintf("\n%s (level %d) spawns in the area.", mobDisplayName(inst, false), mobCombatLevel(inst)))
}
}
}
diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go
index 511688a..2f79a2f 100644
--- a/internal/game/cmd_drop.go
+++ b/internal/game/cmd_drop.go
@@ -2,6 +2,7 @@ package game
import (
"fmt"
+ "strings"
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
@@ -10,6 +11,22 @@ import (
func (g *Game) doDrop(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
+ if input == "all" {
+ count := 0
+ for _, slot := range p.Inventory {
+ if slot != nil && slot.Quantity > 0 {
+ count++
+ }
+ }
+ if count == 0 {
+ sess.WriteLine("You have nothing to drop.")
+ return
+ }
+ sess.State = net.StateDropAllConfirm
+ sess.WriteLine(fmt.Sprintf("\nDrop all %d items? [y/N]", count))
+ return
+ }
+
matches := g.findInventoryMatches(input, p)
if len(matches) == 0 {
sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", input))
@@ -46,3 +63,47 @@ func (g *Game) doDrop(sess *net.Session, input string) {
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("You drop a %s.", name))
}
+
+func (g *Game) handleDropAllConfirm(sess *net.Session, input string) {
+ p, ok := sess.Player.(*player.Player)
+ if !ok {
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ input = strings.TrimSpace(strings.ToLower(input))
+ if input != "y" && input != "yes" {
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ var dropped []string
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot == nil || slot.Quantity <= 0 {
+ continue
+ }
+ g.World.AddGroundItem(p.RoomID, slot.ItemID, slot.Quantity)
+ def, _ := g.ItemStore.Load(slot.ItemID)
+ name := slot.ItemID
+ if def != nil {
+ name = def.Name
+ }
+ dropped = append(dropped, name)
+ p.SetInvSlot(i, nil)
+ }
+
+ g.AccountStore.SaveCharacter(p)
+ sess.State = net.StateGame
+
+ if len(dropped) == 0 {
+ sess.WriteLine("\nYou have nothing to drop.")
+ } else {
+ sess.Write(fmt.Sprintf("\nYou drop your "))
+ innerPickupReport(sess, dropped)
+ sess.WriteLine(".")
+ }
+ sess.Write("\r\n> ")
+}
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index a544ec1..e7d30d6 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -75,7 +75,7 @@ func (g *Game) doLook(sess *net.Session) {
for _, objID := range order {
count := grouped[objID]
def, err := g.ObjectStore.Load(objID)
- if err != nil {
+ if err != nil || def.Hidden {
continue
}
@@ -116,26 +116,46 @@ func (g *Game) doLook(sess *net.Session) {
if len(ground) > 0 {
sess.WriteLine("")
sess.WriteLine("On the ground:")
+
+ type groundLine struct {
+ prefix string
+ reserved string
+ }
+ var lines []groundLine
+ maxPrefix := 0
+
for _, info := range ground {
def, err := g.ItemStore.Load(info.ItemID)
name := info.ItemID
if err == nil {
name = def.Name
}
- line := ""
+ prefix := ""
if info.Quantity > 1 {
- line = fmt.Sprintf(" %d x %s", info.Quantity, name)
+ prefix = fmt.Sprintf(" %d x %s", info.Quantity, name)
} else {
- line = fmt.Sprintf(" %s", name)
+ prefix = fmt.Sprintf(" %s", name)
+ }
+ if len(prefix) > maxPrefix {
+ maxPrefix = len(prefix)
}
+ reserved := ""
if info.ReservedFor != "" {
if p.Toggles["reserve"] {
- line += fmt.Sprintf(" (reserved for %s for %d ticks)", info.ReservedFor, info.ReserveTimer)
+ reserved = fmt.Sprintf(" (reserved for %s for %d ticks)", info.ReservedFor, info.ReserveTimer)
} else {
- line += " (reserved)"
+ reserved = " (reserved)"
}
}
- sess.WriteLine(line)
+ lines = append(lines, groundLine{prefix, reserved})
+ }
+
+ for _, l := range lines {
+ if l.reserved != "" {
+ sess.WriteLine(fmt.Sprintf("%-*s%s", maxPrefix+1, l.prefix, l.reserved))
+ } else {
+ sess.WriteLine(l.prefix)
+ }
}
}
@@ -153,7 +173,7 @@ func (g *Game) doLook(sess *net.Session) {
if err == nil {
targetName = targetRoom.Name
}
- if exitDef.Condition != nil && !g.CheckExitCondition(exitDef.Condition) {
+ if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) {
targetName += " (blocked)"
}
sess.WriteLine(fmt.Sprintf(" %-6s - %s", dir, targetName))
diff --git a/internal/game/cmd_misc.go b/internal/game/cmd_misc.go
index f56a0ff..c02bd35 100644
--- a/internal/game/cmd_misc.go
+++ b/internal/game/cmd_misc.go
@@ -105,13 +105,21 @@ func (g *Game) doStyle(sess *net.Session, input string) {
}
input = strings.ToLower(input)
+ var matches []string
for _, s := range styles {
- if s == input {
- p.AttackStyle = player.AttackStyle(s)
- g.AccountStore.SaveCharacter(p)
- sess.WriteLine(fmt.Sprintf("\nCombat style set to %s.", s))
- return
+ if strings.HasPrefix(s, input) {
+ matches = append(matches, s)
}
}
+ if len(matches) == 1 {
+ p.AttackStyle = player.AttackStyle(matches[0])
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine(fmt.Sprintf("\nCombat style set to %s.", matches[0]))
+ return
+ }
+ if len(matches) > 1 {
+ sess.WriteLine(fmt.Sprintf("\nAmbiguous style: %s. Choices: %s", input, strings.Join(matches, ", ")))
+ return
+ }
sess.WriteLine(fmt.Sprintf("\nUnknown style: %s. Choices: accurate, aggressive, defensive, balanced", input))
}
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index b95f5c5..e288dad 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -28,7 +28,7 @@ func (g *Game) doMove(sess *net.Session, dir string) {
return
}
- if exitDef.Condition != nil && !g.CheckExitCondition(exitDef.Condition) {
+ if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) {
msg := exitDef.BlockedMessage
if msg == "" {
msg = fmt.Sprintf("The way %s is blocked.", exitDir)
diff --git a/internal/game/game.go b/internal/game/game.go
index d793516..08096b4 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -87,6 +87,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
g.handleDescriptionChange(sess, input)
case net.StateTalk:
g.handleTalkInput(sess, input)
+ case net.StateDropAllConfirm:
+ g.handleDropAllConfirm(sess, input)
}
}
@@ -100,6 +102,24 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
g.cancelRest(p.Name)
}
+ if sess.Account != nil && sess.Account.Aliases != nil {
+ firstSpace := strings.Index(input, " ")
+ var firstWord, rest string
+ if firstSpace > 0 {
+ firstWord = input[:firstSpace]
+ rest = strings.TrimLeft(input[firstSpace:], " ")
+ } else {
+ firstWord = input
+ }
+ if expansion, ok := sess.Account.Aliases[strings.ToLower(firstWord)]; ok {
+ if rest != "" {
+ input = expansion + " " + rest
+ } else {
+ input = expansion
+ }
+ }
+ }
+
parts := strings.Fields(strings.ToLower(input))
cmd := parts[0]
args := parts[1:]
@@ -144,7 +164,12 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
if len(args) == 0 {
sess.WriteLine("Say what?")
} else {
- g.doSay(sess, strings.Join(parts[1:], " "))
+ msgStart := strings.Index(strings.ToLower(input), "say ") + 4
+ if msgStart >= 4 && msgStart < len(input) {
+ g.doSay(sess, input[msgStart:])
+ } else {
+ g.doSay(sess, strings.Join(args, " "))
+ }
}
case "sc", "score":
g.doScore(sess)
@@ -183,6 +208,12 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
g.StartAction(sess, "talk", strings.Join(args, " "))
return
}
+ case "alias":
+ g.doAlias(sess, args)
+ return
+ case "unalias":
+ g.doUnalias(sess, args)
+ return
default:
sess.WriteLine("Unknown command.")
}
diff --git a/internal/game/session.go b/internal/game/session.go
index edc904d..7fe45e7 100644
--- a/internal/game/session.go
+++ b/internal/game/session.go
@@ -49,6 +49,10 @@ func (g *Game) handlePassword(sess *net.Session, input string) {
Name: acc.Name,
PasswordHash: acc.PasswordHash,
Characters: acc.Characters,
+ Aliases: acc.Aliases,
+ }
+ if sess.Account.Aliases == nil {
+ sess.Account.Aliases = make(map[string]string)
}
g.showMenu(sess)
}
@@ -106,6 +110,7 @@ func (g *Game) handleNewAccountConfirm(sess *net.Session, input string) {
sess.Account = &net.AccountEntry{
Name: acc.Name,
PasswordHash: acc.PasswordHash,
+ Aliases: make(map[string]string),
}
sess.PendingPass = ""
g.showMenu(sess)