From 66ea811137f42c893463239bae0c03d692e04bb7 Mon Sep 17 00:00:00 2001 From: Lokesh-shiva Date: Mon, 12 Oct 2026 18:53:01 +0530 Subject: [PATCH] fix(firered): match bag, item PC, and field UI --- src/import/RomExtractorGen3.lua | 174 +++++++++++++++++++++++++--- src/import/RomImporter.lua | 2 +- src/render/Font.lua | 60 +++++++++- src/render/TextBox.lua | 21 +++- src/script/Gen3Commands.lua | 69 +++++++++++ src/script/Gen3ScriptVM.lua | 19 ++-- src/ui/Gen3BagMenu.lua | 195 ++++++++++++++++++++++++-------- src/ui/Gen3ItemPcFRLG.lua | 34 +++++- src/ui/Gen3ItemPcMessage.lua | 15 ++- src/ui/Gen3ItemPcQuantity.lua | 26 ++++- src/ui/Gen3ItemPcSubmenu.lua | 28 ++++- src/ui/Gen3PlayerPC.lua | 67 +++++------ 12 files changed, 578 insertions(+), 132 deletions(-) diff --git a/src/import/RomExtractorGen3.lua b/src/import/RomExtractorGen3.lua index 3b0f5fcdf..da9f5af27 100644 --- a/src/import/RomExtractorGen3.lua +++ b/src/import/RomExtractorGen3.lua @@ -6994,28 +6994,107 @@ function RomExtractorGen3:extractFont() -- rounded caps) in sTextWindowPalettes[0]; the user frames only border -- menus. Baked as one 18-tile strip for Font.drawDialogueBox. local dialogueRecord = nil + local signpostRecord = nil + local standardFrameRecord = nil + local fieldTextStyles = nil + local uiTextStyles = nil if (self.manifest or {}).frlgItemMenu ~= nil then local ok, err = pcall(function() - local GFX, PAL, N = 0x41F1C8, 0x471DEC, 18 - local colors = RomGba.palette(self.rom:bytes(PAL, 32)) - local raw = self.rom:bytes(GFX, N * 32) - local px = RomGba.tiles4bpp(raw, N, 1) - local image = ImageWriter.blank(N * 8, 8) + local MSG_GFX = self:symbol("gMenuMessageWindow_Gfx") or 0x41F1C8 + local SIGN_GFX = self:symbol("gSignpostWindow_Gfx") or 0x470B0C + local PAL = self:symbol("gTextWindowPalettes") or 0x471DEC + + local function bakeFrame(gfx, paletteId, count, name) + local colors = RomGba.palette(self.rom:bytes(PAL + paletteId * 32, 32)) + local raw = self.rom:bytes(gfx, count * 32) + local px = RomGba.tiles4bpp(raw, count, 1) + local image = ImageWriter.blank(count * 8, 8) + for y = 1, 8 do + for x = 1, count * 8 do + local v = px[y][x] + -- Colour zero is transparent on the BG layer. Preserve that in + -- the baked strip so the sign/message frame can sit over the field + -- exactly like the cartridge rather than painting its transparent + -- holes with palette entry zero. + local c = v ~= 0 and colors[v + 1] + if c then + image:setPixel(x - 1, y - 1, + c[1] / 255, c[2] / 255, c[3] / 255, 1) + end + end + end + self:saveImage(image, "fonts/" .. name .. ".png") + local fill = colors[2] + return { + image = "assets/generated/fonts/" .. name .. ".png", + tile = 8, tiles = count, + fill = { fill[1] / 255, fill[2] / 255, fill[3] / 255 }, + source = ("ROM:gfx %07X, gTextWindowPalettes[%d] %07X") + :format(gfx, paletteId, PAL + paletteId * 32), + }, colors + end + + local colors + dialogueRecord, colors = bakeFrame(MSG_GFX, 0, 18, "frlg_dialogue") + -- MSGBOX_SIGN uses the exact same tile placement as a field dialogue + -- frame, but loads gSignpostWindow_Gfx and text-window palette 1 first. + -- The asset is 0x260 bytes = 19 tiles; the nineteenth tile is retained + -- even though the common frame path addresses the first eighteen. + signpostRecord = bakeFrame(SIGN_GFX, 1, 19, "frlg_signpost") + signpostRecord.layout = "frlg_signpost" + + -- DrawStdFrameWithCustomTileAndPalette is the 9-tile fixed frame used by + -- the bag DEPOSIT ITEM tab and the Item-PC transient message/question + -- windows. It is NOT the configurable user frame and therefore must not + -- follow the player's Options -> Frame Type setting. + local STD_GFX = self:symbol("gStdTextWindow_Gfx") or 0x471A4C + -- LoadStdWindowGfx pairs these tiles with GetTextWindowPalette(3). + -- Item-PC and bag calls merely choose the destination palette bank. + local STD_PAL = PAL + 3 * 32 + local stdColors = RomGba.palette(self.rom:bytes(STD_PAL, 32)) + local stdRaw = self.rom:bytes(STD_GFX, 9 * 32) + local stdPx = RomGba.tiles4bpp(stdRaw, 9, 1) + local stdImage = ImageWriter.blank(9 * 8, 8) for y = 1, 8 do - for x = 1, N * 8 do - local v = px[y][x] - local c = v ~= 0 and colors[v + 1] + for x = 1, 9 * 8 do + local v = stdPx[y][x] + local c = v ~= 0 and stdColors[v + 1] if c then - image:setPixel(x - 1, y - 1, c[1] / 255, c[2] / 255, c[3] / 255, 1) + stdImage:setPixel(x - 1, y - 1, + c[1] / 255, c[2] / 255, c[3] / 255, 1) end end end - self:saveImage(image, "fonts/frlg_dialogue.png") - local fill = colors[2] - dialogueRecord = { - image = "assets/generated/fonts/frlg_dialogue.png", tile = 8, tiles = N, - fill = { fill[1] / 255, fill[2] / 255, fill[3] / 255 }, - source = ("ROM:gMenuMessageWindow_Gfx %07X, sTextWindowPalettes %07X"):format(GFX, PAL), + self:saveImage(stdImage, "fonts/frlg_standard_frame.png") + local stdFill = stdColors[2] + standardFrameRecord = { + image = "assets/generated/fonts/frlg_standard_frame.png", + tile = 8, tiles = 9, + fill = { stdFill[1] / 255, stdFill[2] / 255, stdFill[3] / 255 }, + source = ("ROM:gStdTextWindow_Gfx %07X, gTextWindowPalettes[3] %07X") + :format(STD_GFX, STD_PAL), + } + + local function rgb(i) + local c = colors[i + 1] + return { c[1] / 255, c[2] / 255, c[3] / 255, 1 } + end + -- AddTextPrinterDiffStyle: female NPCs are red, male NPCs blue, and + -- objects/system text dark gray; all three use the light-gray shadow. + -- These are palette indices, not invented RGB values, so the runtime can + -- reproduce ContextNpcGetTextColor without baking another font page. + fieldTextStyles = { + female = { text = rgb(4), shadow = rgb(3) }, + male = { text = rgb(8), shadow = rgb(3) }, + neutral = { text = rgb(2), shadow = rgb(3) }, + mon = { text = rgb(2), shadow = rgb(3) }, + } + -- bag.c/item_pc.c both use the same first ten standard text colours. + -- Their lower description strips deliberately switch to white text over + -- the blue background, while lists/submenus use dark gray on light paper. + uiTextStyles = { + white = { text = rgb(1), shadow = rgb(2) }, + dark = { text = rgb(2), shadow = rgb(3) }, } end) if not ok then Logger.warn("gen3 frlg dialogue frame: %s", tostring(err)) end @@ -7025,6 +7104,9 @@ function RomExtractorGen3:extractFont() local perRow = 16 local cellW = math.floor(tonumber(font.cellWidth) or 8) local cellH = math.floor(tonumber(font.cellHeight) or 11) + -- gFontInfos gives FireRed FONT_NORMAL a 14px runtime height. + local frlg = (self.manifest or {}).frlgItemMenu ~= nil + if frlg and tonumber(font.fontId) == 1 then cellH = 14 end local top = math.floor(tonumber(font.inkTop) or 0) local rows = math.ceil(count / perRow) @@ -7265,7 +7347,58 @@ function RomExtractorGen3:extractFont() -- Each is a page of its own -- own cell, own widths, own picture -- and they -- are keyed by the ROLE the font id gives them, not by address. local faces = {} - for _, face in ipairs(font.faces or {}) do + local faceDefs = {} + for _, face in ipairs(font.faces or {}) do faceDefs[#faceDefs + 1] = face end + -- FireRed's field dialogue has two extra Latin faces. They are not colour + -- aliases of FONT_NORMAL: text.c decompresses dedicated Male/Female glyph + -- sheets with their own width tables, then AddTextPrinterDiffStyle paints + -- them blue/red. The generic font discovery only needs normal/small for + -- menus, so append these exact named cartridge sheets for field dialogue. + if (self.manifest or {}).frlgItemMenu ~= nil then + -- FireRed keeps NormalCopy1, Normal, Male and Female Latin width tables as + -- byte-identical 512-byte runs. The retail manifest only names Normal, so + -- locate the next two identical runs after it; each corresponding full + -- glyph sheet starts exactly 0x8000 bytes before its width table. This is + -- the same relationship used by text.c, but remains relocation-safe. + local function nextMatchingWidths(from) + local n = math.min(count, 512) + local first = self.rom:u32(widths) + for at = math.max(0, math.floor(from or 0)), self.rom.size - n do + if self.rom:u32(at) == first then + local same = true + for i = 0, n - 1 do + if self.rom:u8(at + i) ~= self.rom:u8(widths + i) then + same = false; break + end + end + if same then return at end + end + end + return nil + end + local maleWidths = self:symbol("sFontMaleLatinGlyphWidths") + or nextMatchingWidths(widths + 1) + local femaleWidths = self:symbol("sFontFemaleLatinGlyphWidths") + or (maleWidths and nextMatchingWidths(maleWidths + 1)) + local extra = { + { role = "male", fontId = 4, + glyphs = self:symbol("sFontMaleLatinGlyphs") + or (maleWidths and maleWidths - 0x8000), + widths = maleWidths }, + { role = "female", fontId = 5, + glyphs = self:symbol("sFontFemaleLatinGlyphs") + or (femaleWidths and femaleWidths - 0x8000), + widths = femaleWidths }, + } + for _, face in ipairs(extra) do + if face.glyphs and face.widths then + face.cellWidth, face.cellHeight = 8, 14 + face.glyphBytes, face.tilesAcross = 0x40, 2 + faceDefs[#faceDefs + 1] = face + end + end + end + for _, face in ipairs(faceDefs) do local role = face.role if role and face.glyphs ~= glyphs then local fw = math.floor(tonumber(face.cellWidth) or cellW) @@ -7388,6 +7521,10 @@ function RomExtractorGen3:extractFont() -- the twenty border sets the OPTION screen's FRAME row picks between frames = frameRecord, dialogueFrame = dialogueRecord, + signpostFrame = signpostRecord, + standardFrame = standardFrameRecord, + fieldTextStyles = fieldTextStyles, + uiTextStyles = uiTextStyles, -- the cursor and the more-below marker, which are drawn shapes in cells -- the cartridge leaves blank; see the note above extractFont's page pass symbols = { cursor = SYMBOLS.cursor, @@ -43869,9 +44006,12 @@ function RomExtractorGen3:extractFireRedItemPc() local cell = ty * S.COLS + tx local e = map[cell * 2 + 1] + map[cell * 2 + 2] * 256 local tid = e % 1024 + local flipX = math.floor(e / 1024) % 2 == 1 + local flipY = math.floor(e / 2048) % 2 == 1 local bank = math.floor(e / 4096) % 16 if tid < tileCount and bank < S.PALETTE_BANKS then - RomExtractorGen3.partyTile(image, tiles, colors, tid, bank, tx * 8, ty * 8) + RomExtractorGen3.partyTile(image, tiles, colors, tid, bank, + tx * 8, ty * 8, flipX, flipY) end end end diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 0591ac8f9..f0dabd0d0 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -1227,7 +1227,7 @@ end -- files/fields, so without a format bump an updated runtime would keep -- drawing the pre-fix handmade help icons and static selection cursor -- until the user happened to force a ROM re-import manually. -local CACHE_FORMAT = "rom-cache-v362:" +local CACHE_FORMAT = "rom-cache-v365:" -- The completion marker is written under each version's cache prefix -- (rom-cache.complete for Red, blue/rom-cache.complete for Blue). local MARKER_PATH = "rom-cache.complete" diff --git a/src/render/Font.lua b/src/render/Font.lua index a27c4f25b..48e236862 100644 --- a/src/render/Font.lua +++ b/src/render/Font.lua @@ -1031,8 +1031,7 @@ function Font.hasDialogueFrame() return not FALLBACK.enabled and def ~= nil and def.dialogueFrame ~= nil end -function Font.drawDialogueBox(tx, ty, tw, th) - local rec = Font.hasDialogueFrame() and state.def.dialogueFrame +local function prepareFrameStrip(rec) if rec and dialogueSheet.path ~= rec.image then local ok, img = pcall(Assets.image, rec.image) dialogueSheet.path, dialogueSheet.image = rec.image, ok and img or nil @@ -1052,6 +1051,11 @@ function Font.drawDialogueBox(tx, ty, tw, th) dialogueSheet.quads = quads end end + return rec and dialogueSheet.quads ~= nil +end + +local function drawDialogueRecord(rec, tx, ty, tw, th) + prepareFrameStrip(rec) if not (rec and dialogueSheet.quads) then return Font.drawBox(tx, ty, tw, th) end @@ -1076,6 +1080,12 @@ function Font.drawDialogueBox(tx, ty, tw, th) for row = 0, th - 1 do local flip = row >= half local depth = flip and (th - 1 - row) or row + -- WindowFunc_DrawDialogueFrame uses a different lower-half set order for + -- MSGBOX_SIGN: set1, set2, set0 instead of set2, set1, set0. + if flip and rec.layout == "frlg_signpost" then + local signDepth = { [0] = 0, [1] = 2, [2] = 1 } + depth = signDepth[depth] or depth + end local set = SETS[math.min(depth, 2) + 1] local y = (ty + row) * 8 for col = 0, cols - 1 do @@ -1091,4 +1101,50 @@ function Font.drawDialogueBox(tx, ty, tw, th) end end +function Font.drawDialogueBox(tx, ty, tw, th) + local rec = Font.hasDialogueFrame() and state.def.dialogueFrame + return drawDialogueRecord(rec, tx, ty, tw, th) +end + +function Font.hasSignpostFrame() + local def = state and state.def + return not FALLBACK.enabled and def ~= nil and def.signpostFrame ~= nil +end + +function Font.drawSignpostBox(tx, ty, tw, th) + local rec = Font.hasSignpostFrame() and state.def.signpostFrame + if not rec then return Font.drawDialogueBox(tx, ty, tw, th) end + return drawDialogueRecord(rec, tx, ty, tw, th) +end + +function Font.drawStandardBox(tx, ty, tw, th) + local def = state and state.def + local rec = not FALLBACK.enabled and def and def.standardFrame or nil + if not (rec and prepareFrameStrip(rec) and dialogueSheet.quads) then + return Font.drawBox(tx, ty, tw, th, 1) + end + if tw < 2 or th < 2 then return Font.drawBox(tx, ty, tw, th, 1) end + local img, q = dialogueSheet.image, dialogueSheet.quads + local fill = rec.fill or { 1, 1, 1 } + love.graphics.setColor(fill[1], fill[2], fill[3], 1) + love.graphics.rectangle("fill", (tx + 1) * 8, (ty + 1) * 8, + math.max(0, tw - 2) * 8, math.max(0, th - 2) * 8) + love.graphics.setColor(1, 1, 1, 1) + for row = 0, th - 1 do + for col = 0, tw - 1 do + local tile + if row == 0 then + tile = col == 0 and 0 or (col == tw - 1 and 2 or 1) + elseif row == th - 1 then + tile = col == 0 and 6 or (col == tw - 1 and 8 or 7) + elseif col == 0 then + tile = 3 + elseif col == tw - 1 then + tile = 5 + end + if tile then love.graphics.draw(img, q[tile], (tx + col) * 8, (ty + row) * 8) end + end + end +end + return Font diff --git a/src/render/TextBox.lua b/src/render/TextBox.lua index bb7db70ba..763dbd917 100644 --- a/src/render/TextBox.lua +++ b/src/render/TextBox.lua @@ -47,6 +47,9 @@ function TextBox.new(game, text, onDone, opts) local self = setmetatable({}, TextBox) self.game = game self.onDone = onDone + self.textFace = opts.face + local facePushed = self.textFace and Font.hasFace and Font.hasFace(self.textFace) + and Font.pushFace(self.textFace) self.choice = opts.choice self.defaultNo = opts.defaultNo self.choiceNoSound = opts.noSound @@ -88,10 +91,14 @@ function TextBox.new(game, text, onDone, opts) self.scrollHoldFrames = tonumber(opts.scrollHoldFrames) self.textX = opts.textX or (self.boxTx + 1) * 8 self.drawFrame = opts.drawFrame ~= false + self.signpost = opts.signpost == true self.fillColor = opts.fillColor self.textStyle = opts.style self.promptArrow = opts.promptArrow or box.promptArrow self.promptArrowAtPen = opts.promptArrowAtPen == true or box.promptArrowAtPen == true + -- GBA field text only draws the pen arrow for explicit page/scroll prompt + -- control codes; reaching the final terminator waits silently for A/B. + self.promptOnlyWhenWaiting = opts.promptOnlyWhenWaiting == true self.promptArrowImage = nil self.clipToBox = opts.clipToBox == true self.uiWidth, self.uiHeight = opts.uiWidth, opts.uiHeight @@ -133,6 +140,7 @@ function TextBox.new(game, text, onDone, opts) self.done = false self.blink = 0 self:beginLine() + if facePushed then Font.popFace() end return self end @@ -620,7 +628,11 @@ function TextBox:draw() self.boxTw * 8, self.boxTh * 8, "bottom") end if self.drawFrame then - Font.drawDialogueBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh) + if self.signpost and Font.drawSignpostBox then + Font.drawSignpostBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh) + else + Font.drawDialogueBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh) + end elseif self.fillColor then local c = self.fillColor love.graphics.setColor(c[1] or 0, c[2] or 0, c[3] or 0, c[4] or 1) @@ -634,6 +646,8 @@ function TextBox:draw() self.boxTw * 8, self.boxTh * 8) end love.graphics.setColor(0, 0, 0, 1) + local facePushed = self.textFace and Font.hasFace and Font.hasFace(self.textFace) + and Font.pushFace(self.textFace) if self.textStyle then Font.pushStyle(self.textStyle) end if self.scrollPx and self.scrollPx > 0 then self.scrollPx = self.scrollPx - (self.scrollStep or 2) @@ -666,8 +680,8 @@ function TextBox:draw() end lastPen, lastY = pen, y end - local prompt = self.waiting or (self.done and not self.choice and not self.auto - and not self.stay) + local prompt = self.waiting or (self.done and not self.promptOnlyWhenWaiting + and not self.choice and not self.auto and not self.stay) if prompt and self.promptArrowAtPen and type(self.promptArrow) == "table" and type(self.promptArrow.path) == "string" and lastPen and lastY then if self.promptArrowImage == nil then @@ -694,6 +708,7 @@ function TextBox:draw() (self.boxTy + self.boxTh - 1) * 8 - 4) end if self.textStyle then Font.popStyle() end + if facePushed then Font.popFace() end if self.clipToBox then if oldScissor and oldScissor[1] ~= nil then love.graphics.setScissor(oldScissor[1], oldScissor[2], diff --git a/src/script/Gen3Commands.lua b/src/script/Gen3Commands.lua index f908d36db..d384d2aeb 100644 --- a/src/script/Gen3Commands.lua +++ b/src/script/Gen3Commands.lua @@ -70,6 +70,75 @@ local VAR_LAST_TALKED = 0x800F -- comes upstairs and then says nothing at all. local VAR_FACING = 0x800C +-- FireRed's ContextNpcGetTextColor maps fixed object-event graphics 0..91 to +-- male/female dialogue faces. IDs 92..108 (and 151) are neutral objects and +-- 109..150 are Pokemon; those two both use the normal dark field face. The +-- female half of the cartridge's packed sTextColorTable is listed explicitly; +-- every other human ID in 0..91 is male. +Gen3Commands._frlgFemaleGfx = Gen3Commands._frlgFemaleGfx or {} +for _, id in ipairs({ + 7,8,9,10,11,12,13,15,17,22,23,24,28,29,31,35,37,40,42,44,46,48,50, + 58,64,65,66,75,76,77,81,83,85,88, +}) do Gen3Commands._frlgFemaleGfx[id] = true end + +function Gen3Commands._frlgSpeakerRole(ctx) + -- Explicit `textcolor` wins over the selected object exactly like + -- ContextNpcGetTextColor. The script constants are male=0, female=1, + -- mon=2, neutral=3; 255 means use the selected object/default again. + local explicit = tonumber(ctx and ctx.g3TextColor) + if explicit and explicit ~= 255 then + if explicit == 0 then return "male" end + if explicit == 1 then return "female" end + if explicit == 2 then return "mon" end + return "neutral" + end + local id = tonumber(ctx and ctx.npc and ctx.npc.def and ctx.npc.def.graphicsId) + if not id then return "neutral" end + if id >= 0 and id <= 91 then + return Gen3Commands._frlgFemaleGfx[id] and "female" or "male" + end + if id >= 109 and id <= 150 then return "mon" end + return "neutral" +end + +function Gen3Commands._frlgFieldTextOpts(ctx, signOverride) + local data = ctx and ctx.game and ctx.game.data + local font = data and data.font + local styles = font and font.fieldTextStyles + -- Only FireRed publishes these styles; other Gen 3 games therefore keep the + -- exact generic show_text behaviour they had before this fidelity pass. + if type(styles) ~= "table" then + return signOverride ~= nil and { signpost = signOverride } or nil + end + local role = Gen3Commands._frlgSpeakerRole(ctx) + local opts = { + style = styles[role] or styles.neutral, + face = (role == "male" or role == "female") and role or nil, + promptOnlyWhenWaiting = true, + -- AddTextPrinterParameterized2: content y=15 tiles, y+1 baseline; + -- 14px Latin face + 1px line spacing. + line1Y = 121, line2Y = 136, scrollDistance = 15, + } + if signOverride ~= nil then + opts.signpost = signOverride and true or false + elseif ctx.g3SignMessage then + opts.signpost = true + end + return opts +end + +function Gen3Commands.g3_text_color(ctx, color) + ctx.g3TextColor = tonumber(color) +end + +function Gen3Commands.g3_sign_message(ctx, enabled) + ctx.g3SignMessage = enabled and true or false +end + +function Gen3Commands.g3_show_text(ctx, text, signOverride) + return Commands.show_text(ctx, text, nil, Gen3Commands._frlgFieldTextOpts(ctx, signOverride)) +end + local function varStore(save) if not save then return nil end save.gen3Vars = save.gen3Vars or {} diff --git a/src/script/Gen3ScriptVM.lua b/src/script/Gen3ScriptVM.lua index 850f6c877..d371faac9 100644 --- a/src/script/Gen3ScriptVM.lua +++ b/src/script/Gen3ScriptVM.lua @@ -239,10 +239,12 @@ L.vcall_if = function(ir, s) return L.call_if(ir, s) end L.vbuffermessage = L.nop L.vbufferstring = L.nop --- Text STYLING, which this engine's one text box does not vary. -L.textcolor = L.nop -L.signmsg = L.nop -L.normalmsg = L.nop +-- FireRed varies field text by speaker and can explicitly override that choice +-- with `textcolor`; `signmsg` swaps the field-message frame until `normalmsg`. +-- Keep those two bits on the Gen 3 runtime context instead of discarding them. +L.textcolor = function(ir, s) emit(s, { "g3_text_color", ir[2] }) end +L.signmsg = function(_, s) emit(s, { "g3_sign_message", true }) end +L.normalmsg = function(_, s) emit(s, { "g3_sign_message", false }) end L.loadhelp = L.nop L.unloadhelp = L.nop L.drawbox = L.nop @@ -302,12 +304,12 @@ L.message = function(ir, s) local key = type(ir[2]) == "string" and ir[2] or s.lastText if key then s.lastText = key - emit(s, { "show_text", key }) + emit(s, { "g3_show_text", key }) end end L.messageautoscroll = function(ir, s) local key = type(ir[3]) == "string" and ir[3] or s.lastText - if key then emit(s, { "show_text", key }) end + if key then emit(s, { "g3_show_text", key }) end end L.messageinstant = L.message @@ -375,7 +377,10 @@ local function std(ir, s, isJump) local index = tonumber(ir[2]) for _, row in ipairs(STD_PROLOGUE[index] or {}) do emit(s, row) end if PLAIN_MSGBOX[index] then - if s.lastText then emit(s, { "show_text", s.lastText }) end + if s.lastText then + emit(s, { "g3_show_text", s.lastText, + index == STD_MSGBOX_SIGN and true or nil }) + end elseif index == STD_MSGBOX_YESNO then -- `ask` leaves the answer on ctx; g3_from_yesno copies it into VAR_RESULT, -- which is what the compare_var_to_value that always follows will read diff --git a/src/ui/Gen3BagMenu.lua b/src/ui/Gen3BagMenu.lua index 6f9356992..69dd61b73 100644 --- a/src/ui/Gen3BagMenu.lua +++ b/src/ui/Gen3BagMenu.lua @@ -262,6 +262,14 @@ function Gen3BagMenu.new(game, opts) self.store = opts.store self.pcList = (self.store == "withdraw" or self.store == "toss") if self.pcList then self.lockPocket = true end + -- FireRed's DEPOSIT ITEM opens the real bag at the ITEMS pocket, but its + -- ITEMMENULOCATION_ITEMPC mode disables pocket switching. The striped bag + -- and the bag sprite are still the real bag; only the title/window semantics + -- and the input mode change (item_menu.c GoToBagMenu / SwitchBagPocket). + if self.frlg and self.itemPc then + self.pocket = 1 + self.lockPocket = true + end if opts.pocket then for i, row in ipairs(self.pockets) do if row.key == opts.pocket then self.pocket = i break end @@ -298,9 +306,32 @@ end -- the importer multiplies out, so everything here is already pixels. function Gen3BagMenu:box(key) local r = self:screen() + if self.frlg and key == "quantity" then + -- sWindowTemplates[0], opened by InitQuantityToTossOrDeposit. + return { x = 192, y = 120, width = 40, height = 32 } + end return (r.windows and r.windows[key]) or FALLBACK.windows[key] end +function Gen3BagMenu:subBox(key) + if self.frlg and key == "quantityPrompt" then + -- ShowBagWindow(6, 2) -> sWindowTemplates[8]. + return { x = 48, y = 120, width = 128, height = 32 } + end + return nil +end + +function Gen3BagMenu:drawContentFrame(b, frameIndex) + if not b then return end + local tx, ty = math.floor(b.x / 8) - 1, math.floor(b.y / 8) - 1 + local tw, th = math.floor(b.width / 8) + 2, math.floor(b.height / 8) + 2 + if frameIndex == "standard" and Font.drawStandardBox then + Font.drawStandardBox(tx, ty, tw, th) + else + Font.drawBox(tx, ty, tw, th, tonumber(frameIndex) or 1) + end +end + function Gen3BagMenu:listRows() local r = self:screen() local n = math.floor(tonumber(r.list and r.list.rows) or FALLBACK.list.rows) @@ -364,9 +395,14 @@ function Gen3BagMenu:rebuild() } end end - -- CLOSE BAG is a row, not a button: B and this land in the same place, and - -- the cartridge lists it at the end of every pocket. - rows[#rows + 1] = { close = true, label = Strings("CLOSE BAG") } + -- The LIST row is CANCEL on FireRed. "Close the BAG." belongs to the + -- description window underneath it (Bag_BuildListMenuTemplate versus + -- PrintItemDescriptionOnMessageWindow); using the description as the row + -- label is what produced the screenshot's incorrect CLOSE BAG entry. + rows[#rows + 1] = { + close = true, + label = self.frlg and Strings("CANCEL") or Strings("CLOSE BAG"), + } self.rows = rows self.index = math.min(self.index, #rows) self.top = math.max(1, math.min(self.top, #rows - self:listRows() + 1)) @@ -494,17 +530,36 @@ end -- ...and one it stops for, which is a real box the player dismisses. Used -- for the answers ("Withdrew 5 POTIONs."), which the cartridge waits on. function Gen3BagMenu:tell(text) + -- FireRed's deposit flow never leaves the bag for its answer. ShowBagWindow + -- overlays sWindowTemplates[9] on the lower-right description area and waits + -- for A/B there. Keep that message owned by this state so the bag, item + -- icon and list remain visible behind it instead of flashing back to field. + if self.frlg and self.itemPc then + self.resultMessage = tostring(text or "") + return + end local TextBox = require("src.render.TextBox") self.game.stack:push(TextBox.new(self.game, text)) end -- Ask how many, then act. An important item never reaches here (the callers --- refuse it first) and a stack of one still asks, because the cartridge's --- quantity window opens on every depositable pick. +-- refuse it first). FireRed's ITEMPC deposit has its own lower-screen prompt +-- geometry; the generic quantity state remains for the shared non-ITEMPC paths. function Gen3BagMenu:askQuantity(id, held, prompt, done) local game = self.game local def = game.data.items and game.data.items[id] local name = (def and def.name) or id + if self.frlg and self.itemPc and self.store == "deposit" then + game.stack:push(require("src.ui.Gen3ItemPcQuantity").new(game, self, { + max = math.max(1, math.floor(held or 1)), + itemName = name, + verb = "DEPOSIT", + onDone = function(qty) + if qty and qty > 0 then done(qty, name) end + end, + })) + return + end local QuantityBox = require("src.ui.QuantityBox") self:say(fillLine(prompt, name, held)) game.stack:push(QuantityBox.new(game, { @@ -528,14 +583,22 @@ function Gen3BagMenu:depositItem(id) return self:tell(bagWord(game, "noRoomStore")) end local held = (game.save.inventory or {})[id] or 1 - self:askQuantity(id, held, bagWord(game, "depositPrompt"), function(qty, name) + local function deposit(qty, name) Bag.remove(game.save, id, qty) game.save.pcItems = game.save.pcItems or {} game.save.pcItems[id] = (game.save.pcItems[id] or 0) + qty self:rebuild() Sound.play(game.data, "Withdraw_Deposit") self:tell(fillLine(bagWord(game, "deposited"), name, qty)) - end) + end + -- item_menu.c Task_ChooseHowManyToDeposit skips the quantity UI when there + -- is exactly one. That is the common early-game POTION case in the user's + -- comparison and it also avoids a one-item selector the cartridge never + -- shows. + local def = game.data.items and game.data.items[id] + local name = (def and def.name) or id + if held <= 1 then return deposit(1, name) end + self:askQuantity(id, held, bagWord(game, "depositPrompt"), deposit) end -- WITHDRAW: the PC's list, the PC's words, the bag as the destination. @@ -800,6 +863,13 @@ function Gen3BagMenu:update(dt) if self.script then self.script(self) end if self.noInput then return end local input = self.game.input + if self.resultMessage then + if input:wasPressed("a") or input:wasPressed("b") then + Sound.play(self.game.data, "Press_AB") + self.resultMessage = nil + end + return + end if input:wasPressed("down") then self:moveCursor(1) elseif input:wasPressed("up") then self:moveCursor(-1) elseif input:wasPressed("right") then self:movePocket(1) @@ -868,39 +938,27 @@ local function drawRows(self, inset) Font.draw(qty, qtyRight - Font.width(qty), y) end if first + i == self.index then - -- WHAT MARKS THE CHOSEN ROW, and it is not an arrow. - -- - -- Reported from play: "the highlighted item is supposed to have a red - -- box flashing around it not a arrow for the bag". The port drew - -- Theme.cursor -- the Game Boy's triangle -- because that is what every - -- other list here uses; Hoenn's bag frames the whole row instead, and - -- the frame blinks. - -- - -- THE COLOUR IS THE CARTRIDGE'S OWN, not a red written down here: the - -- pocket-dot marker's colour is already read out of both palettes - -- (gen3BagScreen.pocketDots.selected.colour), and it is the same - -- selection colour -- red in the boy's palette, blue in the girl's -- - -- so the two marks on this screen agree by construction rather than by - -- a second opinion. A cache without it falls back to red, which is - -- what the boy's palette holds. - local mark = (r.pocketDots or {}).selected - local player = (self.game.save or {}).player or {} - local rgb = (mark and mark.colour - and ((player.gender == "girl" and mark.colour.female) - or mark.colour.male or mark.colour.female)) - or { 255, 0, 0 } - -- half a second lit, half dark, off the screen's own frame count - if math.floor((self.tick or 0) / 30) % 2 == 0 then - local boxY = top + i * pitch - love.graphics.setColor(rgb[1] / 255, rgb[2] / 255, rgb[3] / 255, 1) - love.graphics.rectangle("fill", cursorX, boxY, win.width - (cursorX - win.x), 1) - love.graphics.rectangle("fill", cursorX, boxY + pitch - 1, - win.width - (cursorX - win.x), 1) - love.graphics.rectangle("fill", cursorX, boxY, 1, pitch) - love.graphics.rectangle("fill", win.x + win.width - 1, boxY, 1, pitch) - -- back to the ink the rows are printed in, not to white: the next - -- row's label draws immediately after this - love.graphics.setColor(0, 0, 0, 1) + if self.frlg then + -- FRLG uses the ListMenu triangle cursor at the list template cursor x. + -- The flashing full-row outline belongs to the other Gen 3 bag path. + Font.drawCode(Theme.cursor, cursorX, y) + else + local mark = (r.pocketDots or {}).selected + local player = (self.game.save or {}).player or {} + local rgb = (mark and mark.colour + and ((player.gender == "girl" and mark.colour.female) + or mark.colour.male or mark.colour.female)) + or { 255, 0, 0 } + if math.floor((self.tick or 0) / 30) % 2 == 0 then + local boxY = top + i * pitch + love.graphics.setColor(rgb[1] / 255, rgb[2] / 255, rgb[3] / 255, 1) + love.graphics.rectangle("fill", cursorX, boxY, win.width - (cursorX - win.x), 1) + love.graphics.rectangle("fill", cursorX, boxY + pitch - 1, + win.width - (cursorX - win.x), 1) + love.graphics.rectangle("fill", cursorX, boxY, 1, pitch) + love.graphics.rectangle("fill", win.x + win.width - 1, boxY, 1, pitch) + love.graphics.setColor(0, 0, 0, 1) + end end end end @@ -966,11 +1024,12 @@ end -- The 24x24 picture for one item, off the atlas the importer composes from -- gItemIconTable. The slot IS the item's cartridge index, so there is no -- table to walk: icon n sits at (n % cols, n / cols). -function Gen3BagMenu:itemIcon(id) +function Gen3BagMenu:itemIcon(id, forcedIndex) local rec = (self.game.data.constants or {}).gen3ItemIcons if type(rec) ~= "table" or type(rec.image) ~= "string" then return nil end local def = id and self.game.data.items and self.game.data.items[id] - local index = def and tonumber(def.index) + local index = forcedIndex ~= nil and tonumber(forcedIndex) + or (def and tonumber(def.index)) if not index then return nil end local count = math.floor(tonumber(rec.count) or 0) if index < 0 or index >= count then index = math.floor(tonumber(rec.blank) or 0) end @@ -985,7 +1044,7 @@ function Gen3BagMenu:itemIcon(id) end function Gen3BagMenu:draw() - local inset = math.max(0, math.floor((ROW_PITCH - Font.glyphHeight()) / 2)) + local inset = self.frlg and 0 or math.max(0, math.floor((ROW_PITCH - Font.glyphHeight()) / 2)) local r = self:screen() -- THE PICTURE IS 240 COLUMNS AND THE SURFACE MAY BE WIDER. @@ -1112,8 +1171,11 @@ function Gen3BagMenu:draw() -- half the difference on top of that, as if the record were the box, moved -- it four right and two down and put its right-hand column on the border. local row = self:selected() - if row and row.id then - local icon, iquad = self:itemIcon(row.id) + if row and (row.id or (row.close and self.frlg)) then + -- CreateItemMenuIcon(ITEMS_COUNT) is FireRed's RETURN/CANCEL arrow. It is + -- slot 375 in gItemIconTable (include/constants/items.h), immediately after + -- the last real item; the import already bakes that slot into this atlas. + local icon, iquad = self:itemIcon(row.id, row.close and self.frlg and 375 or nil) if icon and iquad then local cell = r.itemIcon or FALLBACK.itemIcon love.graphics.setColor(1, 1, 1, 1) @@ -1133,12 +1195,29 @@ function Gen3BagMenu:draw() love.graphics.setColor(0, 0, 0, 1) local pocketBox = self:box("pocketName") local name = self.pcList and Gen3BagMenu.pcWord(self.game, "title") + or (self.itemPc and self.frlg and "DEPOSIT ITEM") or self.pockets[self.pocket].name - local nameY = pocketBox.y - + math.max(0, math.floor((pocketBox.height - Font.glyphHeight()) / 2)) + if self.itemPc and self.frlg then + -- sDefaultBagWindowsDeposit[2] is content (1,1) 8x2. Its standard frame + -- sits one tile outside, so the complete fixed Type-1 box is 10x4 at 0,0. + -- The normal pocket-name window is 9 tiles wide and palette 15; deposit is + -- deliberately the narrower 8-tile palette-12 window seen in retail. + pocketBox = { x = 8, y = 8, width = 64, height = 16 } + if Font.drawStandardBox then Font.drawStandardBox(0, 0, 10, 4) + else Font.drawBox(0, 0, 10, 4, 1) end + end + local small = self.itemPc and self.frlg and Font.hasFace and Font.hasFace("small") + local faced = small and Font.pushFace("small") + local uiStyles = self.game.data.font and self.game.data.font.uiTextStyles or {} + local titleStyle = self.frlg and (self.itemPc and uiStyles.dark or uiStyles.white) + if titleStyle then Font.pushStyle(titleStyle) end Font.draw(name, pocketBox.x + math.max(0, math.floor((pocketBox.width - Font.width(name)) / 2)), - nameY) + pocketBox.y + math.max(0, math.floor((pocketBox.height - Font.glyphHeight()) / 2))) + if titleStyle then Font.popStyle() end + if faced then Font.popFace() end + if self.frlg and uiStyles.dark then Font.pushStyle(uiStyles.dark) end drawRows(self, inset) + if self.frlg and uiStyles.dark then Font.popStyle() end -- the description panel, which is the half of this screen the Game Boy bag -- does not have at all @@ -1154,7 +1233,8 @@ function Gen3BagMenu:draw() local describe = ((self.game.data.constants or {}).gen3PCMenu or {}).describe text = (type(describe) == "table" and describe[#describe]) or "" else - text = row and (row.close and Strings("Close the BAG.") + text = row and (row.close and (self.frlg and Strings("CLOSE BAG") + or Strings("Close the BAG.")) or row.description) or "" end -- FireRed prints three lines at the face's own fourteen-pixel pitch from @@ -1162,10 +1242,27 @@ function Gen3BagMenu:draw() local pitch = tonumber(D.lineHeight) or ROW_PITCH local y = desc.y + (tonumber(D.y) or 1) + (D.lineHeight and r.layout == "frlg" and 0 or inset) + local descStyle = self.frlg and uiStyles.white + if descStyle then Font.pushStyle(descStyle) end for line in tostring(text or ""):gmatch("[^\n]+") do Font.draw(line, desc.x + (tonumber(D.x) or 3), y) y = y + pitch end + if descStyle then Font.popStyle() end + if self.resultMessage then + -- sWindowTemplates[9] content: (6,15), 23x4. DrawStdFrameWithCustomTile... + -- adds one tile around it, producing the retail 200x48 panel from x40. + if Font.drawStandardBox then Font.drawStandardBox(5, 14, 25, 6) + else Font.drawBox(5, 14, 25, 6, 1) end + love.graphics.setColor(0, 0, 0, 1) + local my = 122 + if self.frlg and uiStyles.dark then Font.pushStyle(uiStyles.dark) end + for line in (self.resultMessage .. "\n"):gmatch("([^\n]*)\n") do + Font.draw(line, 48, my) + my = my + 14 + end + if self.frlg and uiStyles.dark then Font.popStyle() end + end love.graphics.pop() love.graphics.setColor(1, 1, 1, 1) end diff --git a/src/ui/Gen3ItemPcFRLG.lua b/src/ui/Gen3ItemPcFRLG.lua index 67b2398de..0c830b712 100644 --- a/src/ui/Gen3ItemPcFRLG.lua +++ b/src/ui/Gen3ItemPcFRLG.lua @@ -72,6 +72,7 @@ end function Gen3ItemPcFRLG:uiSize() return GBA_W, GBA_H end function Gen3ItemPcFRLG:wantsFillScale() return true end +function Gen3ItemPcFRLG:wantsEdgeBleed() return false end function Gen3ItemPcFRLG:sgbPalettes() local P = require("src.render.PaletteFX") @@ -95,6 +96,24 @@ function Gen3ItemPcFRLG:subBox(key) return (r.subwindows and r.subwindows[key]) or FALLBACK.subwindows[key] end +-- Every rectangle item_pc.c stores in a WindowTemplate is the CONTENT area. +-- DrawStdFrameWithCustomTileAndPalette adds one 8px tile around it. The old +-- overlays treated those content dimensions as the complete frame, shrinking +-- every transient window and pushing the right-hand submenu partly off-screen. +function Gen3ItemPcFRLG:drawContentFrame(b, frameIndex) + if not b then return end + local tx, ty = math.floor(b.x / 8) - 1, math.floor(b.y / 8) - 1 + local tw, th = math.floor(b.width / 8) + 2, math.floor(b.height / 8) + 2 + if frameIndex == "standard" and Font.drawStandardBox then + Font.drawStandardBox(tx, ty, tw, th) + else + -- ItemPc_SetBorderStyleOnWindow uses the player's current user frame for + -- submenu / quantity windows. Only callers that explicitly request the + -- fixed standard frame should override it. + Font.drawBox(tx, ty, tw, th, tonumber(frameIndex)) + end +end + function Gen3ItemPcFRLG:listRows() local r = self:screen() return math.max(1, math.floor(tonumber(r.list and r.list.rows) @@ -236,6 +255,8 @@ function Gen3ItemPcFRLG:drawRows() local cursorX = win.x + (tonumber(L.cursorX) or FALLBACK.list.cursorX) local quantityX = win.x + (tonumber(L.quantityX) or FALLBACK.list.quantityX) + local uiStyles = self.game.data.font and self.game.data.font.uiTextStyles or {} + if uiStyles.dark then Font.pushStyle(uiStyles.dark) end for i = 0, self:listRows() - 1 do local row = self.rows[self.top + i] if not row then break end @@ -254,6 +275,7 @@ function Gen3ItemPcFRLG:drawRows() Font.drawCode(Theme.cursorHollow, cursorX, y) end end + if uiStyles.dark then Font.popStyle() end end function Gen3ItemPcFRLG:draw() @@ -276,7 +298,10 @@ function Gen3ItemPcFRLG:draw() local label = self:box("label") local labelText = tostring(self.title or ""):gsub(" ", "\n", 1) local faced = Font.hasFace and Font.hasFace("small") and Font.pushFace("small") + local uiStyles = self.game.data.font and self.game.data.font.uiTextStyles or {} + if uiStyles.white then Font.pushStyle(uiStyles.white) end drawLines(labelText, label.x, label.y + 1, 13) + if uiStyles.white then Font.popStyle() end if faced then Font.popFace() end self:drawRows() @@ -287,14 +312,17 @@ function Gen3ItemPcFRLG:draw() local D = self:screen().description or FALLBACK.description local description = row and (row.close and Strings("Return to the PC.") or row.description) or "" + if uiStyles.white then Font.pushStyle(uiStyles.white) end drawLines(description, desc.x + (tonumber(D.x) or 0), desc.y + (tonumber(D.y) or 3), tonumber(D.lineHeight) or FALLBACK.description.lineHeight) + if uiStyles.white then Font.popStyle() end end - if row and row.id then - local ok, icon, quad, size = - pcall(require("src.ui.Gen3BagMenu").itemIcon, { game = self.game }, row.id) + if row and (row.id or row.close) then + local BagScreen = require("src.ui.Gen3BagMenu") + local ok, icon, quad, size = pcall(BagScreen.itemIcon, + { game = self.game }, row.id, row.close and 375 or nil) if ok and icon and quad then local cell = self:screen().itemIcon or FALLBACK.itemIcon love.graphics.setColor(1, 1, 1, 1) diff --git a/src/ui/Gen3ItemPcMessage.lua b/src/ui/Gen3ItemPcMessage.lua index b00efac39..bd85f04d8 100644 --- a/src/ui/Gen3ItemPcMessage.lua +++ b/src/ui/Gen3ItemPcMessage.lua @@ -16,6 +16,10 @@ local Gen3ItemPcMessage = {} Gen3ItemPcMessage.__index = Gen3ItemPcMessage Gen3ItemPcMessage.isOpaque = false +function Gen3ItemPcMessage:uiSize() return 240, 160 end +function Gen3ItemPcMessage:wantsFillScale() return true end +function Gen3ItemPcMessage:wantsEdgeBleed() return false end + function Gen3ItemPcMessage.new(game, itemPc, text, onDone, kind) local self = setmetatable({}, Gen3ItemPcMessage) self.game = game @@ -57,14 +61,21 @@ end function Gen3ItemPcMessage:draw() local b = self:messageBox() - Font.drawBox(math.floor(b.x / 8), math.floor(b.y / 8), - math.floor(b.width / 8), math.floor(b.height / 8)) + if self.itemPc and self.itemPc.drawContentFrame then + self.itemPc:drawContentFrame(b, "standard") + else + Font.drawBox(math.floor(b.x / 8) - 1, math.floor(b.y / 8) - 1, + math.floor(b.width / 8) + 2, math.floor(b.height / 8) + 2, 1) + end love.graphics.setColor(0, 0, 0, 1) + local styles = self.game.data.font and self.game.data.font.uiTextStyles or {} + if styles.dark then Font.pushStyle(styles.dark) end local y = b.y + 2 for line in (self.text .. "\n"):gmatch("([^\n]*)\n") do Font.draw(line, b.x, y) y = y + 14 end + if styles.dark then Font.popStyle() end love.graphics.setColor(1, 1, 1, 1) end diff --git a/src/ui/Gen3ItemPcQuantity.lua b/src/ui/Gen3ItemPcQuantity.lua index bbea9fde7..74cddea0c 100644 --- a/src/ui/Gen3ItemPcQuantity.lua +++ b/src/ui/Gen3ItemPcQuantity.lua @@ -20,6 +20,10 @@ local Gen3ItemPcQuantity = {} Gen3ItemPcQuantity.__index = Gen3ItemPcQuantity Gen3ItemPcQuantity.isOpaque = false +function Gen3ItemPcQuantity:uiSize() return 240, 160 end +function Gen3ItemPcQuantity:wantsFillScale() return true end +function Gen3ItemPcQuantity:wantsEdgeBleed() return false end + function Gen3ItemPcQuantity.new(game, itemPc, opts) opts = opts or {} local self = setmetatable({}, Gen3ItemPcQuantity) @@ -87,20 +91,34 @@ function Gen3ItemPcQuantity:draw() or { x = 192, y = 120, width = 40, height = 32 } local prompt = self.itemPc and self.itemPc:subBox("quantityPrompt") or { x = 48, y = 120, width = 128, height = 32 } + local styles = self.game.data.font and self.game.data.font.uiTextStyles or {} -- sSubwindowTemplates[1], the question beside the number. - Font.drawBox(prompt.x / 8, prompt.y / 8, prompt.width / 8, prompt.height / 8) + if self.itemPc and self.itemPc.drawContentFrame then + self.itemPc:drawContentFrame(prompt, "standard") + else + Font.drawBox(prompt.x / 8 - 1, prompt.y / 8 - 1, + prompt.width / 8 + 2, prompt.height / 8 + 2, 1) + end love.graphics.setColor(0, 0, 0, 1) - drawLines(Strings("Withdraw how many") .. "\n" .. self.itemName .. Strings("(s)?"), + if styles.dark then Font.pushStyle(styles.dark) end + local verb = self.verb == "DEPOSIT" and Strings("Deposit how many") + or Strings("Withdraw how many") + drawLines(verb .. "\n" .. self.itemName .. Strings("(s)?"), prompt.x, prompt.y + 2) -- sWindowTemplates[3] + ItemPc_AddTextPrinterParameterized(..., 8, 10). - Font.drawBox(math.floor(q.x / 8), math.floor(q.y / 8), - math.floor(q.width / 8), math.floor(q.height / 8)) + if self.itemPc and self.itemPc.drawContentFrame then + self.itemPc:drawContentFrame(q) + else + Font.drawBox(math.floor(q.x / 8) - 1, math.floor(q.y / 8) - 1, + math.floor(q.width / 8) + 2, math.floor(q.height / 8) + 2, 1) + end love.graphics.setColor(0, 0, 0, 1) local faced = Font.hasFace and Font.hasFace("small") and Font.pushFace("small") Font.draw(("×%03d"):format(self.qty), q.x + 8, q.y + 10) if faced then Font.popFace() end + if styles.dark then Font.popStyle() end love.graphics.setColor(1, 1, 1, 1) end diff --git a/src/ui/Gen3ItemPcSubmenu.lua b/src/ui/Gen3ItemPcSubmenu.lua index 982eab4c7..0e16d55a6 100644 --- a/src/ui/Gen3ItemPcSubmenu.lua +++ b/src/ui/Gen3ItemPcSubmenu.lua @@ -15,6 +15,10 @@ local Gen3ItemPcSubmenu = {} Gen3ItemPcSubmenu.__index = Gen3ItemPcSubmenu Gen3ItemPcSubmenu.isOpaque = false +function Gen3ItemPcSubmenu:uiSize() return 240, 160 end +function Gen3ItemPcSubmenu:wantsFillScale() return true end +function Gen3ItemPcSubmenu:wantsEdgeBleed() return false end + local ENTRIES = { { label = "WITHDRAW", kind = "withdraw" }, { label = "GIVE", kind = "give" }, @@ -69,20 +73,32 @@ end function Gen3ItemPcSubmenu:draw() local b = self.itemPc and self.itemPc:box("submenu") or { x = 176, y = 104, width = 56, height = 48 } - Font.drawBox(math.floor(b.x / 8), math.floor(b.y / 8), - math.floor(b.width / 8), math.floor(b.height / 8)) + if self.itemPc and self.itemPc.drawContentFrame then + self.itemPc:drawContentFrame(b) + else + Font.drawBox(math.floor(b.x / 8) - 1, math.floor(b.y / 8) - 1, + math.floor(b.width / 8) + 2, math.floor(b.height / 8) + 2, 1) + end love.graphics.setColor(0, 0, 0, 1) + local styles = self.game.data.font and self.game.data.font.uiTextStyles or {} + if styles.dark then Font.pushStyle(styles.dark) end for i, entry in ipairs(ENTRIES) do local y = b.y + 2 + (i - 1) * 16 Font.draw(Strings(entry.label), b.x + 8, y) if i == self.index then Font.drawCode(Theme.cursor, b.x, y) end end - Font.drawBox(SELECTED.x / 8, SELECTED.y / 8, - SELECTED.width / 8, SELECTED.height / 8) + local selected = self.itemPc and self.itemPc:subBox("selected") or SELECTED + if self.itemPc and self.itemPc.drawContentFrame then + self.itemPc:drawContentFrame(selected, "standard") + else + Font.drawBox(selected.x / 8 - 1, selected.y / 8 - 1, + selected.width / 8 + 2, selected.height / 8 + 2, 1) + end love.graphics.setColor(0, 0, 0, 1) - Font.draw(Strings("%s is selected.", self.itemName), - SELECTED.x, SELECTED.y + 2) + Font.draw(self.itemName .. Strings(" is"), selected.x, selected.y + 2) + Font.draw(Strings("selected."), selected.x, selected.y + 16) + if styles.dark then Font.popStyle() end love.graphics.setColor(1, 1, 1, 1) end diff --git a/src/ui/Gen3PlayerPC.lua b/src/ui/Gen3PlayerPC.lua index b72253f16..42b8502d9 100644 --- a/src/ui/Gen3PlayerPC.lua +++ b/src/ui/Gen3PlayerPC.lua @@ -286,7 +286,11 @@ local function g3Withdraw(game, back) if pc[id] <= 0 then pc[id] = nil end pcOrder(game, pc) require("src.core.Sound").play(game.data, "Withdraw_Deposit") - say(game, Strings("Withdrew\n%s.", itemName(game, id)), + local line = require("src.ui.Gen3BagMenu").pcWord(game, "withdrew") + line = tostring(line or "Withdrew {VAR2}\n{VAR1}(s).") + :gsub("{STR_VAR1}", "{VAR1}"):gsub("{STR_VAR2}", "{VAR2}") + :gsub("{VAR2}", tostring(qty)):gsub("{VAR1}", itemName(game, id)) + say(game, line, resume, current) end, current, "WITHDRAW") end, back, function(sourceId, beforeId) @@ -297,42 +301,16 @@ local function g3Withdraw(game, back) open() end --- wNumBoxItems capacity: 50 stacks (PC_ITEM_CAPACITY) -local function pcFull(game, pc, id) - if pc[id] then return false end -- growing an existing stack is fine - local cap = (game.data.field or {}).pcItemCap or 50 - local stacks = 0 - for _ in pairs(pc) do stacks = stacks + 1 end - return stacks >= cap -end - local function g3Deposit(game, back) - local pc = game.save.pcItems - local Bag = require("src.inventory.Bag") - local open - open = function() - -- the REAL bag: depositing is choosing out of your own pockets - local menu = require("src.ui.Gen3BagMenu").new(game, { - pick = true, itemPc = true, - onCancel = back, - onPick = function(id) - if Bag.isBadge and Bag.isBadge(id) then return open() end - local have = (game.save.inventory or {})[id] or 1 - askQuantity(game, id, have, function(qty) - if pcFull(game, pc, id) then - return say(game, Strings("No room left to\nstore items."), open) - end - Bag.remove(game.save, id, qty) - pc[id] = (pc[id] or 0) + qty - pcOrder(game, pc) - require("src.core.Sound").play(game.data, "Withdraw_Deposit") - say(game, Strings("%s was\nstored via PC.", itemName(game, id)), open) - end) - end, - }) - game.stack:push(menu) - end - open() + -- ITEMMENULOCATION_ITEMPC is still the real bag, but it is a BAG MODE, not + -- a generic pick callback that closes the screen. Let Gen3BagMenu own that + -- mode so its deposit title, CANCEL/return row, quantity path and result + -- window stay on the same retail screen throughout the operation. + game.stack:push(require("src.ui.Gen3BagMenu").new(game, { + store = "deposit", + itemPc = true, + onCancel = back, + })) end local function g3Toss(game, back) @@ -516,8 +494,21 @@ function Gen3PlayerPC.new(game, opts) end end elseif word:find("MAILBOX", 1, true) then - row.keepOpen = true - row.onSelect = function() notBuilt(game, Strings(label)) end + if fireRed then + -- Task_PlayerPcMailbox removes the top menu before displaying this + -- exact field message, then Task_ReturnToTopMenu redraws the menu after + -- A/B. The port has no mail-save model yet, so its representable + -- FireRed state is precisely the cartridge's zero-mail arm. + row.onSelect = function() + local TextBox = require("src.render.TextBox") + game.stack:push(TextBox.new(game, Strings("There's no MAIL here."), + function() Gen3PlayerPC.reopen(game, opts) end, + { promptOnlyWhenWaiting = true })) + end + else + row.keepOpen = true + row.onSelect = function() notBuilt(game, Strings(label)) end + end else -- TURN OFF: the row that ends the session row.onSelect = close