diff --git a/assets/app/js/app.js b/assets/app/js/app.js index 40d691e27..cc48841c2 100644 --- a/assets/app/js/app.js +++ b/assets/app/js/app.js @@ -1,4 +1,6 @@ import "phoenix_html" import {channel} from "./socket" +window.gameConfig = {}; + channel.join() diff --git a/assets/app/js/color.js b/assets/app/js/color.js index de7d854d0..fa764b4fe 100644 --- a/assets/app/js/color.js +++ b/assets/app/js/color.js @@ -1,6 +1,54 @@ +const DEFAULT_COLORS = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]; + +let defaultColor = (tag, color) => { + let configuredColor = gameConfig["color_" + tag] || color; + return () => { + return `{${configuredColor}}`; + }; +}; + +export function defaultColorCSS(tag, color) { + let configuredColor = gameConfig["color_" + tag] || color; + if (DEFAULT_COLORS.includes(configuredColor)) { + return configuredColor; + } else { + return `color-code-${configuredColor}`; + } +} + function formatColor(payload) { let string = payload.message; + string = string.replace(/{npc}/g, defaultColor("npc", "yellow")); + string = string.replace(/{item}/g, defaultColor("item", "cyan")); + string = string.replace(/{player}/g, defaultColor("player", "blue")); + string = string.replace(/{skill}/g, defaultColor("skill", "white")); + string = string.replace(/{quest}/g, defaultColor("quest", "yellow")); + string = string.replace(/{room}/g, defaultColor("room", "green")); + string = string.replace(/{say}/g, defaultColor("say", "green")); + string = string.replace(/{shop}/g, defaultColor("shop", "magenta")); + string = string.replace(/{hint}/g, defaultColor("hint", "cyan")); + + string = string.replace(/{exit}/g, () => { + return ``; + }); + + string = string.replace(/{command click=false}/g, defaultColor("command", "white")); + string = string.replace(/{exit click=false}/g, defaultColor("exit", "white")); + + string = string.replace(/{command( send='(.*)')?}/g, (_match, _fullSend, command) => { + let color = defaultColorCSS("command", "white"); + if (payload.delink == undefined || payload.delink == false) { + if (command != undefined) { + return ``; + } else { + return ``; + } + } else { + return ``; + } + }); + string = string.replace(/{black}/g, "") string = string.replace(/{red}/g, "") string = string.replace(/{green}/g, "") @@ -15,29 +63,6 @@ function formatColor(payload) { string = string.replace(/{map:green}/g, "") string = string.replace(/{map:grey}/g, "") string = string.replace(/{map:light-grey}/g, "") - string = string.replace(/{npc}/g, "") - string = string.replace(/{item}/g, "") - string = string.replace(/{player}/g, "") - string = string.replace(/{skill}/g, "") - string = string.replace(/{quest}/g, "") - string = string.replace(/{room}/g, "") - string = string.replace(/{say}/g, "") - string = string.replace(/{exit}/g, "") - string = string.replace(/{shop}/g, "") - string = string.replace(/{hint}/g, "") - - string = string.replace(/{command click=false}/g, ""); - string = string.replace(/{command( send='(.*)')?}/g, (_match, _fullSend, command) => { - if (payload.delink == undefined || payload.delink == false) { - if (command != undefined) { - return ""; - } else { - return ""; - } - } else { - return ""; - } - }); // assume all other tags are custom colors string = string.replace(/{([\w:-]+)}/g, (_match, color) => { @@ -56,8 +81,6 @@ function formatLines(string) { return string; } -function format(payload) { +export function format(payload) { return formatLines(formatColor(payload)); } - -export default format diff --git a/assets/app/js/gmcp.js b/assets/app/js/gmcp.js index 82396a056..e8f8c8aa5 100644 --- a/assets/app/js/gmcp.js +++ b/assets/app/js/gmcp.js @@ -1,7 +1,7 @@ import Sizzle from "sizzle" import _ from "underscore" -import format from "./color" +import {format, defaultColorCSS} from "./color" import Notifacations from "./notifications" @@ -49,6 +49,13 @@ let characterVitals = (channel, data) => { movementStat.innerHTML = `${data.move_points}/${data.max_move_points} mv`; } +/** + * Config.Update module + */ +let configUpdate = (channel, data) => { + window.gameConfig = data; +} + /** * Mail.New module */ @@ -90,16 +97,19 @@ let renderRoom = (channel, room) => { exits.append(html) }) + let npcColor = defaultColorCSS("npc", "yellow"); + let playerColor = defaultColorCSS("player", "blue"); + let characters = _.first(Sizzle(".room-info .characters")) characters.innerHTML = "" _.each(room.npcs, (npc) => { let html = document.createElement('div') - html.innerHTML = `
  • ${npc.name}
  • ` + html.innerHTML = `
  • ${npc.name}
  • ` _.each(html.children, (li) => { characters.append(li) }) }) _.each(room.players, (player) => { let html = document.createElement('div') - html.innerHTML = `
  • ${player.name}
  • ` + html.innerHTML = `
  • ${player.name}
  • ` _.each(html.children, (li) => { characters.append(li) }) }) } @@ -171,6 +181,7 @@ let gmcp = { "Channels.Tell": tell, "Character": character, "Character.Vitals": characterVitals, + "Config.Update": configUpdate, "Mail.New": mailNew, "Room.Heard": roomHeard, "Room.Info": roomInfo, diff --git a/assets/app/js/panel.js b/assets/app/js/panel.js index e4f5db37e..127d96fc9 100644 --- a/assets/app/js/panel.js +++ b/assets/app/js/panel.js @@ -1,7 +1,7 @@ import Sizzle from "sizzle" import _ from "underscore" -import format from "./color" +import {format} from "./color" let scrollToBottom = (callback) => { let panel = _.first(Sizzle(".panel")) diff --git a/lib/data/channel.ex b/lib/data/channel.ex index 852e934a5..cf20ebad0 100644 --- a/lib/data/channel.ex +++ b/lib/data/channel.ex @@ -5,7 +5,7 @@ defmodule Data.Channel do use Data.Schema - alias Game.Color + alias Data.Color schema "channels" do field(:name, :string) diff --git a/lib/data/color.ex b/lib/data/color.ex new file mode 100644 index 000000000..b900477ef --- /dev/null +++ b/lib/data/color.ex @@ -0,0 +1,54 @@ +defmodule Data.Color do + @moduledoc """ + Static color data + """ + + @doc """ + Valid color codes + """ + def options() do + [ + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white" + ] + end + + @doc """ + Valid map color codes + """ + def map_colors() do + [ + "blue", + "brown", + "dark-green", + "green", + "grey", + "light-grey" + ] + end + + @doc """ + Color "tags" or semantic colors for things in the game like an NPC + """ + def color_tags() do + [ + "npc", + "item", + "player", + "skill", + "quest", + "room", + "say", + "command", + "exit", + "shop", + "hint", + ] + end +end diff --git a/lib/data/save.ex b/lib/data/save.ex index cb707d50b..c78745e23 100644 --- a/lib/data/save.ex +++ b/lib/data/save.ex @@ -328,11 +328,21 @@ defmodule Data.Save do def valid_config?(save) def valid_config?(%{config: config}) do - is_map(config) && keys(config) == [:hints, :pager_size, :prompt, :regen_notifications] && + is_map(config) && config_keys(config) == [:hints, :pager_size, :prompt, :regen_notifications] && is_boolean(config.hints) && is_binary(config.prompt) && is_integer(config.pager_size) && is_boolean(config.regen_notifications) end + defp config_keys(config) do + config + |> keys() + |> Enum.reject(fn color -> + color + |> to_string() + |> String.starts_with?("color") + end) + end + @doc """ Validate channels are correct diff --git a/lib/data/save/config.ex b/lib/data/save/config.ex index a67365867..b65409c39 100644 --- a/lib/data/save/config.ex +++ b/lib/data/save/config.ex @@ -3,6 +3,9 @@ defmodule Data.Save.Config do Helpers for the player's configuration """ + alias Data.Color + alias Game.ColorCodes + @doc """ Determine if a string is a configuration option """ @@ -13,7 +16,7 @@ defmodule Data.Save.Config do "hints" -> true "pager_size" -> true "regen_notifications" -> true - _ -> false + config -> color_config?(config) end end @@ -25,7 +28,7 @@ defmodule Data.Save.Config do case to_string(config) do "prompt" -> true "pager_size" -> true - _ -> false + config -> color_config?(config) end end @@ -41,14 +44,50 @@ defmodule Data.Save.Config do "pager_size" -> Ecto.Type.cast(:integer, value) - _ -> + config -> + maybe_cast_color(config, value) + end + end + + defp maybe_cast_color(config, value) do + case color_config?(config) do + true -> + cast_color(value) + + false -> + {:error, :bad_config} + end + end + + defp cast_color(value) do + case is_a_color?(value) do + true -> + {:ok, value} + + false -> {:error, :bad_config} end end + # NOTE: Eventually move this into the Game directory or get all of the caches living in + # the data layer. This shouldn't really be reaching across boundaries. + defp is_a_color?(value) do + Enum.any?(Color.options(), &(&1 == value)) || Enum.any?(ColorCodes.all(), &(&1.key == value)) + end + @doc """ Starting prompt """ @spec default_prompt() :: String.t() def default_prompt(), do: "%h/%Hhp %s/%Ssp %m/%Mmv %xxp" + + @doc """ + Check if a key is color configuration + """ + def color_config?(key) do + Color.color_tags() + |> Enum.any?(fn tag -> + "color_#{tag}" == to_string(key) + end) + end end diff --git a/lib/game/color.ex b/lib/game/color.ex index 1f78caa65..ed759075b 100644 --- a/lib/game/color.ex +++ b/lib/game/color.ex @@ -11,36 +11,6 @@ defmodule Game.Color do def color_regex(), do: @color_regex - @doc """ - Valid color codes - """ - def options() do - [ - "black", - "red", - "green", - "yellow", - "blue", - "magenta", - "cyan", - "white" - ] - end - - @doc """ - Valid map color codes - """ - def map_colors() do - [ - "blue", - "brown", - "dark-green", - "green", - "grey", - "light-grey" - ] - end - @doc """ For commands coming in from a player, delink them so they are only color. """ @@ -52,28 +22,28 @@ defmodule Game.Color do @doc """ Format a string for colors """ - @spec format(String.t()) :: String.t() - def format(string) do + @spec format(String.t(), map()) :: String.t() + def format(string, config \\ %{}) do string = string |> strip_commands() split = Regex.split(@color_regex, string, include_captures: true) split - |> _format([], []) + |> _format([], [], config) |> Enum.reverse() |> Enum.join() end - defp _format([], lines, stack) when length(stack) > 0, do: ["\e[0m" | lines] - defp _format([], lines, _stack), do: lines + defp _format([], lines, stack, _config) when length(stack) > 0, do: ["\e[0m" | lines] + defp _format([], lines, _stack, _config), do: lines - defp _format([head | tail], lines, stack) do + defp _format([head | tail], lines, stack, config) do case Regex.match?(@color_regex, head) do true -> - {code, stack} = format_color_code(head, stack) - _format(tail, [code | lines], stack) + {code, stack} = format_color_code(head, stack, config) + _format(tail, [code | lines], stack, config) false -> - _format(tail, [head | lines], stack) + _format(tail, [head | lines], stack, config) end end @@ -86,6 +56,7 @@ defmodule Game.Color do string |> String.replace(~r/{command send='.*'}/, "{command}") |> String.replace(~r/{command click=false}/, "{command}") + |> String.replace(~r/{exit click=false}/, "{exit}") end @doc """ @@ -98,66 +69,81 @@ defmodule Game.Color do @doc """ Format a color code, opening will add to the stack, closing will read/pull off of the stack """ - def format_color_code(code, stack) do + def format_color_code(code, stack, config) do case color_code_open?(code) do false -> - format_closing_code(stack) + format_closing_code(stack, config) true -> - {format_color(code), [code | stack]} + {format_color(code, config), [code | stack]} end end @doc """ Format the closing code, which pulls off of the stack """ - @spec format_closing_code([]) :: {String.t(), []} - def format_closing_code([_previous | [previous | stack]]) do - {format_color(previous), [previous | stack]} + @spec format_closing_code([], map()) :: {String.t(), []} + def format_closing_code([_previous | [previous | stack]], config) do + {format_color(previous, config), [previous | stack]} end - def format_closing_code([_previous | stack]) do - {format_color("{/color}"), stack} + def format_closing_code([_previous | stack], _config) do + {format_basic_color("{/color}"), stack} end - def format_closing_code(stack) do - {format_color("{/color}"), stack} + def format_closing_code(stack, _config) do + {format_basic_color("{/color}"), stack} end @doc """ Format a specific color tag """ - @spec format_color(String.t()) :: String.t() - def format_color("{black}"), do: "\e[30m" - def format_color("{red}"), do: "\e[31m" - def format_color("{green}"), do: "\e[32m" - def format_color("{yellow}"), do: "\e[33m" - def format_color("{blue}"), do: "\e[34m" - def format_color("{magenta}"), do: "\e[35m" - def format_color("{cyan}"), do: "\e[36m" - def format_color("{white}"), do: "\e[37m" - def format_color("{map:blue}"), do: "\e[38;5;26m" - def format_color("{map:brown}"), do: "\e[38;5;94m" - def format_color("{map:dark-green}"), do: "\e[38;5;22m" - def format_color("{map:green}"), do: "\e[38;5;34m" - def format_color("{map:grey}"), do: "\e[38;5;247m" - def format_color("{map:light-grey}"), do: "\e[38;5;252m" - - def format_color("{npc}"), do: "\e[33m" - def format_color("{item}"), do: "\e[36m" - def format_color("{player}"), do: "\e[34m" - def format_color("{skill}"), do: "\e[37m" - def format_color("{quest}"), do: "\e[33m" - def format_color("{room}"), do: "\e[32m" - def format_color("{say}"), do: "\e[32m" - def format_color("{command}"), do: "\e[37m" - def format_color("{exit}"), do: "\e[37m" - def format_color("{shop}"), do: "\e[35m" - def format_color("{hint}"), do: "\e[36m" - - def format_color("{/" <> _), do: "\e[0m" - - def format_color(key) do + @spec format_color(String.t(), map()) :: String.t() + def format_color(tag, config) do + case format_semantic_color(tag) do + :error -> + format_basic_color(tag) + + {tag, color} -> + color = Map.get(config, :"color_#{tag}", color) + format_basic_color("{#{color}}") + end + end + + def format_semantic_color("{npc}"), do: {:npc, :yellow} #"\e[33m" + def format_semantic_color("{item}"), do: {:item, :cyan} #"\e[36m" + def format_semantic_color("{player}"), do: {:player, :blue} #"\e[34m" + def format_semantic_color("{skill}"), do: {:skill, :white} #"\e[37m" + def format_semantic_color("{quest}"), do: {:quest, :yellow} #"\e[33m" + def format_semantic_color("{room}"), do: {:room, :green} #"\e[32m" + def format_semantic_color("{say}"), do: {:say, :green} #"\e[32m" + def format_semantic_color("{command}"), do: {:command, :white} #"\e[37m" + def format_semantic_color("{exit}"), do: {:exit, :white} #"\e[37m" + def format_semantic_color("{shop}"), do: {:shop, :magenta} #"\e[35m" + def format_semantic_color("{hint}"), do: {:hint, :cyan} #"\e[36m" + def format_semantic_color(_), do: :error + + @doc """ + Format a basic color tag, straight colors + """ + @spec format_basic_color(String.t()) :: String.t() + def format_basic_color("{/" <> _), do: "\e[0m" + + def format_basic_color("{black}"), do: "\e[30m" + def format_basic_color("{red}"), do: "\e[31m" + def format_basic_color("{green}"), do: "\e[32m" + def format_basic_color("{yellow}"), do: "\e[33m" + def format_basic_color("{blue}"), do: "\e[34m" + def format_basic_color("{magenta}"), do: "\e[35m" + def format_basic_color("{cyan}"), do: "\e[36m" + def format_basic_color("{white}"), do: "\e[37m" + def format_basic_color("{map:blue}"), do: "\e[38;5;26m" + def format_basic_color("{map:brown}"), do: "\e[38;5;94m" + def format_basic_color("{map:dark-green}"), do: "\e[38;5;22m" + def format_basic_color("{map:green}"), do: "\e[38;5;34m" + def format_basic_color("{map:grey}"), do: "\e[38;5;247m" + def format_basic_color("{map:light-grey}"), do: "\e[38;5;252m" + def format_basic_color(key) do key = key |> String.replace("{", "") diff --git a/lib/game/command/colors.ex b/lib/game/command/colors.ex index 3997915a9..40dc05199 100644 --- a/lib/game/command/colors.ex +++ b/lib/game/command/colors.ex @@ -5,10 +5,12 @@ defmodule Game.Command.Colors do use Game.Command - alias Game.Color + alias Data.Color + alias Data.Save.Config alias Game.ColorCodes + alias Game.Command.Config, as: CommandConfig - commands(["colors"], parse: false) + commands([{"colors", ["color"]}], parse: false) @impl Game.Command def help(:topic), do: "Colors" @@ -18,8 +20,14 @@ defmodule Game.Command.Colors do """ #{help(:short)}. - Example: + View all colors, including map colors: [ ] > {command}colors{/command} + + View all color 'tags': + [ ] > {command}color tags{/command} + + Reset your configured colors: + [ ] > {command}colors reset{/command} """ end @@ -30,12 +38,27 @@ defmodule Game.Command.Colors do iex> Game.Command.Colors.parse("colors") {:list} + iex> Game.Command.Colors.parse("color list") + {:list} + iex> Game.Command.Colors.parse("colors list") + {:list} + + iex> Game.Command.Colors.parse("colors reset") + {:reset} + + iex> Game.Command.Colors.parse("color tags") + {:semantic} + iex> Game.Command.Colors.parse("unknown") {:error, :bad_parse, "unknown"} """ @spec parse(String.t()) :: {any()} def parse(command) def parse("colors"), do: {:list} + def parse("colors list"), do: {:list} + def parse("color list"), do: {:list} + def parse("colors reset"), do: {:reset} + def parse("color tags"), do: {:semantic} @impl Game.Command def run(command, state) @@ -56,6 +79,40 @@ defmodule Game.Command.Colors do {:paginate, message, state} end + def run({:reset}, state) do + save = state.save + + config = + save.config + |> Enum.reject(fn {key, _val} -> + Config.color_config?(key) + end) + |> Enum.into(%{}) + + save = %{save | config: config} + user = %{state.user | save: save} + state = %{state | user: user, save: save} + + state |> CommandConfig.push_config(config) + state.socket |> @socket.echo("Your colors have been reset") + + {:update, state} + end + + def run({:semantic}, state) do + message = """ + Color Tags + #{Format.underline("Colors")} + + The available color tags are in the following list. You can configure these with + the {command send='help config'}config{/command} command. + + #{color_tags()} + """ + + {:paginate, message, state} + end + defp base_colors() do Color.options() |> Enum.map(fn color -> @@ -79,4 +136,21 @@ defmodule Game.Command.Colors do end) |> Enum.join("\n") end + + defp color_tags() do + Color.color_tags() + |> Enum.map(fn tag -> + case tag do + "command" -> + "{#{tag} click=false}#{tag}{/#{tag}}" + + "exit" -> + "{#{tag} click=false}#{tag}{/#{tag}}" + + _ -> + "{#{tag}}#{tag}{/#{tag}}" + end + end) + |> Enum.join("\n") + end end diff --git a/lib/game/command/config.ex b/lib/game/command/config.ex index 58cd7541e..4d8751455 100644 --- a/lib/game/command/config.ex +++ b/lib/game/command/config.ex @@ -7,6 +7,7 @@ defmodule Game.Command.Config do alias Data.Save.Config, as: PlayerConfig alias Game.Format + alias Game.Session.GMCP commands(["config"], parse: false) @@ -34,6 +35,10 @@ defmodule Game.Command.Config do {white}regen_notifications{/white}: A true/false option that will show regeneration notifications, use {command}config [on|off]{/command} + {white}color_[color]{/white}: + Replace {white}[color]{/white} with a color tag from the {command}color tags{/command} list. You can set via + {command}config set color_npc blue{/command} for instance. You can reset your colors with {command}color reset{/command}. + View a list of configuration options [ ] > {command}config{/command} @@ -87,7 +92,7 @@ defmodule Game.Command.Config do def run({:on, config_name}, state) do case is_config?(config_name) do true -> - {:update, update_config(config_name, true, state)} + update_config(config_name, true, state) false -> state.socket |> @socket.echo("Unknown configuration option, \"#{config_name}\"") @@ -97,7 +102,7 @@ defmodule Game.Command.Config do def run({:off, config_name}, state) do case is_config?(config_name) do true -> - {:update, update_config(config_name, false, state)} + update_config(config_name, false, state) false -> state.socket |> @socket.echo("Unknown configuration option, \"#{config_name}\"") @@ -112,7 +117,7 @@ defmodule Game.Command.Config do true -> case PlayerConfig.settable?(config_name) do true -> - {:update, cast_and_set_config(config_name, value, state)} + cast_and_set_config(config_name, value, state) false -> state.socket @@ -173,6 +178,18 @@ defmodule Game.Command.Config do state.socket |> @socket.echo("#{config_name} is set to \"#{integer}\"") end - state + state |> push_config(config) + + {:update, state} + end + + @doc """ + Push config to the network and client layer + """ + @spec push_config(State.t(), map()) :: :ok + def push_config(state, config) do + state.socket |> @socket.set_config(config) + state |> GMCP.config(config) + :ok end end diff --git a/lib/game/format.ex b/lib/game/format.ex index bf709d652..3127b2f73 100644 --- a/lib/game/format.ex +++ b/lib/game/format.ex @@ -781,7 +781,7 @@ defmodule Game.Format do def npc_name_for_status(npc) do case Map.get(npc, :is_quest_giver, false) do - true -> "#{npc_name(npc)} ({yellow}!{/yellow})" + true -> "#{npc_name(npc)} ({quest}!{/quest})" false -> npc_name(npc) end end @@ -1066,7 +1066,7 @@ defmodule Game.Format do [to_string(key), value] end) - rows = [["Name", "On?"] | rows] + rows = [["Name", "Value"] | rows] max_size = rows diff --git a/lib/game/session/gmcp.ex b/lib/game/session/gmcp.ex index ff3a4634c..2ad33b801 100644 --- a/lib/game/session/gmcp.ex +++ b/lib/game/session/gmcp.ex @@ -239,4 +239,12 @@ defmodule Game.Session.GMCP do socket |> @socket.push_gmcp("Mail.New", Poison.encode!(data)) end + + @doc """ + Push player configuration to the client + """ + @spec config(State.t(), map()) :: :ok + def config(%{socket: socket}, config) do + socket |> @socket.push_gmcp("Config.Update", Poison.encode!(config)) + end end diff --git a/lib/game/session/login.ex b/lib/game/session/login.ex index 958c4d765..248659cf6 100644 --- a/lib/game/session/login.ex +++ b/lib/game/session/login.ex @@ -12,6 +12,7 @@ defmodule Game.Session.Login do require Logger alias Game.Authentication + alias Game.Command.Config, as: CommandConfig alias Game.Config alias Game.Channel alias Game.Mail @@ -58,6 +59,7 @@ defmodule Game.Session.Login do socket |> @socket.echo("Welcome, #{user.name}!") socket |> @socket.set_user_id(user.id) + state |> CommandConfig.push_config(user.save.config) socket |> @socket.echo(Config.after_sign_in_message()) diff --git a/lib/networking/protocol.ex b/lib/networking/protocol.ex index 302cc1b41..3b618959b 100644 --- a/lib/networking/protocol.ex +++ b/lib/networking/protocol.ex @@ -95,6 +95,11 @@ defmodule Networking.Protocol do GenServer.cast(socket, {:user_id, user_id}) end + @impl Networking.Socket + def set_config(socket, config) do + GenServer.cast(socket, {:config, config}) + end + def init(ref, socket, transport) do Logger.info("Player connecting", type: :socket) PlayerInstrumenter.session_started(:telnet) @@ -110,7 +115,8 @@ defmodule Networking.Protocol do transport: transport, gmcp: false, gmcp_supports: [], - user_id: nil + user_id: nil, + config: %{} }) end @@ -146,13 +152,17 @@ defmodule Networking.Protocol do {:noreply, Map.put(state, :user_id, user_id)} end + def handle_cast({:config, config}, state) do + {:noreply, Map.put(state, :config, config)} + end + def handle_cast({:echo, message}, state) do - send_data(state, "\n#{message |> Color.format()}\n") + send_data(state, "\n#{message |> Color.format(state.config)}\n") {:noreply, state} end def handle_cast({:echo, message, :prompt}, state) do - send_data(state, "\n#{message |> Color.format()}") + send_data(state, "\n#{message |> Color.format(state.config)}") {:noreply, state} end diff --git a/lib/networking/socket.ex b/lib/networking/socket.ex index 0583c039d..988e68f11 100644 --- a/lib/networking/socket.ex +++ b/lib/networking/socket.ex @@ -6,6 +6,7 @@ defmodule Networking.Socket do """ @callback set_user_id(socket :: pid, user_id :: integer()) :: :ok + @callback set_config(socket :: pid, config :: map()) :: :ok @callback echo(socket :: pid, message :: String.t()) :: :ok @callback prompt(socket :: pid, message :: String.t()) :: :ok @callback disconnect(socket :: pid) :: :ok diff --git a/lib/web/channels/telnet_channel.ex b/lib/web/channels/telnet_channel.ex index ced864c78..eb113e26e 100644 --- a/lib/web/channels/telnet_channel.ex +++ b/lib/web/channels/telnet_channel.ex @@ -130,7 +130,7 @@ defmodule Web.TelnetChannel do def init(socket) do GenServer.cast(self(), :start_session) - {:ok, %{socket: socket, user_id: nil}} + {:ok, %{socket: socket, user_id: nil, config: %{}}} end def handle_cast({:command, _, command}, state) do @@ -209,6 +209,11 @@ defmodule Web.TelnetChannel do {:noreply, %{state | user_id: user_id}} end + def handle_cast({:config, config}, state) do + send(state.socket.channel_pid, {:config, config}) + {:noreply, %{state | config: config}} + end + def handle_cast(:restart_session, state) do Logger.info(fn -> "Restarting a session" end, type: :session) {:ok, pid} = Game.Session.start_with_user(self(), state.user_id) @@ -253,6 +258,10 @@ defmodule Web.TelnetChannel do {:noreply, Map.put(state, :user_id, user_id)} end + def handle_info({:config, config}, state) do + {:noreply, Map.put(state, :config, config)} + end + def handle_info({:option, :echo, flag}, socket) do push(socket, "option", %{type: "echo", echo: flag, sent_at: Timex.now()}) {:noreply, socket} diff --git a/lib/web/color.ex b/lib/web/color.ex index eff8f6fbc..99669a0fd 100644 --- a/lib/web/color.ex +++ b/lib/web/color.ex @@ -6,7 +6,7 @@ defmodule Web.Color do alias Game.ColorCodes def options() do - Game.Color.options() + Data.Color.options() |> Enum.map(fn color -> {String.capitalize(color), color} end) diff --git a/test/data/save/config_test.exs b/test/data/save/config_test.exs index 4935b867e..9bd81b708 100644 --- a/test/data/save/config_test.exs +++ b/test/data/save/config_test.exs @@ -3,6 +3,7 @@ defmodule Data.Save.ConfigTest do doctest Data.Save.Config alias Data.Save.Config + alias Game.ColorCodes describe "is a configuration option" do test "prompt is" do @@ -20,6 +21,11 @@ defmodule Data.Save.ConfigTest do assert Config.option?(:pager_size) end + test "color config is" do + assert Config.option?("color_npc") + assert Config.option?(:color_npc) + end + test "unknown is not" do refute Config.option?("unknown") refute Config.option?(:unknown) @@ -41,6 +47,11 @@ defmodule Data.Save.ConfigTest do assert Config.settable?("pager_size") assert Config.settable?(:pager_size) end + + test "color config is settable" do + assert Config.settable?("color_npc") + assert Config.settable?(:color_npc) + end end describe "cast configuration" do @@ -56,5 +67,23 @@ defmodule Data.Save.ConfigTest do test "casting non-settable config" do assert Config.cast_config("hint", "true") == {:error, :bad_config} end + + test "casting a color value" do + assert Config.cast_config("color_npc", "green") == {:ok, "green"} + + ColorCodes.insert(%{key: "pink"}) + assert Config.cast_config("color_npc", "pink") == {:ok, "pink"} + end + end + + describe "color config" do + test "true for valid tags" do + assert Config.color_config?("color_npc") + assert Config.color_config?("color_quest") + end + + test "false for invalid tags" do + refute Config.color_config?("color_potion") + end end end diff --git a/test/game/color_test.exs b/test/game/color_test.exs index 3d9c8855d..875c0d507 100644 --- a/test/game/color_test.exs +++ b/test/game/color_test.exs @@ -4,7 +4,7 @@ defmodule Game.ColorTest do alias Game.ColorCodes - import Game.Color, only: [format: 1] + import Game.Color, only: [format: 1, format: 2] test "replaces multiple colors" do assert format("{black}word{/black} {blue}word{/blue}") == "\e[30mword\e[0m \e[34mword\e[0m" @@ -50,7 +50,7 @@ defmodule Game.ColorTest do assert format("{map:dark-green}[ ]{/map:dark-green}") == "\e[38;5;22m[ ]\e[0m" end - describe "statemachine" do + describe "state machine" do test "replaces a color after another color is reset" do assert format("{green}hi there {white}command{/white} green again{/green}") == "\e[32mhi there \e[37mcommand\e[32m green again\e[0m" @@ -82,6 +82,14 @@ defmodule Game.ColorTest do end end + describe "configure the colors" do + test "players can configure semantic colors" do + config = %{color_npc: "green"} + + assert format("{npc}Guard{/npc}", config) == "\e[32mGuard\e[0m" + end + end + describe "handles dynamic colors" do setup do ColorCodes.reload(%{key: "new-white", ansi_escape: "\\e[38;2;255;255;255;m"}) diff --git a/test/game/command/colors_test.exs b/test/game/command/colors_test.exs index 23ee76a80..2f5221bb8 100644 --- a/test/game/command/colors_test.exs +++ b/test/game/command/colors_test.exs @@ -9,7 +9,8 @@ defmodule Game.Command.ColorsTest do setup do @socket.clear_messages() - %{state: %{socket: :socket}} + save = base_save() + %{state: %{socket: :socket, user: %{save: save}, save: save}} end describe "viewing a list of colors" do @@ -33,4 +34,22 @@ defmodule Game.Command.ColorsTest do assert Regex.match?(~r/pink/, echo) end end + + describe "resetting your base colors" do + test "includes npcs, exits, etc in their current colors", %{state: state} do + state = %{state | save: %{state.save | config: %{color_npc: "green"}}} + + {:update, state} = Colors.run({:reset}, state) + + refute Map.has_key?(state.save.config, :color_npc) + end + end + + describe "viewing semantic colors" do + test "includes npcs, exits, etc in their current colors", %{state: state} do + {:paginate, echo, _state} = Colors.run({:semantic}, state) + + assert Regex.match?(~r/npc/i, echo) + end + end end diff --git a/test/game/command/config_test.exs b/test/game/command/config_test.exs index 5ca6753aa..3a821849b 100644 --- a/test/game/command/config_test.exs +++ b/test/game/command/config_test.exs @@ -72,6 +72,17 @@ defmodule Game.Command.ConfigTest do assert Regex.match?(~r/set/, echo) end + test "set to a color - color_npc", %{state: state} do + state = %{state | save: %{state.save | config: %{prompt: ""}}} + + {:update, %{save: save}} = Config.run({:set, "color_npc green"}, state) + + assert save.config.color_npc == "green" + + [{_socket, echo}] = @socket.get_echos() + assert Regex.match?(~r/set/, echo) + end + test "set to an integer - pager_size", %{state: state} do state = %{state | save: %{state.save | config: %{pager_size: 20}}} diff --git a/test/game/format_test.exs b/test/game/format_test.exs index c4afc210e..89ace8991 100644 --- a/test/game/format_test.exs +++ b/test/game/format_test.exs @@ -303,8 +303,8 @@ defmodule Game.FormatTest do test "if a quest giver it includes a quest mark", %{npc: npc} do npc = %{npc | is_quest_giver: true} - assert Format.npc_name_for_status(npc) == "{npc}Guard{/npc} ({yellow}!{/yellow})" - assert Format.npc_status(npc) == "{npc}Guard{/npc} ({yellow}!{/yellow}) is here." + assert Format.npc_name_for_status(npc) == "{npc}Guard{/npc} ({quest}!{/quest})" + assert Format.npc_status(npc) == "{npc}Guard{/npc} ({quest}!{/quest}) is here." end end end diff --git a/test/support/networking.ex b/test/support/networking.ex index 404ed8d1b..1465abaaa 100644 --- a/test/support/networking.ex +++ b/test/support/networking.ex @@ -72,4 +72,7 @@ defmodule Test.Networking.Socket do @impl Networking.Socket def set_user_id(_socket, _user_id), do: :ok + + @impl Networking.Socket + def set_config(_socket, _config), do: :ok end