diff options
| author | historia <[not public]> | 2026-06-16 23:37:50 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-16 23:37:50 -0400 |
| commit | 389e307f205f9b7edf604fb9f86e1038ead736ef (patch) | |
| tree | 595095b5877cc2b0513d31fb126303d8796cc311 /internal | |
| parent | 085e728d22a3369bd110b77409914cfbe9ebe611 (diff) | |
| download | thehouseoficarus-389e307f205f9b7edf604fb9f86e1038ead736ef.tar.gz | |
feat: major xterm256 color overhaul, see worldbuilding docs.
Diffstat (limited to 'internal')
57 files changed, 1085 insertions, 480 deletions
diff --git a/internal/color/color.go b/internal/color/color.go index 4ef66b8..9496a1d 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -2,185 +2,420 @@ package color import ( "fmt" + "math" + "regexp" + "strconv" "strings" ) const Reset = "\033[0m" -var ansiFg = map[string]int{ - "black": 30, - "red": 31, - "green": 32, - "yellow": 33, - "blue": 34, - "magenta": 35, - "cyan": 36, - "white": 37, -} - -var ansiBg = map[string]int{ - "black": 40, - "red": 41, - "green": 42, - "yellow": 43, - "blue": 44, - "magenta": 45, - "cyan": 46, - "white": 47, -} - -var ansiBrightFg = map[string]int{ - "bright_black": 90, - "bright_red": 91, - "bright_green": 92, - "bright_yellow": 93, - "bright_blue": 94, - "bright_magenta": 95, - "bright_cyan": 96, - "bright_white": 97, -} - -var ansiBrightBg = map[string]int{ - "bright_black": 100, - "bright_red": 101, - "bright_green": 102, - "bright_yellow": 103, - "bright_blue": 104, - "bright_magenta": 105, - "bright_cyan": 106, - "bright_white": 107, -} - -var xtermFg = map[string]int{ - "black": 0, - "red": 1, - "green": 2, - "yellow": 3, - "blue": 4, - "magenta": 5, - "cyan": 6, - "white": 7, - "bright_black": 8, - "bright_red": 9, - "bright_green": 10, - "bright_yellow": 11, - "bright_blue": 12, - "bright_magenta": 13, - "bright_cyan": 14, - "bright_white": 15, -} - -var ansiStyles = map[string]string{ - "bold": "\033[1m", - "dim": "\033[2m", - "italic": "\033[3m", - "underline": "\033[4m", -} - -func Fg(mode, name string) string { - if mode == "none" || mode == "" { - return "" +var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + +var ansi16RGB = [16][3]int{ + {0, 0, 0}, // 0 black + {170, 0, 0}, // 1 red + {0, 170, 0}, // 2 green + {170, 170, 0}, // 3 yellow + {0, 0, 170}, // 4 blue + {170, 0, 170}, // 5 magenta + {0, 170, 170}, // 6 cyan + {170, 170, 170}, // 7 white + {85, 85, 85}, // 8 bright black + {255, 85, 85}, // 9 bright red + {85, 255, 85}, // 10 bright green + {255, 255, 85}, // 11 bright yellow + {85, 85, 255}, // 12 bright blue + {255, 85, 255}, // 13 bright magenta + {85, 255, 255}, // 14 bright cyan + {255, 255, 255}, // 15 bright white +} + +var cubeValues = [6]int{0, 95, 135, 175, 215, 255} + +func Xterm256ToRGB(index int) (int, int, int) { + if index < 16 { + return ansi16RGB[index][0], ansi16RGB[index][1], ansi16RGB[index][2] } - if mode == "xterm256" { - if c, ok := xtermFg[name]; ok { - return fmt.Sprintf("\033[38;5;%dm", c) + if index < 232 { + i := index - 16 + b := cubeValues[i%6] + g := cubeValues[(i/6)%6] + r := cubeValues[(i/36)%6] + return r, g, b + } + v := 8 + (index-232)*10 + return v, v, v +} + +func colorDist(r1, g1, b1, r2, g2, b2 int) int { + dr := r1 - r2 + dg := g1 - g2 + db := b1 - b2 + return dr*dr + dg*dg + db*db +} + +func nearestANSI(index int) int { + if index < 16 { + if index < 8 { + return 30 + index } - return "" + return 90 + (index - 8) } - if c, ok := ansiFg[name]; ok { - return fmt.Sprintf("\033[%dm", c) + r, g, b := Xterm256ToRGB(index) + bestDist := math.MaxInt + bestCode := 37 + for i := 0; i < 16; i++ { + dist := colorDist(r, g, b, ansi16RGB[i][0], ansi16RGB[i][1], ansi16RGB[i][2]) + if dist < bestDist { + bestDist = dist + if i < 8 { + bestCode = 30 + i + } else { + bestCode = 90 + (i - 8) + } + } } - if c, ok := ansiBrightFg[name]; ok { - return fmt.Sprintf("\033[%dm", c) + return bestCode +} + +func nearestANSIBg(index int) int { + fg := nearestANSI(index) + if fg >= 90 { + return fg - 90 + 100 } - return "" + return fg - 30 + 40 +} + +func nearestCubeComponent(v int) int { + if v < 48 { + return 0 + } + if v < 115 { + return 1 + } + if v < 155 { + return 2 + } + if v < 195 { + return 3 + } + if v < 235 { + return 4 + } + return 5 } -func Bg(mode, name string) string { - if mode == "none" || mode == "" { +func NearestXterm256(r, g, b int) int { + ri := nearestCubeComponent(r) + gi := nearestCubeComponent(g) + bi := nearestCubeComponent(b) + cubeIdx := 16 + ri*36 + gi*6 + bi + cubeDist := colorDist(r, g, b, cubeValues[ri], cubeValues[gi], cubeValues[bi]) + + avg := (r + g + b) / 3 + grayStep := (avg - 8 + 5) / 10 + if grayStep < 0 { + grayStep = 0 + } + if grayStep > 23 { + grayStep = 23 + } + grayIdx := 232 + grayStep + gv := 8 + grayStep*10 + grayDist := colorDist(r, g, b, gv, gv, gv) + + bestIdx := cubeIdx + bestDist := cubeDist + if grayDist < bestDist { + bestIdx = grayIdx + bestDist = grayDist + } + + for i := 0; i < 16; i++ { + d := colorDist(r, g, b, ansi16RGB[i][0], ansi16RGB[i][1], ansi16RGB[i][2]) + if d < bestDist { + bestDist = d + bestIdx = i + } + } + + return bestIdx +} + +func FgCode(mode string, index int) string { + if mode == "none" || mode == "" || index < 0 || index > 255 { return "" } if mode == "xterm256" { - if c, ok := xtermFg[name]; ok { - return fmt.Sprintf("\033[48;5;%dm", c) - } + return fmt.Sprintf("\033[38;5;%dm", index) + } + return fmt.Sprintf("\033[%dm", nearestANSI(index)) +} + +func BgCode(mode string, index int) string { + if mode == "none" || mode == "" || index < 0 || index > 255 { return "" } - if c, ok := ansiBg[name]; ok { - return fmt.Sprintf("\033[%dm", c) + if mode == "xterm256" { + return fmt.Sprintf("\033[48;5;%dm", index) } - if c, ok := ansiBrightBg[name]; ok { - return fmt.Sprintf("\033[%dm", c) + return fmt.Sprintf("\033[%dm", nearestANSIBg(index)) +} + +func StyleCode(name string) string { + switch name { + case "bold": + return "\033[1m" + case "dim": + return "\033[2m" + case "underline": + return "\033[4m" } return "" } -func Style(name string) string { - if s, ok := ansiStyles[name]; ok { - return s +func VisibleLen(s string) int { + return len(ansiRe.ReplaceAllString(s, "")) +} + +func ContrastFg(mode string, bgIndex int) string { + r, g, b := Xterm256ToRGB(bgIndex) + if r+g+b > 384 { + return FgCode(mode, 0) } - return "" + return FgCode(mode, 15) } type ColorSpec struct { - Fg string - Bg string + Fg int + Bg int Bold bool Dim bool - Italic bool Underline bool + Gradient []int +} + +func NoColor() ColorSpec { + return ColorSpec{Fg: -1, Bg: -1} } func Parse(input string) ColorSpec { - var spec ColorSpec + spec := ColorSpec{Fg: -1, Bg: -1} for _, token := range strings.Fields(input) { switch { case token == "bold": spec.Bold = true case token == "dim": spec.Dim = true - case token == "italic": - spec.Italic = true case token == "underline": spec.Underline = true - case strings.HasPrefix(token, "fg="): - spec.Fg = strings.TrimPrefix(token, "fg=") - case strings.HasPrefix(token, "bg="): - spec.Bg = strings.TrimPrefix(token, "bg=") + case strings.HasPrefix(token, "bg:"): + if n, err := strconv.Atoi(strings.TrimPrefix(token, "bg:")); err == nil && n >= 0 && n <= 255 { + spec.Bg = n + } + case strings.HasPrefix(token, "g:"): + parts := strings.Split(token[2:], ",") + var stops []int + for _, p := range parts { + if n, err := strconv.Atoi(strings.TrimSpace(p)); err == nil && n >= 0 && n <= 255 { + stops = append(stops, n) + } + } + if len(stops) >= 2 { + spec.Gradient = stops + } + default: + if n, err := strconv.Atoi(token); err == nil && n >= 0 && n <= 255 { + spec.Fg = n + } } } return spec } func (s ColorSpec) Empty() bool { - return s.Fg == "" && s.Bg == "" && !s.Bold && !s.Dim && !s.Italic && !s.Underline + return s.Fg < 0 && s.Bg < 0 && !s.Bold && !s.Dim && !s.Underline && len(s.Gradient) == 0 } -func Render(mode string, spec ColorSpec, text string) string { - if mode == "none" || mode == "" || spec.Empty() { - return text - } +func stylePrefix(spec ColorSpec) string { var codes []string if spec.Bold { - codes = append(codes, Style("bold")) + codes = append(codes, StyleCode("bold")) } if spec.Dim { - codes = append(codes, Style("dim")) - } - if spec.Italic { - codes = append(codes, Style("italic")) + codes = append(codes, StyleCode("dim")) } if spec.Underline { - codes = append(codes, Style("underline")) + codes = append(codes, StyleCode("underline")) + } + return strings.Join(codes, "") +} + +func Render(mode string, spec ColorSpec, text string) string { + if mode == "none" || mode == "" || spec.Empty() { + return text + } + if len(spec.Gradient) >= 2 { + return renderGradient(mode, spec, text) } - if spec.Fg != "" { - codes = append(codes, Fg(mode, spec.Fg)) + var codes []string + prefix := stylePrefix(spec) + if prefix != "" { + codes = append(codes, prefix) + } + if spec.Fg >= 0 { + codes = append(codes, FgCode(mode, spec.Fg)) } - if spec.Bg != "" { - codes = append(codes, Bg(mode, spec.Bg)) + if spec.Bg >= 0 { + codes = append(codes, BgCode(mode, spec.Bg)) } if len(codes) == 0 { return text } return strings.Join(codes, "") + text + Reset } + +func interpolateGradient(stops []int, t float64) int { + if t <= 0 { + return stops[0] + } + if t >= 1 { + return stops[len(stops)-1] + } + seg := t * float64(len(stops)-1) + i := int(seg) + if i >= len(stops)-1 { + i = len(stops) - 2 + } + frac := seg - float64(i) + r1, g1, b1 := Xterm256ToRGB(stops[i]) + r2, g2, b2 := Xterm256ToRGB(stops[i+1]) + r := r1 + int(frac*float64(r2-r1)) + g := g1 + int(frac*float64(g2-g1)) + b := b1 + int(frac*float64(b2-b1)) + return NearestXterm256(r, g, b) +} + +func renderGradient(mode string, spec ColorSpec, text string) string { + runes := []rune(text) + n := len(runes) + if n == 0 { + return "" + } + var sb strings.Builder + prefix := stylePrefix(spec) + if spec.Bg >= 0 { + sb.WriteString(BgCode(mode, spec.Bg)) + } + for i, r := range runes { + t := 0.0 + if n > 1 { + t = float64(i) / float64(n-1) + } + idx := interpolateGradient(spec.Gradient, t) + if prefix != "" { + sb.WriteString(prefix) + } + sb.WriteString(FgCode(mode, idx)) + sb.WriteRune(r) + } + sb.WriteString(Reset) + return sb.String() +} + +func ExpandTags(mode string, text string) string { + return ExpandTagsWithDefault(mode, NoColor(), text) +} + +func ExpandTagsWithDefault(mode string, def ColorSpec, text string) string { + if !strings.Contains(text, "{") { + if !def.Empty() { + return Render(mode, def, text) + } + return text + } + + var sb strings.Builder + pos := 0 + for pos < len(text) { + start := strings.Index(text[pos:], "{") + if start < 0 { + remaining := text[pos:] + if remaining != "" { + if !def.Empty() { + sb.WriteString(Render(mode, def, remaining)) + } else { + sb.WriteString(remaining) + } + } + break + } + start += pos + + end := strings.Index(text[start+1:], "}") + if end < 0 { + remaining := text[pos:] + if !def.Empty() { + sb.WriteString(Render(mode, def, remaining)) + } else { + sb.WriteString(remaining) + } + break + } + end += start + 1 + + specStr := text[start+1 : end] + + if specStr == "" || specStr == "/" { + before := text[pos : start+end-start+1] + if !def.Empty() { + sb.WriteString(Render(mode, def, before)) + } else { + sb.WriteString(before) + } + pos = end + 1 + continue + } + + spec := Parse(specStr) + if spec.Empty() { + before := text[pos : end+1] + if !def.Empty() { + sb.WriteString(Render(mode, def, before)) + } else { + sb.WriteString(before) + } + pos = end + 1 + continue + } + + closeStart := strings.Index(text[end+1:], "{/}") + if closeStart < 0 { + remaining := text[pos:] + if !def.Empty() { + sb.WriteString(Render(mode, def, remaining)) + } else { + sb.WriteString(remaining) + } + break + } + closeStart += end + 1 + + before := text[pos:start] + if before != "" { + if !def.Empty() { + sb.WriteString(Render(mode, def, before)) + } else { + sb.WriteString(before) + } + } + + inner := text[end+1 : closeStart] + sb.WriteString(Render(mode, spec, inner)) + + pos = closeStart + 3 + } + + return sb.String() +} diff --git a/internal/config/config.go b/internal/config/config.go index 48f1a69..d2065d2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,37 +7,45 @@ import ( ) type Config struct { - Game GameConfig `yaml:"game"` - Colors ColorsConfig `yaml:"colors"` - Telnet TelnetConfig `yaml:"telnet"` - HTTP HTTPConfig `yaml:"http"` - HTTPS HTTPSConfig `yaml:"https"` + Game GameConfig `yaml:"game"` + Colors ColorsConfig `yaml:"colors"` + Telnet TelnetConfig `yaml:"telnet"` + TelnetTLS TelnetTLSConfig `yaml:"telnet_tls"` + HTTP HTTPConfig `yaml:"http"` + HTTPS HTTPSConfig `yaml:"https"` } type ColorsConfig map[string]string func DefaultColors() ColorsConfig { return ColorsConfig{ - "room_name": "fg=cyan bold", - "room_number": "dim", - "room_desc": "fg=white", - "direction": "fg=cyan", - "exit_direction": "fg=cyan", - "exit_name": "fg=green", - "mob_name": "fg=bright_red", - "friendly_npc": "fg=green", - "hostile_npc": "fg=bright_red", - "damage": "fg=red", - "enemy_hp": "fg=red", - "character_hp": "fg=green", - "xp": "fg=yellow", - "level_up": "fg=bright_yellow bold", - "item": "fg=green", - "player_name": "fg=bright_white", - "death": "fg=red bold", - "victory": "fg=green", - "miss": "dim", - "error": "fg=red", + "room_name": "81 bold", + "room_number": "240", + "room_desc": "252", + "direction": "75", + "exit_direction": "75", + "exit_name": "114", + "mob_name": "203", + "friendly_npc": "120", + "hostile_npc": "203", + "damage": "196", + "enemy_hp": "167", + "character_hp": "84", + "xp": "222", + "level_up": "226 bold", + "item": "223", + "player_name": "189", + "death": "196 bold", + "victory": "83", + "miss": "243", + "error": "209", + "say": "230", + "dialog": "117", + "broadcast": "215", + "fire": "208", + "eat_food": "156", + "drop_message": "186", + "credits_pickup": "220", } } @@ -50,6 +58,13 @@ type TelnetConfig struct { Port int `yaml:"port"` } +type TelnetTLSConfig struct { + Enabled bool `yaml:"enabled"` + Port int `yaml:"port"` + CertFile string `yaml:"cert_file"` + KeyFile string `yaml:"key_file"` +} + type HTTPConfig struct { Enabled bool `yaml:"enabled"` Port int `yaml:"port"` @@ -72,6 +87,10 @@ func Default() *Config { Enabled: true, Port: 4000, }, + TelnetTLS: TelnetTLSConfig{ + Enabled: false, + Port: 4001, + }, HTTP: HTTPConfig{ Enabled: false, Port: 8080, diff --git a/internal/game/action.go b/internal/game/action.go index 3857030..04e2b0a 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -6,12 +6,12 @@ import ( "strconv" "strings" - "thirdcollapse/internal/action" - "thirdcollapse/internal/combat" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) var verbAliases = map[string]string{ diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go index 5c19510..01bf385 100644 --- a/internal/game/action_burn.go +++ b/internal/game/action_burn.go @@ -4,11 +4,11 @@ import ( "fmt" "math/rand" - "thirdcollapse/internal/action" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) { @@ -70,13 +70,13 @@ func (g *Game) startBurn(sess *net.Session, p *player.Player, itemID string, fro } if logDef.BurnTicks <= 0 { - sess.WriteLine(fmt.Sprintf("You can't burn the %s.", logDef.Name)) + sess.WriteLine(fmt.Sprintf("You can't burn the %s.", g.itemColorize(sess, logDef, logDef.Name))) return } skillLevel := p.Level(player.Firemaking) if logDef.FireLevel > 0 && skillLevel < logDef.FireLevel { - sess.WriteLine(fmt.Sprintf("You need level %d firemaking to burn %s.", logDef.FireLevel, logDef.Name)) + sess.WriteLine(fmt.Sprintf("You need level %d firemaking to burn %s.", logDef.FireLevel, g.itemColorize(sess, logDef, logDef.Name))) return } @@ -87,13 +87,13 @@ func (g *Game) startBurn(sess *net.Session, p *player.Player, itemID string, fro if !fromGround { if !p.HasItem(itemID) { - sess.WriteLine(fmt.Sprintf("You don't have any %s.", logDef.Name)) + sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, logDef, logDef.Name))) return } } else { ground := g.World.GroundItems(p.RoomID) if ground[itemID] <= 0 { - sess.WriteLine(fmt.Sprintf("There are no %s on the ground here.", logDef.Name)) + sess.WriteLine(fmt.Sprintf("There are no %s on the ground here.", g.itemColorize(sess, logDef, logDef.Name))) return } } @@ -135,7 +135,7 @@ func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) { } if !p.HasItem(itemID) { - sess.WriteLine(fmt.Sprintf("You don't have any %s.", logDef.Name)) + sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, logDef, logDef.Name))) return } @@ -171,7 +171,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { if phase == 0 { if !p.HasItem(itemID) { - sess.WriteLine(fmt.Sprintf("You're out of %s.", logDef.Name)) + sess.WriteLine(fmt.Sprintf("You're out of %s.", g.itemColorize(sess, logDef, logDef.Name))) g.CancelAction(p) return } @@ -179,7 +179,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { g.World.AddGroundItem(p.RoomID, itemID, 1) g.AccountStore.SaveCharacter(p) - sess.WriteLine(fmt.Sprintf("You drop the %s and begin to make a fire...", logDef.Name)) + sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You drop the %s and begin to make a fire...", g.itemColorize(sess, logDef, logDef.Name)))) g.broadcastDrop(p, logDef.Name) data["phase"] = 1 @@ -195,7 +195,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { } if phase == 1 { - sess.WriteLine(fmt.Sprintf("You try burning the %s on the ground...", logDef.Name)) + sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You try burning the %s on the ground...", g.itemColorize(sess, logDef, logDef.Name)))) data["phase"] = 2 p.Action.WaitLeft = engine.ToTicks(toolSpeed) return @@ -216,26 +216,26 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { g.World.AddObjInstance(p.RoomID, "fire", logDef.BurnTicks) xp := logDef.FireXP - if xp > 0 { - if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 { - sess.WriteLine(fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)) - } + if xp > 0 { + if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel))) } + } - g.AccountStore.SaveCharacter(p) - g.CancelAction(p) + g.AccountStore.SaveCharacter(p) + g.CancelAction(p) - msg := fmt.Sprintf("You manage to get a fire going!") - if xp > 0 && p.OptionBool("xp_drops") { - msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]) - } - sess.WriteLine(msg) + msg := g.colorize(sess, "fire", "You manage to get a fire going!") + if xp > 0 && p.OptionBool("xp_drops") { + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking])) + } + sess.WriteLine(msg) - for _, other := range g.Hub.PlayersInRoom(p.RoomID) { - if other != sess { - other.WriteLine(fmt.Sprintf("\n%s started a fire!", p.Name)) - } + for _, other := range g.Hub.PlayersInRoom(p.RoomID) { + if other != sess { + other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s started a fire!", p.Name))) } + } return } @@ -266,7 +266,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) { } if !p.HasItem(itemID) { - sess.WriteLine(fmt.Sprintf("You're out of %s and the fire is roaring.", name)) + sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name)))) g.CancelAction(p) return } @@ -276,20 +276,20 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) { if xp > 0 { if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 { - sess.WriteLine(fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)) + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel))) } } g.AccountStore.SaveCharacter(p) - msg := fmt.Sprintf("You throw %s onto the fire.", name) + msg := g.colorize(sess, "fire", fmt.Sprintf("You throw %s onto the fire.", name)) if xp > 0 && p.OptionBool("xp_drops") { - msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]) + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking])) } sess.WriteLine(msg) if !p.HasItem(itemID) { - sess.WriteLine(fmt.Sprintf("You're out of %s and the fire is roaring.", name)) + sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name)))) g.CancelAction(p) return } diff --git a/internal/game/action_cook.go b/internal/game/action_cook.go index e4d2597..a5bda41 100644 --- a/internal/game/action_cook.go +++ b/internal/game/action_cook.go @@ -4,10 +4,10 @@ import ( "fmt" "math/rand" - "thirdcollapse/internal/action" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) startCook(sess *net.Session, p *player.Player, recipe *action.RecipeDef, stationName string) { diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go index e3ea105..045c51b 100644 --- a/internal/game/action_gather.go +++ b/internal/game/action_gather.go @@ -5,12 +5,13 @@ import ( "math/rand" "strings" - "thirdcollapse/internal/action" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) { @@ -263,6 +264,8 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { msg := drop.Message if msg == "" { msg = fmt.Sprintf("You manage to get some %s.", g.itemColorize(sess, itemDef, itemName)) + } else { + msg = color.ExpandTags(g.colorMode(sess), msg) } if xp > 0 && p.OptionBool("xp_drops") { msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.SkillName(cfg.Skill)])) diff --git a/internal/game/action_room.go b/internal/game/action_room.go index 426b512..603ac7e 100644 --- a/internal/game/action_room.go +++ b/internal/game/action_room.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "thirdcollapse/internal/net" + "thehouseoficarus/internal/net" ) func (g *Game) RunEnterSteps(sess *net.Session, roomID int) { @@ -40,7 +40,7 @@ func (g *Game) BroadcastRespawns() { msg := strings.ReplaceAll(cfg.RespawnBroadcast, "{name}", name) if g.Hub != nil { for _, sess := range g.Hub.PlayersInRoom(st.RoomID) { - sess.WriteLine(fmt.Sprintf("\n%s", msg)) + sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s", msg))) } } } diff --git a/internal/game/action_search.go b/internal/game/action_search.go index e9d3d46..d45066d 100644 --- a/internal/game/action_search.go +++ b/internal/game/action_search.go @@ -3,10 +3,10 @@ package game import ( "fmt" - "thirdcollapse/internal/action" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, slotIdx int) { @@ -101,23 +101,24 @@ func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *action. } name := drop.ItemID - if def, err := g.ItemStore.Load(drop.ItemID); err == nil { - name = def.Name + lootDef, _ := g.ItemStore.Load(drop.ItemID) + if lootDef != nil { + name = lootDef.Name } if drop.ItemID == "credits" { p.Credits += qty - sess.WriteLine(fmt.Sprintf("You find %d credits.", qty)) + sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You find %d credits.", qty))) return } freeSlot := p.FirstFreeSlot() if freeSlot == -1 { g.World.AddGroundItem(p.RoomID, drop.ItemID, qty) - sess.WriteLine(fmt.Sprintf("You find %s. It falls to the ground.", name)) + sess.WriteLine(fmt.Sprintf("You find %s. It falls to the ground.", g.itemColorize(sess, lootDef, name))) return } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) - sess.WriteLine(fmt.Sprintf("You find %s.", name)) + sess.WriteLine(fmt.Sprintf("You find %s.", g.itemColorize(sess, lootDef, name))) } diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go index 9e90f93..30bf206 100644 --- a/internal/game/action_talk.go +++ b/internal/game/action_talk.go @@ -4,11 +4,11 @@ import ( "fmt" "strings" - "thirdcollapse/internal/action" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobInstance) { @@ -54,7 +54,7 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object } func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) { - sess.WriteLine(fmt.Sprintf("\n%s", node.Message)) + sess.WriteLine(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", node.Message))) if node.Action != nil { g.applyNodeAction(sess, node.Action) diff --git a/internal/game/action_toggle.go b/internal/game/action_toggle.go index f20d03c..db7a199 100644 --- a/internal/game/action_toggle.go +++ b/internal/game/action_toggle.go @@ -3,9 +3,9 @@ package game import ( "fmt" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) startToggle(sess *net.Session, p *player.Player, obj *object.ObjectDef) { @@ -41,7 +41,7 @@ func (g *Game) startToggle(sess *net.Session, p *player.Player, obj *object.Obje if g.Hub != nil { for _, other := range g.Hub.PlayersInRoom(p.RoomID) { if other != sess && other.Player != nil { - other.WriteLine(fmt.Sprintf("\n%s uses the %s.", p.Name, obj.Name)) + other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s uses the %s.", p.Name, obj.Name))) } } } diff --git a/internal/game/action_use.go b/internal/game/action_use.go index d953521..b71ec17 100644 --- a/internal/game/action_use.go +++ b/internal/game/action_use.go @@ -4,11 +4,11 @@ import ( "fmt" "math/rand" - "thirdcollapse/internal/action" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectDef) { @@ -99,16 +99,17 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) { p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: cfg.Reward.ItemID, Quantity: qty}) if cfg.XP > 0 { if newLevel := p.AddSkillXP(player.SkillName(cfg.Skill), cfg.XP); newLevel > 0 { - sess.WriteLine(fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill)) + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill))) } } g.AccountStore.SaveCharacter(p) itemName := cfg.Reward.ItemID - if def, err := g.ItemStore.Load(cfg.Reward.ItemID); err == nil { - itemName = def.Name + rewardDef, _ := g.ItemStore.Load(cfg.Reward.ItemID) + if rewardDef != nil { + itemName = rewardDef.Name } - line := fmt.Sprintf("You make a %s.", itemName) + line := fmt.Sprintf("You make a %s.", g.itemColorize(sess, rewardDef, itemName)) if cfg.XP > 0 && p.OptionBool("xp_drops") { line += fmt.Sprintf(" (+%dxp %s)", cfg.XP, player.SkillAbbr[player.SkillName(cfg.Skill)]) } diff --git a/internal/game/cmd_alias.go b/internal/game/cmd_alias.go index cb0537e..e7e4e12 100644 --- a/internal/game/cmd_alias.go +++ b/internal/game/cmd_alias.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - "thirdcollapse/internal/net" + "thehouseoficarus/internal/net" ) func (g *Game) doAlias(sess *net.Session, args []string) { diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index 2e15419..359e0ac 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -6,12 +6,12 @@ import ( "strconv" "strings" - "thirdcollapse/internal/combat" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func (g *Game) doAttack(sess *net.Session, input string) { @@ -308,7 +308,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst op := other.Player.(*player.Player) mobLvl := mobCombatLevel(mob) levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl)) - other.WriteLine(fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr)) + other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr))) } } } @@ -324,7 +324,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst if !mob.Unique { dropper = "The " + mob.Name } - sess.WriteLine(fmt.Sprintf(" %s drops: %s", dropper, name)) + sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %s", dropper, name))) } if len(mob.Drops.Loot) > 0 { @@ -345,9 +345,9 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst dropper = "The " + mob.Name } if qty > 1 { - sess.WriteLine(fmt.Sprintf(" %s drops: %d x %s", dropper, qty, name)) + sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %d x %s", dropper, qty, name))) } else { - sess.WriteLine(fmt.Sprintf(" %s drops: %s", dropper, name)) + sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %s", dropper, name))) } } } @@ -476,7 +476,7 @@ func (g *Game) respawnMob(instanceID string) { if p, ok := sess.Player.(*player.Player); ok && p.OptionBool("mob_spawn") { mobLvl := mobCombatLevel(inst) levelStr := g.levelColorize(sess, p.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl)) - sess.WriteLine(fmt.Sprintf("\n%s %s spawns in the area.", mobDisplayName(inst, false), levelStr)) + sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s %s spawns in the area.", mobDisplayName(inst, false), levelStr))) } } } diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index 7a208a2..1dd14fa 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" - "thirdcollapse/internal/color" - "thirdcollapse/internal/config" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/config" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doColor(sess *net.Session, input string) { @@ -57,8 +57,8 @@ func (g *Game) doColor(sess *net.Session, input string) { spec := color.Parse(value) if spec.Empty() && value != "" { sess.WriteLine(fmt.Sprintf("\nInvalid color string: %s", value)) - sess.WriteLine("Format: fg=<color> bg=<color> bold dim italic underline") - sess.WriteLine("Example: fg=green bg=black bold") + sess.WriteLine("Format: <0-255> [bg:<0-255>] [bold] [dim] [underline]") + sess.WriteLine("Example: 208 bold") return } @@ -150,17 +150,40 @@ var colorCategoryOrder = []string{ "victory", "miss", "error", + "say", + "dialog", + "broadcast", + "fire", + "eat_food", + "drop_message", + "credits_pickup", } func showColorTable(g *Game, sess *net.Session) { p := sess.Player.(*player.Player) + mode := g.colorMode(sess) table := &Table{ - Columns: []string{"Target", "Color", "Source"}, + Columns: []string{ + color.Render(mode, color.Parse("75"), "Target"), + "Color", + color.Render(mode, color.Parse("243"), "Source"), + }, } for _, cat := range colorCategoryOrder { val := g.getCurrentColor(sess, cat) source := g.colorSource(sess, cat) - table.Rows = append(table.Rows, []string{cat, val, source}) + coloredVal := val + if val != "off" && val != "" { + spec := color.Parse(val) + if !spec.Empty() { + coloredVal = color.Render(mode, spec, val) + } + } + table.Rows = append(table.Rows, []string{ + color.Render(mode, color.Parse("75"), cat), + coloredVal, + color.Render(mode, color.Parse("243"), source), + }) } for _, line := range table.Render(p.OptionBool("unicode")) { sess.WriteLine(line) diff --git a/internal/game/cmd_colortable.go b/internal/game/cmd_colortable.go new file mode 100644 index 0000000..90ae8c4 --- /dev/null +++ b/internal/game/cmd_colortable.go @@ -0,0 +1,151 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) doColortable(sess *net.Session) { + mode := g.colorMode(sess) + if mode == "none" || mode == "" { + sess.WriteLine("\nEnable colors first: option color ansi (or xterm256)") + return + } + + p := sess.Player.(*player.Player) + unicode := p.OptionBool("unicode") + + sess.WriteLine(fmt.Sprintf("\nColor mode: %s", mode)) + + sess.WriteLine("") + writeSectionHeader(sess, "Foreground Colors", unicode) + writeColorBlock(sess, mode, true) + + sess.WriteLine("") + writeSectionHeader(sess, "Background Colors", unicode) + writeColorBlock(sess, mode, false) + + sess.WriteLine("") + writeSectionHeader(sess, "Combinations", unicode) + writeCombinations(sess, mode) + + sess.WriteLine("") + writeSectionHeader(sess, "ANSI Mode (16 colors)", unicode) + writeANSITable(sess, mode) +} + +func writeSectionHeader(sess *net.Session, title string, unicode bool) { + if unicode { + sess.WriteLine(fmt.Sprintf("── %s ──", title)) + } else { + sess.WriteLine(fmt.Sprintf("-- %s --", title)) + } +} + +func writeColorBlock(sess *net.Session, mode string, fg bool) { + sess.WriteLine("Standard:") + writeColorRow(sess, mode, 0, 8, fg) + sess.WriteLine("Bright:") + writeColorRow(sess, mode, 8, 16, fg) + + sess.WriteLine("Color cube:") + for blockRow := 0; blockRow < 2; blockRow++ { + for row := 0; row < 6; row++ { + var sb strings.Builder + for block := 0; block < 3; block++ { + base := 16 + (blockRow*3+block)*36 + row*6 + for col := 0; col < 6; col++ { + idx := base + col + if idx > 231 { + break + } + if fg { + sb.WriteString(color.FgCode(mode, idx)) + } else { + sb.WriteString(color.ContrastFg(mode, idx)) + sb.WriteString(color.BgCode(mode, idx)) + } + sb.WriteString(fmt.Sprintf("%4d", idx)) + sb.WriteString(color.Reset) + } + if block < 2 { + sb.WriteString(" ") + } + } + sess.WriteLine(sb.String()) + } + if blockRow == 0 { + sess.WriteLine("") + } + } + + sess.WriteLine("Grayscale:") + writeColorRow(sess, mode, 232, 244, fg) + writeColorRow(sess, mode, 244, 256, fg) +} + +func writeColorRow(sess *net.Session, mode string, start, end int, fg bool) { + var sb strings.Builder + for i := start; i < end && i < 256; i++ { + if fg { + sb.WriteString(color.FgCode(mode, i)) + } else { + sb.WriteString(color.ContrastFg(mode, i)) + sb.WriteString(color.BgCode(mode, i)) + } + sb.WriteString(fmt.Sprintf("%4d", i)) + sb.WriteString(color.Reset) + } + sess.WriteLine(sb.String()) +} + +func writeCombinations(sess *net.Session, mode string) { + type combo struct { + fg int + bg int + desc string + } + combos := []combo{ + {15, 0, "white on black"}, + {208, 0, "orange on black"}, + {0, 178, "black on gold"}, + {15, 24, "white on dark blue"}, + {0, 15, "black on white"}, + {15, 88, "white on dark red"}, + {178, 52, "gold on brown"}, + {14, 0, "bright cyan on black"}, + {0, 228, "black on pale yellow"}, + {15, 22, "white on dark green"}, + {9, 0, "bright red on black"}, + {11, 17, "bright yellow on navy"}, + } + for _, c := range combos { + spec := color.ColorSpec{Fg: c.fg, Bg: c.bg} + sample := color.Render(mode, spec, fmt.Sprintf(" %3d on %3d ", c.fg, c.bg)) + sess.WriteLine(fmt.Sprintf(" %s %s", sample, c.desc)) + } +} + +func writeANSITable(sess *net.Session, mode string) { + names := [16]string{ + "black", "red", "green", "yellow", + "blue", "magenta", "cyan", "white", + "brt black", "brt red", "brt green", "brt yellow", + "brt blue", "brt magenta", "brt cyan", "brt white", + } + for row := 0; row < 2; row++ { + var sb strings.Builder + for col := 0; col < 8; col++ { + idx := row*8 + col + sb.WriteString(color.FgCode(mode, idx)) + sb.WriteString(fmt.Sprintf("%3d", idx)) + sb.WriteString(color.Reset) + sb.WriteString(fmt.Sprintf(" %-12s", names[idx])) + } + sess.WriteLine(sb.String()) + } +} diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go index be5fa3a..054a4b0 100644 --- a/internal/game/cmd_cook.go +++ b/internal/game/cmd_cook.go @@ -5,9 +5,9 @@ import ( "strconv" "strings" - "thirdcollapse/internal/action" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) type cookableEntry struct { diff --git a/internal/game/cmd_description.go b/internal/game/cmd_description.go index 56ce441..20ada37 100644 --- a/internal/game/cmd_description.go +++ b/internal/game/cmd_description.go @@ -3,8 +3,8 @@ package game import ( "fmt" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doDescription(sess *net.Session) { diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go index b08eadf..702d1db 100644 --- a/internal/game/cmd_drop.go +++ b/internal/game/cmd_drop.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doDropAll(sess *net.Session) { @@ -179,11 +179,11 @@ func (g *Game) doDrop(sess *net.Session, input string) { dropped := qty - remaining g.AccountStore.SaveCharacter(p) if dropped == 0 { - sess.WriteLine(fmt.Sprintf("You don't have any %s.", name)) + sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, def, name))) } else if dropped == 1 { - sess.WriteLine(fmt.Sprintf("You drop a %s.", name)) + sess.WriteLine(fmt.Sprintf("You drop a %s.", g.itemColorize(sess, def, name))) } else { - sess.WriteLine(fmt.Sprintf("You drop %d x %s.", dropped, name)) + sess.WriteLine(fmt.Sprintf("You drop %d x %s.", dropped, g.itemColorize(sess, def, name))) } } @@ -215,7 +215,7 @@ func (g *Game) handleDropAllConfirm(sess *net.Session, input string) { if def != nil { name = def.Name } - dropped = append(dropped, name) + dropped = append(dropped, g.itemColorize(sess, def, name)) p.SetInvSlot(i, nil) } diff --git a/internal/game/cmd_eat.go b/internal/game/cmd_eat.go index 5d6292b..9d4bfd0 100644 --- a/internal/game/cmd_eat.go +++ b/internal/game/cmd_eat.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) doEat(sess *net.Session, input string) { @@ -65,7 +65,7 @@ func (g *Game) doEat(sess *net.Session, input string) { } if !p.OptionBool("queue_silently") { - sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", def.Name)) + sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", g.itemColorize(sess, def, def.Name))) } } @@ -98,7 +98,7 @@ func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def p.ActionState = &ActionState{Type: ActionEating, TargetName: def.Name} - sess.WriteLine(def.EatMessage) + sess.WriteLine(g.colorize(sess, "eat_food", def.EatMessage)) if p.HP <= 0 { g.endCombat(sess, p, nil) diff --git a/internal/game/cmd_equipment.go b/internal/game/cmd_equipment.go index 4947d2b..7113880 100644 --- a/internal/game/cmd_equipment.go +++ b/internal/game/cmd_equipment.go @@ -3,8 +3,8 @@ package game import ( "fmt" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doEquipment(sess *net.Session) { diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go index e75c59e..8237ec9 100644 --- a/internal/game/cmd_get.go +++ b/internal/game/cmd_get.go @@ -3,8 +3,8 @@ package game import ( "fmt" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doGet(sess *net.Session, input string) { @@ -125,17 +125,9 @@ func (g *Game) doGet(sess *net.Session, input string) { if picked == 0 { sess.WriteLine("Your inventory is full.") } else if picked == 1 { - name := itemID - if def != nil { - name = def.Name - } - sess.WriteLine(fmt.Sprintf("You pick up a %s.", name)) + sess.WriteLine(fmt.Sprintf("You pick up a %s.", g.itemColorize(sess, def, name))) } else { - name := itemID - if def != nil { - name = def.Name - } - sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", picked, name)) + sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", picked, g.itemColorize(sess, def, name))) } } @@ -306,7 +298,7 @@ func (g *Game) doGetAll(sess *net.Session) { break } slot.Quantity += qty - picked = append(picked, fmt.Sprintf("%s (now %d)", def.Name, slot.Quantity)) + picked = append(picked, fmt.Sprintf("%s (now %d)", g.itemColorize(sess, def, def.Name), slot.Quantity)) stacked = true qty = 0 break @@ -335,7 +327,7 @@ func (g *Game) doGetAll(sess *net.Session) { if def != nil { name = def.Name } - picked = append(picked, name) + picked = append(picked, g.itemColorize(sess, def, name)) break } @@ -361,7 +353,7 @@ func (g *Game) doGetAll(sess *net.Session) { if def != nil { name = def.Name } - picked = append(picked, name) + picked = append(picked, g.itemColorize(sess, def, name)) qty -= take } } @@ -388,9 +380,9 @@ func (g *Game) pickupCredits(sess *net.Session, p *player.Player, itemID string) p.Credits += qty g.AccountStore.SaveCharacter(p) if qty == 1 { - sess.WriteLine("You pick up 1 credit.") + sess.WriteLine(g.colorize(sess, "credits_pickup", "You pick up 1 credit.")) } else { - sess.WriteLine(fmt.Sprintf("You pick up %d credits. (total: %d)", qty, p.Credits)) + sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You pick up %d credits. (total: %d)", qty, p.Credits))) } } diff --git a/internal/game/cmd_inventory.go b/internal/game/cmd_inventory.go index 7017b18..8684fcd 100644 --- a/internal/game/cmd_inventory.go +++ b/internal/game/cmd_inventory.go @@ -3,21 +3,19 @@ package game import ( "fmt" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doInventory(sess *net.Session) { p := sess.Player.(*player.Player) sess.WriteLine("") - sess.WriteLine("Inventory (28 slots):") + sess.WriteLine(fmt.Sprintf("Inventory (%d free):", p.FreeSlots())) - empty := 0 num := 0 for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot == nil { - empty++ continue } num++ @@ -33,5 +31,4 @@ func (g *Game) doInventory(sess *net.Session) { sess.WriteLine(fmt.Sprintf(" %2d) %s", num, coloredName)) } } - sess.WriteLine(fmt.Sprintf(" (%d empty slots)", empty)) } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index bd738be..8cadfd3 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -2,16 +2,16 @@ package game import ( "fmt" - "regexp" "sort" "strconv" "strings" - "thirdcollapse/internal/combat" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func (g *Game) doLook(sess *net.Session) { @@ -32,9 +32,11 @@ func (g *Game) doLook(sess *net.Session) { descWidth = 70 } rawLines := wrapText(room.Description, descWidth) + mode := g.colorMode(sess) + roomDescSpec := g.resolveColor(sess, "room_desc") descLines := make([]string, len(rawLines)) for i, l := range rawLines { - descLines[i] = g.colorize(sess, "room_desc", l) + descLines[i] = color.ExpandTagsWithDefault(mode, roomDescSpec, l) } wroteDesc := false if p.OptionString("tiny_map") != "off" { @@ -331,7 +333,7 @@ func (g *Game) doLook(sess *net.Session) { for _, other := range others { if other != sess && other.Player != nil { op := other.Player.(*player.Player) - line := fmt.Sprintf("\n%s is here", op.Name) + line := fmt.Sprintf("\n%s is here", g.colorize(sess, "player_name", op.Name)) if cs := combat.GetCombat(op.Name); cs != nil { if mob := g.MobStore.GetInstance(cs.MobID); mob != nil && mob.HP > 0 { name := mobDisplayName(mob, false) @@ -476,7 +478,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { } lines := []string{ "", - def.Name, + g.itemColorize(sess, def, def.Name), fmt.Sprintf(" %s", def.Description), fmt.Sprintf(" Value: %d credits", def.Value), } @@ -606,10 +608,8 @@ func mobInstanceIdx(mob *world.MobInstance, roomMobs []*world.MobInstance) int { return 0 } -var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) - func visibleLen(s string) int { - return len(ansiRe.ReplaceAllString(s, "")) + return color.VisibleLen(s) } func wrapText(text string, width int) []string { diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go index f4ec11c..1c78be3 100644 --- a/internal/game/cmd_map.go +++ b/internal/game/cmd_map.go @@ -1,8 +1,8 @@ package game import ( - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doMap(sess *net.Session) { diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index 11aa17e..a140003 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -3,9 +3,9 @@ package game import ( "fmt" - "thirdcollapse/internal/combat" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doMove(sess *net.Session, dir string) { diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go index 9302baa..3d4d420 100644 --- a/internal/game/cmd_option.go +++ b/internal/game/cmd_option.go @@ -5,22 +5,34 @@ import ( "strconv" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doOption(sess *net.Session, input string) { p := sess.Player.(*player.Player) + mode := g.colorMode(sess) if input == "" { sess.WriteLine("") table := &Table{ - Columns: []string{"Option", "Value", "Valid", "Description"}, + Columns: []string{ + color.Render(mode, color.Parse("75"), "Option"), + color.Render(mode, color.Parse("230"), "Value"), + color.Render(mode, color.Parse("243"), "Valid"), + color.Render(mode, color.Parse("252"), "Description"), + }, } for _, def := range player.OptionDefs { val := formatOptionValue(p, &def) valid := formatValidValues(&def) - table.Rows = append(table.Rows, []string{def.Name, val, valid, def.Description}) + table.Rows = append(table.Rows, []string{ + color.Render(mode, color.Parse("75"), def.Name), + color.Render(mode, color.Parse("230"), val), + color.Render(mode, color.Parse("243"), valid), + color.Render(mode, color.Parse("252"), def.Description), + }) } for _, line := range table.Render(p.OptionBool("unicode")) { sess.WriteLine(line) @@ -56,7 +68,20 @@ func (g *Game) doOption(sess *net.Session, input string) { p.Options = make(map[string]any) } p.Options[def.Name] = parsed - g.AccountStore.SaveCharacter(p) + + if sess.Account.Options == nil { + sess.Account.Options = make(map[string]any) + } + sess.Account.Options[def.Name] = parsed + + acc, err := g.AccountStore.LoadAccount(sess.Account.Name) + if err == nil { + if acc.Options == nil { + acc.Options = make(map[string]any) + } + acc.Options[def.Name] = parsed + g.AccountStore.SaveAccount(acc) + } sess.WriteLine(fmt.Sprintf("\n%s set to %s.", def.Name, formatOptionValue(p, def))) } diff --git a/internal/game/cmd_prompt.go b/internal/game/cmd_prompt.go new file mode 100644 index 0000000..81c3409 --- /dev/null +++ b/internal/game/cmd_prompt.go @@ -0,0 +1,33 @@ +package game + +import ( + "fmt" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) doPrompt(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + + if input == "" { + prompt := p.Prompt + if prompt == "" { + prompt = "> " + } + sess.WriteLine(fmt.Sprintf("\nPrompt: %s", prompt)) + sess.WriteLine("Use 'prompt <value>' to change. Use 'prompt reset' to restore default.") + return + } + + if input == "reset" { + p.Prompt = "> " + g.AccountStore.SaveCharacter(p) + sess.WriteLine("\nPrompt reset to default.") + return + } + + p.Prompt = input + g.AccountStore.SaveCharacter(p) + sess.WriteLine(fmt.Sprintf("\nPrompt set to: %s", input)) +} diff --git a/internal/game/cmd_queued.go b/internal/game/cmd_queued.go index 040ff31..d9e92d5 100644 --- a/internal/game/cmd_queued.go +++ b/internal/game/cmd_queued.go @@ -4,8 +4,8 @@ import ( "fmt" "sort" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doQueued(sess *net.Session) { diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go index 446305b..89b6e7f 100644 --- a/internal/game/cmd_quit.go +++ b/internal/game/cmd_quit.go @@ -1,10 +1,10 @@ package game import ( - "thirdcollapse/internal/combat" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doQuit(sess *net.Session) { diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go index f511ec4..61fdc29 100644 --- a/internal/game/cmd_remove.go +++ b/internal/game/cmd_remove.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) doRemove(sess *net.Session, input string) { @@ -68,8 +68,9 @@ func (g *Game) doRemove(sess *net.Session, input string) { g.AccountStore.SaveCharacter(p) name := foundItemID - if def, err := g.ItemStore.Load(foundItemID); err == nil { + def, _ := g.ItemStore.Load(foundItemID) + if def != nil { name = def.Name } - sess.WriteLine(fmt.Sprintf("You remove %s.", name)) + sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name))) } diff --git a/internal/game/cmd_say.go b/internal/game/cmd_say.go index 35ec5fa..67d8165 100644 --- a/internal/game/cmd_say.go +++ b/internal/game/cmd_say.go @@ -3,8 +3,8 @@ package game import ( "fmt" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doSay(sess *net.Session, msg string) { @@ -13,9 +13,9 @@ func (g *Game) doSay(sess *net.Session, msg string) { for _, other := range g.Hub.PlayersInRoom(roomID) { if other == sess { - other.WriteLine(fmt.Sprintf("You say: %s", msg)) + other.WriteLine(fmt.Sprintf("You say: %s", g.colorize(sess, "say", msg))) } else { - other.WriteLine(fmt.Sprintf("\n%s says: %s", p.Name, msg)) + other.WriteLine(fmt.Sprintf("\n%s says: %s", g.colorize(other, "player_name", p.Name), g.colorize(other, "say", msg))) } } } diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go index 2c62839..40d3007 100644 --- a/internal/game/cmd_score.go +++ b/internal/game/cmd_score.go @@ -4,29 +4,35 @@ import ( "fmt" "strconv" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doScore(sess *net.Session) { p := sess.Player.(*player.Player) + mode := g.colorMode(sess) sess.WriteLines( "", - fmt.Sprintf("Name: %s", p.Name), - fmt.Sprintf("Combat Level: %d", p.CombatLevel()), - fmt.Sprintf("HP: %d/%d", p.HP, p.MaxHP()), - fmt.Sprintf("Credits: %d", p.Credits), + fmt.Sprintf("Name: %s", g.colorize(sess, "player_name", p.Name)), + fmt.Sprintf("Combat Level: %s", color.Render(mode, color.Parse("230"), fmt.Sprint(p.CombatLevel()))), + fmt.Sprintf("HP: %s/%s", g.colorize(sess, "character_hp", fmt.Sprint(p.HP)), color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP()))), + fmt.Sprintf("Credits: %s", g.colorize(sess, "credits_pickup", fmt.Sprint(p.Credits))), ) - t := &Table{Title: "Skills", Columns: []string{"Skill", "Level", "XP"}} + t := &Table{Title: "Skills", Columns: []string{ + color.Render(mode, color.Parse("75"), "Skill"), + color.Render(mode, color.Parse("230"), "Level"), + color.Render(mode, color.Parse("222"), "XP"), + }} for _, s := range player.AllSkills { level := p.Level(s) xp := p.Skills[s] next := player.XPForNextLevel(xp) t.Rows = append(t.Rows, []string{ - string(s), - strconv.Itoa(level), - fmt.Sprintf("%d / %d XP", xp, xp+next), + color.Render(mode, color.Parse("75"), string(s)), + color.Render(mode, color.Parse("230"), strconv.Itoa(level)), + color.Render(mode, color.Parse("222"), fmt.Sprintf("%d / %d XP", xp, xp+next)), }) } for _, line := range t.Render(p.OptionBool("unicode")) { diff --git a/internal/game/cmd_search.go b/internal/game/cmd_search.go index d9a5b1e..d4d2e72 100644 --- a/internal/game/cmd_search.go +++ b/internal/game/cmd_search.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doSearch(sess *net.Session, input string) { diff --git a/internal/game/cmd_style.go b/internal/game/cmd_style.go index d86920c..901d02d 100644 --- a/internal/game/cmd_style.go +++ b/internal/game/cmd_style.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) doStyle(sess *net.Session, input string) { diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go index ef36e12..56aa5a1 100644 --- a/internal/game/cmd_use.go +++ b/internal/game/cmd_use.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) doUse(sess *net.Session, input string) { diff --git a/internal/game/cmd_walk.go b/internal/game/cmd_walk.go index 31f9e18..4ff6187 100644 --- a/internal/game/cmd_walk.go +++ b/internal/game/cmd_walk.go @@ -5,9 +5,9 @@ import ( "strconv" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func (g *Game) doWalk(sess *net.Session, args []string) { diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go index 7c3515f..cbc2ff3 100644 --- a/internal/game/cmd_wear.go +++ b/internal/game/cmd_wear.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) doWear(sess *net.Session, input string) { @@ -39,7 +39,7 @@ func (g *Game) doWear(sess *net.Session, input string) { def, err := g.ItemStore.Load(slot.ItemID) if err != nil || def.EquipSlot == "" { - sess.WriteLine(fmt.Sprintf("You can't wear %s.", match.Name)) + sess.WriteLine(fmt.Sprintf("You can't wear %s.", g.itemColorize(sess, def, match.Name))) return } @@ -76,7 +76,7 @@ func (g *Game) doWear(sess *net.Session, input string) { if def.WeaponType != "" { verb = "wield" } - sess.WriteLine(fmt.Sprintf("You %s %s.", verb, match.Name)) + sess.WriteLine(fmt.Sprintf("You %s %s.", verb, g.itemColorize(sess, def, match.Name))) } func (g *Game) doWearAll(sess *net.Session) { @@ -147,7 +147,7 @@ func (g *Game) doWearAll(sess *net.Session) { if def != nil { name = def.Name } - equipped = append(equipped, name) + equipped = append(equipped, g.itemColorize(sess, def, name)) } if len(equipped) == 0 { diff --git a/internal/game/color.go b/internal/game/color.go index f93f097..4fd8000 100644 --- a/internal/game/color.go +++ b/internal/game/color.go @@ -1,11 +1,11 @@ package game import ( - "thirdcollapse/internal/color" - "thirdcollapse/internal/config" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/config" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" ) func (g *Game) colorMode(sess *net.Session) string { @@ -19,7 +19,7 @@ func (g *Game) resolveColor(sess *net.Session, category string) color.ColorSpec if sess.Account != nil && sess.Account.Colors != nil { if val, ok := sess.Account.Colors[category]; ok { if val == "off" { - return color.ColorSpec{} + return color.NoColor() } if val != "" { return color.Parse(val) @@ -34,7 +34,7 @@ func (g *Game) resolveColor(sess *net.Session, category string) color.ColorSpec if val, ok := config.DefaultColors()[category]; ok && val != "" { return color.Parse(val) } - return color.ColorSpec{} + return color.NoColor() } func (g *Game) colorize(sess *net.Session, category, text string) string { @@ -46,15 +46,15 @@ func levelColorSpec(myLevel, theirLevel int) color.ColorSpec { diff := theirLevel - myLevel switch { case diff == 0: - return color.Parse("fg=white") + return color.Parse("7") case diff > 0 && diff < 5: - return color.Parse("fg=bright_yellow") + return color.Parse("11") case diff >= 5: - return color.Parse("fg=bright_red") + return color.Parse("9") case diff < 0 && diff > -5: - return color.Parse("fg=yellow") + return color.Parse("3") default: - return color.Parse("fg=green") + return color.Parse("2") } } diff --git a/internal/game/game.go b/internal/game/game.go index 33dd0d1..9d07ead 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -7,14 +7,14 @@ import ( "sync" "time" - "thirdcollapse/internal/action" - "thirdcollapse/internal/combat" - "thirdcollapse/internal/config" - "thirdcollapse/internal/engine" - "thirdcollapse/internal/net" - "thirdcollapse/internal/object" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/config" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) type CommandClass int @@ -124,6 +124,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) { g.handleDropAllConfirm(sess, input) case net.StateCookRecipe: g.handleCookRecipe(sess, input) + case net.StateColorChoice: + g.handleColorChoice(sess, input) } } @@ -132,9 +134,10 @@ func classifyCommand(cmd string) CommandClass { case "say", "score", "sc", "inventory", "i", "inv", "equipment", "eq", "look", "l", "exits", "help", "map", "option", "options", "alias", "unalias", - "description", "desc", "queued", "color", "colors": + "description", "desc", "queued", "color", "colors", + "colortable", "prompt", "style": return ClassInstant - case "wear", "wield", "remove", "unwear", "unwield", "style": + case "wear", "wield", "remove", "unwear", "unwield": return ClassFree case "get", "take", "grab", "pick", "drop", "attack", "kill", @@ -310,6 +313,19 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI g.doOption(sess, strings.Join(args, " ")) case "color", "colors": g.doColor(sess, strings.Join(args, " ")) + case "colortable": + g.doColortable(sess) + case "prompt": + if len(args) == 0 { + g.doPrompt(sess, "") + } else { + msgStart := strings.Index(strings.ToLower(rawInput), "prompt ") + 7 + if msgStart >= 7 && msgStart < len(rawInput) { + g.doPrompt(sess, rawInput[msgStart:]) + } else { + g.doPrompt(sess, strings.Join(args, " ")) + } + } case "exits": g.doExits(sess) case "map": diff --git a/internal/game/help.go b/internal/game/help.go index d3f34c3..f9d822c 100644 --- a/internal/game/help.go +++ b/internal/game/help.go @@ -6,8 +6,8 @@ import ( "path/filepath" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" "gopkg.in/yaml.v3" ) @@ -29,6 +29,7 @@ var commandList = []cmdEntry{ {"burn", "Active", "Start a fire"}, {"chop / cut", "Active", "Chop trees (Woodcutting)"}, {"color / colors", "Instant", "Customize display colors"}, + {"colortable", "Instant", "Display color reference chart"}, {"cook", "Active", "Cook raw food on a fire or range"}, {"description / desc", "Instant", "Set your character description"}, {"drop", "Active", "Drop items to the ground"}, @@ -44,6 +45,7 @@ var commandList = []cmdEntry{ {"mine", "Active", "Mine rocks (Mining)"}, {"north / south / east / west / up / down", "Active", "Move in a direction"}, {"option / options", "Instant", "View or change settings"}, + {"prompt", "Instant", "Set custom command prompt"}, {"pull / push", "Active", "Toggle levers and switches"}, {"queued", "Instant", "Show pending tick actions"}, {"quit", "Active", "Rest and disconnect"}, @@ -52,7 +54,7 @@ var commandList = []cmdEntry{ {"score / sc", "Instant", "View your stats and skills"}, {"search", "Active", "Search items for loot"}, {"stoke", "Active", "Add logs to a fire"}, - {"style", "Free", "Change combat style"}, + {"style", "Instant", "Change combat style"}, {"talk / speak / ask", "Active", "Talk to NPCs"}, {"unalias", "Instant", "Remove command shortcuts"}, {"use", "Active", "Use an object (crafting)"}, diff --git a/internal/game/login_account.go b/internal/game/login_account.go index 3ad1fee..176a1ee 100644 --- a/internal/game/login_account.go +++ b/internal/game/login_account.go @@ -5,8 +5,8 @@ import ( "os" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) handleAccountName(sess *net.Session, input string) { @@ -69,6 +69,7 @@ func (g *Game) handlePassword(sess *net.Session, input string) { Characters: acc.Characters, Aliases: acc.Aliases, Colors: acc.Colors, + Options: acc.Options, } if sess.Account.Aliases == nil { sess.Account.Aliases = make(map[string]string) @@ -76,6 +77,9 @@ func (g *Game) handlePassword(sess *net.Session, input string) { if sess.Account.Colors == nil { sess.Account.Colors = make(map[string]string) } + if sess.Account.Options == nil { + sess.Account.Options = make(map[string]any) + } g.showMenu(sess) } @@ -144,8 +148,27 @@ func (g *Game) handleNewAccountConfirm(sess *net.Session, input string) { Name: acc.Name, PasswordHash: acc.PasswordHash, Aliases: make(map[string]string), + Colors: make(map[string]string), + Options: make(map[string]any), } sess.PendingPass = "" + sess.State = net.StateColorChoice + sess.Write("\nShould I disable color? [y/N] ") +} + +func (g *Game) handleColorChoice(sess *net.Session, input string) { + input = strings.ToLower(strings.TrimSpace(input)) + if input == "y" || input == "yes" { + sess.Account.Options["color"] = "none" + acc, err := g.AccountStore.LoadAccount(sess.Account.Name) + if err == nil { + if acc.Options == nil { + acc.Options = make(map[string]any) + } + acc.Options["color"] = "none" + g.AccountStore.SaveAccount(acc) + } + } g.showMenu(sess) } @@ -158,7 +181,7 @@ func (g *Game) showMenu(sess *net.Session) { } if len(sess.Account.Characters) > 0 { lines = append(lines, - " (C)onnect character to TC", + " (C)onnect character to THOI", "", " (L)ist characters", " (R)ename character", @@ -171,9 +194,9 @@ func (g *Game) showMenu(sess *net.Session) { " (A)ccount rename", " (Q)uit", "", - "> ", ) sess.WriteLines(lines...) + sess.Write("> ") } func (g *Game) handleMenu(sess *net.Session, input string) { diff --git a/internal/game/login_char.go b/internal/game/login_char.go index ba034bb..82065d3 100644 --- a/internal/game/login_char.go +++ b/internal/game/login_char.go @@ -5,8 +5,8 @@ import ( "os" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) handleNewCharName(sess *net.Session, input string) { @@ -82,6 +82,13 @@ func (g *Game) connectCharacter(sess *net.Session, name string) { g.loggedInChars[name] = sess g.charsMu.Unlock() + if sess.Account != nil && sess.Account.Options != nil { + p.Options = make(map[string]any) + for k, v := range sess.Account.Options { + p.Options[k] = v + } + } + g.World.SeedGroundItems(p.RoomID) g.seedRoomMobs(p.RoomID) g.seedRoomObjects(p.RoomID) diff --git a/internal/game/map.go b/internal/game/map.go index 8b14e25..32d3d7c 100644 --- a/internal/game/map.go +++ b/internal/game/map.go @@ -3,7 +3,7 @@ package game import ( "strings" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/world" ) type mapGlyphs struct { diff --git a/internal/game/map_test.go b/internal/game/map_test.go index 8c82f4a..a695c1b 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/world" ) func TestBuildTinyMap(t *testing.T) { diff --git a/internal/game/prompt.go b/internal/game/prompt.go index 2fc36ac..de88bc6 100644 --- a/internal/game/prompt.go +++ b/internal/game/prompt.go @@ -4,17 +4,17 @@ import ( "fmt" "strings" - "thirdcollapse/internal/color" - "thirdcollapse/internal/combat" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" ) func (g *Game) promptStr(sess *net.Session) string { prompt := "> " if p, ok := sess.Player.(*player.Player); ok && p != nil { - if custom := p.OptionString("prompt"); custom != "" { - prompt = custom + if p.Prompt != "" { + prompt = p.Prompt } } @@ -24,42 +24,7 @@ func (g *Game) promptStr(sess *net.Session) string { } func (g *Game) expandPromptColors(sess *net.Session, text string) string { - mode := g.colorMode(sess) - for { - start := strings.Index(text, "{") - if start < 0 { - break - } - - end := strings.Index(text[start+1:], "}") - if end < 0 { - break - } - end += start + 1 - specStr := text[start+1 : end] - - if specStr == "" || specStr == "/" { - break - } - - spec := color.Parse(specStr) - if spec.Empty() { - break - } - - closeStart := strings.Index(text[end+1:], "{/}") - if closeStart < 0 { - break - } - closeStart += end + 1 - closeEnd := closeStart + 3 - - inner := text[end+1 : closeStart] - colored := color.Render(mode, spec, inner) - - text = text[:start] + colored + text[closeEnd:] - } - return text + return color.ExpandTags(g.colorMode(sess), text) } func (g *Game) expandPromptVars(sess *net.Session, text string) string { diff --git a/internal/game/table.go b/internal/game/table.go index 56c0079..b96dca1 100644 --- a/internal/game/table.go +++ b/internal/game/table.go @@ -1,8 +1,9 @@ package game import ( - "fmt" "strings" + + "thehouseoficarus/internal/color" ) type tableGlyphs struct { @@ -50,15 +51,16 @@ func (t *Table) Render(unicode bool) []string { colWidths := make([]int, nCols) for i, h := range t.Columns { - colWidths[i] = len(h) + colWidths[i] = color.VisibleLen(h) } for _, row := range t.Rows { for i, cell := range row { if i >= nCols { break } - if len(cell) > colWidths[i] { - colWidths[i] = len(cell) + vl := color.VisibleLen(cell) + if vl > colWidths[i] { + colWidths[i] = vl } } } @@ -87,7 +89,11 @@ func (t *Table) Render(unicode bool) []string { if i < len(cells) { cell = cells[i] } - b.WriteString(fmt.Sprintf(" %-*s ", colWidths[i], cell)) + pad := colWidths[i] - color.VisibleLen(cell) + if pad < 0 { + pad = 0 + } + b.WriteString(" " + cell + strings.Repeat(" ", pad) + " ") } b.WriteRune(g.side) return b.String() @@ -115,7 +121,7 @@ func (t *Table) Render(unicode bool) []string { tr.WriteRune(g.side) tr.WriteString(" ") tr.WriteString(t.Title) - padding := innerWidth - 2 - len(t.Title) + padding := innerWidth - 2 - color.VisibleLen(t.Title) if padding < 0 { padding = 0 } diff --git a/internal/game/tick.go b/internal/game/tick.go index 7567ca7..ab82254 100644 --- a/internal/game/tick.go +++ b/internal/game/tick.go @@ -4,9 +4,9 @@ import ( "fmt" "math/rand" - "thirdcollapse/internal/combat" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func (g *Game) DisconnectTick() { @@ -127,21 +127,21 @@ func (g *Game) WanderTick() { if !ok { continue } - if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") { - if m.level > 0 { - sess.WriteLine(fmt.Sprintf("\n%s (level %d) leaves.", m.name, m.level)) - } else { - sess.WriteLine(fmt.Sprintf("\n%s moves away.", m.name)) - } + if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") { + if m.level > 0 { + sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s (level %d) leaves.", m.name, m.level))) + } else { + sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s moves away.", m.name))) } - if p.RoomID == m.toRoom && p.OptionBool("mob_enter") { - if m.level > 0 { - sess.WriteLine(fmt.Sprintf("\n%s (level %d) enters.", m.name, m.level)) - } else { - sess.WriteLine(fmt.Sprintf("\n%s drifts in.", m.name)) - } + } + if p.RoomID == m.toRoom && p.OptionBool("mob_enter") { + if m.level > 0 { + sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s (level %d) enters.", m.name, m.level))) + } else { + sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s drifts in.", m.name))) } } + } } } diff --git a/internal/game/types.go b/internal/game/types.go index faed9e1..d41b18a 100644 --- a/internal/game/types.go +++ b/internal/game/types.go @@ -1,6 +1,6 @@ package game -import "thirdcollapse/internal/object" +import "thehouseoficarus/internal/object" var EquipSlots = []object.EquipSlot{ object.SlotHead, object.SlotNeck, object.SlotTorso, object.SlotLegs, diff --git a/internal/game/utils.go b/internal/game/utils.go index 729ea97..2379fc2 100644 --- a/internal/game/utils.go +++ b/internal/game/utils.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func plural(n int) string { diff --git a/internal/net/server.go b/internal/net/server.go index 1b6c99b..203539c 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -6,8 +6,10 @@ import ( "log" "net" "net/http" + "strings" - "thirdcollapse/internal/config" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/config" ) type SessionState int @@ -30,6 +32,7 @@ const ( StateTalk StateDropAllConfirm StateCookRecipe + StateColorChoice ) type Session struct { @@ -51,11 +54,13 @@ type AccountEntry struct { Characters []string Aliases map[string]string Colors map[string]string + Options map[string]any } type Server struct { config *config.Config telnetLn net.Listener + telnetTLSLn net.Listener httpServer *http.Server httpsServer *http.Server hub *Hub @@ -149,6 +154,26 @@ func NewServer(cfg *config.Config) (*Server, error) { s.telnetLn = ln } + if cfg.TelnetTLS.Enabled { + if cfg.TelnetTLS.CertFile == "" || cfg.TelnetTLS.KeyFile == "" { + return nil, fmt.Errorf("telnet_tls enabled but cert_file and key_file are required") + } + cert, err := tls.LoadX509KeyPair(cfg.TelnetTLS.CertFile, cfg.TelnetTLS.KeyFile) + if err != nil { + return nil, fmt.Errorf("telnet_tls cert: %w", err) + } + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + addr := fmt.Sprintf(":%d", cfg.TelnetTLS.Port) + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, fmt.Errorf("telnet_tls listen: %w", err) + } + s.telnetTLSLn = tls.NewListener(ln, tlsCfg) + } + if cfg.HTTP.Enabled { s.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", cfg.HTTP.Port), @@ -181,6 +206,10 @@ func (s *Server) ListenAndServe(handler func(*Session, string)) error { go s.serveTelnet() } + if s.telnetTLSLn != nil { + go s.serveTelnetTLS() + } + if s.httpServer != nil { go s.serveHTTP() } @@ -189,7 +218,7 @@ func (s *Server) ListenAndServe(handler func(*Session, string)) error { go s.serveHTTPS() } - if s.telnetLn == nil && s.httpServer == nil && s.httpsServer == nil { + if s.telnetLn == nil && s.telnetTLSLn == nil && s.httpServer == nil && s.httpsServer == nil { return fmt.Errorf("no listeners configured") } @@ -211,6 +240,21 @@ func (s *Server) serveTelnet() { } } +func (s *Server) serveTelnetTLS() { + for { + conn, err := s.telnetTLSLn.Accept() + if err != nil { + return + } + sess := &Session{ + Conn: newTCPConn(conn), + State: StateAccountName, + } + s.hub.Add(sess) + go s.handleSession(sess, s.handler) + } +} + func (s *Server) serveHTTP() { err := s.httpServer.ListenAndServe() if err != nil && err != http.ErrServerClosed { @@ -225,6 +269,53 @@ func (s *Server) serveHTTPS() { } } +func artColor(r rune) string { + switch r { + case '@': + return color.FgCode("ansi", 4) + case '!', ':', '.': + return color.StyleCode("dim") + color.FgCode("ansi", 4) + case ',': + return color.FgCode("ansi", 5) + case '*': + return color.FgCode("ansi", 5) + case '+', '|', '-': + return color.FgCode("ansi", 3) + } + return "" +} + +func colorizeRunes(s string) string { + var sb strings.Builder + current := "" + for _, r := range s { + want := artColor(r) + if want != current { + if current != "" { + sb.WriteString(color.Reset) + } + if want != "" { + sb.WriteString(want) + } + current = want + } + sb.WriteRune(r) + } + if current != "" { + sb.WriteString(color.Reset) + } + return sb.String() +} + +func colorizeArt(art string) string { + welcomeText := "welcome to the house of icarus" + before, after, found := strings.Cut(art, welcomeText) + if found { + return colorizeRunes(before) + color.FgCode("ansi", 7) + welcomeText + color.Reset + colorizeRunes(after) + } + return colorizeRunes(art) +} + func (s *Server) handleSession(sess *Session, handler func(*Session, string)) { defer func() { if r := recover(); r != nil { @@ -236,22 +327,32 @@ func (s *Server) handleSession(sess *Session, handler func(*Session, string)) { sess.Conn.Write([]byte("\033[2J\033[H")) - welcomeart := ` - , 3333333 333 333 333 3333333 3333333 - | 33! 33! 333 33! 33! 333 33! 333 ':. + - -+- * 3!! 3!3!3!3! !!3 3!3!!3! 3!3 !3! '::._ * - | !!: !!: !!! !!: !!: :!! !!: !!! '._) - : : : : : : : : :: : : , - . * , - , welcome to third collapse . * - - 3333333 333333 333 , 333 333333 3333333 333333 33333333 - !33 33! 333 33! 33! 33! 333 33! 333 !33 33! - !3! 3!3 !3! 3!! 3!! + 3!3!3!3! 3!33!3! !33!! 3!!!:! - :!! !!: !!! !!: !!: !!: !!! !!: !:! !!: - :: :: : : :. : : ::.: : : ::.: : : : : : ::.: : : :: ::: - -` + welcomeart := colorizeArt(` + | * + * + -+- + . , | +. , . , + * + @@@@@@@ @@@ @@@ @@@@@@@@ @@@ @@@ @@@@@@ @@@ @@@ @@@@@@ @@@@@@@@ + + @@! @@! @@@ @@! + @@! @@@ @@! @@@ @@! @@@ !@@ @@! + @!! @!@!@!@! @!!!:! @!@!@!@! @!@ !@! @!@ !@! !@@!! @!!!:! * + !!: !!: !!! !!: . !!: !!! !!: !!! !!: !!! !:! !!: + : : : : : :: ::: : : : : :. : :.:: : ::.: : : :: ::: . + . + + * welcome to the house of icarus , , + , . + @@@@@@ @@@@@@@@ @@@ @@@@@@@ @@@@@@ @@@@@@@ @@@ @@@ @@@@@@ + @@! @@@ @@! , @@! !@@ @@! @@@ @@! @@@ @@! @@@ !@@ + | @!@ !@! @!!!:! !!@ !@! @!@!@!@! @!@!!@! @!@ !@! !@@!! + -+- !!: !!! !!: !!: :!! !!: !!! !!: :!! !!: !!! !:! + | : :. : : . : :: :: : : : : : : : :.:: : ::.: : + , + . + , . + . , + * . , * + + +`) + sess.Write(welcomeart) sess.Write("What's your account name? ") diff --git a/internal/net/terminal.html b/internal/net/terminal.html index 2d98178..3e2506a 100644 --- a/internal/net/terminal.html +++ b/internal/net/terminal.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> -<title>Third Collapse</title> +<title>The House of Icarus</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { diff --git a/internal/object/item.go b/internal/object/item.go index 7b2a107..6078712 100644 --- a/internal/object/item.go +++ b/internal/object/item.go @@ -3,7 +3,7 @@ package object import ( "strings" - "thirdcollapse/internal/world" + "thehouseoficarus/internal/world" ) type EquipSlot string diff --git a/internal/player/account.go b/internal/player/account.go index 36fab43..99a052c 100644 --- a/internal/player/account.go +++ b/internal/player/account.go @@ -6,4 +6,5 @@ type Account struct { Characters []string `yaml:"characters"` Aliases map[string]string `yaml:"aliases"` Colors map[string]string `yaml:"colors"` + Options map[string]any `yaml:"options"` } diff --git a/internal/player/player.go b/internal/player/player.go index e0c7ab9..1cd8677 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -1,7 +1,7 @@ package player -import "thirdcollapse/internal/action" -import "thirdcollapse/internal/object" +import "thehouseoficarus/internal/action" +import "thehouseoficarus/internal/object" type SkillName string @@ -115,7 +115,7 @@ var OptionDefs = []OptionDef{ {"reserve", OptBool, true, nil, "Show full reserved item details"}, {"depletion", OptBool, false, nil, "Show depletion and despawn timers"}, {"despawn", OptBool, false, nil, "Show ground item despawn timers"}, - {"color", OptString, "ansi", []string{"none", "ansi", "xterm256"}, "Color output mode"}, + {"color", OptString, "xterm256", []string{"none", "ansi", "xterm256"}, "Color output mode"}, {"mapwidth", OptInt, 30, nil, "Map width for the map command"}, {"mapheight", OptInt, 20, nil, "Map height for the map command"}, {"mappadding", OptString, "none", []string{"none", "x", "y", "xy"}, "Map output padding mode"}, @@ -123,7 +123,6 @@ var OptionDefs = []OptionDef{ {"queue_silently", OptBool, true, nil, "Suppress messages for queued tick actions"}, {"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"}, {"unicode", OptBool, true, nil, "Unicode box-drawing characters"}, - {"prompt", OptString, "> ", nil, "Custom command prompt"}, } var optionByName map[string]*OptionDef @@ -150,7 +149,8 @@ type Player struct { Credits int `yaml:"credits"` Description string `yaml:"description"` AttackStyle AttackStyle `yaml:"attack_style"` - Options map[string]any `yaml:"options"` + Prompt string `yaml:"prompt"` + Options map[string]any `yaml:"-"` Flags map[string]any `yaml:"flags"` RegenerateTick int Action *action.Action `yaml:"-"` @@ -258,12 +258,9 @@ func New(name string) *Player { Skills: make(map[SkillName]int), Equipment: make(map[object.EquipSlot]string), AttackStyle: Accurate, - Options: make(map[string]any), + Prompt: "> ", RoomID: 0, } - for _, def := range OptionDefs { - p.Options[def.Name] = def.Default - } for _, s := range AllSkills { p.Skills[s] = 0 } diff --git a/internal/player/store.go b/internal/player/store.go index c19d9fb..04af594 100644 --- a/internal/player/store.go +++ b/internal/player/store.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "thirdcollapse/internal/object" + "thehouseoficarus/internal/object" "gopkg.in/yaml.v3" ) diff --git a/internal/world/mob.go b/internal/world/mob.go index c161e10..d801b59 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -8,7 +8,7 @@ import ( "strings" "sync" - "thirdcollapse/internal/action" + "thehouseoficarus/internal/action" "gopkg.in/yaml.v3" ) diff --git a/internal/world/room.go b/internal/world/room.go index 16fd3fd..c0f531e 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -2,7 +2,7 @@ package world import ( "gopkg.in/yaml.v3" - "thirdcollapse/internal/action" + "thehouseoficarus/internal/action" ) type ExitDir string |
