DramaticShapeVoxelMod archival mirror · read-only
lib/SettingsHelp.lua

SettingsHelp.lua

lib/SettingsHelp.lua6.9 KB10064459fa617d73
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
-- SELECT on a row explains what it does.
--
-- ------- why this exists at all
--
-- Every setting in this mod has ALWAYS carried a paragraph of help. It goes
-- into the schema handed to the mod manager (ModSetting:schema takes it), it
-- has been written and kept up to date beside every row in main.lua's
-- SETTINGS -- and nothing in the engine has ever drawn it. Not the OPTIONS
-- menu, whose row is a label and a value and has no room for a third thing;
-- not the mod manager's own page, which renders the same two lines. It was
-- authored, structured, accurate prose sitting in a field with no reader.
--
-- So it gets one. A row on this mod's menus says what it IS on one line and
-- what it is SET TO on the next, and SELECT says what that means -- which is
-- the question a row like RENDER DIST or 2D-3D B cannot answer in eighteen
-- characters however the label is worded.
--
-- SELECT rather than a button that already does something: A steps a setting,
-- B leaves, and the d-pad moves. SELECT is free on a menu -- the mod's own
-- SELECT hotkey is installed on OverworldController:handleInput, which only
-- runs while the overworld is the top state, so a menu can have the button
-- without taking anything from the map.
--
-- ------- the shape of it
--
-- The game's own dialogue box: drawn with Font.drawBox, so the border is the
-- ROM's own glyphs and a mod-supplied font theme retextures this along with
-- everything else (Font.BORDER) -- and anchored to the BOTTOM of the screen
-- with the menu still visible above it, which is where this game has put
-- every line of text anybody has ever read in it.
--
-- Sized to what it holds rather than to the screen. Each description is one
-- sentence, so most of these are five or six tiles tall and the row being
-- asked about is still on screen over the top of the box. A sentence long
-- enough to overflow scrolls instead of growing past MAX_LINES, a line at a
-- time on the d-pad -- which is a fallback, not the design: the answer to a
-- description that needs scrolling is a shorter description.

-- the mod namespace (see main.lua)
local V = ...

local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local PaletteFX = require("src.render.PaletteFX")

local SettingsHelp = {}
SettingsHelp.__index = SettingsHelp

-- NOT opaque: the menu stays drawn underneath, so the row being asked about
-- is still on screen above the box. That is most of why the box is only as
-- tall as it needs to be.
SettingsHelp.isOpaque = false

-- The box spans the screen's twenty tiles and its border owns the outer ring,
-- so text runs from tile 1. Seventeen columns rather than eighteen: tile 18
-- is kept clear for the more-arrow, which would otherwise land on top of the
-- last character of any line that filled the width.
local COLS = 17
local PEN_X = 8
local SCREEN_ROWS = 18
-- title, plus the body, plus the two border rows
local CHROME_ROWS = 3
-- A sentence needing more than this scrolls. Eight lines of seventeen is 136
-- characters, which is a long sentence and a box two thirds up the screen.
local MAX_LINES = 8

-- Break a string into lines that fit, on word boundaries. Unbounded, unlike
-- StadiumScreen's -- that one is capping a save path to what a fixed plate can
-- show, and this one is the whole point of the screen.
local function wrapped(str, cols)
  cols = cols or COLS
  local lines, line = {}, nil
  for word in tostring(str or ""):gmatch("%S+") do
    local try = line and (line .. " " .. word) or word
    if #try <= cols then
      line = try
    else
      if line then lines[#lines + 1] = line end
      -- a word longer than the line is broken across lines rather than cut;
      -- nothing in the help text is that long today, but losing the end of a
      -- sentence silently is not a failure mode worth leaving open
      while #word > cols do
        lines[#lines + 1] = word:sub(1, cols)
        word = word:sub(cols + 1)
      end
      line = word
    end
  end
  if line then lines[#lines + 1] = line end
  return lines
end

SettingsHelp.wrapped = wrapped

function SettingsHelp.new(game, title, body)
  return setmetatable({
    game = game,
    title = tostring(title or ""):gsub("%.%.$", ""),
    lines = wrapped(body),
    top = 0,
  }, SettingsHelp)
end

-- How many body lines this box shows: all of them, unless there are more than
-- a box is allowed to be tall.
function SettingsHelp:bodyRows()
  return math.min(#self.lines, MAX_LINES)
end

function SettingsHelp:maxTop()
  return math.max(0, #self.lines - self:bodyRows())
end

-- Every button that could mean "done" closes it, including SELECT itself --
-- the press that opened the box is the one a player is most likely to reach
-- for to get rid of it. A is in there too: it steps a setting everywhere else
-- on these menus, and stepping one you cannot see would be worse than an
-- extra way out.
local DISMISS = { "a", "b", "start", "select" }

function SettingsHelp:update()
  local input = self.game and self.game.input
  if not input then return end
  local maxTop = self:maxTop()
  -- the d-pad only does anything when there is something below the fold; a
  -- box showing its whole sentence has nowhere to go and says so by not
  -- moving
  if input:wasPressed("down") then
    self.top = math.min(maxTop, self.top + 1)
    return
  elseif input:wasPressed("up") then
    self.top = math.max(0, self.top - 1)
    return
  end
  for _, btn in ipairs(DISMISS) do
    if input:wasPressed(btn) then
      local stack = self.game.stack
      if self.game.data then
        require("src.core.Sound").play(self.game.data, "Press_AB")
      end
      if stack and stack:top() == self then stack:pop() end
      return
    end
  end
end

function SettingsHelp:draw()
  local body = self:bodyRows()
  local th = body + CHROME_ROWS
  local ty = SCREEN_ROWS - th          -- anchored to the bottom of the screen
  Font.drawBox(0, ty, 20, th)
  love.graphics.setColor(0, 0, 0, 1)
  -- the row's own name, so the box says what it is about even where it covers
  -- the row that was asked
  Font.draw(self.title, PEN_X, (ty + 1) * 8)
  for i = 1, body do
    local line = self.lines[self.top + i]
    if not line then break end
    Font.draw(line, PEN_X, (ty + 1 + i) * 8)
  end
  -- the same marker the options list uses for "there is more below this", so
  -- it means here what it means there
  if self.top < self:maxTop() then
    Font.drawCode(Theme.moreArrow, 144, (ty + th - 2) * 8)
  end
  love.graphics.setColor(1, 1, 1, 1)
end

-- Game:draw stops at the first state that HAS this method, so without one the
-- box would inherit whatever is underneath -- which is a menu of ours, whose
-- answer happens to be right. Stated anyway: the reason that answer is right
-- is not a property of this screen, and a future menu that paints something
-- of its own would silently repaint this box with it.
function SettingsHelp:sgbPalettes(game)
  return PaletteFX.wholeNamed(game.data, "MEWMON")
end

return SettingsHelp