aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/act_effects.go10
-rw-r--r--internal/game/act_gather.go3
-rw-r--r--internal/game/act_talk.go9
-rw-r--r--internal/game/casino.go54
-rw-r--r--internal/game/casino_test.go23
-rw-r--r--internal/game/cmd_color.go3
-rw-r--r--internal/game/look_entities.go2
-rw-r--r--internal/game/look_room.go3
-rw-r--r--internal/game/look_target.go11
-rw-r--r--internal/game/production_engine.go3
-rw-r--r--internal/game/render_color.go55
-rw-r--r--internal/game/render_help.go5
-rw-r--r--internal/game/render_prompt.go3
-rw-r--r--internal/game/sys_safespot.go9
14 files changed, 142 insertions, 51 deletions
diff --git a/internal/game/act_effects.go b/internal/game/act_effects.go
index d1effac..a49bd84 100644
--- a/internal/game/act_effects.go
+++ b/internal/game/act_effects.go
@@ -5,7 +5,6 @@ import (
"strconv"
"thehouseoficarus/internal/behavior"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
@@ -75,8 +74,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
if step.Broadcast != "" {
raw := expandTemplate(step.Broadcast, playerName, flagValue)
for _, other := range g.Hub.PlayersInRoom(roomID) {
- otherMode := g.colorMode(other)
- rendered := color.ExpandTagsDefault(otherMode, broadcastSpec, raw)
+ rendered := g.expandTagsDefault(other, broadcastSpec, raw)
other.WriteLine(g.colorize(other, "broadcast", rendered))
}
}
@@ -86,8 +84,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
if other.Player == nil {
continue
}
- otherMode := g.colorMode(other)
- rendered := color.ExpandTagsDefault(otherMode, broadcastSpec, raw)
+ rendered := g.expandTagsDefault(other, broadcastSpec, raw)
other.WriteLine(g.colorize(other, "broadcast", rendered))
}
}
@@ -212,8 +209,7 @@ func (g *Game) writePlayerMessage(sess *net.Session, msg string, playerName stri
}
rendered := expandTemplate(msg, playerName, flagValue)
seqSpec := g.resolveColor(sess, "sequence")
- mode := g.colorMode(sess)
- rendered = color.ExpandTagsDefault(mode, seqSpec, rendered)
+ rendered = g.expandTagsDefault(sess, seqSpec, rendered)
sess.WriteLine(rendered)
}
diff --git a/internal/game/act_gather.go b/internal/game/act_gather.go
index 8743a46..0701fc4 100644
--- a/internal/game/act_gather.go
+++ b/internal/game/act_gather.go
@@ -6,7 +6,6 @@ import (
"strings"
"thehouseoficarus/internal/behavior"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/engine"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
@@ -248,7 +247,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
msg = fmt.Sprintf("You manage to get some %s.", coloredName)
} else {
msg = strings.ReplaceAll(msg, "%n", coloredName)
- msg = color.ExpandTags(g.colorMode(sess), msg)
+ msg = g.expandTags(sess, msg)
}
if xp > 0 && p.OptionBool("xp_drops") {
msg += g.formatXpDropSingle(sess, p, player.SkillName(cfg.Skill), xp)
diff --git a/internal/game/act_talk.go b/internal/game/act_talk.go
index 8de58df..dfa7b94 100644
--- a/internal/game/act_talk.go
+++ b/internal/game/act_talk.go
@@ -5,7 +5,6 @@ import (
"strings"
"thehouseoficarus/internal/behavior"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
@@ -100,7 +99,7 @@ func (g *Game) showTalkNode(sess *net.Session, node behavior.TalkNode) {
td.SeqIndex = 0
msg := node.Messages[0]
if msg != "" {
- sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg))
+ sess.WriteLine(g.expandDialogTags(sess, dialogSpec, msg))
}
td.SeqIndex = 1
if td.SeqIndex < len(node.Messages) {
@@ -118,7 +117,7 @@ func (g *Game) showTalkNode(sess *net.Session, node behavior.TalkNode) {
visible := g.visibleOptions(sess, node.Options)
if len(visible) > 0 {
for i, opt := range visible {
- sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, color.ExpandDialogTags(g.colorMode(sess), g.resolveColor(sess, "dialog_options"), opt.Text)))
+ sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, g.expandDialogTags(sess, g.resolveColor(sess, "dialog_options"), opt.Text)))
}
sess.State = net.StateTalk
g.reprompt(sess)
@@ -158,7 +157,7 @@ func (g *Game) finishSequence(sess *net.Session, node behavior.TalkNode) {
visible := g.visibleOptions(sess, node.Options)
if len(visible) > 0 {
for i, opt := range visible {
- sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, color.ExpandDialogTags(g.colorMode(sess), g.resolveColor(sess, "dialog_options"), opt.Text)))
+ sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, g.expandDialogTags(sess, g.resolveColor(sess, "dialog_options"), opt.Text)))
}
sess.State = net.StateTalk
g.reprompt(sess)
@@ -294,7 +293,7 @@ func (g *Game) advanceTalk(sess *net.Session, p *player.Player) {
if td.SeqIndex < len(node.Messages) {
msg := node.Messages[td.SeqIndex]
if msg != "" {
- sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg))
+ sess.WriteLine(g.expandDialogTags(sess, dialogSpec, msg))
}
td.SeqIndex++
}
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 97372f4..785666e 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -285,7 +285,7 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
// renderCasinoMessage colorizes a casino message unless it is an
// already-composed structured menu.
func (g *Game) renderCasinoMessage(sess *net.Session, category string, menu *casino.MenuView, message string) string {
- if category == "casino_menu" && menu != nil && menu.Kind != casino.MenuSlotCommands {
+ if category == "casino_menu" && menu != nil {
return message
}
return g.colorize(sess, category, message)
@@ -306,29 +306,47 @@ func (g *Game) casinoPrivateMessage(sess *net.Session, category string, menu *ca
credits := g.colorize(sess, "casino_credits", fmt.Sprintf("%d", sess.Player.Credits))
balance := fmt.Sprintf("You have %s chips and %s credits.", chips, credits)
switch menu.Kind {
- case casino.MenuBlackjackBet:
- command := func(value string) string { return g.colorize(sess, "casino_command", value) }
- return fmt.Sprintf("%s\n\nNew hand! You can %s, %s (inc. min/max), or %s.",
- balance, command("bet"), command("bet <amount>"), command("stop"))
+ case casino.MenuBlackjackBet, casino.MenuBaccaratBet:
+ return g.renderCasinoCommands(sess, balance, message, menu.Commands)
case casino.MenuBlackjackTurn:
- commands := make([]string, len(menu.Actions))
- for i, action := range menu.Actions {
- commands[i] = g.colorize(sess, "casino_command", action)
- }
- return menu.Title + "\nActions: " + strings.Join(commands, ", ")
- case casino.MenuBaccaratBet:
- command := func(value string) string { return g.colorize(sess, "casino_command", value) }
- return fmt.Sprintf("%s\n\nNew hand! You can %s, %s on player|banker|tie (inc. min/max), or %s.",
- balance, command("bet"), command("bet <amount>"), command("stop"))
+ return g.renderCasinoCommands(sess, "", menu.Title, menu.Commands)
case casino.MenuSlotCommands:
- if message == "" {
- return balance + "\n\n" + menu.Body
- }
- return message + "\n\n" + balance + "\n\n" + menu.Body
+ return g.renderCasinoCommands(sess, balance, message, menu.Commands)
}
return message
}
+// renderCasinoCommands lays every casino menu out in the same style: an
+// optional balance line, a heading, and an aligned command list with each
+// command in the "command" color and its description in plain text.
+func (g *Game) renderCasinoCommands(sess *net.Session, balance, heading string, commands []casino.MenuCommand) string {
+ var sb strings.Builder
+ if balance != "" {
+ sb.WriteString(balance)
+ sb.WriteString("\n\n")
+ }
+ if heading != "" {
+ sb.WriteString(heading)
+ sb.WriteString("\n\n")
+ }
+ width := 0
+ for _, c := range commands {
+ if n := utf8.RuneCountInString(c.Command); n > width {
+ width = n
+ }
+ }
+ for _, c := range commands {
+ padded := c.Command + strings.Repeat(" ", width-utf8.RuneCountInString(c.Command))
+ cmd := g.colorize(sess, "command", padded)
+ if c.Desc != "" {
+ fmt.Fprintf(&sb, " %s %s\n", cmd, c.Desc)
+ } else {
+ fmt.Fprintf(&sb, " %s\n", cmd)
+ }
+ }
+ return strings.TrimRight(sb.String(), "\n")
+}
+
func renderBlackjackSnapshot(snapshot *casino.BlackjackSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store) string {
if snapshot == nil {
return ""
diff --git a/internal/game/casino_test.go b/internal/game/casino_test.go
index 0394437..8affe29 100644
--- a/internal/game/casino_test.go
+++ b/internal/game/casino_test.go
@@ -81,6 +81,29 @@ func TestExecuteBetParsesBaccaratSpots(t *testing.T) {
}
}
+func TestCasinoMenuRendersAlignedColoredCommands(t *testing.T) {
+ g := &Game{Casino: casino.NewManager(1)}
+ conn := &casinoTestConn{}
+ sess := &net.Session{Player: player.New("alice"), Conn: conn}
+ g.loggedInChars = map[string]*net.Session{"alice": sess}
+ menu := &casino.MenuView{Kind: casino.MenuSlotCommands, Commands: []casino.MenuCommand{
+ {Command: "bet <amount>", Desc: "Set your wager"},
+ {Command: "spin", Desc: "Spin using your current wager"},
+ }}
+ msg := g.casinoPrivateMessage(sess, "casino_menu", menu, "Your slot machine bet is set to 10.")
+ if !strings.Contains(msg, "You have 0 chips and 0 credits.") {
+ t.Fatalf("missing balance line: %q", msg)
+ }
+ // Commands are painted with the "command" color (0A bold).
+ if !strings.Contains(msg, "\033[1m\033[38;5;10mbet <amount>\033[0m") {
+ t.Fatalf("command not colored: %q", msg)
+ }
+ // Shorter commands are right-padded to align the descriptions.
+ if !strings.Contains(msg, "\033[1m\033[38;5;10mspin \033[0m Spin using your current wager") {
+ t.Fatalf("commands not aligned: %q", msg)
+ }
+}
+
func TestRenderBaccaratSnapshot(t *testing.T) {
snapshot := &casino.BaccaratSnapshot{
Player: []casino.CardView{{Rank: "5", Suit: "clubs"}, {Rank: "4", Suit: "hearts"}},
diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go
index 01912a7..b659795 100644
--- a/internal/game/cmd_color.go
+++ b/internal/game/cmd_color.go
@@ -173,6 +173,9 @@ var colorCategoryOrder = []string{
"dialog",
"dialog_options",
+ // Commands
+ "command",
+
// Drops / consume / fire
"drop_message",
"eat_food",
diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go
index 4ed47c9..0d08551 100644
--- a/internal/game/look_entities.go
+++ b/internal/game/look_entities.go
@@ -158,7 +158,7 @@ func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.
roomDesc := def.InRoomDescription
if roomDesc != "" {
- roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc)
+ roomDesc = g.expandTags(sess, roomDesc)
}
coloredName := g.objColorize(sess, def, def.Name)
coloredPlural := g.objColorize(sess, def, def.Name+"s")
diff --git a/internal/game/look_room.go b/internal/game/look_room.go
index dc488f0..9d77ebe 100644
--- a/internal/game/look_room.go
+++ b/internal/game/look_room.go
@@ -58,11 +58,10 @@ func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *wo
descWidth = 70
}
rawLines := wrapText(g.roomDescription(sess, room), descWidth)
- mode := g.colorMode(sess)
roomDescSpec := g.resolveColor(sess, "room_desc")
lines := make([]string, len(rawLines))
for i, l := range rawLines {
- lines[i] = color.ExpandTagsDefault(mode, roomDescSpec, l)
+ lines[i] = g.expandTagsDefault(sess, roomDescSpec, l)
}
return lines
}
diff --git a/internal/game/look_target.go b/internal/game/look_target.go
index 9257940..8368946 100644
--- a/internal/game/look_target.go
+++ b/internal/game/look_target.go
@@ -4,7 +4,6 @@ import (
"fmt"
"strings"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/item"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
@@ -74,7 +73,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
fmt.Sprintf("%s %s", g.colorize(sess, mobColor, best.Name), levelStr),
)
if best.Description != "" {
- sess.WriteLine(color.ExpandTags(g.colorMode(sess), best.Description))
+ sess.WriteLine(g.expandTags(sess, best.Description))
}
if !best.Protected {
sess.WriteLines(
@@ -151,7 +150,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
}
if objDescText != "" && !strings.Contains(objDescText, "{quality}") {
- sess.WriteLine(color.ExpandTags(g.colorMode(sess), objDescText))
+ sess.WriteLine(g.expandTags(sess, objDescText))
}
if len(def.OnLook) > 0 {
@@ -200,7 +199,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
for _, ist := range instances {
if ist.Quality > 0 && strings.Contains(objDescText, "{quality}") {
desc := strings.ReplaceAll(objDescText, "{quality}", fmt.Sprintf("%.0f", ist.Quality))
- sess.WriteLine(color.ExpandTags(g.colorMode(sess), desc))
+ sess.WriteLine(g.expandTags(sess, desc))
}
}
@@ -228,7 +227,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
sess.WriteLines(
"",
g.itemColorize(sess, def, def.Name),
- color.ExpandTags(g.colorMode(sess), def.Description),
+ g.expandTags(sess, def.Description),
fmt.Sprintf("Value: %d credits", def.Value),
)
g.showItemStats(sess, def)
@@ -247,7 +246,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
lines := []string{
"",
g.itemColorize(sess, def, def.Name),
- color.ExpandTags(g.colorMode(sess), def.Description),
+ g.expandTags(sess, def.Description),
fmt.Sprintf("Value: %d credits", def.Value),
}
if slot.MaxQuality > 0 {
diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go
index 2fb7968..260cef0 100644
--- a/internal/game/production_engine.go
+++ b/internal/game/production_engine.go
@@ -6,7 +6,6 @@ import (
"strings"
"thehouseoficarus/internal/behavior"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/engine"
"thehouseoficarus/internal/item"
"thehouseoficarus/internal/net"
@@ -59,7 +58,7 @@ func (g *Game) resolveCraftVar(sess *net.Session, varSpec string, craft *item.Cr
}
}
- return color.ExpandTags(g.colorMode(sess), varSpec)
+ return g.expandTags(sess, varSpec)
}
func (g *Game) resolveCraftMessage(sess *net.Session, itemMsg, defaultMsg string, craft *item.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string {
diff --git a/internal/game/render_color.go b/internal/game/render_color.go
index 958b671..ab3692e 100644
--- a/internal/game/render_color.go
+++ b/internal/game/render_color.go
@@ -107,3 +107,58 @@ func (g *Game) itemColorize(sess *net.Session, itemDef *item.ItemDef, text strin
}
return g.colorize(sess, "item", text)
}
+
+// namedColorSpecs maps every color category name to its resolved ColorSpec
+// for the session, letting inline tags like {command} bind to a color
+// category. All player-overridable categories, server defaults, and constant
+// colors are included so any color can be tagged in data content.
+func (g *Game) namedColorSpecs(sess *net.Session) map[string]color.ColorSpec {
+ names := make(map[string]color.ColorSpec)
+ for name := range config.DefaultColors() {
+ if spec := g.resolveColor(sess, name); !spec.Empty() {
+ names[name] = spec
+ }
+ }
+ if g.ColorConfig != nil {
+ for name := range *g.ColorConfig {
+ if _, ok := names[name]; ok {
+ continue
+ }
+ if spec := g.resolveColor(sess, name); !spec.Empty() {
+ names[name] = spec
+ }
+ }
+ }
+ for name := range config.DefaultConstantColors() {
+ if spec := g.resolveConstant(sess, name); !spec.Empty() {
+ names[name] = spec
+ }
+ }
+ if g.ConstantColorConfig != nil {
+ for name := range *g.ConstantColorConfig {
+ if _, ok := names[name]; ok {
+ continue
+ }
+ if spec := g.resolveConstant(sess, name); !spec.Empty() {
+ names[name] = spec
+ }
+ }
+ }
+ return names
+}
+
+// expandTags expands inline {spec}text{/} tags, including named color
+// categories such as {command}, using the session's resolved colors.
+func (g *Game) expandTags(sess *net.Session, text string) string {
+ return color.ExpandTagsNamed(g.colorMode(sess), g.namedColorSpecs(sess), text)
+}
+
+// expandTagsDefault is expandTags with a default spec applied to untagged text.
+func (g *Game) expandTagsDefault(sess *net.Session, def color.ColorSpec, text string) string {
+ return color.ExpandTagsDefaultNamed(g.colorMode(sess), def, g.namedColorSpecs(sess), text)
+}
+
+// expandDialogTags expands dialog text (quoted speech) with named color tags.
+func (g *Game) expandDialogTags(sess *net.Session, dialogSpec color.ColorSpec, text string) string {
+ return color.ExpandDialogTagsNamed(g.colorMode(sess), dialogSpec, g.namedColorSpecs(sess), text)
+}
diff --git a/internal/game/render_help.go b/internal/game/render_help.go
index 4a283d1..e2893b5 100644
--- a/internal/game/render_help.go
+++ b/internal/game/render_help.go
@@ -7,6 +7,7 @@ import (
"gopkg.in/yaml.v3"
"thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/ui"
)
@@ -117,8 +118,10 @@ func (g *Game) doHelp(sess *net.Session, topic string) {
Title: "Commands",
Columns: []string{"Command", "Type", "Description"},
}
+ commandSpec := g.resolveColor(sess, "command")
for _, c := range commandList {
- t.Rows = append(t.Rows, []string{c.Name, c.Type, c.Desc})
+ name := color.Render(g.colorMode(sess), commandSpec, c.Name)
+ t.Rows = append(t.Rows, []string{name, c.Type, c.Desc})
}
for _, line := range t.Render(unicode, wrapWidth) {
sess.WriteLine(line)
diff --git a/internal/game/render_prompt.go b/internal/game/render_prompt.go
index 9d7b20a..2b4f50c 100644
--- a/internal/game/render_prompt.go
+++ b/internal/game/render_prompt.go
@@ -4,7 +4,6 @@ import (
"fmt"
"strings"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
"thehouseoficarus/internal/world"
@@ -22,7 +21,7 @@ func (g *Game) promptStr(sess *net.Session) string {
}
func (g *Game) expandPromptColors(sess *net.Session, text string) string {
- return color.ExpandTags(g.colorMode(sess), text)
+ return g.expandTags(sess, text)
}
func (g *Game) expandPromptVars(sess *net.Session, text string) string {
diff --git a/internal/game/sys_safespot.go b/internal/game/sys_safespot.go
index 41fe19f..d833b62 100644
--- a/internal/game/sys_safespot.go
+++ b/internal/game/sys_safespot.go
@@ -4,7 +4,6 @@ import (
"fmt"
"math/rand"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/engine"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
@@ -183,7 +182,7 @@ func (g *Game) degradeSafespot(st *world.ObjState, objDef *object.ObjectDef, cfg
if msg != "" {
for _, rs := range roomSessions {
rs.WriteLine("")
- rs.WriteLine(color.ExpandTags(g.colorMode(rs), msg))
+ rs.WriteLine(g.expandTags(rs, msg))
}
}
}
@@ -195,7 +194,7 @@ func (g *Game) degradeSafespot(st *world.ObjState, objDef *object.ObjectDef, cfg
for _, name := range st.SafespotOccupants {
g.safespot.Delete(name)
if occSess := g.sessionInRoom(name, roomSessions); occSess != nil {
- occSess.WriteLine(color.ExpandTags(g.colorMode(occSess),
+ occSess.WriteLine(g.expandTags(occSess,
fmt.Sprintf("The %s crumbles away!", objDef.Name)))
occSess.WriteLine(g.colorize(occSess, "safespot_alert", "Your safespot has been compromised!"))
g.writePrompt(occSess)
@@ -216,7 +215,7 @@ func (g *Game) degradeSafespot(st *world.ObjState, objDef *object.ObjectDef, cfg
for _, name := range st.SafespotOccupants {
if occSess := g.sessionInRoom(name, roomSessions); occSess != nil {
occSess.WriteLine("")
- occSess.WriteLine(color.ExpandTags(g.colorMode(occSess), msg))
+ occSess.WriteLine(g.expandTags(occSess, msg))
}
}
}
@@ -237,7 +236,7 @@ func (g *Game) scheduleSafespotRespawn(st *world.ObjState, objDef *object.Object
respawnSt.SafespotTicksAtLevel = 0
for _, sess := range g.Hub.PlayersInRoom(roomID) {
sess.WriteLine("")
- sess.WriteLine(color.ExpandTags(g.colorMode(sess),
+ sess.WriteLine(g.expandTags(sess,
fmt.Sprintf("The %s reforms and looks stable again.", objDef.Name)))
}
return false