diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml index eda22803..6a838178 100755 --- a/_datafiles/config.yaml +++ b/_datafiles/config.yaml @@ -186,6 +186,12 @@ ShopRestockRate: 6 hours # varied. # Note: see _datafiles/combat-messages/* ConsistentAttackMessages: true +# - CorpsesEnabled - +# If set to true, corpses are left behind by players or mobs that are killed. +CorpsesEnabled: true +# - CorpseDecayTime - +# How long until corpses crumble to dust (Go away). +CorpseDecayTime: 1 hour # - MaxAltCharacters - # How many characters beyond their original character can they create? Players # can swap between characters and work on them independently if this is set diff --git a/_datafiles/world/default/ansi-aliases.yaml b/_datafiles/world/default/ansi-aliases.yaml index 7597f32a..fb926147 100644 --- a/_datafiles/world/default/ansi-aliases.yaml +++ b/_datafiles/world/default/ansi-aliases.yaml @@ -7,7 +7,7 @@ color8: username: 93 # Bright yellow username-aggro: red username-downed: 90 # Bright black - mobname: 14 + mobname: 14 mobname-aggro: 91 # Bright red mobname-downed: red petname: 3 @@ -156,6 +156,8 @@ color8: script-text: 10 broadcast-prefix: 8 broadcast-body: 13 + mob-corpse: 8 + user-corpse: 8 color256: table-title: 2 userid: black @@ -306,4 +308,5 @@ color256: script-text: 155 broadcast-prefix: 135 broadcast-body: 164 - \ No newline at end of file + mob-corpse: 67 + user-corpse: 143 \ No newline at end of file diff --git a/_datafiles/world/default/keywords.yaml b/_datafiles/world/default/keywords.yaml index add800b3..79270420 100644 --- a/_datafiles/world/default/keywords.yaml +++ b/_datafiles/world/default/keywords.yaml @@ -27,6 +27,7 @@ help: - character - pets - train + - bury communication: - emote - say diff --git a/_datafiles/world/default/templates/character/description-corpse.template b/_datafiles/world/default/templates/character/description-corpse.template new file mode 100644 index 00000000..ecf56a63 --- /dev/null +++ b/_datafiles/world/default/templates/character/description-corpse.template @@ -0,0 +1,8 @@ + +.: {{ .Name }} corpse + ┌─ .:Description ────────────────────────────────────────────────────────────┐ + ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ + {{ splitstring .GetDescription 72 " "}} + ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ + This is a corpse. They are dead. + └────────────────────────────────────────────────────────────────────────────┘ diff --git a/_datafiles/world/default/templates/character/description.template b/_datafiles/world/default/templates/character/description.template index e3e72a55..7cc17d5a 100644 --- a/_datafiles/world/default/templates/character/description.template +++ b/_datafiles/world/default/templates/character/description.template @@ -1,9 +1,9 @@ -.: {{ .Character.Name }} ({{ .Character.AlignmentName }}) -{{- $tnl := .Character.XPTNL -}} -{{- $pct := (pct .Character.Experience $tnl ) -}} -{{- $exp := printf "%d/%d (%d%%)" .Character.Experience $tnl $pct }} +.: {{ .Name }} ({{ .AlignmentName }}) +{{- $tnl := .XPTNL -}} +{{- $pct := (pct .Experience $tnl ) -}} +{{- $exp := printf "%d/%d (%d%%)" .Experience $tnl $pct }} ┌─ .:Description ────────────────────────────────────────────────────────────┐ - {{ splitstring .Character.GetDescription 72 " "}} - {{ .Character.GetHealthAppearance }} + {{ splitstring .GetDescription 72 " "}} + {{ .GetHealthAppearance }} └────────────────────────────────────────────────────────────────────────────┘ diff --git a/_datafiles/world/default/templates/character/status-lite.template b/_datafiles/world/default/templates/character/status-lite.template index 545db7cf..ce9dbfb6 100644 --- a/_datafiles/world/default/templates/character/status-lite.template +++ b/_datafiles/world/default/templates/character/status-lite.template @@ -1,12 +1,12 @@ - .: {{ .Character.Name }} the {{ profession .Character }} -{{- $tnl := .Character.XPTNL -}} -{{- $pct := (pct .Character.Experience $tnl ) -}} -{{- $exp := printf "%d/%d (%d%%)" .Character.Experience $tnl $pct }} + .: {{ .Name }} the {{ profession . }} +{{- $tnl := .XPTNL -}} +{{- $pct := (pct .Experience $tnl ) -}} +{{- $exp := printf "%d/%d (%d%%)" .Experience $tnl $pct }} ┌─ .:Info ──────────────────────┐ ┌─ .:Attributes ───────────────────────────┐ - │ Health: {{ printf "%-10d" .Character.Health }} Max: {{ printf "%-6d" .Character.HealthMax.Value }}│ │ Strength: {{ printf "%-4d(%-3d)" .Character.Stats.Strength.Value (.Character.StatMod "strength") }} Vitality: {{ printf "%-4d(%-3d)" .Character.Stats.Vitality.Value (.Character.StatMod "vitality") }} │ - Mana: {{ printf "%-10d" .Character.Mana }} Max: {{ printf "%-6d" .Character.ManaMax.Value }} Speed: {{ printf "%-4d(%-3d)" .Character.Stats.Speed.Value (.Character.StatMod "speed") }} Mysticism: {{ printf "%-4d(%-3d)" .Character.Stats.Mysticism.Value (.Character.StatMod "mysticism") }} - Armor: {{ printf "%-22s" ( printf "%d" (.Character.GetDefense)) }} Smarts: {{ printf "%-4d(%-3d)" .Character.Stats.Smarts.Value (.Character.StatMod "smarts") }} Percept: {{ printf "%-4d(%-3d)" .Character.Stats.Perception.Value (.Character.StatMod "perception") }} - Level: {{ printf "%-22d" .Character.Level }} - │ Gold: {{ printf "%-22s" (numberFormat .Character.Gold) }}│ │ │ + │ Health: {{ printf "%-10d" .Health }} Max: {{ printf "%-6d" .HealthMax.Value }}│ │ Strength: {{ printf "%-4d(%-3d)" .Stats.Strength.Value (.StatMod "strength") }} Vitality: {{ printf "%-4d(%-3d)" .Stats.Vitality.Value (.StatMod "vitality") }} │ + Mana: {{ printf "%-10d" .Mana }} Max: {{ printf "%-6d" .ManaMax.Value }} Speed: {{ printf "%-4d(%-3d)" .Stats.Speed.Value (.StatMod "speed") }} Mysticism: {{ printf "%-4d(%-3d)" .Stats.Mysticism.Value (.StatMod "mysticism") }} + Armor: {{ printf "%-22s" ( printf "%d" (.GetDefense)) }} Smarts: {{ printf "%-4d(%-3d)" .Stats.Smarts.Value (.StatMod "smarts") }} Percept: {{ printf "%-4d(%-3d)" .Stats.Perception.Value (.StatMod "perception") }} + Level: {{ printf "%-22d" .Level }} + │ Gold: {{ printf "%-22s" (numberFormat .Gold) }}│ │ │ └───────────────────────────────┘ └──────────────────────────────────────────┘ \ No newline at end of file diff --git a/_datafiles/world/default/templates/help/bury.template b/_datafiles/world/default/templates/help/bury.template new file mode 100644 index 00000000..084ba917 --- /dev/null +++ b/_datafiles/world/default/templates/help/bury.template @@ -0,0 +1,9 @@ +.: Help for bury + +The bury command cleans up corpses from the room. + +Usage: + + bury rat corpse + + diff --git a/_datafiles/world/empty/ansi-aliases.yaml b/_datafiles/world/empty/ansi-aliases.yaml index 7597f32a..fb926147 100644 --- a/_datafiles/world/empty/ansi-aliases.yaml +++ b/_datafiles/world/empty/ansi-aliases.yaml @@ -7,7 +7,7 @@ color8: username: 93 # Bright yellow username-aggro: red username-downed: 90 # Bright black - mobname: 14 + mobname: 14 mobname-aggro: 91 # Bright red mobname-downed: red petname: 3 @@ -156,6 +156,8 @@ color8: script-text: 10 broadcast-prefix: 8 broadcast-body: 13 + mob-corpse: 8 + user-corpse: 8 color256: table-title: 2 userid: black @@ -306,4 +308,5 @@ color256: script-text: 155 broadcast-prefix: 135 broadcast-body: 164 - \ No newline at end of file + mob-corpse: 67 + user-corpse: 143 \ No newline at end of file diff --git a/_datafiles/world/empty/keywords.yaml b/_datafiles/world/empty/keywords.yaml index add800b3..79270420 100644 --- a/_datafiles/world/empty/keywords.yaml +++ b/_datafiles/world/empty/keywords.yaml @@ -27,6 +27,7 @@ help: - character - pets - train + - bury communication: - emote - say diff --git a/_datafiles/world/empty/templates/character/description-corpse.template b/_datafiles/world/empty/templates/character/description-corpse.template new file mode 100644 index 00000000..ecf56a63 --- /dev/null +++ b/_datafiles/world/empty/templates/character/description-corpse.template @@ -0,0 +1,8 @@ + +.: {{ .Name }} corpse + ┌─ .:Description ────────────────────────────────────────────────────────────┐ + ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ + {{ splitstring .GetDescription 72 " "}} + ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ + This is a corpse. They are dead. + └────────────────────────────────────────────────────────────────────────────┘ diff --git a/_datafiles/world/empty/templates/character/description.template b/_datafiles/world/empty/templates/character/description.template index e3e72a55..7cc17d5a 100644 --- a/_datafiles/world/empty/templates/character/description.template +++ b/_datafiles/world/empty/templates/character/description.template @@ -1,9 +1,9 @@ -.: {{ .Character.Name }} ({{ .Character.AlignmentName }}) -{{- $tnl := .Character.XPTNL -}} -{{- $pct := (pct .Character.Experience $tnl ) -}} -{{- $exp := printf "%d/%d (%d%%)" .Character.Experience $tnl $pct }} +.: {{ .Name }} ({{ .AlignmentName }}) +{{- $tnl := .XPTNL -}} +{{- $pct := (pct .Experience $tnl ) -}} +{{- $exp := printf "%d/%d (%d%%)" .Experience $tnl $pct }} ┌─ .:Description ────────────────────────────────────────────────────────────┐ - {{ splitstring .Character.GetDescription 72 " "}} - {{ .Character.GetHealthAppearance }} + {{ splitstring .GetDescription 72 " "}} + {{ .GetHealthAppearance }} └────────────────────────────────────────────────────────────────────────────┘ diff --git a/_datafiles/world/empty/templates/character/status-lite.template b/_datafiles/world/empty/templates/character/status-lite.template index 545db7cf..ce9dbfb6 100644 --- a/_datafiles/world/empty/templates/character/status-lite.template +++ b/_datafiles/world/empty/templates/character/status-lite.template @@ -1,12 +1,12 @@ - .: {{ .Character.Name }} the {{ profession .Character }} -{{- $tnl := .Character.XPTNL -}} -{{- $pct := (pct .Character.Experience $tnl ) -}} -{{- $exp := printf "%d/%d (%d%%)" .Character.Experience $tnl $pct }} + .: {{ .Name }} the {{ profession . }} +{{- $tnl := .XPTNL -}} +{{- $pct := (pct .Experience $tnl ) -}} +{{- $exp := printf "%d/%d (%d%%)" .Experience $tnl $pct }} ┌─ .:Info ──────────────────────┐ ┌─ .:Attributes ───────────────────────────┐ - │ Health: {{ printf "%-10d" .Character.Health }} Max: {{ printf "%-6d" .Character.HealthMax.Value }}│ │ Strength: {{ printf "%-4d(%-3d)" .Character.Stats.Strength.Value (.Character.StatMod "strength") }} Vitality: {{ printf "%-4d(%-3d)" .Character.Stats.Vitality.Value (.Character.StatMod "vitality") }} │ - Mana: {{ printf "%-10d" .Character.Mana }} Max: {{ printf "%-6d" .Character.ManaMax.Value }} Speed: {{ printf "%-4d(%-3d)" .Character.Stats.Speed.Value (.Character.StatMod "speed") }} Mysticism: {{ printf "%-4d(%-3d)" .Character.Stats.Mysticism.Value (.Character.StatMod "mysticism") }} - Armor: {{ printf "%-22s" ( printf "%d" (.Character.GetDefense)) }} Smarts: {{ printf "%-4d(%-3d)" .Character.Stats.Smarts.Value (.Character.StatMod "smarts") }} Percept: {{ printf "%-4d(%-3d)" .Character.Stats.Perception.Value (.Character.StatMod "perception") }} - Level: {{ printf "%-22d" .Character.Level }} - │ Gold: {{ printf "%-22s" (numberFormat .Character.Gold) }}│ │ │ + │ Health: {{ printf "%-10d" .Health }} Max: {{ printf "%-6d" .HealthMax.Value }}│ │ Strength: {{ printf "%-4d(%-3d)" .Stats.Strength.Value (.StatMod "strength") }} Vitality: {{ printf "%-4d(%-3d)" .Stats.Vitality.Value (.StatMod "vitality") }} │ + Mana: {{ printf "%-10d" .Mana }} Max: {{ printf "%-6d" .ManaMax.Value }} Speed: {{ printf "%-4d(%-3d)" .Stats.Speed.Value (.StatMod "speed") }} Mysticism: {{ printf "%-4d(%-3d)" .Stats.Mysticism.Value (.StatMod "mysticism") }} + Armor: {{ printf "%-22s" ( printf "%d" (.GetDefense)) }} Smarts: {{ printf "%-4d(%-3d)" .Stats.Smarts.Value (.StatMod "smarts") }} Percept: {{ printf "%-4d(%-3d)" .Stats.Perception.Value (.StatMod "perception") }} + Level: {{ printf "%-22d" .Level }} + │ Gold: {{ printf "%-22s" (numberFormat .Gold) }}│ │ │ └───────────────────────────────┘ └──────────────────────────────────────────┘ \ No newline at end of file diff --git a/_datafiles/world/empty/templates/help/bury.template b/_datafiles/world/empty/templates/help/bury.template new file mode 100644 index 00000000..084ba917 --- /dev/null +++ b/_datafiles/world/empty/templates/help/bury.template @@ -0,0 +1,9 @@ +.: Help for bury + +The bury command cleans up corpses from the room. + +Usage: + + bury rat corpse + + diff --git a/go.mod b/go.mod index 2de7544f..cc89a53c 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,19 @@ require ( github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d github.com/gorilla/websocket v1.5.3 github.com/natefinch/lumberjack v2.0.0+incompatible + github.com/stretchr/testify v1.8.0 ) require ( github.com/BurntSushi/toml v1.4.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/text v0.3.8 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( diff --git a/go.sum b/go.sum index d8487d53..abe10d55 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= @@ -39,6 +40,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -77,5 +81,6 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/configs/configs.go b/internal/configs/configs.go index af476371..29c1604f 100644 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -76,13 +76,13 @@ type Config struct { EnterRoomMessageWrapper ConfigString `yaml:"EnterRoomMessageWrapper"` ExitRoomMessageWrapper ConfigString `yaml:"ExitRoomMessageWrapper"` - MaxIdleSeconds ConfigInt `yaml:"MaxIdleSeconds"` // How many seconds a player can go without a command in game before being kicked. TimeoutMods ConfigBool `yaml:"TimeoutMods"` // Whether to kick admin/mods when idle too long. ZombieSeconds ConfigInt `yaml:"ZombieSeconds"` // How many seconds a player will be a zombie allowing them to reconnect. LogoutRounds ConfigInt `yaml:"LogoutRounds"` // How many rounds of uninterrupted meditation must be completed to log out. StartRoom ConfigInt `yaml:"StartRoom"` // Default starting room. DeathRecoveryRoom ConfigInt `yaml:"DeathRecoveryRoom"` // Recovery room after dying. TutorialStartRooms ConfigSliceString `yaml:"TutorialStartRooms"` // List of all rooms that can be used to begin the tutorial process + MaxIdleSeconds ConfigInt `yaml:"MaxIdleSeconds"` // How many seconds a player can go without a command in game before being kicked. // Perma-death related configs PermaDeath ConfigBool `yaml:"PermaDeath"` // Is permadeath enabled? @@ -92,9 +92,11 @@ type Config struct { PricePerLife ConfigInt `yaml:"PricePerLife"` // Price in gold to buy new lives ShopRestockRate ConfigString `yaml:"ShopRestockRate"` // Default time it takes to restock 1 quantity in shops - ConsistentAttackMessages ConfigBool `yaml:"ConsistentAttackMessages"` // Whether each weapon has consistent attack messages MaxAltCharacters ConfigInt `yaml:"MaxAltCharacters"` // How many characters beyond the default character can they create? AfkSeconds ConfigInt `yaml:"AfkSeconds"` // How long until a player is marked as afk? + ConsistentAttackMessages ConfigBool `yaml:"ConsistentAttackMessages"` // Whether each weapon has consistent attack messages + CorpsesEnabled ConfigBool `yaml:"CorpsesEnabled"` // Whether corpses are left behind after mob/player deaths + CorpseDecayTime ConfigString `yaml:"CorpseDecayTime"` // How long until corpses decay to dust (go away) LeaderboardSize ConfigInt `yaml:"LeaderboardSize"` // Maximum size of leaderboard diff --git a/internal/mobcommands/bury.go b/internal/mobcommands/bury.go new file mode 100644 index 00000000..83914a35 --- /dev/null +++ b/internal/mobcommands/bury.go @@ -0,0 +1,38 @@ +package mobcommands + +import ( + "fmt" + "strings" + + "github.com/volte6/gomud/internal/mobs" + "github.com/volte6/gomud/internal/rooms" + "github.com/volte6/gomud/internal/util" +) + +func Bury(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { + + args := util.SplitButRespectQuotes(strings.ToLower(rest)) + + if len(args) == 0 { + return true, nil + } + + if corpse, corpseFound := room.FindCorpse(rest); corpseFound { + + if room.RemoveCorpse(corpse) { + + corpseColor := `mob-corpse` + if corpse.UserId > 0 { + corpseColor = `user-corpse` + } + + room.SendText(fmt.Sprintf(`%s buries the %s corpse.`, mob.Character.Name, corpseColor, corpse.Character.Name)) + return true, nil + + } + + return true, nil + } + + return true, nil +} diff --git a/internal/mobcommands/mobcommands.go b/internal/mobcommands/mobcommands.go index c9ff7a7f..8900e524 100644 --- a/internal/mobcommands/mobcommands.go +++ b/internal/mobcommands/mobcommands.go @@ -29,6 +29,7 @@ var ( "befriend": {Befriend, false}, "break": {Break, false}, "broadcast": {Broadcast, false}, + `bury`: {Bury, false}, "cast": {Cast, false}, "converse": {Converse, false}, "callforhelp": {CallForHelp, false}, diff --git a/internal/mobcommands/suicide.go b/internal/mobcommands/suicide.go index d2757ad6..ff170946 100644 --- a/internal/mobcommands/suicide.go +++ b/internal/mobcommands/suicide.go @@ -7,6 +7,7 @@ import ( "github.com/volte6/gomud/internal/buffs" "github.com/volte6/gomud/internal/combat" + "github.com/volte6/gomud/internal/configs" "github.com/volte6/gomud/internal/mobs" "github.com/volte6/gomud/internal/parties" "github.com/volte6/gomud/internal/rooms" @@ -18,6 +19,9 @@ import ( func Suicide(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { + config := configs.GetConfig() + currentRound := util.GetRoundCount() + if rest != `vanish` && mob.Character.HasBuffFlag(buffs.ReviveOnDeath) { mob.Character.Health = mob.Character.HealthMax.Value @@ -65,7 +69,7 @@ func Suicide(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { if mob.MobId == 38 { if mob.Character.Charmed != nil { if tmpU := users.GetByUserId(mob.Character.Charmed.UserId); tmpU != nil { - tmpU.SetTempData(`lastGuideRound`, util.GetRoundCount()) + tmpU.SetTempData(`lastGuideRound`, currentRound) } } } @@ -316,5 +320,13 @@ func Suicide(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { // Remove from current room room.RemoveMob(mob.InstanceId) + if config.CorpsesEnabled { + room.AddCorpse(rooms.Corpse{ + MobId: int(mob.MobId), + Character: mob.Character, + RoundCreated: currentRound, + }) + } + return true, nil } diff --git a/internal/rooms/corpse.go b/internal/rooms/corpse.go new file mode 100644 index 00000000..583a1786 --- /dev/null +++ b/internal/rooms/corpse.go @@ -0,0 +1,34 @@ +package rooms + +import ( + "github.com/volte6/gomud/internal/characters" + "github.com/volte6/gomud/internal/gametime" +) + +type Corpse struct { + UserId int + MobId int + Character characters.Character + RoundCreated uint64 + Prunable bool // Whether it can be removed +} + +func (c *Corpse) Update(roundNow uint64, decayRate string) { + + if c.Prunable { + return + } + + if decayRate == `` { + decayRate = `1 week` + } + + gd := gametime.GetDate(c.RoundCreated) + decayRound := gd.AddPeriod(decayRate) + + // Has enough time passed to do the respawn? + if roundNow >= decayRound { + c.Prunable = true + } + +} diff --git a/internal/rooms/corpse_test.go b/internal/rooms/corpse_test.go new file mode 100644 index 00000000..54481c37 --- /dev/null +++ b/internal/rooms/corpse_test.go @@ -0,0 +1,116 @@ +package rooms + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/volte6/gomud/internal/characters" + "github.com/volte6/gomud/internal/gametime" +) + +// Test that if the corpse is already prunable, calling Update does nothing. +func TestCorpseUpdate_PrunableAlreadyTrue(t *testing.T) { + corpse := &Corpse{ + UserId: 1, + MobId: 2, + Character: characters.Character{}, + RoundCreated: 100, + Prunable: true, + } + + // Even if we call Update with a large roundNow, prunable should remain true. + corpse.Update(9999, "1 week") + assert.True(t, corpse.Prunable, "Corpse should remain prunable when already set to true") +} + +// Test that if decayRate is empty, it defaults to "1 week". We assume "1 week" +// is longer than the provided roundNow in this example, so it should remain false. +func TestCorpseUpdate_EmptyDecayRateNotEnoughTime(t *testing.T) { + // Suppose RoundCreated=0 and roundNow=5000, + // we assume "1 week" (whatever that translates to in rounds) is > 5000. + corpse := &Corpse{ + UserId: 1, + MobId: 2, + Character: characters.Character{}, + RoundCreated: 5000, + Prunable: false, + } + + corpse.Update(5000, "") + assert.False(t, corpse.Prunable, "Corpse should not be prunable yet if not enough time has elapsed for default decay rate") +} + +// Test that if decayRate is empty, it defaults to "1 week". We assume "1 week" +// is shorter than the provided roundNow, so it becomes prunable. +func TestCorpseUpdate_EmptyDecayRateSufficientTime(t *testing.T) { + // Suppose RoundCreated=0 and roundNow=20000, + // we assume "1 week" < 20000 (whatever that is in your game logic). + corpse := &Corpse{ + UserId: 1, + MobId: 2, + Character: characters.Character{}, + RoundCreated: 0, + Prunable: false, + } + + corpse.Update(20000, "") + assert.True(t, corpse.Prunable, "Corpse should become prunable if enough time has elapsed for default decay rate") +} + +// Test a custom decay rate, ensuring that when roundNow < decay threshold, +// the corpse is not prunable, and once roundNow >= threshold, it becomes prunable. +func TestCorpseUpdate_CustomDecayRate(t *testing.T) { + // For demonstration, let's assume "2 weeks" is a certain number of rounds, + // but the exact number depends on your game logic. + // We'll test a transition point: just before and just after the threshold. + corpse := &Corpse{ + UserId: 1, + MobId: 2, + Character: characters.Character{}, + RoundCreated: 1000, // The starting round + Prunable: false, + } + + // Just before decay threshold + // e.g., if "2 weeks" from round 1000 is 21000, pass in 20999 + corpse.Update(1005, "2 weeks") + assert.False(t, corpse.Prunable, "Corpse should not be prunable just before custom decay threshold") + + // At or after decay threshold + // e.g., pass in 21000 or greater + corpse.Update(21000, "2 weeks") + assert.True(t, corpse.Prunable, "Corpse should become prunable at or after custom decay threshold") +} + +// Example test that checks repeated updates before and after crossing the threshold. +func TestCorpseUpdate_MultipleCalls(t *testing.T) { + // For demonstration, let's assume "1 week" is N rounds in your logic. + corpse := &Corpse{ + RoundCreated: 100, + Prunable: false, + } + + // First update: not enough rounds have passed + corpse.Update(105, "1 week") + assert.False(t, corpse.Prunable, "Corpse should still not be prunable if not enough rounds elapsed") + + // Second update: enough rounds have passed + corpse.Update(20000, "1 week") + assert.True(t, corpse.Prunable, "Corpse should become prunable after sufficient rounds elapsed") +} + +// If you want to specifically test or mock the gametime package, you can do so +// in a separate test or by creating a mock gametime function. This example +// simply relies on the assumption that gametime.GetDate and its AddPeriod logic +// are correct. If those are crucial, consider additional testing around them. +func TestCorpseUpdate_GametimeIntegration(t *testing.T) { + // If you have a need to test how gametime transforms RoundCreated + decayRate, + // you might do something like: + + gd := gametime.GetDate(0) + decayRound := gd.AddPeriod("1 week") + + // Ensure that the decayRound is what you expect... + // But that might be tested better in the gametime package itself. + assert.NotZero(t, decayRound, "Decay round should not be zero") +} diff --git a/internal/rooms/roomdetails.go b/internal/rooms/roomdetails.go index fce28873..dcf2223e 100644 --- a/internal/rooms/roomdetails.go +++ b/internal/rooms/roomdetails.go @@ -21,6 +21,7 @@ import ( type RoomTemplateDetails struct { VisiblePlayers []string VisibleMobs []string + VisibleCorpses []string VisibleExits map[string]exit.RoomExit TemporaryExits map[string]exit.TemporaryRoomExit UserId int @@ -64,6 +65,7 @@ func GetDetails(r *Room, user *users.UserRecord) RoomTemplateDetails { details := RoomTemplateDetails{ VisiblePlayers: []string{}, VisibleMobs: []string{}, + VisibleCorpses: []string{}, VisibleExits: make(map[string]exit.RoomExit), TemporaryExits: make(map[string]exit.TemporaryRoomExit), Zone: r.Zone, @@ -320,6 +322,41 @@ func GetDetails(r *Room, user *users.UserRecord) RoomTemplateDetails { } } + // add any corpses present + mobCorpses := map[string]int{} + playerCorpses := map[string]int{} + + for _, c := range r.Corpses { + if c.Prunable { + continue + } + + if c.MobId > 0 { + mobCorpses[c.Character.Name] = mobCorpses[c.Character.Name] + 1 + } + + if c.UserId > 0 { + playerCorpses[c.Character.Name] = playerCorpses[c.Character.Name] + 1 + } + + } + + for name, qty := range playerCorpses { + if qty == 1 { + details.VisibleCorpses = append(details.VisibleCorpses, fmt.Sprintf(`%s corpse`, name)) + } else { + details.VisibleCorpses = append(details.VisibleCorpses, fmt.Sprintf(`%d %s corpses`, qty, name)) + } + } + + for name, qty := range mobCorpses { + if qty == 1 { + details.VisibleCorpses = append(details.VisibleCorpses, fmt.Sprintf(`%s corpse`, name)) + } else { + details.VisibleCorpses = append(details.VisibleCorpses, fmt.Sprintf(`%d %s corpses`, qty, name)) + } + } + // assign mutator exits last so that they can overwrite normal exits for mut := range r.ActiveMutators { spec := mut.GetSpec() diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go index c534a022..72d7203a 100644 --- a/internal/rooms/rooms.go +++ b/internal/rooms/rooms.go @@ -79,6 +79,7 @@ type Room struct { Nouns map[string]string `yaml:"nouns,omitempty"` // Interesting nouns to highlight in the room or reveal on succesful searches. Items []items.Item `yaml:"items,omitempty"` Stash []items.Item `yaml:"stash,omitempty"` // list of items in the room that are not visible to players + Corpses []Corpse `yaml:"-"` // Any corpses laying around from recent deaths Gold int `yaml:"gold,omitempty"` // How much gold is on the ground? SpawnInfo []SpawnInfo `yaml:"spawninfo,omitempty"` // key is creature ID, value is spawn chance SkillTraining map[string]TrainingRange `yaml:"skilltraining,omitempty"` // list of skills that can be trained in this room @@ -169,6 +170,60 @@ func (r *Room) GetVisibility() int { return visibility } +func (r *Room) AddCorpse(c Corpse) { + r.Corpses = append(r.Corpses, c) +} + +func (r *Room) RemoveCorpse(c Corpse) bool { + for idx, corpse := range r.Corpses { + if corpse.MobId != c.MobId { + continue + } + if corpse.UserId != c.UserId { + continue + } + if corpse.Character.Name != c.Character.Name { + continue + } + if corpse.RoundCreated != c.RoundCreated { + continue + } + + r.Corpses = append(r.Corpses[:idx], r.Corpses[idx+1:]...) + + return true + } + return false +} + +func (r *Room) UpdateCorpses(roundNow uint64) { + + c := configs.GetConfig() + + if !c.CorpsesEnabled { + return + } + + removeIdx := []int{} + for idx, corpse := range r.Corpses { + corpse.Update(roundNow, c.CorpseDecayTime.String()) + if corpse.Prunable { + removeIdx = append(removeIdx, idx) + if corpse.MobId > 0 { + r.SendText(fmt.Sprintf(`A %s corpse crumbles to dust.`, corpse.Character.Name)) + } + if corpse.UserId > 0 { + r.SendText(fmt.Sprintf(`A %s corpse crumbles to dust.`, corpse.Character.Name)) + } + } + r.Corpses[idx] = corpse + } + + for i := len(removeIdx) - 1; i >= 0; i-- { + r.Corpses = append(r.Corpses[:removeIdx[i]], r.Corpses[removeIdx[i]+1:]...) + } +} + func (r *Room) SendTextCommunication(txt string, excludeUserIds ...int) { events.AddToQueue(events.Message{ @@ -842,6 +897,58 @@ func (r *Room) GetAllFloorItems(stash bool) []items.Item { return found } +func (r *Room) FindCorpse(searchName string) (Corpse, bool) { + + // First search for player corpses that match + + playerCorpseLookup := map[string]int{} + playerCorpses := []string{} + + mobCorpseLookup := map[string]int{} + mobCorpses := []string{} + + for idx, c := range r.Corpses { + + if c.Prunable { + continue + } + + if c.UserId > 0 { + name := c.Character.Name + ` corpse` + if _, ok := playerCorpseLookup[name]; !ok { + playerCorpseLookup[name] = idx + playerCorpses = append(playerCorpses, name) + } + } + + if c.MobId > 0 { + name := c.Character.Name + ` corpse` + if _, ok := mobCorpseLookup[name]; !ok { + mobCorpseLookup[name] = idx + mobCorpses = append(mobCorpses, name) + } + } + } + + userMatch, closeUserMatch := util.FindMatchIn(searchName, playerCorpses...) + if userMatch != `` { + return r.Corpses[playerCorpseLookup[userMatch]], true + } + + mobMatch, closeMobMatch := util.FindMatchIn(searchName, mobCorpses...) + if mobMatch != `` { + return r.Corpses[mobCorpseLookup[mobMatch]], true + } + + if closeUserMatch != `` { + return r.Corpses[playerCorpseLookup[closeUserMatch]], true + } else if closeMobMatch != `` { + return r.Corpses[mobCorpseLookup[closeMobMatch]], true + } + + return Corpse{}, false +} + func (r *Room) FindOnFloor(itemName string, stash bool) (items.Item, bool) { if stash { @@ -1784,7 +1891,6 @@ func (r *Room) RoundTick() { r.Mutators.Update(roundNow) for mut := range r.ActiveMutators { - spec := mut.GetSpec() r.ApplyBuffIdToPlayers(spec.PlayerBuffIds...) r.ApplyBuffIdToMobs(spec.MobBuffIds...) @@ -1816,6 +1922,11 @@ func (r *Room) RoundTick() { } } } + + // + // Decay any corpses + // + r.UpdateCorpses(roundNow) } func (r *Room) addPlayer(userId int) int { diff --git a/internal/rooms/rooms_test.go b/internal/rooms/rooms_test.go new file mode 100644 index 00000000..2f6ed4c2 --- /dev/null +++ b/internal/rooms/rooms_test.go @@ -0,0 +1,96 @@ +package rooms + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/volte6/gomud/internal/characters" +) + +func TestRoom_AddCorpse(t *testing.T) { + r := &Room{} + assert.Empty(t, r.Corpses, "Expected no corpses initially") + + corpse := Corpse{ + MobId: 0, + UserId: 123, + Character: characters.Character{Name: "TestPlayer"}, + RoundCreated: 10, + } + r.AddCorpse(corpse) + assert.Len(t, r.Corpses, 1, "Expected exactly one corpse after adding") + assert.Equal(t, corpse, r.Corpses[0], "Expected the added corpse to match") +} + +func TestRoom_RemoveCorpse(t *testing.T) { + r := &Room{} + corpse1 := Corpse{ + MobId: 0, + UserId: 123, + Character: characters.Character{Name: "PlayerOne"}, + RoundCreated: 10, + } + corpse2 := Corpse{ + MobId: 456, + UserId: 0, + Character: characters.Character{Name: "MobOne"}, + RoundCreated: 11, + } + + r.AddCorpse(corpse1) + r.AddCorpse(corpse2) + + // Removing existing corpse + removed := r.RemoveCorpse(corpse1) + assert.True(t, removed, "Expected to remove an existing corpse successfully") + assert.Len(t, r.Corpses, 1, "Expected exactly one corpse remaining") + + // Try removing a corpse that does not exist + nonExistent := Corpse{ + MobId: 999, + UserId: 999, + Character: characters.Character{Name: "Ghost"}, + RoundCreated: 99, + } + removed = r.RemoveCorpse(nonExistent) + assert.False(t, removed, "Expected removal to fail for non-existent corpse") + assert.Len(t, r.Corpses, 1, "Expected no change in corpses") +} + +func TestRoom_FindCorpse(t *testing.T) { + r := &Room{} + + playerCorpse := Corpse{ + UserId: 123, + Character: characters.Character{Name: "PlayerOne"}, + RoundCreated: 5, + Prunable: false, + } + mobCorpse := Corpse{ + MobId: 456, + Character: characters.Character{Name: "MobOne"}, + RoundCreated: 6, + Prunable: false, + } + r.AddCorpse(playerCorpse) + r.AddCorpse(mobCorpse) + + // Exact search + found, ok := r.FindCorpse("PlayerOne corpse") + assert.True(t, ok, "Expected to find player corpse by exact name") + assert.Equal(t, "PlayerOne", found.Character.Name, "Expected found corpse to match the correct character") + + // Searching for mob + found, ok = r.FindCorpse("MobOne corpse") + assert.True(t, ok, "Expected to find mob corpse by exact name") + assert.Equal(t, "MobOne", found.Character.Name, "Expected found corpse to match the correct character") + + // Searching partial name (depends on your util.FindMatchIn logic) + found, ok = r.FindCorpse("player") + assert.True(t, ok, "Expected to find a close match for player corpse") + assert.Equal(t, "PlayerOne", found.Character.Name, "Expected found corpse to be the player's") + + // Non-existent + found, ok = r.FindCorpse("NonExistent") + assert.False(t, ok, "Expected not to find a missing corpse") +} diff --git a/internal/usercommands/bury.go b/internal/usercommands/bury.go new file mode 100644 index 00000000..1a580863 --- /dev/null +++ b/internal/usercommands/bury.go @@ -0,0 +1,42 @@ +package usercommands + +import ( + "fmt" + "strings" + + "github.com/volte6/gomud/internal/rooms" + "github.com/volte6/gomud/internal/users" + "github.com/volte6/gomud/internal/util" +) + +func Bury(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { + + args := util.SplitButRespectQuotes(strings.ToLower(rest)) + + if len(args) == 0 { + user.SendText("Bury what?") + return true, nil + } + + if corpse, corpseFound := room.FindCorpse(rest); corpseFound { + + if room.RemoveCorpse(corpse) { + + corpseColor := `mob-corpse` + if corpse.UserId > 0 { + corpseColor = `user-corpse` + } + + user.SendText(fmt.Sprintf(`You bury the %s corpse.`, corpseColor, corpse.Character.Name)) + room.SendText(fmt.Sprintf(`%s buries the %s corpse.`, user.Character.Name, corpseColor, corpse.Character.Name), user.UserId) + return true, nil + + } + + return true, nil + } + + user.SendText(fmt.Sprintf("You don't see a %s around for burying.", rest)) + + return true, nil +} diff --git a/internal/usercommands/get.go b/internal/usercommands/get.go index c443d8c8..a509cf61 100644 --- a/internal/usercommands/get.go +++ b/internal/usercommands/get.go @@ -309,6 +309,11 @@ func Get(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { } + if _, corpseFound := room.FindCorpse(rest); corpseFound { + user.SendText(`You can't pick up corpses. What would people think?`) + return true, nil + } + user.SendText(fmt.Sprintf("You don't see a %s around.", rest)) return true, nil diff --git a/internal/usercommands/look.go b/internal/usercommands/look.go index 7ceb99b9..a382d0e2 100644 --- a/internal/usercommands/look.go +++ b/internal/usercommands/look.go @@ -87,7 +87,7 @@ func Look(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { u.UserId) } - descTxt, _ := templates.Process("character/description", u) + descTxt, _ := templates.Process("character/description", u.Character) user.SendText(descTxt) itemNames := []string{} @@ -115,7 +115,7 @@ func Look(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { ) } - descTxt, _ := templates.Process("character/description", m) + descTxt, _ := templates.Process("character/description", &m.Character) user.SendText(descTxt) itemNames := []string{} @@ -317,6 +317,54 @@ func Look(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { } } + if len(room.Corpses) > 0 { + + mobCorpseLookup := map[string]int{} + mobCorpses := []string{} + + playerCorpseLookup := map[string]int{} + playerCorpses := []string{} + for idx, c := range room.Corpses { + if c.Prunable { + continue + } + + if c.MobId > 0 { + name := c.Character.Name + ` corpse` + if _, ok := mobCorpseLookup[name]; !ok { + mobCorpseLookup[name] = idx + mobCorpses = append(mobCorpses, name) + } + } + + if c.UserId > 0 { + name := c.Character.Name + ` corpse` + if _, ok := playerCorpseLookup[name]; !ok { + playerCorpseLookup[name] = idx + playerCorpses = append(playerCorpses, name) + } + } + } + + if corpse, corpseFound := room.FindCorpse(rest); corpseFound { + + corpseColor := `mob-corpse` + if corpse.UserId > 0 { + corpseColor = `user-corpse` + } + + user.SendText(fmt.Sprintf(`You look at the %s corpse.`, corpseColor, corpse.Character.Name)) + room.SendText(fmt.Sprintf(`%s is looking at the %s corpse.`, user.Character.Name, corpseColor, corpse.Character.Name), user.UserId) + + descTxt, _ := templates.Process("character/description-corpse", &corpse.Character) + user.SendText(descTxt) + + return true, nil + + } + + } + // Nothing found user.SendText("Look at what???") @@ -428,6 +476,8 @@ func lookRoom(user *users.UserRecord, roomId int, secretLook bool) { groundStuff = append(groundStuff, name) } + groundStuff = append(groundStuff, details.VisibleCorpses...) + groundDetails := map[string]any{ `GroundStuff`: groundStuff, `IsDark`: room.GetBiome().IsDark(), diff --git a/internal/usercommands/skill.peep.go b/internal/usercommands/skill.peep.go index dd359438..a49189ea 100644 --- a/internal/usercommands/skill.peep.go +++ b/internal/usercommands/skill.peep.go @@ -63,7 +63,7 @@ func Peep(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { targetName := u.Character.GetPlayerName(user.UserId).String() if skillLevel >= 2 { - statusTxt, _ = templates.Process("character/status-lite", u) + statusTxt, _ = templates.Process("character/status-lite", u.Character) } if skillLevel >= 3 { @@ -128,7 +128,7 @@ func Peep(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { targetName := m.Character.GetMobName(user.UserId).String() if skillLevel >= 2 { - statusTxt, _ = templates.Process("character/status-lite", m) + statusTxt, _ = templates.Process("character/status-lite", &m.Character) } if skillLevel >= 3 { diff --git a/internal/usercommands/suicide.go b/internal/usercommands/suicide.go index 6e1d6e9c..8678f5b9 100644 --- a/internal/usercommands/suicide.go +++ b/internal/usercommands/suicide.go @@ -20,6 +20,7 @@ import ( func Suicide(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { config := configs.GetConfig() + currentRound := util.GetRoundCount() if user.Character.Zone == `Shadow Realm` { user.SendText(`You're already dead!`) @@ -193,5 +194,13 @@ func Suicide(rest string, user *users.UserRecord, room *rooms.Room) (bool, error rooms.MoveToRoom(user.UserId, int(config.DeathRecoveryRoom)) + if config.CorpsesEnabled { + room.AddCorpse(rooms.Corpse{ + UserId: user.UserId, + Character: *user.Character, + RoundCreated: currentRound, + }) + } + return true, nil } diff --git a/internal/usercommands/usercommands.go b/internal/usercommands/usercommands.go index 306d4271..f73dded7 100644 --- a/internal/usercommands/usercommands.go +++ b/internal/usercommands/usercommands.go @@ -38,6 +38,7 @@ var ( `badcommands`: {BadCommands, true, true}, // Admin only `biome`: {Biome, true, false}, `broadcast`: {Broadcast, true, false}, + `bury`: {Bury, false, false}, `character`: {Character, true, false}, `tackle`: {Tackle, false, false}, `bank`: {Bank, false, false},