diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml
index d42ea917..b18c7d8a 100755
--- a/_datafiles/config.yaml
+++ b/_datafiles/config.yaml
@@ -51,16 +51,16 @@ Server:
# Commands to run when a user logs in. These commands are run by the user
# and can be anything they have access to.
OnLoginCommands:
- - 'emote @appears before you in a flash of ⚡lightning⚡!'
- - print
- - motd
- - print
- - online
- - print
- - inbox check
- - print
- - mudletmap
- - checkclient
+ - 'emote @appears before you in a flash of ⚡lightning⚡!'
+ - print
+ - motd
+ - print
+ - online
+ - print
+ - inbox check
+ - print
+ - mudletmap
+ - checkclient
# - Motd -
# Message of the day. This is displayed when the motd command is run.
Motd: '{{ t "Motd" }}'
@@ -77,12 +77,12 @@ Server:
# It is a good idea to lock configs related to folder/file paths to prevent
# accidental changes that could break the game.
Locked:
- - FilePaths
- - Server.CurrentVersion
- - Server.NextRoomId
- - Server.Seed
- - Server.OnLoginCommands
- - Server.BannedNames
+ - FilePaths
+ - Server.CurrentVersion
+ - Server.NextRoomId
+ - Server.Seed
+ - Server.OnLoginCommands
+ - Server.BannedNames
################################################################################
#
@@ -140,7 +140,6 @@ LootGoblin:
# suddenly the goblin shows up.
IncludeRecentRooms: true
-
################################################################################
#
# ENGINE TIMING
@@ -321,34 +320,44 @@ Integrations:
Discord:
# Optional webhook URL to send mud event messages to, such as joins/disconnects
# Can also be set via environment variable: DISCORD_WEBHOOK_URL
- WebhookUrl: ''
-
+ WebhookUrl: ""
################################################################################
#
-# TEXT FORMATS
-# Basic text strings that occur often, and may need customization
-#
-################################################################################
-TextFormats:
- # - Prompt -
- # Default prompt formatting.
- # See: "help prompt" in game to learn more about this.
- Prompt: '{8}[{t} {T} {255}HP:{hp}{8}/{HP} {255}MP:{13}{mp}{8}/{13}{MP}{8}]{239}{h}{8}:'
- # - TimeFormat -
- # When real world time is shown, what format should be used?
- # This uses a Go time format string, which is kinda weird.
- # See: https://go.dev/src/time/format.go
- Time: 'Monday, 02-Jan-2006 3:04:05PM'
- # - TimeFormatShort -
- # Same as TimeFormat, but shorter form
- TimeShort: 'Jan 2 ''06 3:04PM'
- # - EnterRoomMessageWrapper -
- # Decorate entrance text with this. Put a %s where the message should be.
- EnterRoomMessageWrapper: " >>> %s\n"
- # - ExitRoomMessageWrapper -
- # Decorate exit text with this. Put a %s where the message should be.
- ExitRoomMessageWrapper: " >>> %s\n"
+# USER INTERFACE
+# Visual preferences and text formatting options
+#
+################################################################################
+UserInterface:
+ # - Formats -
+ # Text format strings that control how various messages appear
+ Formats:
+ # - Prompt -
+ # Default prompt formatting.
+ # See: "help prompt" in game to learn more about this.
+ Prompt: "{8}[{t} {T} {255}HP:{hp}{8}/{HP} {255}MP:{13}{mp}{8}/{13}{MP}{8}]{239}{h}{8}:"
+ # - Time -
+ # When real world time is shown, what format should be used?
+ # This uses a Go time format string, which is kinda weird.
+ # See: https://go.dev/src/time/format.go
+ Time: "Monday, 02-Jan-2006 3:04:05PM"
+ # - TimeShort -
+ # Same as Time, but shorter form
+ TimeShort: "Jan 2 '06 3:04PM"
+ # - EnterRoomMessageWrapper -
+ # Decorate entrance text with this. Put a %s where the message should be.
+ EnterRoomMessageWrapper: " >>> %s\n"
+ # - ExitRoomMessageWrapper -
+ # Decorate exit text with this. Put a %s where the message should be.
+ ExitRoomMessageWrapper: " >>> %s\n"
+ # - Display -
+ # Visual display preferences
+ Display:
+ # - ShowEmptyEquipmentSlots -
+ # Whether to show empty equipment slots when looking at characters/mobs
+ # true = show all slots including empty ones (traditional MUD style)
+ # false = only show equipped items (cleaner display)
+ ShowEmptyEquipmentSlots: true
################################################################################
#
@@ -359,15 +368,15 @@ TextFormats:
Translation:
# - DefaultLanguage -
# Specify the default game language (fallback)
- DefaultLanguage: 'en'
+ DefaultLanguage: "en"
# - Language -
# Specify the game language
- Language: 'en'
+ Language: "en"
# - LanguagePaths -
# Specify the game language file paths
LanguagePaths:
- - '_datafiles/localize'
- - '_datafiles/world/default/localize'
+ - "_datafiles/localize"
+ - "_datafiles/world/default/localize"
################################################################################
#
@@ -412,7 +421,7 @@ Network:
# Whether Admin/Mod users get timed out when reaching MaxIdleSeconds
# If set to false, Admins & Mods never get force disconnected.
TimeoutMods: false
- # - ZombieSeconds -
+ # - ZombieSeconds -
# How many seconds a character stays active/in game after a network connection
# is lost. Set to 0 to instantly log out characters (exploitable).
ZombieSeconds: 60
@@ -488,7 +497,7 @@ Validation:
# (This is a regular expression, you must understand how to create them)
# Set to empty to disable. If the regex is invalid it will revert to the
# default of '^[a-zA-Z0-9_]+$'
- NameRejectRegex: '^[a-zA-Z0-9_]+$'
+ NameRejectRegex: "^[a-zA-Z0-9_]+$"
NameRejectReason: "Must only contain Alpha-numeric and underscores."
# - BannedNames -
# Names that are not allowed to be used by players. This is a good place to
@@ -502,39 +511,39 @@ Validation:
# that contain that name. For example, "*admin*" would ban "admin", "superadmin",
# "administrator", etc.
BannedNames:
- - "*admin*"
- - "*moderator*"
- - "player*"
- - "user*"
- - "me"
- - "myself"
- - "self"
- - "us"
- - "you"
- - "them"
- - "everyone"
- - "someone"
- - "anyone"
- - "nobody"
- - "somebody"
- - "anybody"
- - "none"
- - "nothing"
- - "something"
- - "anything"
- - "everything"
- - "all"
- - "north*"
- - "south*"
- - "east*"
- - "west*"
- - "up"
- - "down"
- - "chest"
- - "door"
- - "new"
- - "join"
- - "register"
+ - "*admin*"
+ - "*moderator*"
+ - "player*"
+ - "user*"
+ - "me"
+ - "myself"
+ - "self"
+ - "us"
+ - "you"
+ - "them"
+ - "everyone"
+ - "someone"
+ - "anyone"
+ - "nobody"
+ - "somebody"
+ - "anybody"
+ - "none"
+ - "nothing"
+ - "something"
+ - "anything"
+ - "everything"
+ - "all"
+ - "north*"
+ - "south*"
+ - "east*"
+ - "west*"
+ - "up"
+ - "down"
+ - "chest"
+ - "door"
+ - "new"
+ - "join"
+ - "register"
################################################################################
#
@@ -555,7 +564,6 @@ Roles:
builder: ["room.info", "build"]
helper: ["paz", "teleport.playername", "locate"]
-
################################################################################
#
# Modules
diff --git a/_datafiles/world/default/templates/character/description.screenreader.template b/_datafiles/world/default/templates/character/description.screenreader.template
new file mode 100644
index 00000000..58aebe21
--- /dev/null
+++ b/_datafiles/world/default/templates/character/description.screenreader.template
@@ -0,0 +1,8 @@
+{{ .Name }} ({{ .AlignmentName }})
+{{- $tnl := .XPTNL -}}
+{{- $pct := (pct .Experience $tnl ) -}}
+
+Description:
+{{ .GetDescription }}
+
+Health Status: {{ .GetHealthAppearance }}
\ No newline at end of file
diff --git a/_datafiles/world/default/templates/character/inventory-look.screenreader.template b/_datafiles/world/default/templates/character/inventory-look.screenreader.template
new file mode 100644
index 00000000..2f58cc51
--- /dev/null
+++ b/_datafiles/world/default/templates/character/inventory-look.screenreader.template
@@ -0,0 +1,22 @@
+Equipment:
+{{ if not .Equipment.Weapon.IsDisabled }}Weapon: {{ .Equipment.Weapon.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Offhand.IsDisabled }}Offhand: {{ .Equipment.Offhand.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Head.IsDisabled }}Head: {{ .Equipment.Head.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Neck.IsDisabled }}Neck: {{ .Equipment.Neck.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Body.IsDisabled }}Body: {{ .Equipment.Body.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Belt.IsDisabled }}Belt: {{ .Equipment.Belt.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Gloves.IsDisabled }}Gloves: {{ .Equipment.Gloves.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Ring.IsDisabled }}Ring: {{ .Equipment.Ring.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Legs.IsDisabled }}Legs: {{ .Equipment.Legs.NameSimple }}
+{{ end -}}
+{{- if not .Equipment.Feet.IsDisabled }}Feet: {{ .Equipment.Feet.NameSimple }}
+{{ end }}
+Carrying: {{ $itmCt := len .ItemNames }}{{ if eq $itmCt 0 }}no{{ else if lt $itmCt 4 }}a few{{ else if lt $itmCt 7 }}several{{ else }}lots of{{ end }} objects
\ No newline at end of file
diff --git a/_datafiles/world/default/templates/descriptions/look-item.screenreader.template b/_datafiles/world/default/templates/descriptions/look-item.screenreader.template
new file mode 100644
index 00000000..5e81a101
--- /dev/null
+++ b/_datafiles/world/default/templates/descriptions/look-item.screenreader.template
@@ -0,0 +1,37 @@
+{{/* Screenreader-friendly template for looking at items */}}
+You look at the {{ .Item.Name }} {{ .Location }}:
+
+{{ .ItemSpec.Description }}
+{{- if eq .Type "readable" }}
+You should probably read this.
+{{- else if eq .Subtype "drinkable" }}
+You could probably drink this.
+{{- else if eq .Subtype "edible" }}
+You could probably eat this.
+{{- else if eq .Type "lockpicks" }}
+These are used with the picklock command.
+{{- else if eq .Type "key" }}
+When you find the right door, keys are added to your keyring automatically.
+{{- else if eq .Subtype "wearable" }}
+It looks like wearable {{ .ItemSpec.Type }} equipment.
+{{- end }}
+{{- if eq .Type "weapon" }}
+It looks like a {{ .WeaponHands }}-Handed weapon.
+{{- if eq .WeaponType "claws" }}
+It looks like a claws weapon. These can be dual wielded without training.
+{{- else if eq .WeaponType "shooting" }}
+This can fired into adjacent areas. (help shoot)
+{{- end }}
+{{- if gt .WaitRounds 0 }}
+It requires an extra {{ .WaitRounds }} round(s) between attacks.
+{{- end }}
+{{- end }}
+{{- if .HasUses }}
+It has {{ .UsesRemaining }}/{{ .MaxUses }} uses remaining.
+{{- end }}
+{{- if index .Adjectives "cursed" }}
+It's CURSED! Once equipped, it cannot be removed without magical help.
+{{- end }}
+{{- if index .Adjectives "enchanted" }}
+It glows with a magical aura (enchantment level {{ .EnchantLevel }}).
+{{- end }}
\ No newline at end of file
diff --git a/_datafiles/world/default/templates/descriptions/look-item.template b/_datafiles/world/default/templates/descriptions/look-item.template
new file mode 100644
index 00000000..b8690d2d
--- /dev/null
+++ b/_datafiles/world/default/templates/descriptions/look-item.template
@@ -0,0 +1,37 @@
+{{/* Template for looking at items */}}
+You look at the {{ .Item.DisplayName }} {{ .Location }}:
+
+{{ .ItemSpec.Description }}
+{{- if eq .Type "readable" }}
+- You should probably read this.
+{{- else if eq .Subtype "drinkable" }}
+- You could probably drink this.
+{{- else if eq .Subtype "edible" }}
+- You could probably eat this.
+{{- else if eq .Type "lockpicks" }}
+- These are used with the picklock command.
+{{- else if eq .Type "key" }}
+- When you find the right door, keys are added to your keyring automatically.
+{{- else if eq .Subtype "wearable" }}
+- It looks like wearable {{ .ItemSpec.Type }} equipment.
+{{- end }}
+{{- if eq .Type "weapon" }}
+- It looks like a {{ .WeaponHands }}-Handed weapon.
+{{- if eq .WeaponType "claws" }}
+- It looks like a claws weapon. These can be dual wielded without training.
+{{- else if eq .WeaponType "shooting" }}
+- This can fired into adjacent areas. (help shoot)
+{{- end }}
+{{- if gt .WaitRounds 0 }}
+- It requires an extra {{ .WaitRounds }} round(s) between attacks.
+{{- end }}
+{{- end }}
+{{- if .HasUses }}
+- It has {{ .UsesRemaining }}/{{ .MaxUses }} uses remaining.
+{{- end }}
+{{- if index .Adjectives "cursed" }}
+- It's CURSED! Once equipped, it cannot be removed without magical help.
+{{- end }}
+{{- if index .Adjectives "enchanted" }}
+- It glows with a magical aura (enchantment level {{ .EnchantLevel }}).
+{{- end }}
\ No newline at end of file
diff --git a/_datafiles/world/default/templates/descriptions/look-mob.screenreader.template b/_datafiles/world/default/templates/descriptions/look-mob.screenreader.template
new file mode 100644
index 00000000..d32be275
--- /dev/null
+++ b/_datafiles/world/default/templates/descriptions/look-mob.screenreader.template
@@ -0,0 +1,69 @@
+{{/* Screenreader-friendly template for looking at mobs */}}
+Name: {{ .Character.Name }}{{ if .IsCharmed }} (charmed by {{ .CharmedBy }}){{ end }}{{ if .IsShop }} (merchant){{ end }}
+
+Description:
+{{ .Character.GetDescription }}
+
+Health Status: {{ .HealthStatus }}
+{{- $showEmpty := showEmptyEquipmentSlots }}
+{{- $hasEquipment := false }}
+{{- if and (not .Equipment.Weapon.IsDisabled) (gt .Equipment.Weapon.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Offhand.IsDisabled) (gt .Equipment.Offhand.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Head.IsDisabled) (gt .Equipment.Head.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Neck.IsDisabled) (gt .Equipment.Neck.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Body.IsDisabled) (gt .Equipment.Body.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Belt.IsDisabled) (gt .Equipment.Belt.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Gloves.IsDisabled) (gt .Equipment.Gloves.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Ring.IsDisabled) (gt .Equipment.Ring.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Legs.IsDisabled) (gt .Equipment.Legs.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Feet.IsDisabled) (gt .Equipment.Feet.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not $hasEquipment) (not $showEmpty) }}
+
+{{ .Character.Name }} is not wearing anything.
+{{- else if or $hasEquipment $showEmpty }}
+
+Equipment:
+{{- $showItem := false }}
+{{- $showItem = or (and (not .Equipment.Weapon.IsDisabled) (gt .Equipment.Weapon.ItemId 0)) (and $showEmpty (not .Equipment.Weapon.IsDisabled)) }}
+{{- if $showItem }}
+Weapon: {{ if gt .Equipment.Weapon.ItemId 0 }}{{ .Equipment.Weapon.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Offhand.IsDisabled) (gt .Equipment.Offhand.ItemId 0)) (and $showEmpty (not .Equipment.Offhand.IsDisabled)) }}
+{{- if $showItem }}
+Offhand: {{ if gt .Equipment.Offhand.ItemId 0 }}{{ .Equipment.Offhand.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Head.IsDisabled) (gt .Equipment.Head.ItemId 0)) (and $showEmpty (not .Equipment.Head.IsDisabled)) }}
+{{- if $showItem }}
+Head: {{ if gt .Equipment.Head.ItemId 0 }}{{ .Equipment.Head.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Neck.IsDisabled) (gt .Equipment.Neck.ItemId 0)) (and $showEmpty (not .Equipment.Neck.IsDisabled)) }}
+{{- if $showItem }}
+Neck: {{ if gt .Equipment.Neck.ItemId 0 }}{{ .Equipment.Neck.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Body.IsDisabled) (gt .Equipment.Body.ItemId 0)) (and $showEmpty (not .Equipment.Body.IsDisabled)) }}
+{{- if $showItem }}
+Body: {{ if gt .Equipment.Body.ItemId 0 }}{{ .Equipment.Body.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Belt.IsDisabled) (gt .Equipment.Belt.ItemId 0)) (and $showEmpty (not .Equipment.Belt.IsDisabled)) }}
+{{- if $showItem }}
+Belt: {{ if gt .Equipment.Belt.ItemId 0 }}{{ .Equipment.Belt.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Gloves.IsDisabled) (gt .Equipment.Gloves.ItemId 0)) (and $showEmpty (not .Equipment.Gloves.IsDisabled)) }}
+{{- if $showItem }}
+Gloves: {{ if gt .Equipment.Gloves.ItemId 0 }}{{ .Equipment.Gloves.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Ring.IsDisabled) (gt .Equipment.Ring.ItemId 0)) (and $showEmpty (not .Equipment.Ring.IsDisabled)) }}
+{{- if $showItem }}
+Ring: {{ if gt .Equipment.Ring.ItemId 0 }}{{ .Equipment.Ring.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Legs.IsDisabled) (gt .Equipment.Legs.ItemId 0)) (and $showEmpty (not .Equipment.Legs.IsDisabled)) }}
+{{- if $showItem }}
+Legs: {{ if gt .Equipment.Legs.ItemId 0 }}{{ .Equipment.Legs.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Feet.IsDisabled) (gt .Equipment.Feet.ItemId 0)) (and $showEmpty (not .Equipment.Feet.IsDisabled)) }}
+{{- if $showItem }}
+Feet: {{ if gt .Equipment.Feet.ItemId 0 }}{{ .Equipment.Feet.NameSimple }}{{ else }}nothing{{ end }}
+{{- end }}
+{{- end }}
+
+Carrying: {{ .CarryingStatus }} objects
\ No newline at end of file
diff --git a/_datafiles/world/default/templates/descriptions/look-mob.template b/_datafiles/world/default/templates/descriptions/look-mob.template
new file mode 100644
index 00000000..b10dc025
--- /dev/null
+++ b/_datafiles/world/default/templates/descriptions/look-mob.template
@@ -0,0 +1,66 @@
+{{/* Template for looking at mobs */}}
+.: {{ .Character.Name }}{{ if .IsCharmed }} (charmed by {{ .CharmedBy }}){{ end }}{{ if .IsShop }} (merchant){{ end }}
+ ┌─ .:Description ────────────────────────────────────────────────────────────┐
+ {{ splitstring .Character.GetDescription 72 " "}}
+ {{ .HealthStatus }}
+ └────────────────────────────────────────────────────────────────────────────┘
+{{- $showEmpty := showEmptyEquipmentSlots }}
+{{- $hasEquipment := false }}
+{{- if and (not .Equipment.Weapon.IsDisabled) (gt .Equipment.Weapon.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Offhand.IsDisabled) (gt .Equipment.Offhand.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Head.IsDisabled) (gt .Equipment.Head.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Neck.IsDisabled) (gt .Equipment.Neck.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Body.IsDisabled) (gt .Equipment.Body.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Belt.IsDisabled) (gt .Equipment.Belt.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Gloves.IsDisabled) (gt .Equipment.Gloves.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Ring.IsDisabled) (gt .Equipment.Ring.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Legs.IsDisabled) (gt .Equipment.Legs.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not .Equipment.Feet.IsDisabled) (gt .Equipment.Feet.ItemId 0) }}{{ $hasEquipment = true }}{{ end }}
+{{- if and (not $hasEquipment) (not $showEmpty) }}
+ {{ .Character.Name }} is not wearing anything.
+{{- else if or $hasEquipment $showEmpty }}
+ ┌─ .:Equipment ──────────────────────────────────────────────────────────────┐
+{{- $showItem := false }}
+{{- $showItem = or (and (not .Equipment.Weapon.IsDisabled) (gt .Equipment.Weapon.ItemId 0)) (and $showEmpty (not .Equipment.Weapon.IsDisabled)) }}
+{{- if $showItem }}
+ Weapon: {{ if gt .Equipment.Weapon.ItemId 0 }}{{ .Equipment.Weapon.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Offhand.IsDisabled) (gt .Equipment.Offhand.ItemId 0)) (and $showEmpty (not .Equipment.Offhand.IsDisabled)) }}
+{{- if $showItem }}
+ Offhand: {{ if gt .Equipment.Offhand.ItemId 0 }}{{ .Equipment.Offhand.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Head.IsDisabled) (gt .Equipment.Head.ItemId 0)) (and $showEmpty (not .Equipment.Head.IsDisabled)) }}
+{{- if $showItem }}
+ Head: {{ if gt .Equipment.Head.ItemId 0 }}{{ .Equipment.Head.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Neck.IsDisabled) (gt .Equipment.Neck.ItemId 0)) (and $showEmpty (not .Equipment.Neck.IsDisabled)) }}
+{{- if $showItem }}
+ Neck: {{ if gt .Equipment.Neck.ItemId 0 }}{{ .Equipment.Neck.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Body.IsDisabled) (gt .Equipment.Body.ItemId 0)) (and $showEmpty (not .Equipment.Body.IsDisabled)) }}
+{{- if $showItem }}
+ Body: {{ if gt .Equipment.Body.ItemId 0 }}{{ .Equipment.Body.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Belt.IsDisabled) (gt .Equipment.Belt.ItemId 0)) (and $showEmpty (not .Equipment.Belt.IsDisabled)) }}
+{{- if $showItem }}
+ Belt: {{ if gt .Equipment.Belt.ItemId 0 }}{{ .Equipment.Belt.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Gloves.IsDisabled) (gt .Equipment.Gloves.ItemId 0)) (and $showEmpty (not .Equipment.Gloves.IsDisabled)) }}
+{{- if $showItem }}
+ Gloves: {{ if gt .Equipment.Gloves.ItemId 0 }}{{ .Equipment.Gloves.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Ring.IsDisabled) (gt .Equipment.Ring.ItemId 0)) (and $showEmpty (not .Equipment.Ring.IsDisabled)) }}
+{{- if $showItem }}
+ Ring: {{ if gt .Equipment.Ring.ItemId 0 }}{{ .Equipment.Ring.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Legs.IsDisabled) (gt .Equipment.Legs.ItemId 0)) (and $showEmpty (not .Equipment.Legs.IsDisabled)) }}
+{{- if $showItem }}
+ Legs: {{ if gt .Equipment.Legs.ItemId 0 }}{{ .Equipment.Legs.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+{{- $showItem = or (and (not .Equipment.Feet.IsDisabled) (gt .Equipment.Feet.ItemId 0)) (and $showEmpty (not .Equipment.Feet.IsDisabled)) }}
+{{- if $showItem }}
+ Feet: {{ if gt .Equipment.Feet.ItemId 0 }}{{ .Equipment.Feet.NameSimple }}{{ else }}-nothing-{{ end }}
+{{- end }}
+ └────────────────────────────────────────────────────────────────────────────┘
+{{- end }}
+ Carrying: {{ .CarryingStatus }} objects
\ No newline at end of file
diff --git a/internal/configs/config.textformats.go b/internal/configs/config.textformats.go
deleted file mode 100644
index 8db34fa7..00000000
--- a/internal/configs/config.textformats.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package configs
-
-import (
- "strings"
-)
-
-type TextFormats struct {
- Prompt ConfigString `yaml:"Prompt"` // The in-game status prompt style
- EnterRoomMessageWrapper ConfigString `yaml:"EnterRoomMessageWrapper"` // Special enter messages
- ExitRoomMessageWrapper ConfigString `yaml:"ExitRoomMessageWrapper"` // Special exit messages
- Time ConfigString `yaml:"Time"` // How to format time when displaying real time
- TimeShort ConfigString `yaml:"TimeShort"` // How to format time when displaying real time (shortform)
-}
-
-func (m *TextFormats) Validate() {
-
- if m.Prompt == `` {
- m.Prompt = `{8}[{t} {T} {255}HP:{hp}{8}/{HP} {255}MP:{13}{mp}{8}/{13}{MP}{8}]{239}{h}{8}:`
- }
-
- // Must have a message wrapper...
- if m.EnterRoomMessageWrapper == `` {
- m.EnterRoomMessageWrapper = `%s` // default
- }
- if strings.LastIndex(string(m.EnterRoomMessageWrapper), `%s`) < 0 {
- m.EnterRoomMessageWrapper += `%s` // default
- }
-
- // Must have a message wrapper...
- if m.ExitRoomMessageWrapper == `` {
- m.ExitRoomMessageWrapper = `%s` // default
- }
- if strings.LastIndex(string(m.ExitRoomMessageWrapper), `%s`) < 0 {
- m.ExitRoomMessageWrapper += `%s` // default
- }
-
- if m.Time == `` {
- m.Time = `Monday, 02-Jan-2006 03:04:05PM`
- }
-
- if m.TimeShort == `` {
- m.TimeShort = `Jan 2 '06 3:04PM`
- }
-
-}
-
-func GetTextFormatsConfig() TextFormats {
- configDataLock.RLock()
- defer configDataLock.RUnlock()
-
- if !configData.validated {
- configData.Validate()
- }
- return configData.TextFormats
-}
diff --git a/internal/configs/config.userinterface.go b/internal/configs/config.userinterface.go
new file mode 100644
index 00000000..de351df6
--- /dev/null
+++ b/internal/configs/config.userinterface.go
@@ -0,0 +1,72 @@
+package configs
+
+import (
+ "strings"
+)
+
+type UserInterface struct {
+ Formats UserInterfaceFormats `yaml:"Formats"`
+ Display UserInterfaceDisplay `yaml:"Display"`
+}
+
+type UserInterfaceFormats struct {
+ Prompt ConfigString `yaml:"Prompt"` // The in-game status prompt style
+ EnterRoomMessageWrapper ConfigString `yaml:"EnterRoomMessageWrapper"` // Special enter messages
+ ExitRoomMessageWrapper ConfigString `yaml:"ExitRoomMessageWrapper"` // Special exit messages
+ Time ConfigString `yaml:"Time"` // How to format time when displaying real time
+ TimeShort ConfigString `yaml:"TimeShort"` // How to format time when displaying real time (shortform)
+}
+
+type UserInterfaceDisplay struct {
+ ShowEmptyEquipmentSlots ConfigBool `yaml:"ShowEmptyEquipmentSlots"` // Whether to show empty equipment slots when looking at characters/mobs
+}
+
+func (u *UserInterface) Validate() {
+ u.Formats.Validate()
+ u.Display.Validate()
+}
+
+func (f *UserInterfaceFormats) Validate() {
+ if f.Prompt == `` {
+ f.Prompt = `{8}[{t} {T} {255}HP:{hp}{8}/{HP} {255}MP:{13}{mp}{8}/{13}{MP}{8}]{239}{h}{8}:`
+ }
+
+ // Must have a message wrapper...
+ if f.EnterRoomMessageWrapper == `` {
+ f.EnterRoomMessageWrapper = `%s` // default
+ }
+ if strings.LastIndex(string(f.EnterRoomMessageWrapper), `%s`) < 0 {
+ f.EnterRoomMessageWrapper += `%s` // append if missing
+ }
+
+ // Must have a message wrapper...
+ if f.ExitRoomMessageWrapper == `` {
+ f.ExitRoomMessageWrapper = `%s` // default
+ }
+ if strings.LastIndex(string(f.ExitRoomMessageWrapper), `%s`) < 0 {
+ f.ExitRoomMessageWrapper += `%s` // append if missing
+ }
+
+ if f.Time == `` {
+ f.Time = `Monday, 02-Jan-2006 03:04:05PM`
+ }
+
+ if f.TimeShort == `` {
+ f.TimeShort = `Jan 2 '06 3:04PM`
+ }
+}
+
+func (d *UserInterfaceDisplay) Validate() {
+ // ShowEmptyEquipmentSlots defaults to true (show all slots)
+ // The ConfigBool type handles the default value
+}
+
+// Convenience method to check if empty equipment slots should be shown
+func (c Config) ShouldShowEmptyEquipmentSlots() bool {
+ return bool(c.UserInterface.Display.ShowEmptyEquipmentSlots)
+}
+
+// GetUserInterfaceConfig returns the UserInterface configuration
+func GetUserInterfaceConfig() UserInterface {
+ return GetConfig().UserInterface
+}
diff --git a/internal/configs/configs.go b/internal/configs/configs.go
index 5018a06a..a45e3813 100644
--- a/internal/configs/configs.go
+++ b/internal/configs/configs.go
@@ -34,20 +34,20 @@ var (
type Config struct {
// Start config subsections
- Server Server `yaml:"Server"`
- Memory Memory `yaml:"Memory"`
- LootGoblin LootGoblin `yaml:"LootGoblin"`
- Timing Timing `yaml:"Timing"`
- FilePaths FilePaths `yaml:"FilePaths"`
- GamePlay GamePlay `yaml:"GamePlay"`
- Integrations Integrations `yaml:"Integrations"`
- TextFormats TextFormats `yaml:"TextFormats"`
- Translation Translation `yaml:"Translation"`
- Network Network `yaml:"Network"`
- Scripting Scripting `yaml:"Scripting"`
- SpecialRooms SpecialRooms `yaml:"SpecialRooms"`
- Validation Validation `yaml:"Validation"`
- Roles Roles `yaml:"Roles"`
+ Server Server `yaml:"Server"`
+ Memory Memory `yaml:"Memory"`
+ LootGoblin LootGoblin `yaml:"LootGoblin"`
+ Timing Timing `yaml:"Timing"`
+ FilePaths FilePaths `yaml:"FilePaths"`
+ GamePlay GamePlay `yaml:"GamePlay"`
+ UserInterface UserInterface `yaml:"UserInterface"`
+ Integrations Integrations `yaml:"Integrations"`
+ Translation Translation `yaml:"Translation"`
+ Network Network `yaml:"Network"`
+ Scripting Scripting `yaml:"Scripting"`
+ SpecialRooms SpecialRooms `yaml:"SpecialRooms"`
+ Validation Validation `yaml:"Validation"`
+ Roles Roles `yaml:"Roles"`
// Plugins is a special case
Modules Modules `yaml:"Modules"`
@@ -191,7 +191,7 @@ func (c *Config) Validate() {
c.FilePaths.Validate()
c.GamePlay.Validate()
c.Integrations.Validate()
- c.TextFormats.Validate()
+ c.UserInterface.Validate()
c.Translation.Validate()
c.Network.Validate()
c.Scripting.Validate()
diff --git a/internal/items/items.go b/internal/items/items.go
index 79a35e48..c58f4c54 100644
--- a/internal/items/items.go
+++ b/internal/items/items.go
@@ -168,6 +168,69 @@ func (i *Item) Validate() {
}
+// ItemLookData contains all the data needed for the look-item template
+type ItemLookData struct {
+ Item *Item
+ ItemSpec ItemSpec
+ Location string // "in your backpack", "you are wearing"
+ Type string // Item type for template comparisons
+ Subtype string // Item subtype for template comparisons
+ Adjectives map[string]bool // Extensible flags (cursed, enchanted, etc)
+ WeaponHands int
+ WeaponType string // "claws", "shooting", etc.
+ WaitRounds int
+ HasUses bool
+ UsesRemaining int
+ MaxUses int
+ EnchantLevel int
+}
+
+// GetLookData returns data structure for template-based item descriptions
+func (i *Item) GetLookData(location string) ItemLookData {
+ // Defensive check for nil item
+ if i == nil {
+ return ItemLookData{
+ Location: location,
+ Type: "",
+ Subtype: "",
+ Adjectives: make(map[string]bool),
+ }
+ }
+
+ iSpec := i.GetSpec()
+
+ data := ItemLookData{
+ Item: i,
+ ItemSpec: iSpec,
+ Location: location,
+ Type: string(iSpec.Type),
+ Subtype: string(iSpec.Subtype),
+ Adjectives: make(map[string]bool),
+ HasUses: iSpec.Uses > 0,
+ UsesRemaining: i.Uses,
+ MaxUses: iSpec.Uses,
+ EnchantLevel: int(i.Enchantments),
+ }
+
+ // Set adjectives for extensibility
+ data.Adjectives["cursed"] = i.IsCursed()
+ data.Adjectives["enchanted"] = i.Enchantments > 0
+
+ if iSpec.Type == Weapon {
+ data.WeaponHands = iSpec.Hands
+ data.WaitRounds = iSpec.WaitRounds
+
+ switch iSpec.Subtype {
+ case Claws:
+ data.WeaponType = "claws"
+ case Shooting:
+ data.WeaponType = "shooting"
+ }
+ }
+
+ return data
+}
+
func (i *Item) GetLongDescription() string {
iSpec := i.GetSpec()
diff --git a/internal/mapper/mapper.go b/internal/mapper/mapper.go
index 044d5d50..a6c15492 100644
--- a/internal/mapper/mapper.go
+++ b/internal/mapper/mapper.go
@@ -998,15 +998,15 @@ func PreCacheMaps() {
func validateRoomBiomes() {
missingBiomeCount := 0
invalidBiomeCount := 0
-
+
for _, roomId := range rooms.GetAllRoomIds() {
room := rooms.LoadRoom(roomId)
if room == nil {
continue
}
-
+
originalBiome := room.Biome
-
+
// Check if room has no biome
if originalBiome == "" {
zoneBiome := rooms.GetZoneBiome(room.Zone)
@@ -1022,7 +1022,7 @@ func validateRoomBiomes() {
}
}
}
-
+
if missingBiomeCount > 0 || invalidBiomeCount > 0 {
mudlog.Info("Biome validation complete", "missing", missingBiomeCount, "invalid", invalidBiomeCount)
}
diff --git a/internal/mobcommands/go.go b/internal/mobcommands/go.go
index 231b6ec0..c3aadc92 100644
--- a/internal/mobcommands/go.go
+++ b/internal/mobcommands/go.go
@@ -30,7 +30,7 @@ func Go(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
}
if !foundRoomExit {
- c := configs.GetTextFormatsConfig()
+ c := configs.GetUserInterfaceConfig().Formats
if forceRoomId == room.RoomId {
return true, nil
@@ -112,7 +112,7 @@ func Go(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
room.RemoveMob(mob.InstanceId)
destRoom.AddMob(mob.InstanceId)
- c := configs.GetTextFormatsConfig()
+ c := configs.GetUserInterfaceConfig().Formats
// Tell the old room they are leaving
room.SendText(
diff --git a/internal/mobs/mobs.go b/internal/mobs/mobs.go
index 24625fb6..483a4ae9 100644
--- a/internal/mobs/mobs.go
+++ b/internal/mobs/mobs.go
@@ -708,6 +708,72 @@ func (m *Mob) GetScriptPath() string {
return util.FilePath(fullScriptPath)
}
+// MobLookData contains all the data needed for the look-mob template
+type MobLookData struct {
+ Mob *Mob
+ Character *characters.Character
+ HealthStatus string
+ Equipment *characters.Worn
+ ItemCount int
+ CarryingStatus string // "no", "a few", "several", "lots of"
+ IsCharmed bool
+ IsShop bool
+ CharmedBy string // charmer's name if applicable
+}
+
+// GetLookData returns data structure for template-based mob descriptions
+func (m *Mob) GetLookData(charmerName string) MobLookData {
+ // Defensive check for nil mob
+ if m == nil {
+ return MobLookData{
+ CharmedBy: charmerName,
+ HealthStatus: "Unknown health status.",
+ CarryingStatus: "unknown",
+ }
+ }
+
+ data := MobLookData{
+ Mob: m,
+ Character: &m.Character,
+ Equipment: &m.Character.Equipment,
+ IsCharmed: m.Character.IsCharmed(),
+ IsShop: m.HasShop(),
+ CharmedBy: charmerName,
+ }
+
+ // Calculate health status with defensive checks
+ if m.Character.HealthMax.Value <= 0 {
+ data.HealthStatus = m.Character.Name + " appears to be in an unknown state."
+ } else {
+ healthPct := int(math.Ceil((float64(m.Character.Health) / float64(m.Character.HealthMax.Value)) * 100))
+ if healthPct >= 100 {
+ data.HealthStatus = m.Character.Name + " is in perfect health."
+ } else if healthPct >= 80 {
+ data.HealthStatus = m.Character.Name + " has a few scratches."
+ } else if healthPct >= 50 {
+ data.HealthStatus = m.Character.Name + " has some cuts and bruises."
+ } else if healthPct >= 15 {
+ data.HealthStatus = m.Character.Name + " looks to be in pretty bad shape."
+ } else {
+ data.HealthStatus = m.Character.Name + " looks like they're about to die!"
+ }
+ }
+
+ // Calculate carrying status
+ data.ItemCount = len(m.Character.Items)
+ if data.ItemCount == 0 {
+ data.CarryingStatus = "no"
+ } else if data.ItemCount < 4 {
+ data.CarryingStatus = "a few"
+ } else if data.ItemCount < 7 {
+ data.CarryingStatus = "several"
+ } else {
+ data.CarryingStatus = "lots of"
+ }
+
+ return data
+}
+
func ReduceHostility() {
for groupName, group := range mobsHatePlayers {
diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go
index 49906960..285d34f1 100644
--- a/internal/rooms/rooms.go
+++ b/internal/rooms/rooms.go
@@ -2198,7 +2198,6 @@ func (r *Room) Validate() error {
}
}
-
// Make sure all items are validated (and have uids)
for i := range r.Items {
r.Items[i].Validate()
diff --git a/internal/templates/templatesfunctions.go b/internal/templates/templatesfunctions.go
index 0a4fe1f4..a5a7ee2d 100644
--- a/internal/templates/templatesfunctions.go
+++ b/internal/templates/templatesfunctions.go
@@ -199,6 +199,9 @@ var (
"permadeath": func() bool {
return bool(configs.GetGamePlayConfig().Death.PermaDeath)
},
+ "showEmptyEquipmentSlots": func() bool {
+ return configs.GetConfig().ShouldShowEmptyEquipmentSlots()
+ },
"zodiac": func(year int) string {
return gametime.GetZodiac(year)
},
diff --git a/internal/usercommands/go.go b/internal/usercommands/go.go
index 5a8c1834..1c41c498 100644
--- a/internal/usercommands/go.go
+++ b/internal/usercommands/go.go
@@ -29,7 +29,7 @@ func Go(rest string, user *users.UserRecord, room *rooms.Room, flags events.Even
return true, nil
}
- c := configs.GetTextFormatsConfig()
+ c := configs.GetUserInterfaceConfig().Formats
isSneaking := user.Character.HasBuffFlag(buffs.Hidden)
diff --git a/internal/usercommands/history.go b/internal/usercommands/history.go
index 1a6e4a03..afccf2f1 100644
--- a/internal/usercommands/history.go
+++ b/internal/usercommands/history.go
@@ -21,7 +21,7 @@ func History(rest string, user *users.UserRecord, room *rooms.Room, flags events
`%s`,
}
- tFormat := string(configs.GetTextFormatsConfig().TimeShort)
+ tFormat := string(configs.GetUserInterfaceConfig().Formats.TimeShort)
for itm := range user.EventLog.Items {
diff --git a/internal/usercommands/look.go b/internal/usercommands/look.go
index 05d86ac6..01a2e21a 100644
--- a/internal/usercommands/look.go
+++ b/internal/usercommands/look.go
@@ -122,21 +122,16 @@ func Look(rest string, user *users.UserRecord, room *rooms.Room, flags events.Ev
)
}
- descTxt, _ := templates.Process("character/description", &m.Character, user.UserId)
- user.SendText(descTxt)
-
- itemNames := []string{}
- for _, item := range m.Character.Items {
- itemNames = append(itemNames, item.DisplayName())
- }
-
- invData := map[string]any{
- `Equipment`: &m.Character.Equipment,
- `ItemNames`: itemNames,
+ // Use the new template-based look system for mobs
+ charmerName := ""
+ if m.Character.IsCharmed() && m.Character.Charmed.UserId > 0 {
+ if charmer := users.GetByUserId(m.Character.Charmed.UserId); charmer != nil {
+ charmerName = charmer.Character.Name
+ }
}
-
- inventoryTxt, _ := templates.Process("character/inventory-look", invData, user.UserId)
- user.SendText(inventoryTxt)
+ mobLookData := m.GetLookData(charmerName)
+ mobDescTxt, _ := templates.Process("descriptions/look-mob", mobLookData, user.UserId)
+ user.SendText(mobDescTxt)
}
user.SendText(statusTxt)
@@ -283,14 +278,6 @@ func Look(rest string, user *users.UserRecord, room *rooms.Room, flags events.Ev
if foundItem {
- user.SendText(``)
-
- user.SendText(
- fmt.Sprintf(`You look at the %s %s:`, lookItem.DisplayName(), lookDestination),
- )
-
- user.SendText(``)
-
if !isSneaking {
room.SendText(
fmt.Sprintf(`%s is admiring their %s.`, user.Character.Name, lookItem.DisplayName()),
@@ -298,10 +285,12 @@ func Look(rest string, user *users.UserRecord, room *rooms.Room, flags events.Ev
)
}
- user.SendText(
- lookItem.GetLongDescription(),
- )
+ // Use the new template-based look system for items
+ itemLookData := lookItem.GetLookData(lookDestination)
+ itemDescTxt, _ := templates.Process("descriptions/look-item", itemLookData, user.UserId)
+ user.SendText(``)
+ user.SendText(itemDescTxt)
user.SendText(``)
return true, nil
diff --git a/internal/usercommands/set.go b/internal/usercommands/set.go
index 6bfee604..fac62c1e 100644
--- a/internal/usercommands/set.go
+++ b/internal/usercommands/set.go
@@ -15,7 +15,7 @@ import (
func Set(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) {
args := util.SplitButRespectQuotes(strings.ToLower(rest))
- c := configs.GetTextFormatsConfig()
+ c := configs.GetUserInterfaceConfig().Formats
if len(args) == 0 {
diff --git a/internal/usercommands/start.go b/internal/usercommands/start.go
index f99c43da..93135a18 100644
--- a/internal/usercommands/start.go
+++ b/internal/usercommands/start.go
@@ -211,7 +211,7 @@ func Start(rest string, user *users.UserRecord, room *rooms.Room, flags events.E
// Tell the new room they have arrived
destRoom.SendText(
- fmt.Sprintf(configs.GetTextFormatsConfig().EnterRoomMessageWrapper.String(),
+ fmt.Sprintf(configs.GetUserInterfaceConfig().Formats.EnterRoomMessageWrapper.String(),
fmt.Sprintf(`%s enters from somewhere.`, user.Character.Name),
),
user.UserId,
diff --git a/internal/users/inbox.go b/internal/users/inbox.go
index d23e018c..753df06c 100644
--- a/internal/users/inbox.go
+++ b/internal/users/inbox.go
@@ -57,6 +57,6 @@ func (i *Inbox) Empty() {
}
func (m Message) DateString() string {
- tFormat := string(configs.GetConfig().TextFormats.Time)
+ tFormat := string(configs.GetConfig().UserInterface.Formats.Time)
return m.DateSent.Format(tFormat)
}
diff --git a/internal/users/userrecord.prompt.go b/internal/users/userrecord.prompt.go
index 3a070879..3ca97f12 100644
--- a/internal/users/userrecord.prompt.go
+++ b/internal/users/userrecord.prompt.go
@@ -48,7 +48,7 @@ func (u *UserRecord) GetCommandPrompt() string {
if len(promptOut) == 0 {
if promptDefaultCompiled == `` {
- promptDefaultCompiled = util.ConvertColorShortTags(configs.GetTextFormatsConfig().Prompt.String())
+ promptDefaultCompiled = util.ConvertColorShortTags(configs.GetUserInterfaceConfig().Formats.Prompt.String())
}
var customPrompt any = nil
diff --git a/modules/gmcp/gmcp.Game.go b/modules/gmcp/gmcp.Game.go
index 3d14336d..737dd1ba 100644
--- a/modules/gmcp/gmcp.Game.go
+++ b/modules/gmcp/gmcp.Game.go
@@ -40,7 +40,7 @@ func (g *GMCPGameModule) onJoinLeave(e events.Event) events.ListenerReturn {
c := configs.GetConfig()
- tFormat := string(c.TextFormats.Time)
+ tFormat := string(c.UserInterface.Formats.Time)
whoPayload := `"Who": { "Players": [`