From 2b5d8f595f96a2864899f4c7e28bc06fa52d2603 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 18 Jul 2026 04:44:50 -0400 Subject: feat: expose all colors in config.yaml for easier theming --- internal/game/cmd_aps.go | 15 ++++--- internal/game/cmd_color.go | 63 +++++++++++++++++++++++------ internal/game/cmd_mods.go | 16 ++++---- internal/game/cmd_option.go | 20 ++++++---- internal/game/cmd_score.go | 85 ++++++++++++++-------------------------- internal/game/cmd_skills.go | 24 +++++++----- internal/game/cmd_tech.go | 12 +++--- internal/game/game.go | 30 +++++++------- internal/game/look_entities.go | 11 ++++-- internal/game/look_target.go | 2 +- internal/game/production_menu.go | 2 +- internal/game/render_color.go | 43 ++++++++++++++++---- internal/game/render_combat.go | 44 ++++++++++++++++----- internal/game/sys_labor.go | 2 +- 14 files changed, 225 insertions(+), 144 deletions(-) (limited to 'internal/game') diff --git a/internal/game/cmd_aps.go b/internal/game/cmd_aps.go index 727bf8b..56820ad 100644 --- a/internal/game/cmd_aps.go +++ b/internal/game/cmd_aps.go @@ -125,15 +125,18 @@ func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int) }) mode := g.colorMode(sess) + hLabel := g.resolveConstant(sess, "table_header_label") + hDetail := g.resolveConstant(sess, "table_header_detail") + hValue := g.resolveConstant(sess, "table_header_value") rows := make([][]string, len(infos)) for i, info := range infos { distStr := strconv.Itoa(info.Distance) if info.Distance == 0 { - distStr = color.Render(mode, color.Parse("6C"), "here") + distStr = color.Render(mode, g.resolveConstant(sess, "aps_here"), "here") } rows[i] = []string{ - color.Render(mode, color.Parse("F5"), fmt.Sprintf("#%d", info.ID)), - color.Render(mode, color.Parse("49"), info.Name), + color.Render(mode, hDetail, fmt.Sprintf("#%d", info.ID)), + color.Render(mode, hLabel, info.Name), distStr, } } @@ -141,9 +144,9 @@ func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int) t := &ui.Table{ Title: "APS Datapad", Columns: []string{ - color.Render(mode, color.Parse("F5"), "Room"), - color.Render(mode, color.Parse("49"), "Name"), - color.Render(mode, color.Parse("FA"), "Distance"), + color.Render(mode, hDetail, "Room"), + color.Render(mode, hLabel, "Name"), + color.Render(mode, hValue, "Distance"), }, Rows: rows, } diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index 01e2b9a..01912a7 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -134,39 +134,78 @@ func (g *Game) executeColor(sess *net.Session, args []string, rawInput string) { } var colorCategoryOrder = []string{ + // Room / look "room_name", "room_number", "room_desc", - "direction", "exit_direction", "exit_name", - "protected_mob", + "dim", "mob", + "protected_mob", + "player_name", + "item", + + // Movement + "direction", + + // Combat "damage_dealt", "damage_taken", - - "xp", - "level_up", - "item", - "player_name", + "miss", "death", "victory", - "miss", + "warning", "error", + "science_mod", + + // XP / level + "xp", + "level_up", + + // Pickups / economy + "currency_pickup", + + // Chat / dialog "say", "global", + "broadcast", "dialog", "dialog_options", - "broadcast", - "fire", - "eat_food", + + // Drops / consume / fire "drop_message", - "currency_pickup", + "eat_food", + "fire", + + // Tick / status + "visual_tick", + "visual_first_tick", + // Tech / farm + "battery", + "tech_depleted", + "farm_grow", + "farm_disease", + + // Assassin / map / safespot + "assassin_task", "map_at", "map_blocked", "map_agility", "safespot_alert", + + // System + "sequence", +} + +// PlayerColorCategoryOrder is the canonical ordered list of player-overridable +// color categories, shared with the admin GUI so its left-side editor list +// mirrors the in-game `colors` command output exactly. +func PlayerColorCategoryOrder() []string { + out := make([]string, len(colorCategoryOrder)) + copy(out, colorCategoryOrder) + return out } func showColorTable(g *Game, sess *net.Session) { diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go index 26b0e95..0126b5f 100644 --- a/internal/game/cmd_mods.go +++ b/internal/game/cmd_mods.go @@ -55,10 +55,10 @@ func (g *Game) doMods(sess *net.Session, showAll bool) { t := &ui.Table{ Title: cat.Name, Columns: []string{ - color.Render(mode, color.Parse("49"), "Module"), - color.Render(mode, color.Parse("F5"), "Level"), - color.Render(mode, color.Parse("B3"), "Cost"), - color.Render(mode, color.Parse("B3"), "XP"), + color.Render(mode, g.resolveConstant(sess, "table_header_label"), "Module"), + color.Render(mode, g.resolveConstant(sess, "table_header_detail"), "Level"), + color.Render(mode, g.resolveConstant(sess, "table_header_xp"), "Cost"), + color.Render(mode, g.resolveConstant(sess, "table_header_xp"), "XP"), }, } @@ -77,10 +77,10 @@ func (g *Game) doMods(sess *net.Session, showAll bool) { }) } else if unlocked { t.Rows = append(t.Rows, []string{ - color.Render(mode, color.Parse("49"), m.Name), - color.Render(mode, color.Parse("F5"), levelStr), - color.Render(mode, color.Parse("B3"), costStr), - color.Render(mode, color.Parse("B3"), xpStr), + color.Render(mode, g.resolveConstant(sess, "table_header_label"), m.Name), + color.Render(mode, g.resolveConstant(sess, "table_header_detail"), levelStr), + color.Render(mode, g.resolveConstant(sess, "table_header_xp"), costStr), + color.Render(mode, g.resolveConstant(sess, "table_header_xp"), xpStr), }) } else { t.Rows = append(t.Rows, []string{ diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go index 2d6f3f0..55a04de 100644 --- a/internal/game/cmd_option.go +++ b/internal/game/cmd_option.go @@ -16,12 +16,16 @@ func (g *Game) doOption(sess *net.Session, input string) { mode := g.colorMode(sess) if input == "" { + hLabel := g.resolveConstant(sess, "table_header_label") + hValue := g.resolveConstant(sess, "table_header_value") + hDetail := g.resolveConstant(sess, "table_header_detail") + hDesc := g.resolveConstant(sess, "table_header_desc") table := &ui.Table{ Columns: []string{ - color.Render(mode, color.Parse("49"), "Option"), - color.Render(mode, color.Parse("FA"), "Value"), - color.Render(mode, color.Parse("F5"), "Valid"), - color.Render(mode, color.Parse("FF"), "Description"), + color.Render(mode, hLabel, "Option"), + color.Render(mode, hValue, "Value"), + color.Render(mode, hDetail, "Valid"), + color.Render(mode, hDesc, "Description"), }, } for _, def := range player.OptionDefs { @@ -29,10 +33,10 @@ func (g *Game) doOption(sess *net.Session, input string) { val = truncateForTable(val, p.OptionBool("unicode")) valid := formatValidValues(&def) table.Rows = append(table.Rows, []string{ - color.Render(mode, color.Parse("49"), def.Name), - color.Render(mode, color.Parse("FA"), val), - color.Render(mode, color.Parse("F5"), valid), - color.Render(mode, color.Parse("FF"), def.Description), + color.Render(mode, hLabel, def.Name), + color.Render(mode, hValue, val), + color.Render(mode, hDetail, valid), + color.Render(mode, hDesc, def.Description), }) } for _, line := range table.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go index 31caf1e..3949486 100644 --- a/internal/game/cmd_score.go +++ b/internal/game/cmd_score.go @@ -92,43 +92,46 @@ func (g *Game) doScore(sess *net.Session) { sep() nameStr := g.colorize(sess, "player_name", p.Name) - clStr := color.Render(mode, color.Parse("FA"), fmt.Sprintf("Combat Lvl %d", p.CombatLevel())) + clStr := g.colorizeConstant(sess, "score_max_value", fmt.Sprintf("Combat Lvl %d", p.CombatLevel())) roomStr := g.colorize(sess, "room_number", fmt.Sprintf("Room #%d", p.RoomID)) row1 := padded(nameStr, 20) + padded(clStr, 24) + roomStr content(row1) creditsStr := g.colorize(sess, "currency_pickup", formatInt(p.Credits)) chipsStr := g.colorize(sess, "currency_pickup", formatInt(countChips(p))) - styleStr := color.Render(mode, color.Parse("49"), fmt.Sprintf("Style: %s", p.AttackStyle)) + styleStr := color.Render(mode, g.resolveConstant(sess, "table_header_label"), fmt.Sprintf("Style: %s", p.AttackStyle)) invStr := fmt.Sprintf("Inv: %d/28", 28-p.FreeSlots()) row2 := padded("Credits: "+creditsStr, 19) + padded("Chips: "+chipsStr, 16) + padded(styleStr, 22) + invStr content(row2) gap() - hpStyle := hpBarStyle(p.HP, p.MaxHP()) + hpStyle := g.hpBarStyle(sess, p.HP, p.MaxHP()) hpBarStr := ui.RenderColoredBar(p.HP, p.MaxHP(), 26, unicode, hpStyle, mode) - hpFg := hpBarFg(p.HP, p.MaxHP()) + hpFg := g.hpBarStyle(sess, p.HP, p.MaxHP()).FilledColor hpLine := fmt.Sprintf("HP [%s] %s / %s", hpBarStr, color.Render(mode, color.ColorSpec{Fg: hpFg}, fmt.Sprint(p.HP)), - color.Render(mode, color.Parse("FA"), fmt.Sprint(p.MaxHP()))) + g.colorizeConstant(sess, "score_max_value", fmt.Sprint(p.MaxHP()))) content(hpLine) batBarRaw := ui.RenderBarF(p.Battery, p.MaxBattery(), 26, unicode) - batBarStr := color.Render(mode, color.ColorSpec{Fg: 110}, batBarRaw) + batBarStr := color.Render(mode, g.resolveConstant(sess, "battery_bar"), batBarRaw) + batFg := g.resolveConstant(sess, "battery_bar").Fg batLine := fmt.Sprintf("BAT [%s] %s / %s", batBarStr, - color.Render(mode, color.ColorSpec{Fg: 110}, fmt.Sprintf("%.1f", p.Battery)), - color.Render(mode, color.Parse("FA"), fmt.Sprintf("%.0f", p.MaxBattery()))) + color.Render(mode, color.ColorSpec{Fg: batFg}, fmt.Sprintf("%.1f", p.Battery)), + g.colorizeConstant(sess, "score_max_value", fmt.Sprintf("%.0f", p.MaxBattery()))) content(batLine) - engStyle := &ui.BarStyle{FilledColor: 108, EmptyColor: 237, EmptyDim: true} + engSpec := g.resolveConstant(sess, "run_energy_bar") + engEmpty := g.resolveConstant(sess, "hp_bar_empty") + engStyle := &ui.BarStyle{FilledColor: engSpec.Fg, EmptyColor: engEmpty.Fg, EmptyDim: engEmpty.Dim} engBarStr := ui.RenderColoredBar(p.RunEnergy, p.MaxRunEnergy(), 26, unicode, engStyle, mode) engLine := fmt.Sprintf("ENG [%s] %s / %s", engBarStr, - color.Render(mode, color.ColorSpec{Fg: 108}, fmt.Sprint(p.RunEnergy)), - color.Render(mode, color.Parse("FA"), fmt.Sprint(p.MaxRunEnergy()))) + color.Render(mode, color.ColorSpec{Fg: engSpec.Fg}, fmt.Sprint(p.RunEnergy)), + g.colorizeConstant(sess, "score_max_value", fmt.Sprint(p.MaxRunEnergy()))) content(engLine) gap() @@ -155,8 +158,8 @@ func (g *Game) doScore(sess *net.Session) { buildSkillCell := func(s player.SkillName) string { level := p.Level(s) - lvlColor := skillLevelColor(level) - name := padded(color.Render(mode, color.ColorSpec{Fg: 110}, skillDisplay[s]), 12) + lvlColor := g.skillLevelColor(sess, level) + name := padded(color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), skillDisplay[s]), 12) lvl := color.Render(mode, color.ColorSpec{Fg: lvlColor}, fmt.Sprintf("%2d", level)) xpBar := xpProgressBar(p.Skills[s], 12, unicode) return fmt.Sprintf("%s %s [%s]", name, lvl, xpBar) @@ -195,8 +198,8 @@ func (g *Game) doScore(sess *net.Session) { for _, id := range p.ActiveTechList() { def := GetTechDef(id) if def != nil { - on := color.Render(mode, color.ColorSpec{Fg: 108}, "[ON]") - name := color.Render(mode, color.ColorSpec{Fg: 110}, def.Name) + on := color.Render(mode, g.resolveConstant(sess, "active_tech_on"), "[ON]") + name := color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), def.Name) techParts = append(techParts, on+" "+name) } } @@ -205,9 +208,9 @@ func (g *Game) doScore(sess *net.Session) { if len(p.ActiveBuffs) > 0 { for _, buff := range p.ActiveBuffs { buffLine := fmt.Sprintf("%s +%d%% %s (%d ticks remaining)", - color.Render(mode, color.ColorSpec{Fg: 108}, "[BUFF]"), + color.Render(mode, g.resolveConstant(sess, "active_tech_status"), "[BUFF]"), buff.BonusPercent, - color.Render(mode, color.ColorSpec{Fg: 110}, buff.Stat), + color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), buff.Stat), buff.TicksLeft) content(buffLine) } @@ -252,51 +255,21 @@ func formatInt(n int) string { return strings.Join(parts, ",") } -func hpBarStyle(current, max int) *ui.BarStyle { - if max <= 0 { - max = 1 - } - pct := float64(current) / float64(max) - var fg int - switch { - case pct >= 0.7: - fg = 108 - case pct >= 0.4: - fg = 179 - default: - fg = 167 - } - return &ui.BarStyle{FilledColor: fg, EmptyColor: 237, EmptyDim: true} -} - -func hpBarFg(current, max int) int { - if max <= 0 { - max = 1 - } - pct := float64(current) / float64(max) - switch { - case pct >= 0.7: - return 108 - case pct >= 0.4: - return 179 - default: - return 167 - } -} - -func skillLevelColor(level int) int { +// skillLevelColor returns the xterm256 palette index for a skill level cell +// on the datapad score panel, sourcing it from the constant_colors section. +func (g *Game) skillLevelColor(sess *net.Session, level int) int { + spec := g.resolveConstant(sess, "skill_lvl_base") switch { case level >= 99: - return 180 + spec = g.resolveConstant(sess, "skill_lvl_max") case level >= 80: - return 179 + spec = g.resolveConstant(sess, "skill_lvl_high") case level >= 60: - return 116 + spec = g.resolveConstant(sess, "skill_lvl_mid") case level >= 40: - return 73 - default: - return 66 + spec = g.resolveConstant(sess, "skill_lvl_low") } + return spec.Fg } func xpProgressBar(xp int, width int, unicode bool) string { diff --git a/internal/game/cmd_skills.go b/internal/game/cmd_skills.go index 2eeb149..2f9f52e 100644 --- a/internal/game/cmd_skills.go +++ b/internal/game/cmd_skills.go @@ -16,23 +16,27 @@ func (g *Game) executeSkills(sess *net.Session, args []string, rawInput string) func (g *Game) doSkills(sess *net.Session) { p := sess.Player mode := g.colorMode(sess) + hLabel := g.resolveConstant(sess, "table_header_label") + hDetail := g.resolveConstant(sess, "table_header_detail") + hValue := g.resolveConstant(sess, "table_header_value") + hXP := g.resolveConstant(sess, "table_header_xp") t := &ui.Table{Title: "Skills", Columns: []string{ - color.Render(mode, color.Parse("49"), "Skill"), - color.Render(mode, color.Parse("F5"), "Abbr"), - color.Render(mode, color.Parse("FA"), "Level"), - color.Render(mode, color.Parse("B3"), "XP"), - color.Render(mode, color.Parse("B3"), "XP to Next"), + color.Render(mode, hLabel, "Skill"), + color.Render(mode, hDetail, "Abbr"), + color.Render(mode, hValue, "Level"), + color.Render(mode, hXP, "XP"), + color.Render(mode, hXP, "XP to Next"), }} for _, s := range player.AllSkills { level := p.Level(s) xp := p.Skills[s] next := player.XPForNextLevel(xp) t.Rows = append(t.Rows, []string{ - color.Render(mode, color.Parse("49"), string(s)), - color.Render(mode, color.Parse("F5"), player.SkillAbbr[s]), - color.Render(mode, color.Parse("FA"), strconv.Itoa(level)), - color.Render(mode, color.Parse("B3"), strconv.Itoa(xp)), - color.Render(mode, color.Parse("B3"), strconv.Itoa(next)), + color.Render(mode, hLabel, string(s)), + color.Render(mode, hDetail, player.SkillAbbr[s]), + color.Render(mode, hValue, strconv.Itoa(level)), + color.Render(mode, hXP, strconv.Itoa(xp)), + color.Render(mode, hXP, strconv.Itoa(next)), }) } for _, line := range t.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go index 4ca1382..0a5580c 100644 --- a/internal/game/cmd_tech.go +++ b/internal/game/cmd_tech.go @@ -118,7 +118,7 @@ func (g *Game) doTechList(sess *net.Session) { "", fmt.Sprintf("Battery: %s/%s", g.colorize(sess, "battery", fmt.Sprintf("%.1f", p.Battery)), - color.Render(mode, color.Parse("FA"), fmt.Sprintf("%.0f", p.MaxBattery())), + g.colorizeConstant(sess, "score_max_value", fmt.Sprintf("%.0f", p.MaxBattery())), ), ) @@ -129,14 +129,14 @@ func (g *Game) doTechList(sess *net.Session) { nameStr := tech.Name if tech.Level <= techLevel { if p.HasActiveTech(tech.ID) { - status = color.Render(mode, color.Parse("6C"), "ON") + status = color.Render(mode, g.resolveConstant(sess, "tech_status_on"), "ON") } else { - status = color.Render(mode, color.Parse("F0"), "off") + status = color.Render(mode, g.resolveConstant(sess, "tech_status_off"), "off") } - nameStr = color.Render(mode, color.Parse("49"), tech.Name) + nameStr = color.Render(mode, g.resolveConstant(sess, "tech_name_unlocked"), tech.Name) } else { - status = color.Render(mode, color.Parse("A7"), "locked") - nameStr = color.Render(mode, color.Parse("F0"), tech.Name) + status = color.Render(mode, g.resolveConstant(sess, "tech_status_locked"), "locked") + nameStr = color.Render(mode, g.resolveConstant(sess, "tech_name_locked"), tech.Name) } effect := techEffectString(tech) diff --git a/internal/game/game.go b/internal/game/game.go index eed61f5..700877b 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -38,9 +38,10 @@ type Deps struct { MobStore *world.MobStore CraftIndex *CraftIndex CourseStore *CourseStore - Ticks *engine.Engine - ColorConfig *config.ColorsConfig - DataDir string + Ticks *engine.Engine + ColorConfig *config.ColorsConfig + ConstantColorConfig *config.ColorsConfig + DataDir string } // Game is the central orchestrator: it owns the data stores (via the embedded @@ -75,19 +76,20 @@ type Game struct { shutdownMu sync.Mutex } -func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.ValidationConfig, startingRoom int) *Game { +func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig *config.ColorsConfig, valConfig config.ValidationConfig, startingRoom int) *Game { g := &Game{ Deps: Deps{ - World: world.New(dataDir), - ObjectStore: object.NewObjectStore(dataDir), - ItemStore: item.NewItemStore(dataDir), - AccountStore: player.NewAccountStore(dataDir), - MobStore: world.NewMobStore(dataDir), - CraftIndex: NewCraftIndex(), - CourseStore: NewCourseStore(dataDir), - Ticks: engine.New(), - ColorConfig: colorConfig, - DataDir: dataDir, + World: world.New(dataDir), + ObjectStore: object.NewObjectStore(dataDir), + ItemStore: item.NewItemStore(dataDir), + AccountStore: player.NewAccountStore(dataDir), + MobStore: world.NewMobStore(dataDir), + CraftIndex: NewCraftIndex(), + CourseStore: NewCourseStore(dataDir), + Ticks: engine.New(), + ColorConfig: colorConfig, + ConstantColorConfig: constantColorConfig, + DataDir: dataDir, }, GlobalFlags: NewGlobalFlagStore(), Combat: combat.NewTracker(), diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go index c1befe3..dd2cdf0 100644 --- a/internal/game/look_entities.go +++ b/internal/game/look_entities.go @@ -37,7 +37,7 @@ func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Roo } hp = fmt.Sprintf(" [%d%% complete]", pct) } else { - hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("A7"), fmt.Sprint(m.HP)), m.MaxHP) + hp = fmt.Sprintf(" [%s/%dhp]", g.colorizeConstant(sess, "mob_hp_inline", fmt.Sprint(m.HP)), m.MaxHP) } } var desc string @@ -103,13 +103,16 @@ func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world. var godPrefix string if p.GodMode { var tags []string + tagLabel := func(t string) string { + return color.Render(g.colorMode(sess), g.resolveConstant(sess, "god_tag_label"), t) + } if isLocal { - tags = append(tags, color.Render(g.colorMode(sess), color.ColorSpec{Fg: -1, Bold: true}, "[LOCAL]")) + tags = append(tags, tagLabel("[LOCAL]")) } else { - tags = append(tags, color.Render(g.colorMode(sess), color.ColorSpec{Fg: -1, Bold: true}, "[GLOBAL]")) + tags = append(tags, tagLabel("[GLOBAL]")) } if def.Hidden { - tags = append(tags, color.Render(g.colorMode(sess), color.ColorSpec{Fg: -1, Bold: true}, "(HIDDEN)")) + tags = append(tags, tagLabel("(HIDDEN)")) } godPrefix = strings.Join(tags, " ") + " " } diff --git a/internal/game/look_target.go b/internal/game/look_target.go index bb9b5da..9257940 100644 --- a/internal/game/look_target.go +++ b/internal/game/look_target.go @@ -64,7 +64,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { } var levelStr string if best.Protected { - levelStr = color.Render(g.colorMode(sess), color.Parse("FA"), "(protected)") + levelStr = g.colorizeConstant(sess, "protected_label", "(protected)") } else { mobLevel := mobCombatLevel(best) levelStr = g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel)) diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go index ff9fc51..10b7eb0 100644 --- a/internal/game/production_menu.go +++ b/internal/game/production_menu.go @@ -127,7 +127,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items [] mode := g.colorMode(sess) skillLevel := p.Level(player.SkillName(skill)) - dimSpec := color.Parse("F0") + dimSpec := g.resolveConstant(sess, "production_locked") tbl := &ui.Table{ Title: title, diff --git a/internal/game/render_color.go b/internal/game/render_color.go index 3464f26..958b671 100644 --- a/internal/game/render_color.go +++ b/internal/game/render_color.go @@ -42,24 +42,53 @@ func (g *Game) colorize(sess *net.Session, category, text string) string { return color.Render(g.colorMode(sess), spec, text) } -func levelColorSpec(myLevel, theirLevel int) color.ColorSpec { +// resolveConstant returns the live spec for a constant (player-invisible) +// color category. Unlike resolveColor, it never consults the account's +// per-player override map — constants are admin-tunable only — so players +// have no way to change them. Falls back through the file's constant_colors +// map, then to the built-in DefaultConstantColors. +func (g *Game) resolveConstant(sess *net.Session, category string) color.ColorSpec { + if g.ConstantColorConfig != nil { + if val, ok := (*g.ConstantColorConfig)[category]; ok && val != "" { + return color.Parse(val) + } + } + if val, ok := config.DefaultConstantColors()[category]; ok && val != "" { + return color.Parse(val) + } + return color.NoColor() +} + +// colorizeConstant paints text with a constant color spec, bypassing the +// player override tier entirely. +func (g *Game) colorizeConstant(sess *net.Session, category, text string) string { + spec := g.resolveConstant(sess, category) + return color.Render(g.colorMode(sess), spec, text) +} + +// levelColorSpec returns the color spec for the level-difference band shown +// next to mob/player levels (green/red/white by how far the target is from +// the viewer). Uses admin-tunable constant_colors; falls back to built-ins. +func (g *Game) levelColorSpec(sess *net.Session, myLevel, theirLevel int) color.ColorSpec { diff := theirLevel - myLevel + var name string switch { case diff == 0: - return color.Parse("FA") + name = "level_diff_equal" case diff > 0 && diff < 5: - return color.Parse("B3") + name = "level_diff_up_small" case diff >= 5: - return color.Parse("A7") + name = "level_diff_up_big" case diff < 0 && diff > -5: - return color.Parse("6E") + name = "level_diff_down_small" default: - return color.Parse("41") + name = "level_diff_down_big" } + return g.resolveConstant(sess, name) } func (g *Game) levelColorize(sess *net.Session, myLevel, theirLevel int, text string) string { - spec := levelColorSpec(myLevel, theirLevel) + spec := g.levelColorSpec(sess, myLevel, theirLevel) return color.Render(g.colorMode(sess), spec, text) } diff --git a/internal/game/render_combat.go b/internal/game/render_combat.go index e4a0d55..4055b4d 100644 --- a/internal/game/render_combat.go +++ b/internal/game/render_combat.go @@ -3,6 +3,7 @@ package game import ( "fmt" + "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/ui" ) @@ -14,7 +15,10 @@ func (g *Game) dmgDisplay(sess *net.Session, dmg int) string { return fmt.Sprintf("> %d <", dmg) } -func (g *Game) renderBar(sess *net.Session, current, max, lowColor int) string { +// renderBarGeneric renders a high/med/low tiered bar at width 10 using the +// named constant-color family (prefix: "hp_bar" or "task_bar"). The empty +// cells are painted with hp_bar_empty. +func (g *Game) renderBarGeneric(sess *net.Session, current, max int, prefix string) string { if max <= 0 { max = 1 } @@ -25,23 +29,43 @@ func (g *Game) renderBar(sess *net.Session, current, max, lowColor int) string { current = max } - pct := float64(current) / float64(max) * 100.0 - var fgColor int + pct := float64(current) / float64(max) + var spec color.ColorSpec switch { - case pct >= 70: - fgColor = 108 - case pct >= 40: - fgColor = 179 + case pct >= 0.7: + spec = g.resolveConstant(sess, prefix+"_high") + case pct >= 0.4: + spec = g.resolveConstant(sess, prefix+"_med") default: - fgColor = lowColor + spec = g.resolveConstant(sess, prefix+"_low") } + empty := g.resolveConstant(sess, "hp_bar_empty") unicode := sess.Player != nil && sess.Player.OptionBool("unicode") - style := &ui.BarStyle{FilledColor: fgColor, EmptyColor: 237, EmptyDim: true} + style := &ui.BarStyle{FilledColor: spec.Fg, EmptyColor: empty.Fg, EmptyDim: empty.Dim} bar := ui.RenderColoredBar(current, max, 10, unicode, style, g.colorMode(sess)) return "[" + bar + "]" } +// hpBarStyle returns the per-tier fill/empty color spec for an HP bar at the +// given ratio (high>=70%, med>=40%, low<40%). Colors come from the +// constant_colors map (hp_bar_high/med/low/empty). +func (g *Game) hpBarStyle(sess *net.Session, current, max int) *ui.BarStyle { + if max <= 0 { + max = 1 + } + pct := float64(current) / float64(max) + spec := g.resolveConstant(sess, "hp_bar_low") + switch { + case pct >= 0.7: + spec = g.resolveConstant(sess, "hp_bar_high") + case pct >= 0.4: + spec = g.resolveConstant(sess, "hp_bar_med") + } + empty := g.resolveConstant(sess, "hp_bar_empty") + return &ui.BarStyle{FilledColor: spec.Fg, EmptyColor: empty.Fg, EmptyDim: empty.Dim} +} + func (g *Game) hpBar(sess *net.Session, current, max int) string { - return g.renderBar(sess, current, max, 167) + return g.renderBarGeneric(sess, current, max, "hp_bar") } diff --git a/internal/game/sys_labor.go b/internal/game/sys_labor.go index 6004b75..f0e1d01 100644 --- a/internal/game/sys_labor.go +++ b/internal/game/sys_labor.go @@ -22,7 +22,7 @@ func taskWorkVerb(mob *world.MobInstance) string { // inverse of hpBar. It is used for task worksites whose internal HP drains to 0 // to complete the work (progress = MaxHP - HP). func (g *Game) progressBar(sess *net.Session, progress, max int) string { - return g.renderBar(sess, progress, max, 67) + return g.renderBarGeneric(sess, progress, max, "task_bar") } // writeTaskProgress prints the per-tick progress line for a task worksite, -- cgit v1.2.3