diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/action/behavior.go | 20 | ||||
| -rw-r--r-- | internal/game/action.go | 77 | ||||
| -rw-r--r-- | internal/game/cmd_alias.go | 102 | ||||
| -rw-r--r-- | internal/game/cmd_attack.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_drop.go | 61 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 36 | ||||
| -rw-r--r-- | internal/game/cmd_misc.go | 18 | ||||
| -rw-r--r-- | internal/game/cmd_move.go | 2 | ||||
| -rw-r--r-- | internal/game/game.go | 33 | ||||
| -rw-r--r-- | internal/game/session.go | 5 | ||||
| -rw-r--r-- | internal/net/server.go | 2 | ||||
| -rw-r--r-- | internal/object/object.go | 1 | ||||
| -rw-r--r-- | internal/player/account.go | 7 | ||||
| -rw-r--r-- | internal/player/player.go | 1 | ||||
| -rw-r--r-- | internal/world/room.go | 21 |
15 files changed, 335 insertions, 53 deletions
diff --git a/internal/action/behavior.go b/internal/action/behavior.go index d7f683a..d8eb909 100644 --- a/internal/action/behavior.go +++ b/internal/action/behavior.go @@ -38,16 +38,22 @@ type TalkOption struct { } type NodeAction struct { - SetFlags map[string]any `yaml:"set_flags"` - GiveItem string `yaml:"give_item"` - TakeItem string `yaml:"take_item"` + SetFlags map[string]any `yaml:"set_flags"` + SetPlayerFlags map[string]any `yaml:"set_player_flags"` + GiveItem string `yaml:"give_item"` + TakeItem string `yaml:"take_item"` + Teleport int `yaml:"teleport"` + Heal int `yaml:"heal"` } type Condition struct { - Flag string `yaml:"flag"` - Value any `yaml:"value"` - Not bool `yaml:"not"` - HasItem string `yaml:"has_item"` + Flag string `yaml:"flag"` + Value any `yaml:"value"` + Not bool `yaml:"not"` + PlayerFlag string `yaml:"player_flag"` + HasItem string `yaml:"has_item"` + AllOf []Condition `yaml:"all_of"` + AnyOf []Condition `yaml:"any_of"` } type UseConfig struct { 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) diff --git a/internal/net/server.go b/internal/net/server.go index 8fb975f..08d427f 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -26,6 +26,7 @@ const ( StateGame StateChangeDescription StateTalk + StateDropAllConfirm ) type Session struct { @@ -44,6 +45,7 @@ type AccountEntry struct { Name string PasswordHash string Characters []string + Aliases map[string]string } type Server struct { diff --git a/internal/object/object.go b/internal/object/object.go index edee3ab..9e06804 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -4,5 +4,6 @@ type ObjectDef struct { ID string `yaml:"id"` Name string `yaml:"name"` BehaviorID string `yaml:"behavior"` + Hidden bool `yaml:"hidden"` Props map[string]any `yaml:"props"` } diff --git a/internal/player/account.go b/internal/player/account.go index 5072009..679c5bc 100644 --- a/internal/player/account.go +++ b/internal/player/account.go @@ -1,7 +1,8 @@ package player type Account struct { - Name string `yaml:"name"` - PasswordHash string `yaml:"password_hash"` - Characters []string `yaml:"characters"` + Name string `yaml:"name"` + PasswordHash string `yaml:"password_hash"` + Characters []string `yaml:"characters"` + Aliases map[string]string `yaml:"aliases"` } diff --git a/internal/player/player.go b/internal/player/player.go index d6e450a..001eb8b 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -94,6 +94,7 @@ type Player struct { Description string `yaml:"description"` AttackStyle AttackStyle `yaml:"attack_style"` Toggles map[string]bool `yaml:"toggles"` + Flags map[string]any `yaml:"flags"` RegenerateTick int Action *action.Action `yaml:"-"` } diff --git a/internal/world/room.go b/internal/world/room.go index 6c2d0e3..58fa82b 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -1,6 +1,9 @@ package world -import "gopkg.in/yaml.v3" +import ( + "gopkg.in/yaml.v3" + "thirdcollapse/internal/action" +) type ExitDir string @@ -44,9 +47,9 @@ type SpawnDef struct { } type ExitDef struct { - Room int `yaml:"room"` - Condition *ExitCondition `yaml:"condition"` - BlockedMessage string `yaml:"blocked_message"` + Room int `yaml:"room"` + Condition *action.Condition `yaml:"condition"` + BlockedMessage string `yaml:"blocked_message"` } func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { @@ -62,12 +65,6 @@ func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { return value.Decode((*raw)(e)) } -type ExitCondition struct { - Flag string `yaml:"flag"` - Value any `yaml:"value"` - Not bool `yaml:"not"` -} - type Room struct { ID int `yaml:"id"` Name string `yaml:"name"` @@ -81,8 +78,8 @@ type Room struct { } type EnterStep struct { - Message string `yaml:"message"` - Condition *ExitCondition `yaml:"condition"` + Message string `yaml:"message"` + Condition *action.Condition `yaml:"condition"` } type RoomObject struct { |
