diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e123eab748..7bd666036e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,99 +1,85 @@ name: Bug report -description: Create a report to help us understand and diagnose your issue. Your contribution is welcomed and valued! It is highly recommended that you are using the latest version before submitting a bug report. -title: "[ bug Report ]" -labels: Awaiting Triage +description: Create a report to help us understand and triage your issue. +labels: + - Awaiting Triage body: + - type: markdown + attributes: + value: | + ## Bug Report -- type: markdown - attributes: - value: | - Please follow this document in order to report a bug. It is highly recommended that you are using the latest version before submitting a bug report. + Please follow this document carefully to report a bug. -- type: dropdown - attributes: - label: TombEngine version - description: | - Please select the TombEngine Version from the dropdown list. - options: - - Development Build - - v1.8.0 (latest version) - - v1.7.1 - - v1.7.0 - - v1.5 - validations: - required: true + > **Important**: It is highly recommended that you use the latest version before submitting a bug report. -- type: dropdown - attributes: - label: Tomb Editor version - description: | - Please select the Tomb Editor version used from the dropdown list. - options: - - Development Build - - v1.8.0 (latest version) - - v1.7.2 - - v1.7.1 - - v1.7.0 - validations: - required: true + - type: dropdown + attributes: + label: Tomb Engine Version + description: | + Please select the TombEngine version you are using. + options: + - Development build + - v1.11 (latest public release) + - v1.10.1 + validations: + required: true -- type: textarea - attributes: - label: Describe the bug - description: | - Please provide A clear and concise description of what the bug is. - placeholder: | - Your bug report here. - validations: - required: true + - type: checkboxes + attributes: + label: Development Version + description: Are you submitting this report from a development build that has not been officially released? + options: + - label: "I am using an unofficial development version." + - label: "I am using an official release." + - label: "I am using an official pre-release." + validations: + required: true -- type: textarea - attributes: - label: To Reproduce - description: | - To reproduce the behaviour, please provide detailed steps for the development team to follow. This can be done through screenshots or a written guide + - type: textarea + attributes: + label: Describe the Bug + description: | + Please provide a clear and concise description of what the issue is. + placeholder: | + Your bug report here. + validations: + required: true - **If the bug cannot be reproduced, and if the issue is not adequately explained, it will be closed without further investigation** - placeholder: | - Provide detailed reproducible steps here. - validations: - required: true + - type: textarea + attributes: + label: To Reproduce + description: | + Please provide detailed steps to reproduce the issue. + + **Note**: If the bug cannot be reproduced or the issue is not clearly explained, it may be closed without further investigation. + placeholder: | + Step-by-step reproduction instructions here. + validations: + required: true -- type: textarea - attributes: - label: Expected Behaviour - description: | - A clear and concise description of what you expected to happen. - placeholder: | - A description of what should happen here. - validations: - required: true + - type: textarea + attributes: + label: Expected Behaviour + description: | + What did you expect to happen? -- type: textarea - attributes: - label: Additional Content - description: | - Add any other context about the problem here. + **Note**: If the bug cannot be reproduced or the issue is not clearly explained, it may be closed without further investigation. + placeholder: | + A description of what should happen here. + validations: + required: true - * Are you testing an build of a TombEngine that has not yet been released? If so please give some context. - * Did you get any asset from the TombEngine website that has presented a bug? - placeholder: | - A description of any additional content here. - validations: - required: false - -- type: textarea - attributes: - label: Minimal reproduction project - description: | - **Please upload a .zip file containing your level and all assets needed to compile the level and a cut-down version of your level where the bug presents itself** - The project can be uploaded as a zip file (10 mb max) or provide a link from google drive, dropbox etc. - **Note** if you do not provide this, your issue may be rejected - placeholder: | - Download link to your project - validations: - required: true - + - type: textarea + attributes: + label: Minimal Reproduction Project + description: | + Please upload a .zip file (10 MB max) containing your level and all assets needed to compile the level, including a minimal version where the bug occurs. + Alternatively, provide a download link from a cloud storage service (e.g., Google Drive, Dropbox). + > **Important**: If you do not provide a minimal reproduction project, your issue may be rejected. + placeholder: | + Download link to your project or attach a .zip file. + validations: + required: true diff --git a/.github/workflows/cross-repo-dependency.yml b/.github/workflows/cross-repo-dependency.yml new file mode 100644 index 0000000000..afad434580 --- /dev/null +++ b/.github/workflows/cross-repo-dependency.yml @@ -0,0 +1,87 @@ +name: Cross repo dependency + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + pull_request_review: + types: [submitted] + +jobs: + check-linked-pr: + runs-on: ubuntu-latest + steps: + - name: Check for dependency + id: parse + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ""; + const match = body.match(/Depends on:\s+([\w-]+)\/([\w-]+)#(\d+)/); + + if (!match) { + // No dependency → do NOT create a status check + core.setOutput("skip", "true"); + return; + } + + core.setOutput("skip", "false"); + core.setOutput("depOwner", match[1]); + core.setOutput("depRepo", match[2]); + core.setOutput("depNumber", match[3]); + + - name: Exit early if no dependency + if: steps.parse.outputs.skip == 'true' + run: echo "No dependency found — skipping." + + - name: Check linked PR status + if: steps.parse.outputs.skip == 'false' + id: check + uses: actions/github-script@v7 + with: + script: | + const owner = steps.parse.outputs.depOwner; + const repo = steps.parse.outputs.depRepo; + const number = Number(steps.parse.outputs.depNumber); + + const linked = await github.rest.pulls.get({ + owner, + repo, + pull_number: number + }); + + const reviews = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: number + }); + + const approved = reviews.data.some(r => r.state === "APPROVED"); + const mergeable = linked.data.mergeable === true; + + core.setOutput("approved", approved); + core.setOutput("mergeable", mergeable); + + - name: Set dependency status + if: steps.parse.outputs.skip == 'false' + uses: actions/github-script@v7 + with: + script: | + const approved = steps.check.outputs.approved === 'true'; + const mergeable = steps.check.outputs.mergeable === 'true'; + + let state = "pending"; + let description = "Waiting for linked PR to be approved and mergeable"; + + if (approved && mergeable) { + state = "success"; + description = "Linked PR is approved and mergeable"; + } + + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.payload.pull_request.head.sha, + state, + context: "cross-repo-dependency", + description + }); diff --git a/README.md b/README.md index 5f699f1622..88b45dd54d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - *Lua* as the native scripting language. - Many objects from the original series (1-5). - Support for high framerate, antialiasing, mipmapping, and SSAO. +- Full skinning support for all objects. - Full diagonal geometry support. - Uncapped map size. - A streamlined player control scheme. @@ -38,4 +39,4 @@ Once done, you should be able to build a level with *Tomb Editor* and run it in Contributions are welcome. If you would like to participate in development to any degree, whether that be through suggestions, bug reports, or code, join our [Discord server](https://discord.gg/h5tUYFmres). # Disclaimer -Tomb Engine uses modified MIT license for non-commercial use only. For more information, see [license](https://github.com/TombEngine/TombEngine?tab=License-1-ov-file#readme). Tomb Engine is unaffiliated with the Crystal Dynamics group of companies or Embracer Group AB. *Tomb Raider* is a trademark of the Crystal Dynamics group of companies. Tomb Engine team is not responsible for illegal use of this source code and built binaries alone or in combination with third-party assets or components. This source code is released as-is and continues to be maintained by non-paid contributors in their free time. \ No newline at end of file +Tomb Engine uses modified MIT license for non-commercial use only. For more information, see [license](https://github.com/TombEngine/TombEngine?tab=License-1-ov-file#readme). Tomb Engine is unaffiliated with the Crystal Dynamics group of companies or Embracer Group AB. *Tomb Raider* is a trademark of the Crystal Dynamics group of companies. Tomb Engine team is not responsible for illegal use of this source code and built binaries alone or in combination with third-party assets or components. This source code is released as-is and continues to be maintained by non-paid contributors in their free time. diff --git a/Scripts/Engine/Achievements/Achievements.lua b/Scripts/Engine/Achievements/Achievements.lua new file mode 100644 index 0000000000..28b65113aa --- /dev/null +++ b/Scripts/Engine/Achievements/Achievements.lua @@ -0,0 +1,222 @@ +--- Achievement system entry point. +-- Loads achievement definitions from a setup file, persists unlock state across +-- levels via GameVars, and exposes a simple API for game scripts. +-- +-- Popup notifications (POSTLOOP) and the list viewer (PREFREEZE) are registered +-- automatically when ImportAchievements() is called. +-- +-- Quick-start (add to LevelFuncs.OnStart in every level that uses achievements): +-- +-- local Achievements = require("Engine.Achievements.Achievements") +-- Achievements.ImportAchievements("AchievementSetup") +-- +-- Unlock an achievement at runtime: +-- Achievements.Unlock("treasure_hunter") +-- +-- Open the full list (e.g. from a key bind or trigger): +-- Achievements.ShowAchievementList() +-- +-- @module Engine.Achievements.Achievements + +local Settings = require("Engine.Achievements.Settings") +local Block = require("Engine.Achievements.Block") -- referenced by sub-modules +local Popup = require("Engine.Achievements.Popup") +local List = require("Engine.Achievements.List") + +LevelFuncs.Engine.Achievements = LevelFuncs.Engine.Achievements or {} + +-- Persist unlock state across levels and save/loads. +GameVars.Engine.Achievements = GameVars.Engine.Achievements or { unlocked = {} } + +local Achievements = {} + +-- Module-level definition tables (runtime only; not persisted). +local Defs = {} -- ordered array of definition tables +local DefMap = {} -- id string -> definition table + +-- Guard: prevents duplicate AddCallback registrations within one Lua session. +local _callbacksRegistered = false + +-- ============================================================================ +-- LevelFuncs callbacks +-- Must live in LevelFuncs so that AddCallback / RemoveCallback can reference +-- them by value after a level reload. +-- ============================================================================ + +LevelFuncs.Engine.Achievements.OnLoop = function() + Popup.Tick() + Popup.Draw() +end + +LevelFuncs.Engine.Achievements.OnFreeze = function() + List.Tick() + List.Draw() +end + +-- ============================================================================ +-- Internal helpers +-- ============================================================================ + +local function PlaySound(soundId) + if soundId and soundId > 0 then + TEN.Sound.PlaySound(soundId) + end +end + +-- ============================================================================ +-- Public API +-- ============================================================================ + +--- Load achievement definitions from an external file and start the system. +-- Can be called from LevelFuncs.OnStart and LevelFuncs.OnLoad; duplicate +-- registrations are guarded internally. +-- @tparam string fileName Name of the Lua file (without extension) in the +-- script folder (e.g. "AchievementSetup"). +function Achievements.ImportAchievements(fileName) + if type(fileName) ~= "string" then + TEN.Util.PrintLog("Achievements.ImportAchievements: 'fileName' must be a string.", + TEN.Util.LogLevel.WARNING) + return + end + + local ok, data = pcall(require, fileName) + if not ok or type(data) ~= "table" then + TEN.Util.PrintLog("Achievements.ImportAchievements: could not load '" .. fileName .. "'.", + TEN.Util.LogLevel.WARNING) + return + end + + Defs = {} + DefMap = {} + + for i, entry in ipairs(data) do + if type(entry.id) ~= "string" then + TEN.Util.PrintLog("Achievements: entry " .. i .. " missing string 'id'. Skipped.", + TEN.Util.LogLevel.WARNING) + elseif type(entry.title) ~= "string" then + TEN.Util.PrintLog("Achievements: entry '" .. entry.id .. "' missing string 'title'. Skipped.", + TEN.Util.LogLevel.WARNING) + elseif type(entry.description) ~= "string" then + TEN.Util.PrintLog("Achievements: entry '" .. entry.id .. "' missing string 'description'. Skipped.", + TEN.Util.LogLevel.WARNING) + elseif type(entry.spriteId) ~= "number" then + TEN.Util.PrintLog("Achievements: entry '" .. entry.id .. "' missing number 'spriteId'. Skipped.", + TEN.Util.LogLevel.WARNING) + else + local def = { + id = entry.id, + title = entry.title, + description = entry.description, + spriteId = entry.spriteId, + hidden = entry.hidden == true, + } + Defs[#Defs + 1] = def + DefMap[entry.id] = def + end + end + + -- Ensure GameVars structure is valid (e.g. after ClearAll or first run). + GameVars.Engine.Achievements = GameVars.Engine.Achievements or {} + GameVars.Engine.Achievements.unlocked = GameVars.Engine.Achievements.unlocked or {} + + -- Inject runtime tables into sub-modules. + Popup.Init(DefMap) + List.Init(Defs) + + Achievements.Status(true) + + TEN.Util.PrintLog("Achievements: loaded " .. #Defs .. " definition(s) from '" .. fileName .. "'.", + TEN.Util.LogLevel.INFO) +end + +--- Enable or disable the achievement callbacks. +-- ImportAchievements() calls this automatically with value = true. +-- @tparam bool value True to activate, false to deactivate. +function Achievements.Status(value) + if value then + if not _callbacksRegistered then + TEN.Logic.AddCallback(TEN.Logic.CallbackPoint.POSTLOOP, + LevelFuncs.Engine.Achievements.OnLoop) + TEN.Logic.AddCallback(TEN.Logic.CallbackPoint.PREFREEZE, + LevelFuncs.Engine.Achievements.OnFreeze) + _callbacksRegistered = true + end + else + TEN.Logic.RemoveCallback(TEN.Logic.CallbackPoint.POSTLOOP, + LevelFuncs.Engine.Achievements.OnLoop) + TEN.Logic.RemoveCallback(TEN.Logic.CallbackPoint.PREFREEZE, + LevelFuncs.Engine.Achievements.OnFreeze) + _callbacksRegistered = false + end +end + +--- Unlock an achievement. +-- Has no effect if the achievement is already unlocked or the ID is unknown. +-- Enqueues a slide-in popup notification automatically. +-- @tparam string id Achievement ID as defined in the setup file. +function Achievements.Unlock(id) + if not DefMap[id] then + TEN.Util.PrintLog("Achievements.Unlock: unknown id '" .. tostring(id) .. "'.", + TEN.Util.LogLevel.WARNING) + return + end + + if GameVars.Engine.Achievements.unlocked[id] then return end + + GameVars.Engine.Achievements.unlocked[id] = true + Popup.Enqueue(id) + + TEN.Util.PrintLog("Achievements: unlocked '" .. id .. "'.", TEN.Util.LogLevel.INFO) +end + +--- Check whether a specific achievement is unlocked. +-- @tparam string id Achievement ID. +-- @treturn bool True if unlocked. +function Achievements.IsUnlocked(id) + return GameVars.Engine.Achievements.unlocked[id] == true +end + +--- Returns true if every loaded achievement has been unlocked. +-- Returns false (not true) when no definitions have been loaded yet. +-- @treturn bool +function Achievements.IsAllUnlocked() + if #Defs == 0 then return false end + for _, def in ipairs(Defs) do + if not GameVars.Engine.Achievements.unlocked[def.id] then + return false + end + end + return true +end + +--- Returns ratio of unlocked achievements. +-- Returns false (not true) when no definitions have been loaded yet. +-- @treturn number|bool Ratio of unlocked achievements (0.0 to 1.0) or false if no definitions. +function Achievements.GetUnlockRatio() + if #Defs == 0 then return false end + local unlockedCount = 0 + for _, def in ipairs(Defs) do + if GameVars.Engine.Achievements.unlocked[def.id] then + unlockedCount = unlockedCount + 1 + end + end + local total = (#Defs > 0) and #Defs or 1 + return unlockedCount / total +end + +--- Clear all unlock state and discard any pending popup notifications. +-- Unlocked achievements will appear locked again the next time the list opens. +function Achievements.ClearAll() + GameVars.Engine.Achievements.unlocked = {} + Popup.ClearQueue() + TEN.Util.PrintLog("Achievements: all achievements cleared.", TEN.Util.LogLevel.INFO) +end + +--- Open the full-screen achievement list (enters FULL freeze mode). +-- The list stays open until the player presses the exit action defined in +-- Settings.List.exitAction (default: Inventory key). +function Achievements.ShowAchievementList() + List.Open() +end + +return Achievements diff --git a/Scripts/Engine/Achievements/Block.lua b/Scripts/Engine/Achievements/Block.lua new file mode 100644 index 0000000000..4711b9910e --- /dev/null +++ b/Scripts/Engine/Achievements/Block.lua @@ -0,0 +1,79 @@ +--- Shared achievement block renderer. +-- Draws a single achievement block (background panel, icon, title, description) +-- at a given center position in screen-percent coordinates. +-- Used by both the popup notification and the list viewer. +-- @module Engine.Achievements.Block +-- @local + +local Settings = require("Engine.Achievements.Settings") + +local Block = {} + +-- ============================================================================ +-- Internal helpers +-- ============================================================================ + +local function DrawSprite(objectId, spriteId, pos, rot, scale, color, layer, alignMode, scaleMode, blendMode) + local sprite = TEN.View.DisplaySprite(objectId, spriteId, pos, rot, scale, color) + sprite:Draw(layer, alignMode, scaleMode, blendMode) +end + +local function DrawText(text, offsetX, offsetY, baseX, baseY, scale, color, options) + local px = TEN.Vec2(TEN.Util.PercentToScreen(baseX + offsetX, baseY + offsetY)) + local str = TEN.Strings.DisplayString(text, px, scale, color, false, options) + TEN.Strings.ShowString(str, 1 / 30) +end + +local function ClampAlpha(a) + return math.floor(math.max(0, math.min(255, a))) +end + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Draw an achievement block centered at (posX, posY) in screen percent. +-- @tparam table def Achievement definition {id, title, description, spriteId, hidden}. +-- @tparam bool isUnlocked Whether the achievement has been unlocked. +-- @tparam number posX Horizontal center of the block in screen percent (0-100). +-- @tparam number posY Vertical center of the block in screen percent (0-100). +-- @tparam number alpha Opacity 0-255 for the entire block. +function Block.Draw(def, isUnlocked, posX, posY, alpha) + local B = Settings.Block + local IC = Settings.Icons + local a = ClampAlpha(alpha) + + -- isLocked: any achievement not yet unlocked → use locked icon + grey title colour + -- isHiddenLocked: hidden AND not unlocked → substitute title text, suppress description + local isLocked = not isUnlocked + local isHiddenLocked = def.hidden and isLocked + + -- Background panel (Vec2 created here from plain numbers) + local bgColor = TEN.Color(B.bgColor.r, B.bgColor.g, B.bgColor.b, B.bgColor.a * a / 255) + DrawSprite(B.bgObjectId, B.bgSpriteId, TEN.Vec2(posX, posY), 0, B.bgSize, bgColor, + 0, B.bgAlignMode, B.bgScaleMode, B.bgBlendMode) + + -- Icon: locked sprite for ANY locked achievement, real sprite when unlocked + local iconSpriteId = isLocked and IC.lockedSpriteId or def.spriteId + local iconPos = TEN.Vec2(posX + B.iconOffset.x, posY + B.iconOffset.y) + local iconColor = TEN.Color(B.iconColor.r, B.iconColor.g, B.iconColor.b, a) + DrawSprite(IC.objectId, iconSpriteId, iconPos, 0, B.iconSize, iconColor, + 1, B.iconAlignMode, B.iconScaleMode, B.iconBlendMode) + + -- Title text: "Locked Achievement" for hidden-locked; real title otherwise + -- Title colour: grey (lockedTitleColor) for any locked achievement; gold when unlocked + local titleText = isHiddenLocked and B.lockedTitle or def.title + local tc = isLocked and B.lockedTitleColor or B.titleColor + local titleColor = TEN.Color(tc.r, tc.g, tc.b, a) + DrawText(titleText, B.titleOffset.x, B.titleOffset.y, posX, posY, + B.titleScale, titleColor, B.titleOptions) + + -- Description: only shown for unlocked achievements + if not isLocked then + local descColor = TEN.Color(B.descColor.r, B.descColor.g, B.descColor.b, a) + DrawText(def.description, B.descOffset.x, B.descOffset.y, posX, posY, + B.descScale, descColor, B.descOptions) + end +end + +return Block diff --git a/Scripts/Engine/Achievements/Input.lua b/Scripts/Engine/Achievements/Input.lua new file mode 100644 index 0000000000..dfa68e9bfa --- /dev/null +++ b/Scripts/Engine/Achievements/Input.lua @@ -0,0 +1,49 @@ +--- Scroll input handler with hold-time acceleration for the achievement list. +-- Call Input.GetScrollDelta() once per frame inside the list tick. +-- Positive delta = scroll down (toward later entries). +-- Negative delta = scroll up (toward earlier entries). +-- @module Engine.Achievements.Input +-- @local + +local Settings = require("Engine.Achievements.Settings") + +local Input = {} + +local holdFramesFwd = 0 +local holdFramesBack = 0 + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Returns the scroll delta (screen %) for this frame and updates hold counters. +-- @treturn number Delta to add to the current scrollOffset. +function Input.GetScrollDelta() + local L = Settings.List + + if TEN.Input.IsKeyHeld(TEN.Input.ActionID.FORWARD) then + holdFramesFwd = holdFramesFwd + 1 + holdFramesBack = 0 + local speed = math.min(L.scrollSpeed + holdFramesFwd * L.scrollAccel, L.maxScrollSpeed) + return -speed -- FORWARD / up = negative (scroll toward top) + + elseif TEN.Input.IsKeyHeld(TEN.Input.ActionID.BACK) then + holdFramesBack = holdFramesBack + 1 + holdFramesFwd = 0 + local speed = math.min(L.scrollSpeed + holdFramesBack * L.scrollAccel, L.maxScrollSpeed) + return speed -- BACK / down = positive (scroll toward bottom) + + else + holdFramesFwd = 0 + holdFramesBack = 0 + return 0 + end +end + +--- Reset all hold counters. Call this when the list closes. +function Input.Reset() + holdFramesFwd = 0 + holdFramesBack = 0 +end + +return Input diff --git a/Scripts/Engine/Achievements/List.lua b/Scripts/Engine/Achievements/List.lua new file mode 100644 index 0000000000..643a7d3bdf --- /dev/null +++ b/Scripts/Engine/Achievements/List.lua @@ -0,0 +1,230 @@ +--- Achievement list viewer. +-- Opens in FULL freeze mode and renders all achievements as scrollable blocks. +-- A progress bar and a percentage label are drawn at the top of the screen. +-- Hidden achievements that are still locked are collected into a single summary +-- line at the bottom of the list rather than shown individually. +-- @module Engine.Achievements.List +-- @local + +local Settings = require("Engine.Achievements.Settings") +local Block = require("Engine.Achievements.Block") +local InputModule = require("Engine.Achievements.Input") + +local List = {} + +-- ============================================================================ +-- State +-- ============================================================================ + +local Defs = nil -- injected via List.Init() +local visible = false +local displayList = {} -- built each time the list opens +local scrollOffset = 0 -- current scroll position in screen percent +local maxScroll = 0 -- maximum scroll position + +-- Arrow alpha state (smoothly fade in/out). +local arrowUpAlpha = 0 +local arrowDownAlpha = 0 + +-- ============================================================================ +-- Internal helpers +-- ============================================================================ + +local function PlaySound(soundId) + if soundId and soundId > 0 then + + if not TEN.Sound.IsSoundPlaying(soundId) then + TEN.Sound.PlaySound(soundId) + end + end +end + +local function CountUnlocked() + local count = 0 + for _, def in ipairs(Defs) do + if GameVars.Engine.Achievements.unlocked[def.id] then + count = count + 1 + end + end + return count +end + +local function BuildDisplayList() + displayList = {} + local hiddenLockedCount = 0 + + for _, def in ipairs(Defs) do + local isUnlocked = GameVars.Engine.Achievements.unlocked[def.id] == true + if def.hidden and not isUnlocked then + hiddenLockedCount = hiddenLockedCount + 1 + else + displayList[#displayList + 1] = { def = def, isUnlocked = isUnlocked } + end + end + + -- Append a summary entry for still-locked hidden achievements. + if hiddenLockedCount > 0 then + displayList[#displayList + 1] = { isSummary = true, count = hiddenLockedCount } + end +end + +local function StepAlpha(current, target, speed) + if current < target then + return math.min(current + speed, target) + else + return math.max(current - speed, target) + end +end + +local function DrawArrow(rot, x, y, alpha) + if alpha <= 0 then return end + local L = Settings.List + local color = TEN.Color(L.arrowColor.r, L.arrowColor.g, L.arrowColor.b, math.floor(alpha)) + local sprite = TEN.View.DisplaySprite(L.arrowObjectId, L.arrowSpriteId, + TEN.Vec2(x, y), rot, L.arrowSize, color) + sprite:Draw(2, L.arrowAlignMode, L.arrowScaleMode, L.arrowBlendMode) +end + +local function DrawProgressBar() + local total = (#Defs > 0) and #Defs or 1 + local unlocked = CountUnlocked() + local progress = unlocked / total -- 0.0 to 1.0 + local pct = math.floor(progress * 100) + + local PB = Settings.ProgressBar + + -- Background sprite + local bgSprite = TEN.View.DisplaySprite(PB.bgObjectId, PB.bgSpriteId, + PB.bgPos, 0, PB.bgSize, PB.bgColor) + bgSprite:Draw(0, PB.bgAlignMode, PB.bgScaleMode, PB.bgBlendMode) + + -- Fill sprite (width scaled by progress) + if progress > 0 then + local fillSize = TEN.Vec2(PB.fillMaxSize.x * progress, PB.fillMaxSize.y) + local fillSprite = TEN.View.DisplaySprite(PB.fillObjectId, PB.fillSpriteId, + PB.fillPos, 0, fillSize, PB.fillColor) + fillSprite:Draw(1, PB.fillAlignMode, PB.fillScaleMode, PB.fillBlendMode) + end + + -- Label text: "3 / 10 (30%)" + local labelText = unlocked .. " / " .. total .. " (" .. pct .. "%)" + local px = TEN.Vec2(TEN.Util.PercentToScreen(PB.labelPos.x, PB.labelPos.y)) + local str = TEN.Strings.DisplayString(labelText, px, + PB.labelScale, PB.labelColor, + false, PB.labelOptions) + TEN.Strings.ShowString(str, 1 / 30) +end + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Inject the definitions array. Called by Achievements.lua after loading the +-- setup file. +-- @tparam table defs Ordered array of achievement definition tables. +function List.Init(defs) + Defs = defs +end + +--- Returns true while the list is open (game is frozen). +-- @treturn bool +function List.IsVisible() + return visible +end + +--- Open the achievement list. Enters FULL freeze mode. +function List.Open() + if visible then return end + if not Defs then return end + + BuildDisplayList() + scrollOffset = 0 + + -- Calculate maximum scroll so the last row is fully visible. + local L = Settings.List + local totalRows = math.ceil(#displayList / 2) + local visibleCount = math.max(1, math.floor((100 - L.startY) / L.blockSpacing)) + maxScroll = math.max(0, (totalRows - visibleCount) * L.blockSpacing) + + InputModule.Reset() + TEN.Input.ClearAllKeys() + TEN.Flow.SetFreezeMode(TEN.Flow.FreezeMode.FULL) + PlaySound(Settings.SoundMap.openList) + visible = true +end + +--- Close the list. Exits freeze mode. +function List.Close() + if not visible then return end + + InputModule.Reset() + arrowUpAlpha = 0 + arrowDownAlpha = 0 + TEN.Input.ClearAllKeys() + PlaySound(Settings.SoundMap.closeList) + visible = false + TEN.Flow.SetFreezeMode(TEN.Flow.FreezeMode.NONE) +end + +--- Update scroll offset and handle exit input. Intended for the PREFREEZE callback. +function List.Tick() + if not visible then return end + + -- Exit + if TEN.Input.IsKeyHit(Settings.List.exitAction) then + List.Close() + return + end + + -- Scroll with acceleration; only play sound when actually moving. + local delta = InputModule.GetScrollDelta() + local prevOffset = scrollOffset + scrollOffset = math.max(0, math.min(maxScroll, scrollOffset + delta)) + if scrollOffset ~= prevOffset then + PlaySound(Settings.SoundMap.scroll) + end + + -- Update arrow alphas. + local L = Settings.List + arrowUpAlpha = StepAlpha(arrowUpAlpha, scrollOffset > 0 and 255 or 0, L.arrowFadeSpeed) + arrowDownAlpha = StepAlpha(arrowDownAlpha, scrollOffset < maxScroll and 255 or 0, L.arrowFadeSpeed) +end + +--- Draw all achievement blocks and the progress bar. Intended for the PREFREEZE callback. +function List.Draw() + if not visible then return end + + local L = Settings.List + + DrawProgressBar() + + -- Scroll arrows (left side of screen, up arrow rotated 180° vs down arrow) + DrawArrow(180, L.arrowUpX, L.arrowUpY, arrowUpAlpha) + DrawArrow(0, L.arrowUpX, L.arrowDownY, arrowDownAlpha) + + for i, entry in ipairs(displayList) do + -- Two-column layout: odd entries go left, even entries go right. + -- Both entries in the same pair share the same row Y. + local row = math.ceil(i / 2) + local col = ((i - 1) % 2) -- 0 = left, 1 = right + local blockX = (col == 0) and L.col1X or L.col2X + local blockY = L.startY + (row - 1) * L.blockSpacing - scrollOffset + + -- Cull rows that are entirely off-screen. + if blockY >= -8 and blockY <= 108 then + if entry.isSummary then + -- Summary spans the full width; centre it. + local text = string.gsub(L.hiddenCountText, "{n}", tostring(entry.count)) + local px = TEN.Vec2(TEN.Util.PercentToScreen((L.col1X + L.col2X) / 2, blockY)) + local str = TEN.Strings.DisplayString(text, px, + L.hiddenTextScale, L.hiddenTextColor, + false, L.hiddenTextOptions) + TEN.Strings.ShowString(str, 1 / 30) + else + Block.Draw(entry.def, entry.isUnlocked, blockX, blockY, 255) + end + end + end +end + +return List diff --git a/Scripts/Engine/Achievements/Popup.lua b/Scripts/Engine/Achievements/Popup.lua new file mode 100644 index 0000000000..f4225d382a --- /dev/null +++ b/Scripts/Engine/Achievements/Popup.lua @@ -0,0 +1,152 @@ +--- Popup notification system for achievements. +-- Maintains a FIFO queue of achievement IDs and animates each one as a block +-- that slides up from below the screen, holds, then slides back down. +-- The popup automatically pauses when the achievement list viewer is open +-- (because POSTLOOP callbacks do not fire during FULL freeze mode). +-- @module Engine.Achievements.Popup +-- @local + +local Settings = require("Engine.Achievements.Settings") +local Block = require("Engine.Achievements.Block") + +local Popup = {} + +-- ============================================================================ +-- State +-- ============================================================================ + +-- Injected via Popup.Init() after definitions are loaded. +local DefMap = nil + +local Queue = {} -- FIFO queue of achievement IDs +local state = "IDLE" -- "IDLE" | "SLIDING_IN" | "HOLDING" | "SLIDING_OUT" +local currentDef = nil -- definition table for the currently displayed popup +local posY = 0 -- current Y position in screen percent +local alpha = 0 -- current alpha 0-255 (float, floored on draw) +local holdTimer = 0 -- elapsed hold time in seconds + +local STATE_IDLE = "IDLE" +local STATE_SLIDING_IN = "SLIDING_IN" +local STATE_HOLDING = "HOLDING" +local STATE_SLIDING_OUT = "SLIDING_OUT" + +-- ============================================================================ +-- Internal +-- ============================================================================ + +local function PlaySound(soundId) + if soundId and soundId > 0 then + TEN.Sound.PlaySound(soundId) + end +end + +local function Lerp(current, target, speed) + return current + (target - current) * speed +end + +local function StartNext() + if #Queue == 0 then + state = STATE_IDLE + currentDef = nil + return + end + + local id = table.remove(Queue, 1) + local def = DefMap and DefMap[id] or nil + + if not def then + -- Unknown ID; skip silently and try the next entry. + StartNext() + return + end + + currentDef = def + posY = Settings.Popup.startPosY + alpha = 0 + holdTimer = 0 + state = STATE_SLIDING_IN + + PlaySound(Settings.Popup.sound) +end + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Inject definition tables. Called by Achievements.lua after loading the setup file. +-- @tparam table defMap id-to-definition map { [id] = def } +function Popup.Init(defMap) + DefMap = defMap +end + +--- Add an achievement ID to the display queue. +-- If the popup is currently idle the new entry starts immediately. +-- @tparam string id Achievement ID as defined in the setup file. +function Popup.Enqueue(id) + Queue[#Queue + 1] = id + if state == STATE_IDLE then + StartNext() + end +end + +--- Update popup animation. Intended for the POSTLOOP callback. +-- Automatically does nothing when the FULL freeze list is open because +-- POSTLOOP callbacks do not fire during freeze mode. +function Popup.Tick() + if state == STATE_IDLE then return end + + local P = Settings.Popup + + if state == STATE_SLIDING_IN then + posY = Lerp(posY, P.targetPosY, P.slideSpeed) + alpha = Lerp(alpha, 255, P.alphaSpeed) + + if math.abs(posY - P.targetPosY) < 0.15 then + posY = P.targetPosY + alpha = 255 + state = STATE_HOLDING + end + + elseif state == STATE_HOLDING then + holdTimer = holdTimer + 1 / 30 + if holdTimer >= P.holdTime then + state = STATE_SLIDING_OUT + end + + elseif state == STATE_SLIDING_OUT then + posY = Lerp(posY, P.startPosY, P.slideSpeed) + alpha = Lerp(alpha, 0, P.alphaSpeed) + + if math.abs(posY - P.startPosY) < 0.5 then + posY = P.startPosY + alpha = 0 + StartNext() -- advances queue (or goes IDLE) + end + end +end + +--- Draw the current popup block. Intended for the POSTLOOP callback. +function Popup.Draw() + if state == STATE_IDLE then return end + if not currentDef then return end + + local isUnlocked = GameVars.Engine.Achievements.unlocked[currentDef.id] == true + Block.Draw(currentDef, isUnlocked, Settings.Popup.posX, posY, math.floor(alpha)) +end + +--- Returns true while a popup is visible or entries are waiting in the queue. +-- @treturn bool +function Popup.IsActive() + return state ~= STATE_IDLE or #Queue > 0 +end + +--- Discard all queued and in-progress popups (e.g. after ClearAll). +function Popup.ClearQueue() + Queue = {} + state = STATE_IDLE + currentDef = nil + alpha = 0 + posY = Settings.Popup.startPosY +end + +return Popup diff --git a/Scripts/Engine/Achievements/Settings.lua b/Scripts/Engine/Achievements/Settings.lua new file mode 100644 index 0000000000..c607cc40df --- /dev/null +++ b/Scripts/Engine/Achievements/Settings.lua @@ -0,0 +1,181 @@ +--- Settings for the Achievements module. +-- Edit this file to customise the visual appearance and behaviour of achievement +-- blocks, the slide-in popup, the list viewer, the progress bar and sounds. +-- +-- All screen positions and sizes use screen-percent units (0-100). +-- @module Engine.Achievements.Settings +-- @local + +local Settings = {} + +-- ============================================================================ +-- Sounds +-- ============================================================================ +-- Set any value to 0 to disable that sound. + +Settings.SoundMap = +{ + unlock = 114, -- played when Achievements.Unlock() is called + openList = 109, -- played when the list opens + closeList = 109, -- played when the list closes + scroll = 108, -- played on each scroll step (optional; can be noisy) +} + +-- ============================================================================ +-- Achievement Icons +-- ============================================================================ +-- All per-achievement icons and the locked mystery icon come from the +-- ACHIEVEMENT_SPRITES object slot. +-- spriteId == 0 : locked / mystery icon (used for hidden-locked entries) +-- spriteId == 1+ : individual achievement icons, matching 'spriteId' in the +-- setup file (AchievementSetup.lua). + +Settings.Icons = +{ + objectId = TEN.Objects.ObjID.MOTORBOAT_FOAM_SPRITES, + lockedSpriteId = 0, +} + +-- ============================================================================ +-- Achievement Block +-- ============================================================================ +-- The block is the visual unit shared by both the popup and the list. +-- Positions are screen-percent offsets applied to the block's center point. + +Settings.Block = +{ + -- Background panel sprite. + -- Point bgObjectId / bgSpriteId at whichever sprite slot holds your + -- block background graphic (separate from ACHIEVEMENT_SPRITES). + bgObjectId = TEN.Objects.ObjID.MOTORBOAT_FOAM_SPRITES, + bgSpriteId = 5, + bgColor = TEN.Color(128, 128, 128, 128), + bgSize = TEN.Vec2(45, 12), + bgAlignMode = TEN.View.AlignMode.CENTER, + bgScaleMode = TEN.View.ScaleMode.STRETCH, + bgBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Icon (drawn to the left of the text). + iconOffset = TEN.Vec2(-17, 0), + iconSize = TEN.Vec2(10, 10), + iconColor = TEN.Color(255, 255, 255), + iconAlignMode = TEN.View.AlignMode.CENTER, + iconScaleMode = TEN.View.ScaleMode.FIT, + iconBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Title text. + titleOffset = TEN.Vec2(-10, -2), + titleScale = 0.7, + titleColor = TEN.Color(255, 220, 100), -- colour for unlocked achievements + lockedTitleColor = TEN.Color(140, 140, 140), -- grey colour for locked achievements + titleOptions = { TEN.Strings.DisplayStringOption.SHADOW, TEN.Strings.DisplayStringOption.VERTICAL_CENTER }, + + -- Description text (hidden when the achievement is hidden and locked). + descOffset = TEN.Vec2(-10, 2), + descScale = 0.55, + descColor = TEN.Color(200, 200, 200), + descOptions = { TEN.Strings.DisplayStringOption.SHADOW, TEN.Strings.DisplayStringOption.VERTICAL_CENTER }, + + -- Text substituted for the real title on hidden-locked achievements. + lockedTitle = "Locked Achievement", +} + +-- ============================================================================ +-- Popup Notification +-- ============================================================================ +-- The popup block slides up from below the screen at the bottom-right, +-- holds for holdTime seconds, then slides back down. +-- posX / startPosY / targetPosY are in screen percent. + +Settings.Popup = +{ + posX = 75, -- horizontal center of the popup block + startPosY = 115, -- Y when off-screen (below the viewport) + targetPosY = 94, -- Y when fully visible + slideSpeed = 0.12, -- lerp factor per frame (0-1; higher = snappier) + alphaSpeed = 0.20, -- lerp factor per frame for alpha + holdTime = 3.0, -- seconds the popup stays visible before sliding out + sound = Settings.SoundMap.unlock, -- sound ID played on unlock (0 = silence) +} + +-- ============================================================================ +-- Achievement List Viewer +-- ============================================================================ +-- Opened via Achievements.ShowAchievementList(); enters FULL freeze mode. + +Settings.List = +{ + col1X = 27, -- horizontal center of the left column (screen %) + col2X = 73, -- horizontal center of the right column (screen %) + startY = 15, -- Y of the first row center (screen %) + blockSpacing = 15, -- vertical gap between row centers (screen %) + + -- Scroll acceleration: speed starts at scrollSpeed and increases by + -- scrollAccel per frame while the key is held, capped at maxScrollSpeed. + scrollSpeed = 0.5, + scrollAccel = 0.03, + maxScrollSpeed = 3.0, + + -- Action used to close the list. + exitAction = TEN.Input.ActionID.INVENTORY, + + -- Summary line shown at the bottom for still-locked hidden achievements. + -- Use {n} as a placeholder for the count. + hiddenCountText = "{n} Hidden Achievement(s)", + hiddenTextScale = 0.7, + hiddenTextColor = TEN.Color(150, 150, 150), + hiddenTextOptions = { TEN.Strings.DisplayStringOption.SHADOW, + TEN.Strings.DisplayStringOption.CENTER }, + + -- Scroll arrows drawn on the left side of the screen. + -- objectId / spriteId point at whichever sprite slot holds your arrow graphic. + arrowObjectId = TEN.Objects.ObjID.MOTORBOAT_FOAM_SPRITES, + arrowSpriteId = 0, + arrowSize = TEN.Vec2(5, 5), + arrowColor = TEN.Color(255, 255, 255), + arrowAlignMode = TEN.View.AlignMode.CENTER, + arrowScaleMode = TEN.View.ScaleMode.FIT, + arrowBlendMode = TEN.Effects.BlendID.ALPHABLEND, + arrowUpX = 5, -- screen % X for both arrows + arrowUpY = 10, -- screen % Y for the up arrow + arrowDownY = 90, -- screen % Y for the down arrow + arrowFadeSpeed = 20, -- alpha steps per frame (0-255) +} + +-- ============================================================================ +-- Progress Bar +-- ============================================================================ +-- Drawn directly during the freeze frame (PREFREEZE). +-- Background and fill sprites use separate object/sprite references. + +Settings.ProgressBar = +{ + -- Background sprite (full width of the bar). + bgObjectId = TEN.Objects.ObjID.CUSTOM_BAR_GRAPHICS, + bgSpriteId = 0, + bgColor = TEN.Color(60, 60, 60), + bgPos = TEN.Vec2(75, 7), + bgSize = TEN.Vec2(20, 3), + bgAlignMode = TEN.View.AlignMode.CENTER_LEFT, + bgScaleMode = TEN.View.ScaleMode.STRETCH, + bgBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Fill sprite (width scaled by progress fraction 0-1). + fillObjectId = TEN.Objects.ObjID.CUSTOM_BAR_GRAPHICS, + fillSpriteId = 1, + fillColor = TEN.Color(100, 220, 100), + fillPos = TEN.Vec2(75.2, 7), + fillMaxSize = TEN.Vec2(19.6, 2.4), + fillAlignMode = TEN.View.AlignMode.CENTER_LEFT, + fillScaleMode = TEN.View.ScaleMode.STRETCH, + fillBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Label drawn above the bar: "3 / 10 (30%)". + labelPos = TEN.Vec2(85, 4), + labelScale = 0.55, + labelColor = TEN.Color(255, 255, 255), + labelOptions = { TEN.Strings.DisplayStringOption.SHADOW, + TEN.Strings.DisplayStringOption.CENTER }, +} + +return Settings diff --git a/TombEngine/Game/Hud/DrawItems/DisplayItem.cpp b/TombEngine/Game/Hud/DrawItems/DisplayItem.cpp index 99108ee9b2..188b555715 100644 --- a/TombEngine/Game/Hud/DrawItems/DisplayItem.cpp +++ b/TombEngine/Game/Hud/DrawItems/DisplayItem.cpp @@ -240,11 +240,38 @@ namespace TEN::Hud } } + void DisplayItem::SetScissor(const Vector2& pos, const Vector2& size) + { + _hasScissor = true; + _scissorPos = pos; + _scissorSize = size; + } + + void DisplayItem::ClearScissor() + { + _hasScissor = false; + } + bool DisplayItem::GetVisible() const { return _visible; } + bool DisplayItem::GetHasScissor() const + { + return _hasScissor; + } + + Vector2 DisplayItem::GetScissorPos() const + { + return _scissorPos; + } + + Vector2 DisplayItem::GetScissorSize() const + { + return _scissorSize; + } + bool DisplayItem::GetDisposing() const { return _disposing; diff --git a/TombEngine/Game/Hud/DrawItems/DisplayItem.h b/TombEngine/Game/Hud/DrawItems/DisplayItem.h index c3f6796697..c3fbe2a2d7 100644 --- a/TombEngine/Game/Hud/DrawItems/DisplayItem.h +++ b/TombEngine/Game/Hud/DrawItems/DisplayItem.h @@ -63,6 +63,9 @@ namespace TEN::Hud bool GetVisible() const; bool GetDisposing() const; bool GetMeshVisible(int meshIndex) const; + bool GetHasScissor() const; + Vector2 GetScissorPos() const; + Vector2 GetScissorSize() const; int GetAnimNumber() const; int GetFrameNumber() const; int GetEndFrameNumber() const; @@ -88,6 +91,8 @@ namespace TEN::Hud void SetMeshOrientation(int meshIndex, const EulerAngles& orient, bool disableInterpolation); void SetAnimation(int animNumber); void SetFrame(int frameNumber); + void SetScissor(const Vector2& pos, const Vector2& size); + void ClearScissor(); // Inquirers diff --git a/TombEngine/Game/Hud/DrawItems/DrawItems.cpp b/TombEngine/Game/Hud/DrawItems/DrawItems.cpp index a8f053a419..4d93a2afb8 100644 --- a/TombEngine/Game/Hud/DrawItems/DrawItems.cpp +++ b/TombEngine/Game/Hud/DrawItems/DrawItems.cpp @@ -7,6 +7,7 @@ #include "Specific/clock.h" using namespace TEN::Math; +using namespace TEN::Renderer::Structures; using TEN::Renderer::g_Renderer; namespace TEN::Hud @@ -205,7 +206,25 @@ namespace TEN::Hud return; for (const auto& item : _displayItems) + { + if (item.GetHasScissor()) + { + auto screenRes = g_Renderer.GetScreenResolution(); + auto pos = item.GetScissorPos(); + auto size = item.GetScissorSize(); + auto rect = RendererRectangle( + (int)(pos.x * screenRes.x / 100.0f), + (int)(pos.y * screenRes.y / 100.0f), + (int)((pos.x + size.x) * screenRes.x / 100.0f), + (int)((pos.y + size.y) * screenRes.y / 100.0f)); + g_Renderer.SetDisplayScissor(rect); + } + g_Renderer.DrawObjectIn3DSpace(item); + + if (item.GetHasScissor()) + g_Renderer.ResetDisplayScissor(); + } } void DrawItemsController::Clear() diff --git a/TombEngine/Game/effects/DisplaySprite.cpp b/TombEngine/Game/effects/DisplaySprite.cpp index dd3713d6b3..0b195cf683 100644 --- a/TombEngine/Game/effects/DisplaySprite.cpp +++ b/TombEngine/Game/effects/DisplaySprite.cpp @@ -13,6 +13,30 @@ namespace TEN::Effects::DisplaySprite { std::vector DisplaySprites = {}; + static bool g_hasActiveDisplayScissor = false; + static TEN::Renderer::Structures::RendererRectangle g_activeDisplayScissorRect = {}; + + void SetActiveDisplayScissor(const TEN::Renderer::Structures::RendererRectangle& rect) + { + g_hasActiveDisplayScissor = true; + g_activeDisplayScissorRect = rect; + } + + void ClearActiveDisplayScissor() + { + g_hasActiveDisplayScissor = false; + } + + bool HasActiveDisplayScissor() + { + return g_hasActiveDisplayScissor; + } + + const TEN::Renderer::Structures::RendererRectangle& GetActiveDisplayScissor() + { + return g_activeDisplayScissorRect; + } + void AddDisplaySprite(GAME_OBJECT_ID objectID, int spriteID, const Vector2& pos, short orient, const Vector2& scale, const Vector4& color, int priority, DisplaySpriteAlignMode alignMode, DisplaySpriteScaleMode scaleMode, BlendMode blendMode, DisplaySpritePhase source) @@ -29,6 +53,8 @@ namespace TEN::Effects::DisplaySprite displaySprite.ScaleMode = scaleMode; displaySprite.BlendMode = blendMode; displaySprite.Source = source; + displaySprite.HasScissor = g_hasActiveDisplayScissor; + displaySprite.ScissorRect = g_activeDisplayScissorRect; DisplaySprites.push_back(displaySprite); } diff --git a/TombEngine/Game/effects/DisplaySprite.h b/TombEngine/Game/effects/DisplaySprite.h index cb325ea63b..103310fe52 100644 --- a/TombEngine/Game/effects/DisplaySprite.h +++ b/TombEngine/Game/effects/DisplaySprite.h @@ -2,6 +2,7 @@ #include "Math/Math.h" #include "Objects/game_object_ids.h" #include "Renderer/RendererEnums.h" +#include "Renderer/Structures/RendererRectangle.h" namespace TEN::Effects::DisplaySprite { @@ -47,6 +48,9 @@ namespace TEN::Effects::DisplaySprite BlendMode BlendMode = BlendMode::AlphaBlend; DisplaySpritePhase Source = DisplaySpritePhase::Control; + + bool HasScissor = false; + TEN::Renderer::Structures::RendererRectangle ScissorRect = {}; }; // Result of display sprite layout calculation. @@ -65,6 +69,11 @@ namespace TEN::Effects::DisplaySprite void ClearAllDisplaySprites(); void ClearDrawPhaseDisplaySprites(); + void SetActiveDisplayScissor(const TEN::Renderer::Structures::RendererRectangle& rect); + void ClearActiveDisplayScissor(); + bool HasActiveDisplayScissor(); + const TEN::Renderer::Structures::RendererRectangle& GetActiveDisplayScissor(); + // Calculate complete layout data for a display sprite. // // NOTE: This function is defined inline in the header for performance reasons. diff --git a/TombEngine/Renderer/Renderer.cpp b/TombEngine/Renderer/Renderer.cpp index eec814b486..d3a850a1ed 100644 --- a/TombEngine/Renderer/Renderer.cpp +++ b/TombEngine/Renderer/Renderer.cpp @@ -404,6 +404,9 @@ namespace TEN::Renderer _graphicsDevice->SetScissor(s); } + void Renderer::SetDisplayScissor(RendererRectangle rect) { SetScissor(rect); } + void Renderer::ResetDisplayScissor() { ResetScissor(); } + void Renderer::SetGraphicsSettingsChanged() { _graphicsSettingsChanged = true; diff --git a/TombEngine/Renderer/Renderer.h b/TombEngine/Renderer/Renderer.h index b7ac436078..e1d4c5160a 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -209,6 +209,7 @@ namespace TEN::Renderer std::vector _lines2DToDraw = {}; std::vector _lines3DToDraw = {}; std::vector _triangles3DToDraw = {}; + std::vector> _debugDisplayRects = {}; // Textures, objects and sprites @@ -382,7 +383,9 @@ namespace TEN::Renderer void InitializeGameBars(); void InitializeMenuBars(int y); void InitializeSky(); + void AddStringInternal(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, const Color& color, const Vector2& scale, float rotation, int flags, int priority, BlendMode blendMode); void DrawAllStrings(); + void DrawDebugDisplayRects(); void PrepareDynamicLight(RendererLight& light); void PrepareLaserBarriers(RenderView& view); void PrepareSingleLaserBeam(RenderView& view); @@ -413,6 +416,7 @@ namespace TEN::Renderer void DrawSprites(RenderView& view, RendererPass rendererPass); void DrawDisplaySprites(RenderView& view, bool negativePriority); void DrawDisplayItems(); + void DrawAllDisplayLayers(RenderView& view); void DrawSortedFaces(RenderView& view); void DrawSingleSprite(RendererSortableObject* object, RendererObjectType lastObjectType, RenderView& view); void DrawRoomSorted(RendererSortableObject* objectInfo, RendererObjectType lastObjectType, RenderView& view); @@ -698,6 +702,8 @@ namespace TEN::Renderer void AddString(const std::string& string, const Vector2& pos, const Color& color, float scale, int flags); void AddString(const std::string& string, const Vector2& pos, const Vector2& area, const Color& color, float scale, int flags); void AddString(const std::string& string, const Vector2& currentPos, const Vector2& prevPos, const Vector2& area, const Color& color, float scale, int flags); + void AddString(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, const Color& color, const Vector2& scale, float rotation, int flags, int priority, BlendMode blendMode); + Vector2 GetDisplayStringSize(const std::string& text, const Vector2& scale = Vector2::One) const; void AddDebugString(const std::string& string, const Vector2& pos, const Color& color, float scale, RendererDebugPage page = RendererDebugPage::None); void FreeRendererData(); void AddDynamicPointLight(const Vector3& pos, float radius, const Color& color, bool castShadows, int hash = 0); @@ -728,6 +734,7 @@ namespace TEN::Renderer void AddDebugCylinder(const Vector3& center, const Quaternion& orient, float radius, float length, const Color& color, RendererDebugPage page = RendererDebugPage::None, bool isWireframe = true); void AddDebugSphere(const Vector3& center, float radius, const Color& color, RendererDebugPage page = RendererDebugPage::None, bool isWireframe = true); void AddDebugSphere(const BoundingSphere& sphere, const Color& color, RendererDebugPage page = RendererDebugPage::None, bool isWireframe = true); + void AddDebugDisplayRect(const RendererRectangle& rect, const Vector4& color); void PrintDebugMessage(const char* msg, va_list args); void PrintDebugMessage(const char* msg, ...); @@ -763,6 +770,8 @@ namespace TEN::Renderer void AddDisplaySprite(const RendererSprite& sprite, const Vector2& pos2D, short orient, const Vector2& size, const Vector4& color, int priority, BlendMode blendMode, const Vector2& aspectCorrection, RenderView& renderView); + void SetDisplayScissor(RendererRectangle rect); + void ResetDisplayScissor(); void CollectDisplaySprites(RenderView& renderView); PostProcessMode GetPostProcessMode(); diff --git a/TombEngine/Renderer/RendererDraw.cpp b/TombEngine/Renderer/RendererDraw.cpp index 062280f969..675c0491cc 100644 --- a/TombEngine/Renderer/RendererDraw.cpp +++ b/TombEngine/Renderer/RendererDraw.cpp @@ -1768,6 +1768,7 @@ namespace TEN::Renderer _lines3DToDraw.clear(); _triangles3DToDraw.clear(); _stringsToDraw.clear(); + _debugDisplayRects.clear(); _currentCausticsFrame++; _currentCausticsFrame %= 32; @@ -2045,11 +2046,10 @@ namespace TEN::Renderer if (renderMode == SceneRenderMode::Full && g_GameFlow->LastGameStatus == GameStatus::Normal) { CollectDisplaySprites(view); - DrawDisplaySprites(view, false); + DrawAllDisplayLayers(view); DrawDebugRenderTargets(view); - DrawAllStrings(); - DrawDisplaySprites(view, true); + DrawDebugDisplayRects(); } time2 = std::chrono::high_resolution_clock::now(); diff --git a/TombEngine/Renderer/RendererDraw2D.cpp b/TombEngine/Renderer/RendererDraw2D.cpp index 9998ddeaba..6276e880a6 100644 --- a/TombEngine/Renderer/RendererDraw2D.cpp +++ b/TombEngine/Renderer/RendererDraw2D.cpp @@ -328,6 +328,25 @@ namespace TEN::Renderer DrawFullScreenQuad(texture, Vector3(fade), true); } + void Renderer::AddDebugDisplayRect(const RendererRectangle& rect, const Vector4& color) + { + _debugDisplayRects.push_back({ rect, color }); + } + + void Renderer::DrawDebugDisplayRects() + { + if (_debugDisplayRects.empty()) + return; + + ResetScissor(); + _spriteBatch->Begin(SpriteSortingMode::Deferred, BlendMode::AlphaBlend); + + for (const auto& [rect, color] : _debugDisplayRects) + _spriteBatch->Draw(_whiteTexture.get(), rect, color); + + _spriteBatch->End(); + } + void Renderer::DrawDisplaySprites(RenderView& renderView, bool negativePriority) { constexpr auto VERTEX_COUNT = 4; @@ -336,11 +355,22 @@ namespace TEN::Renderer return; ITexture2D* texture2DPtr = nullptr; + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + for (const auto& spriteToDraw : renderView.DisplaySpritesToDraw) { if ((spriteToDraw.Priority >= 0) == negativePriority) continue; + // Handle scissor rect changes. + bool scissorChanged = (spriteToDraw.HasScissor != currentHasScissor) || + (spriteToDraw.HasScissor && + (spriteToDraw.ScissorRect.Left != currentScissor.Left || + spriteToDraw.ScissorRect.Top != currentScissor.Top || + spriteToDraw.ScissorRect.Right != currentScissor.Right || + spriteToDraw.ScissorRect.Bottom != currentScissor.Bottom)); + if (texture2DPtr == nullptr) { _shaders.Bind(Shader::FullScreenQuad); @@ -348,14 +378,32 @@ namespace TEN::Renderer _graphicsDevice->SetPrimitiveType(PrimitiveType::TriangleList); _graphicsDevice->SetInputLayout(_vertexInputLayout.get()); + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + _primitiveBatch->Begin(); BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); SetBlendMode(spriteToDraw.BlendMode); } - else if (texture2DPtr != spriteToDraw.SpritePtr->Texture || _lastBlendMode != spriteToDraw.BlendMode) + else if (texture2DPtr != spriteToDraw.SpritePtr->Texture || _lastBlendMode != spriteToDraw.BlendMode || scissorChanged) { _primitiveBatch->End(); + + if (scissorChanged) + { + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + else + ResetScissor(); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + } + _primitiveBatch->Begin(); BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); @@ -403,6 +451,10 @@ namespace TEN::Renderer if (texture2DPtr != nullptr) _primitiveBatch->End(); + + // Reset scissor if it was active. + if (currentHasScissor) + ResetScissor(); } void Renderer::DrawFullScreenQuad(ITextureBase* texture, Vector3 color, bool fit, float customAspect) @@ -554,10 +606,221 @@ namespace TEN::Renderer spriteToDraw.Priority = priority; spriteToDraw.BlendMode = blendMode; spriteToDraw.AspectCorrection = aspectCorrection; + spriteToDraw.HasScissor = false; renderView.DisplaySpritesToDraw.push_back(spriteToDraw); } + // Draw all display sprites and strings interleaved by priority. + // Sprites at the same priority draw before strings. 3D display items are inserted + // at the transition from negative to non-negative priority. + void Renderer::DrawAllDisplayLayers(RenderView& view) + { + constexpr auto VERTEX_COUNT = 4; + + auto& sprites = view.DisplaySpritesToDraw; + + std::stable_sort(_stringsToDraw.begin(), _stringsToDraw.end(), + [](const auto& a, const auto& b) { return a.Priority < b.Priority; }); + + if (sprites.empty() && _stringsToDraw.empty()) + { + DrawDisplayItems(); + return; + } + + // Gather sorted unique priorities from both lists. + auto priorities = std::vector{}; + priorities.reserve(sprites.size() + _stringsToDraw.size()); + for (const auto& sprite : sprites) priorities.push_back(sprite.Priority); + for (const auto& str : _stringsToDraw) priorities.push_back(str.Priority); + std::sort(priorities.begin(), priorities.end()); + priorities.erase(std::unique(priorities.begin(), priorities.end()), priorities.end()); + + float shadowOffset = 1.5f / (REFERENCE_FONT_SIZE / _gameFont->GetLineSpacing()); + auto shadowColor = (Vector4)g_GameFlow->GetSettings()->UI.ShadowTextColor; + + bool itemsDrawn = false; + int spriteIdx = 0; + int stringIdx = 0; + + for (int priority : priorities) + { + // Insert 3D display items at the transition from negative to non-negative priority. + if (!itemsDrawn && priority >= 0) + { + DrawDisplayItems(); + itemsDrawn = true; + } + + // Draw sprites at this priority level. + { + ITexture2D* texture2DPtr = nullptr; + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + + while (spriteIdx < (int)sprites.size() && sprites[spriteIdx].Priority == priority) + { + const auto& spriteToDraw = sprites[spriteIdx++]; + + bool scissorChanged = + (spriteToDraw.HasScissor != currentHasScissor) || + (spriteToDraw.HasScissor && + (spriteToDraw.ScissorRect.Left != currentScissor.Left || + spriteToDraw.ScissorRect.Top != currentScissor.Top || + spriteToDraw.ScissorRect.Right != currentScissor.Right || + spriteToDraw.ScissorRect.Bottom != currentScissor.Bottom)); + + if (texture2DPtr == nullptr) + { + _shaders.Bind(Shader::FullScreenQuad); + _graphicsDevice->SetPrimitiveType(PrimitiveType::TriangleList); + _graphicsDevice->SetInputLayout(_vertexInputLayout.get()); + + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + + _primitiveBatch->Begin(); + BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); + SetBlendMode(spriteToDraw.BlendMode); + } + else if (texture2DPtr != spriteToDraw.SpritePtr->Texture || _lastBlendMode != spriteToDraw.BlendMode || scissorChanged) + { + _primitiveBatch->End(); + + if (scissorChanged) + { + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + else + ResetScissor(); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + } + + _primitiveBatch->Begin(); + BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); + SetBlendMode(spriteToDraw.BlendMode); + } + + auto vertices = std::array + { + spriteToDraw.Size / 2, + Vector2(-spriteToDraw.Size.x, spriteToDraw.Size.y) / 2, + -spriteToDraw.Size / 2, + Vector2(spriteToDraw.Size.x, -spriteToDraw.Size.y) / 2 + }; + + // NOTE: Must rotate 180 degrees to account for +Y being down. + auto rotMatrix = Matrix::CreateRotationZ(TO_RAD(spriteToDraw.Orientation + ANGLE(180.0f))); + for (auto& vertex : vertices) + { + vertex = Vector2::Transform(vertex, rotMatrix); + vertex *= spriteToDraw.AspectCorrection; + vertex += spriteToDraw.Position; + vertex = TEN::Utils::Convert2DPositionToNDC(vertex); + } + + auto rVertices = std::array{}; + for (int i = 0; i < (int)rVertices.size(); i++) + { + rVertices[i].Position = Vector3(vertices[i]); + rVertices[i].UV = spriteToDraw.SpritePtr->UV[i]; + rVertices[i].Color = VectorColorToRGBA(Vector4( + spriteToDraw.Color.x, spriteToDraw.Color.y, + spriteToDraw.Color.z, spriteToDraw.Color.w)); + } + + _primitiveBatch->DrawQuad(rVertices[0], rVertices[1], rVertices[2], rVertices[3]); + texture2DPtr = spriteToDraw.SpritePtr->Texture; + } + + if (texture2DPtr != nullptr) + { + _primitiveBatch->End(); + + if (currentHasScissor) + ResetScissor(); + } + } + + // Draw strings at this priority level. + if (stringIdx < (int)_stringsToDraw.size() && _stringsToDraw[stringIdx].Priority == priority) + { + auto currentBlend = BlendMode::AlphaBlend; + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + + ResetScissor(); + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); + + while (stringIdx < (int)_stringsToDraw.size() && _stringsToDraw[stringIdx].Priority == priority) + { + const auto& rString = _stringsToDraw[stringIdx++]; + + if (rString.Blend != currentBlend) + { + _spriteBatch->End(); + currentBlend = rString.Blend; + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); + } + + bool scissorChanged = + (rString.HasScissor != currentHasScissor) || + (rString.HasScissor && + (rString.ScissorRect.Left != currentScissor.Left || + rString.ScissorRect.Top != currentScissor.Top || + rString.ScissorRect.Right != currentScissor.Right || + rString.ScissorRect.Bottom != currentScissor.Bottom)); + + if (scissorChanged) + { + _spriteBatch->End(); + + if (rString.HasScissor) + SetScissor(rString.ScissorRect); + else + ResetScissor(); + + currentHasScissor = rString.HasScissor; + currentScissor = rString.ScissorRect; + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); + } + + auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor()); + + if (rString.Flags & (int)PrintStringFlags::Outline) + { + auto shadowPos = Vector2(drawPos.x + shadowOffset * rString.Scale.y, drawPos.y + shadowOffset * rString.Scale.y); + _gameFont->DrawString( + _spriteBatch.get(), rString.String, + shadowPos, + (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, + rString.Rotation, Vector2::Zero, rString.Scale.x); + } + + _gameFont->DrawString( + _spriteBatch.get(), rString.String, + drawPos, + (rString.Color * rString.Color.w) * ScreenFadeCurrent, + rString.Rotation, Vector2::Zero, rString.Scale.x); + } + + _spriteBatch->End(); + + if (currentHasScissor) + ResetScissor(); + } + } + + if (!itemsDrawn) + DrawDisplayItems(); + } + void Renderer::CollectDisplaySprites(RenderView& renderView) { constexpr auto DISPLAY_SPACE_ASPECT = DISPLAY_SPACE_RES.x / DISPLAY_SPACE_RES.y; @@ -596,6 +859,14 @@ namespace TEN::Renderer displaySprite.BlendMode, layout.AspectCorrection, renderView); + + // Transfer scissor rect from source sprite. + if (displaySprite.HasScissor) + { + auto& last = renderView.DisplaySpritesToDraw.back(); + last.HasScissor = true; + last.ScissorRect = displaySprite.ScissorRect; + } } std::sort( diff --git a/TombEngine/Renderer/RendererDrawMenu.cpp b/TombEngine/Renderer/RendererDrawMenu.cpp index 14dd29692b..42a079279d 100644 --- a/TombEngine/Renderer/RendererDrawMenu.cpp +++ b/TombEngine/Renderer/RendererDrawMenu.cpp @@ -1418,12 +1418,10 @@ namespace TEN::Renderer RenderScene(_backBuffer.get(), _gameCamera, SceneRenderMode::NoHud); } - // Draw display sprites sorted by priority. + // Draw display sprites and strings sorted by priority. CollectDisplaySprites(_gameCamera); - DrawDisplaySprites(_gameCamera, false); - DrawDisplayItems(); - DrawDisplaySprites(_gameCamera, true); - DrawAllStrings(); + DrawAllDisplayLayers(_gameCamera); + DrawDebugDisplayRects(); if (staticBackground) { diff --git a/TombEngine/Renderer/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index c4aa239305..9fdc2de905 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -1,9 +1,13 @@ #include "framework.h" #include "Renderer/Renderer.h" +#include + +#include "Game/effects/DisplaySprite.h" #include "Scripting/Include/Flow/ScriptInterfaceFlowHandler.h" #include "Specific/trutils.h" +using namespace TEN::Effects::DisplaySprite; using namespace TEN::Utils; namespace TEN::Renderer @@ -26,6 +30,25 @@ namespace TEN::Renderer AddString(string, Vector2(x, y), Color(color), 1.0f, flags); } + Vector2 Renderer::GetDisplayStringSize(const std::string& text, const Vector2& scale) const + { + if (text.empty() || _gameFont == nullptr) + return Vector2::Zero; + + auto screenRes = GetScreenResolution(); + auto factor = Vector2((float)screenRes.x / DISPLAY_SPACE_RES.x, (float)screenRes.y / DISPLAY_SPACE_RES.y); + float uiScale = (screenRes.x > screenRes.y) ? factor.y : factor.x; + float fontSpacing = _gameFont->GetLineSpacing(); + float fontScale = REFERENCE_FONT_SIZE / fontSpacing; + auto stringScale = Vector2(uiScale * fontScale) * scale; + float baseScale = stringScale.y; + + auto measured = Vector2(_gameFont->MeasureString(text)) * baseScale; + + // Convert pixel size back to display space (800x600 units). + return Vector2(measured.x / factor.x, measured.y / factor.y); + } + void Renderer::AddString(const std::string& string, const Vector2& pos, const Color& color, float scale, int flags) { AddString(string, pos, Vector2::Zero, Color(color), scale, flags); @@ -36,7 +59,21 @@ namespace TEN::Renderer AddString(string, pos, pos, area, color, scale, flags); } + void Renderer::AddString(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, + const Color& color, const Vector2& scale, float rotation, int flags, + int priority, BlendMode blendMode) + { + AddStringInternal(string, pos, prevPos, area, color, scale, rotation, flags, priority, blendMode); + } + void Renderer::AddString(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, const Color& color, float scale, int flags) + { + AddStringInternal(string, pos, prevPos, area, color, Vector2(scale), 0.0f, flags, 0, BlendMode::AlphaBlend); + } + + void Renderer::AddStringInternal(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, + const Color& color, const Vector2& scale, float rotation, int flags, + int priority, BlendMode blendMode) { if (_isLocked) return; @@ -51,8 +88,9 @@ namespace TEN::Renderer float uiScale = (screenRes.x > screenRes.y) ? factor.y : factor.x; float fontSpacing = _gameFont->GetLineSpacing(); float fontScale = REFERENCE_FONT_SIZE / fontSpacing; - float stringScale = (uiScale * fontScale) * scale; - float spaceWidth = Vector3(_gameFont->MeasureString(" ")).x * stringScale; + auto stringScale = Vector2(uiScale * fontScale) * scale; + float baseScale = stringScale.y; + float spaceWidth = Vector3(_gameFont->MeasureString(" ")).x * baseScale; std::vector stringLines; @@ -76,7 +114,7 @@ namespace TEN::Renderer for (const auto& word : words) { - float wordWidth = Vector3(_gameFont->MeasureString(word)).x * stringScale; + float wordWidth = Vector3(_gameFont->MeasureString(word)).x * baseScale; if (!currentLine.empty() && (currentLineWidth + wordWidth + spaceWidth > area.x * factor.x)) { @@ -109,9 +147,9 @@ namespace TEN::Renderer for (const auto& line : stringLines) { if (line.empty()) - totalHeight += fontSpacing * stringScale; + totalHeight += fontSpacing * baseScale; else - totalHeight += Vector2(_gameFont->MeasureString(line)).y * stringScale; + totalHeight += Vector2(_gameFont->MeasureString(line.c_str())).y * stringScale.y; } // Calculate maximum textbox height. @@ -144,9 +182,15 @@ namespace TEN::Renderer rString.Position = Vector2::Zero; rString.Color = color; rString.Scale = stringScale; + rString.Rotation = rotation; + rString.Priority = priority; + rString.Blend = blendMode; + rString.HasScissor = HasActiveDisplayScissor(); + if (rString.HasScissor) + rString.ScissorRect = GetActiveDisplayScissor(); // Measure string. - auto stringSize = line.empty() ? Vector2(0, fontSpacing * rString.Scale) : Vector2(_gameFont->MeasureString(line)) * rString.Scale; + auto stringSize = line.empty() ? Vector2(0, fontSpacing * rString.Scale.y) : Vector2(_gameFont->MeasureString(line)) * rString.Scale.y; // If height clipping enabled, stop drawing when exceeding maxHeight. if (maxHeight > 0.0f && (yOffset + stringSize.y) > maxHeight) @@ -166,7 +210,7 @@ namespace TEN::Renderer else { // Calculate indentation to account for string scaling. - auto indent = line.empty() ? 0 : _gameFont->FindGlyph(line.at(0)).XAdvance * rString.Scale; + auto indent = line.empty() ? 0 : _gameFont->FindGlyph(line.at(0)).XAdvance * rString.Scale.y; rString.Position.x = pos.x * factor.x + indent; rString.PrevPosition.x = prevPos.x * factor.x + indent; @@ -196,35 +240,78 @@ namespace TEN::Renderer if (_stringsToDraw.empty()) return; - SetBlendMode(BlendMode::AlphaBlend); + // Sort by priority (lower priority draws first, i.e. behind higher). + std::stable_sort(_stringsToDraw.begin(), _stringsToDraw.end(), + [](const auto& a, const auto& b) { return a.Priority < b.Priority; }); float shadowOffset = 1.5f / (REFERENCE_FONT_SIZE / _gameFont->GetLineSpacing()); auto shadowColor = (Vector4)g_GameFlow->GetSettings()->UI.ShadowTextColor; - _spriteBatch->Begin(SpriteSortingMode::Deferred, BlendMode::PremultipliedAlphaBlend); + ResetScissor(); + + auto currentBlend = BlendMode::AlphaBlend; + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); for (const auto& rString : _stringsToDraw) { - auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor(true)); + // Switch blend mode per string if needed. + if (rString.Blend != currentBlend) + { + _spriteBatch->End(); + currentBlend = rString.Blend; + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); + } + + // Handle scissor rect changes. + bool scissorChanged = (rString.HasScissor != currentHasScissor) || + (rString.HasScissor && + (rString.ScissorRect.Left != currentScissor.Left || + rString.ScissorRect.Top != currentScissor.Top || + rString.ScissorRect.Right != currentScissor.Right || + rString.ScissorRect.Bottom != currentScissor.Bottom)); + + if (scissorChanged) + { + _spriteBatch->End(); + + if (rString.HasScissor) + SetScissor(rString.ScissorRect); + else + ResetScissor(); + + currentHasScissor = rString.HasScissor; + currentScissor = rString.ScissorRect; + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); + } + + auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor()); // Draw shadow. if (rString.Flags & (int)PrintStringFlags::Outline) { + auto shadowPos = Vector2(drawPos.x + shadowOffset * rString.Scale.y, drawPos.y + shadowOffset * rString.Scale.y); + _gameFont->DrawString( _spriteBatch.get(), rString.String, - Vector2(drawPos.x + shadowOffset * rString.Scale, drawPos.y + shadowOffset * rString.Scale), + shadowPos, (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, - 0.0f, Vector2::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale.x); } // Draw string. _gameFont->DrawString( _spriteBatch.get(), rString.String, - Vector2(drawPos.x, drawPos.y), + drawPos, (rString.Color * rString.Color.w) * ScreenFadeCurrent, - 0.0f, Vector2::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale.x); } _spriteBatch->End(); + + if (currentHasScissor) + ResetScissor(); } } diff --git a/TombEngine/Renderer/Structures/RendererSprite2D.h b/TombEngine/Renderer/Structures/RendererSprite2D.h index 87eb067bdc..0ef2f4c4da 100644 --- a/TombEngine/Renderer/Structures/RendererSprite2D.h +++ b/TombEngine/Renderer/Structures/RendererSprite2D.h @@ -1,6 +1,7 @@ #pragma once #include "Renderer/Structures/RendererSprite.h" #include "Renderer/RendererEnums.h" +#include "Renderer/Structures/RendererRectangle.h" namespace TEN::Renderer::Structures { @@ -17,5 +18,8 @@ namespace TEN::Renderer::Structures BlendMode BlendMode = BlendMode::AlphaBlend; Vector2 AspectCorrection = Vector2::One; + + bool HasScissor = false; + RendererRectangle ScissorRect = {}; }; } diff --git a/TombEngine/Renderer/Structures/RendererStringToDraw.h b/TombEngine/Renderer/Structures/RendererStringToDraw.h index 9c197f69e0..9b1f5d4858 100644 --- a/TombEngine/Renderer/Structures/RendererStringToDraw.h +++ b/TombEngine/Renderer/Structures/RendererStringToDraw.h @@ -1,6 +1,9 @@ #pragma once #include +#include "Renderer/RendererEnums.h" +#include "Renderer/Structures/RendererRectangle.h" + namespace TEN::Renderer::Structures { using namespace DirectX::SimpleMath; @@ -12,6 +15,12 @@ namespace TEN::Renderer::Structures int Flags; std::string String; Vector4 Color; - float Scale; + Vector2 Scale; + float Rotation = 0.0f; + int Priority = 0; + BlendMode Blend = BlendMode::AlphaBlend; + + bool HasScissor = false; + RendererRectangle ScissorRect = {}; }; } diff --git a/TombEngine/Scripting/Internal/ReservedScriptNames.h b/TombEngine/Scripting/Internal/ReservedScriptNames.h index 33a79336fe..68f4655320 100644 --- a/TombEngine/Scripting/Internal/ReservedScriptNames.h +++ b/TombEngine/Scripting/Internal/ReservedScriptNames.h @@ -52,6 +52,11 @@ static constexpr char ScriptReserved_GetPlayerInteractedMoveable[] = "GetInterac static constexpr char ScriptReserved_DisplaySprite[] = "DisplaySprite"; static constexpr char ScriptReserved_DisplayStringGetObjectID[] = "GetObjectID"; static constexpr char ScriptReserved_DisplayStringGetSpriteID[] = "GetSpriteID"; + +// DisplayString (View) object +static constexpr char ScriptReserved_GetText[] = "GetText"; +static constexpr char ScriptReserved_SetText[] = "SetText"; +static constexpr char ScriptReserved_GetTranslated[] = "GetTranslated"; static constexpr char ScriptReserved_DisplayStringGetPosition[] = "GetPosition"; static constexpr char ScriptReserved_DisplayStringGetRotation[] = "GetRotation"; static constexpr char ScriptReserved_DisplayStringGetScale[] = "GetScale"; @@ -65,6 +70,17 @@ static constexpr char ScriptReserved_DisplayStringSetScale[] = "SetScale"; static constexpr char ScriptReserved_DisplayStringSetColor[] = "SetColor"; static constexpr char ScriptReserved_DisplaySpriteDraw[] = "Draw"; +// DisplayArea (View) object +static constexpr char ScriptReserved_DisplayArea[] = "DisplayArea"; +static constexpr char ScriptReserved_GetSize[] = "GetSize"; +static constexpr char ScriptReserved_SetSize[] = "SetSize"; +static constexpr char ScriptReserved_AddItem[] = "AddItem"; +static constexpr char ScriptReserved_RemoveItem[] = "RemoveItem"; +static constexpr char ScriptReserved_Clear[] = "Clear"; +static constexpr char ScriptReserved_Debug[] = "Debug"; +static constexpr char ScriptReserved_SetScissor[] = "SetScissor"; +static constexpr char ScriptReserved_ClearScissor[] = "ClearScissor"; + static constexpr char ScriptReserved_EndReasonDeath[] = "DEATH"; static constexpr char ScriptReserved_EndReasonExitToTitle[] = "EXIT_TO_TITLE"; static constexpr char ScriptReserved_EndReasonLevelComplete[] = "LEVEL_COMPLETE"; diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp new file mode 100644 index 0000000000..d5bebbdda3 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp @@ -0,0 +1,240 @@ +#include "framework.h" +#include "Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h" + +#include "Game/effects/DisplaySprite.h" +#include "Renderer/Renderer.h" +#include "Renderer/Structures/RendererRectangle.h" +#include "Scripting/Internal/LuaHandler.h" +#include "Scripting/Internal/ReservedScriptNames.h" +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" + +using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Renderer::Structures; +using namespace TEN::Scripting::Types; +using TEN::Renderer::g_Renderer; + +/// Represents a clipping area for display items. +// +// @tenclass View.DisplayArea +// @pragma nostrip + +namespace TEN::Scripting::DisplayArea +{ + void ScriptDisplayArea::Register(sol::state& state, sol::table& parent) + { + using ctors = sol::constructors< + ScriptDisplayArea(const Vec2&, const Vec2&), + ScriptDisplayArea(const Vec2&, const Vec2&, sol::table)>; + + // Register type. + parent.new_usertype( + ScriptReserved_DisplayArea, + ctors(), + sol::call_constructor, ctors(), + + ScriptReserved_DisplayStringGetPosition, &ScriptDisplayArea::GetPosition, + ScriptReserved_DisplayStringSetPosition, &ScriptDisplayArea::SetPosition, + ScriptReserved_GetSize, &ScriptDisplayArea::GetSize, + ScriptReserved_SetSize, &ScriptDisplayArea::SetSize, + ScriptReserved_AddItem, &ScriptDisplayArea::AddItem, + ScriptReserved_RemoveItem, &ScriptDisplayArea::RemoveItem, + ScriptReserved_Clear, &ScriptDisplayArea::Clear, + ScriptReserved_Debug, &ScriptDisplayArea::Debug, + ScriptReserved_DisplaySpriteDraw, &ScriptDisplayArea::Draw); + } + + /// Create a DisplayArea object. + // @function DisplayArea + // @tparam Vec2 pos Top-left position of the area in percent. + // @tparam Vec2 size Width and height of the area in percent. + // @tparam[opt] table items Sequence of items. Each entry is either a bare DisplayString or DisplaySprite, + // or a table { item, { priority, alignMode, scaleMode, blendMode } }. + // @treturn DisplayArea A new DisplayArea object. + ScriptDisplayArea::ScriptDisplayArea(const Vec2& pos, const Vec2& size) + { + _position = pos; + _size = size; + } + + ScriptDisplayArea::ScriptDisplayArea(const Vec2& pos, const Vec2& size, sol::table items) + { + _position = pos; + _size = size; + + for (auto& [key, val] : items) + { + // Entry is { item, { args } }. + if (val.is()) + { + auto entry = val.as(); + auto entryItem = entry.get>(1); + auto entryArgs = entry.get>(2); + + if (entryItem.has_value()) + AddItem(entryItem.value(), entryArgs); + } + // Entry is a bare item. + else + { + AddItem(val.as(), sol::nullopt); + } + } + } + + /// Get the top-left position of the area in percent. + // @function DisplayArea:GetPosition + // @treturn Vec2 Top-left position in percent. + Vec2 ScriptDisplayArea::GetPosition() const + { + return _position; + } + + /// Get the size of the area in percent. + // @function DisplayArea:GetSize + // @treturn Vec2 Width and height in percent. + Vec2 ScriptDisplayArea::GetSize() const + { + return _size; + } + + /// Set the top-left position of the area in percent. + // @function DisplayArea:SetPosition + // @tparam Vec2 position New top-left position in percent. + void ScriptDisplayArea::SetPosition(const Vec2& pos) + { + _position = pos; + } + + /// Set the size of the area in percent. + // @function DisplayArea:SetSize + // @tparam Vec2 size New width and height in percent. + void ScriptDisplayArea::SetSize(const Vec2& size) + { + _size = size; + } + + /// Add a display item to the area. + // Items can be View.DisplayString or View.DisplaySprite objects. + // Arguments are forwarded to the item's Draw() method. + // @function DisplayArea:AddItem + // @tparam object item A DisplayString or DisplaySprite to clip within this area. + // @tparam[opt] table args Draw arguments forwarded to the item, e.g. { priority, alignMode, scaleMode, blendMode }. + void ScriptDisplayArea::AddItem(sol::object item, sol::optional args) + { + auto argsObj = args.has_value() ? sol::object(args.value()) : sol::object(sol::lua_nil); + _items.push_back({ item, argsObj }); + } + + /// Remove a display item from the area. + // @function DisplayArea:RemoveItem + // @tparam object item The item to remove. + void ScriptDisplayArea::RemoveItem(sol::object item) + { + for (auto it = _items.begin(); it != _items.end(); ++it) + { + if (it->first == item) + { + _items.erase(it); + return; + } + } + } + + /// Remove all display items from the area. + // @function DisplayArea:Clear + void ScriptDisplayArea::Clear() + { + _items.clear(); + } + + /// Draw all items in the area, clipped to the area bounds. + // @function DisplayArea:Draw + void ScriptDisplayArea::Draw() + { + if (_items.empty()) + return; + + // Convert percent to pixel coordinates. + auto screenRes = g_Renderer.GetScreenResolution(); + float screenWidth = (float)screenRes.x; + float screenHeight = (float)screenRes.y; + + int left = (int)(_position.x * screenWidth / 100.0f); + int top = (int)(_position.y * screenHeight / 100.0f); + int right = (int)((_position.x + _size.x) * screenWidth / 100.0f); + int bottom = (int)((_position.y + _size.y) * screenHeight / 100.0f); + + auto scissorRect = RendererRectangle(left, top, right, bottom); + + // Activate scissor for all items queued during this scope. + SetActiveDisplayScissor(scissorRect); + + for (auto& [item, args] : _items) + { + // Retrieve the Draw method via __index (works for both tables and userdata). + auto* L = item.lua_state(); + item.push(L); // stack: [obj] + lua_getfield(L, -1, ScriptReserved_DisplaySpriteDraw); // stack: [obj, func] + + if (lua_isnil(L, -1)) + { + lua_pop(L, 2); + continue; + } + + // Anchor the function in the registry before clearing the stack. + auto drawFunc = sol::protected_function(L, -1); + lua_pop(L, 2); // stack: [] + + sol::protected_function_result result; + + if (args.is()) + { + auto argsTable = args.as(); + auto argCount = (int)argsTable.size(); + + std::vector argVec; + argVec.reserve(argCount); + for (int i = 1; i <= argCount; i++) + argVec.push_back(argsTable.raw_get(i)); + + result = drawFunc(item, sol::as_args(argVec)); + } + else + { + result = drawFunc(item); + } + + if (!result.valid()) + { + sol::error err = result; + TENLog(std::string("DisplayArea: Error drawing item: ") + err.what(), LogLevel::Warning); + } + } + + ClearActiveDisplayScissor(); + } + + /// Draw a debug overlay showing the area bounds for the current frame. + // @function DisplayArea:Debug + // @tparam[opt=Color(255, 0, 0, 128)] Color color Fill color of the debug overlay. + void ScriptDisplayArea::Debug(sol::optional colorOpt) + { + auto color = colorOpt.value_or(ScriptColor(255, 0, 0, 128)); + + auto screenRes = g_Renderer.GetScreenResolution(); + float screenWidth = (float)screenRes.x; + float screenHeight = (float)screenRes.y; + + int left = (int)(_position.x * screenWidth / 100.0f); + int top = (int)(_position.y * screenHeight / 100.0f); + int right = (int)((_position.x + _size.x) * screenWidth / 100.0f); + int bottom = (int)((_position.y + _size.y) * screenHeight / 100.0f); + + auto rect = RendererRectangle(left, top, right, bottom); + auto rgba = Vector4(color.GetR(), color.GetG(), color.GetB(), color.GetA()) / (float)UCHAR_MAX; + + g_Renderer.AddDebugDisplayRect(rect, rgba); + } +} diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h new file mode 100644 index 0000000000..8cf150f149 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" + +using namespace TEN::Scripting::Types; + +namespace TEN::Scripting::DisplayArea +{ + class ScriptDisplayArea + { + public: + static void Register(sol::state& state, sol::table& parent); + + private: + // Members + Vec2 _position = Vec2(0.0f, 0.0f); + Vec2 _size = Vec2(100.0f, 100.0f); + + // Each entry: { drawable object, optional args table }. + std::vector> _items = {}; + + public: + // Constructors + ScriptDisplayArea(const Vec2& pos, const Vec2& size); + ScriptDisplayArea(const Vec2& pos, const Vec2& size, sol::table items); + + // Getters + Vec2 GetPosition() const; + Vec2 GetSize() const; + + // Setters + void SetPosition(const Vec2& pos); + void SetSize(const Vec2& size); + + // Item management + void AddItem(sol::object item, sol::optional args); + void RemoveItem(sol::object item); + void Clear(); + + // Utilities + void Draw(); + void Debug(sol::optional color); + }; +} diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.cpp index 55fbc7d71a..5deb2247ff 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.cpp @@ -51,6 +51,8 @@ namespace TEN::Scripting::DisplayItem ScriptReserved_SetFrameNumber, &ScriptDisplayItem::SetFrameNumber, ScriptReserved_DisplayItemSetMeshBits, &ScriptDisplayItem::SetMeshBits, + ScriptReserved_SetScissor, &ScriptDisplayItem::SetScissor, + ScriptReserved_ClearScissor, &ScriptDisplayItem::ClearScissor, ScriptReserved_GetObjectID, &ScriptDisplayItem::GetObjectID, ScriptReserved_GetPosition, &ScriptDisplayItem::GetPosition, @@ -309,6 +311,25 @@ namespace TEN::Scripting::DisplayItem item->SetMeshBits(meshBits); } + /// Set a scissor clipping rectangle for the display item. + // Clips the item to the specified rectangle when drawn. + // @function DisplayItem:SetScissor + // @tparam Vec2 pos Top-left position of the scissor rectangle in percent. + // @tparam Vec2 size Width and height of the scissor rectangle in percent. + void ScriptDisplayItem::SetScissor(const Vec2& pos, const Vec2& size) + { + if (auto* item = TryGetItem()) + item->SetScissor(Vector2(pos.x, pos.y), Vector2(size.x, size.y)); + } + + /// Clear the scissor clipping rectangle from the display item. + // @function DisplayItem:ClearScissor + void ScriptDisplayItem::ClearScissor() + { + if (auto* item = TryGetItem()) + item->ClearScissor(); + } + /// Make the specified mesh of a display item visible or invisible. // @function DisplayItem:SetMeshVisible // @tparam int meshIndex Mesh index. diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.h b/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.h index 25506ae341..534e8f766c 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.h +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.h @@ -50,6 +50,8 @@ namespace TEN::Scripting::DisplayItem void SetAnimNumber(int animNumber); void SetFrameNumber(int frameNumber); void SetMeshBits(int meshbits); + void SetScissor(const Vec2& pos, const Vec2& size); + void ClearScissor(); // Getters diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.cpp index 0c9a651b7d..6216164537 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.cpp @@ -12,6 +12,8 @@ #include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" #include "Specific/level.h" +using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Renderer::Structures; using namespace TEN::Scripting::Types; using TEN::Renderer::g_Renderer; @@ -60,6 +62,8 @@ namespace TEN::Scripting::DisplaySprite ScriptReserved_DisplayStringSetRotation, &ScriptDisplaySprite::SetRotation, ScriptReserved_DisplayStringSetScale, &ScriptDisplaySprite::SetScale, ScriptReserved_DisplayStringSetColor, &ScriptDisplaySprite::SetColor, + ScriptReserved_SetScissor, &ScriptDisplaySprite::SetScissor, + ScriptReserved_ClearScissor, &ScriptDisplaySprite::ClearScissor, ScriptReserved_DisplaySpriteDraw, &ScriptDisplaySprite::Draw); } @@ -337,6 +341,25 @@ namespace TEN::Scripting::DisplaySprite _color = color; } + /// Set a scissor clipping rectangle for the display sprite. + // Clips the sprite to the specified rectangle when drawn. + // @function DisplaySprite:SetScissor + // @tparam Vec2 pos Top-left position of the scissor rectangle in percent. + // @tparam Vec2 size Width and height of the scissor rectangle in percent. + void ScriptDisplaySprite::SetScissor(const Vec2& pos, const Vec2& size) + { + _hasScissor = true; + _scissorPos = pos; + _scissorSize = size; + } + + /// Clear the scissor clipping rectangle from the display sprite. + // @function DisplaySprite:ClearScissor + void ScriptDisplaySprite::ClearScissor() + { + _hasScissor = false; + } + /// Draw the display sprite in display space for the current frame. // @function DisplaySprite:Draw // @tparam[opt=0] int priority Draw priority. Can be thought of as a layer, with higher values having precedence. @@ -371,6 +394,17 @@ namespace TEN::Scripting::DisplaySprite auto convertedScale = Vector2(_scale.x, _scale.y) * SCALE_CONVERSION_COEFF; auto convertedColor = Vector4(_color.GetR(), _color.GetG(), _color.GetB(), _color.GetA()) / UCHAR_MAX; + if (_hasScissor) + { + auto screenRes = g_Renderer.GetScreenResolution(); + auto rect = RendererRectangle( + (int)(_scissorPos.x * screenRes.x / 100.0f), + (int)(_scissorPos.y * screenRes.y / 100.0f), + (int)((_scissorPos.x + _scissorSize.x) * screenRes.x / 100.0f), + (int)((_scissorPos.y + _scissorSize.y) * screenRes.y / 100.0f)); + SetActiveDisplayScissor(rect); + } + AddDisplaySprite( _objectID, _spriteID, convertedPos, convertedRot, convertedScale, convertedColor, @@ -379,5 +413,8 @@ namespace TEN::Scripting::DisplaySprite scaleMode.value_or(DEFAULT_SCALE_MODE), blendMode.value_or(DEFAULT_BLEND_MODE), DisplaySpritePhase::Control); + + if (_hasScissor) + ClearActiveDisplayScissor(); } } diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.h b/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.h index dd4a8277b9..2c93229e90 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.h +++ b/TombEngine/Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.h @@ -28,6 +28,10 @@ namespace TEN::Scripting::DisplaySprite Vec2 _scale = Vec2(0.0f, 0.0f); ScriptColor _color = ScriptColor(255, 255, 255, 255); + bool _hasScissor = false; + Vec2 _scissorPos = Vec2(0.0f, 0.0f); + Vec2 _scissorSize = Vec2(100.0f, 100.0f); + public: // Constructors ScriptDisplaySprite(GAME_OBJECT_ID objectID, int spriteID, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color); @@ -51,6 +55,8 @@ namespace TEN::Scripting::DisplaySprite void SetRotation(float rot); void SetScale(const Vec2& scale); void SetColor(const ScriptColor& color); + void SetScissor(const Vec2& pos, const Vec2& size); + void ClearScissor(); // Utilities void Draw(sol::optional priority, sol::optional alignMode, diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp new file mode 100644 index 0000000000..e0da94f093 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp @@ -0,0 +1,447 @@ +#include "framework.h" +#include "Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h" + +#include "Game/effects/DisplaySprite.h" +#include "Game/Setup.h" +#include "Renderer/Renderer.h" +#include "Renderer/RendererEnums.h" +#include "Scripting/Include/Flow/ScriptInterfaceFlowHandler.h" +#include "Scripting/Internal/LuaHandler.h" +#include "Scripting/Internal/ReservedScriptNames.h" +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" +#include "Scripting/Internal/TEN/View/DisplayAnchors/ScriptDisplayAnchors.h" + +using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Renderer::Structures; +using namespace TEN::Scripting::Types; +using TEN::Renderer::g_Renderer; + +/// Represents a display string. +// +// @tenclass View.DisplayString +// @pragma nostrip + +namespace TEN::Scripting::DisplayString +{ + // NOTE: Conversion from 100x100 percent screen space to internal 800x600. + constexpr auto POS_CONVERSION_COEFF = Vector2(DISPLAY_SPACE_RES.x / 100, DISPLAY_SPACE_RES.y / 100); + constexpr auto SCALE_CONVERSION_COEFF = 0.01f; + + static int FlagArrayToFlags(const FlagArray& flags) + { + int result = 0; + + if (flags[(int)DisplayStringOptions::Center]) + result |= (int)PrintStringFlags::Center; + + if (flags[(int)DisplayStringOptions::Right]) + result |= (int)PrintStringFlags::Right; + + if (flags[(int)DisplayStringOptions::Outline]) + result |= (int)PrintStringFlags::Outline; + + if (flags[(int)DisplayStringOptions::Blink]) + result |= (int)PrintStringFlags::Blink; + + if (flags[(int)DisplayStringOptions::VerticalCenter]) + result |= (int)PrintStringFlags::VerticalCenter; + + if (flags[(int)DisplayStringOptions::VerticalBottom]) + result |= (int)PrintStringFlags::VerticalBottom; + + return result; + } + + void ScriptDisplayString::Register(sol::state& state, sol::table& parent) + { + using ctors = sol::constructors< + ScriptDisplayString(const std::string&, const Vec2&, float, const Vec2&, const ScriptColor&, bool), + ScriptDisplayString(const std::string&, const Vec2&, float, const Vec2&, const ScriptColor&), + ScriptDisplayString(const std::string&, const Vec2&, float, const Vec2&), + ScriptDisplayString(const std::string&, const Vec2&)>; + + // Register type. + parent.new_usertype( + ScriptReserved_DisplayString, + ctors(), + sol::call_constructor, ctors(), + + ScriptReserved_GetText, &ScriptDisplayString::GetText, + ScriptReserved_SetText, &ScriptDisplayString::SetText, + ScriptReserved_GetTranslated, &ScriptDisplayString::GetTranslated, + ScriptReserved_SetTranslated, &ScriptDisplayString::SetTranslated, + ScriptReserved_DisplayStringGetPosition, &ScriptDisplayString::GetPosition, + ScriptReserved_DisplayStringSetPosition, &ScriptDisplayString::SetPosition, + ScriptReserved_DisplayStringGetRotation, &ScriptDisplayString::GetRotation, + ScriptReserved_DisplayStringSetRotation, &ScriptDisplayString::SetRotation, + ScriptReserved_DisplayStringGetScale, &ScriptDisplayString::GetScale, + ScriptReserved_DisplayStringSetScale, &ScriptDisplayString::SetScale, + ScriptReserved_DisplayStringGetColor, &ScriptDisplayString::GetColor, + ScriptReserved_DisplayStringSetColor, &ScriptDisplayString::SetColor, + ScriptReserved_GetArea, &ScriptDisplayString::GetArea, + ScriptReserved_SetArea, &ScriptDisplayString::SetArea, + ScriptReserved_GetFlags, &ScriptDisplayString::GetFlags, + ScriptReserved_SetFlags, &ScriptDisplayString::SetFlags, + ScriptReserved_SetScissor, &ScriptDisplayString::SetScissor, + ScriptReserved_ClearScissor, &ScriptDisplayString::ClearScissor, + ScriptReserved_DisplayStringGetAnchors, &ScriptDisplayString::GetAnchors, + ScriptReserved_DisplaySpriteDraw, &ScriptDisplayString::Draw); + } + + /// Create a DisplayString object. + // @function DisplayString + // @tparam string text Text to display, or a translation key if isTranslated is true. + // @tparam Vec2 pos Display position in percent. + // @tparam[opt=0] float rot Rotation in degrees. + // @tparam[opt=Vec2(100, 100)] Vec2 scale Horizontal and vertical scale in percent. + // @tparam[opt=Color(255, 255, 255)] Color color Color. + // @tparam[opt=false] bool isTranslated Whether the text is a translation key. + // @treturn DisplayString A new DisplayString object. + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color, bool isTranslated) + { + _text = text; + _position = pos; + _rotation = rot; + _scale = scale; + _color = color; + _isTranslated = isTranslated; + } + + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color) + { + *this = ScriptDisplayString(text, pos, rot, scale, color, false); + } + + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale) + { + *this = ScriptDisplayString(text, pos, rot, scale, ScriptColor(255, 255, 255, 255), false); + } + + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos) + { + *this = ScriptDisplayString(text, pos, 0.0f, Vec2(100.0f, 100.0f), ScriptColor(255, 255, 255, 255), false); + } + + /// Get the text of the display string. + // @function DisplayString:GetText + // @treturn string Text or translation key. + std::string ScriptDisplayString::GetText() const + { + return _text; + } + + /// Get whether the text is a translation key. + // @function DisplayString:GetTranslated + // @treturn bool True if the text is a translation key. + bool ScriptDisplayString::GetTranslated() const + { + return _isTranslated; + } + + /// Get the display position in percent. + // @function DisplayString:GetPosition + // @treturn Vec2 Display position in percent. + Vec2 ScriptDisplayString::GetPosition() const + { + return _position; + } + + /// Get the rotation in degrees. + // @function DisplayString:GetRotation + // @treturn float Rotation in degrees. + float ScriptDisplayString::GetRotation() const + { + return _rotation; + } + + /// Get the scale in percent. + // @function DisplayString:GetScale + // @treturn Vec2 Horizontal and vertical scale in percent. + Vec2 ScriptDisplayString::GetScale() const + { + return _scale; + } + + /// Get the color. + // @function DisplayString:GetColor + // @treturn Color Color. + ScriptColor ScriptDisplayString::GetColor() const + { + return _color; + } + + /// Get the text wrapping area in percent. + // Vec2(0, 0) means no wrapping. + // @function DisplayString:GetArea + // @treturn Vec2 Wrapping area in percent. + Vec2 ScriptDisplayString::GetArea() const + { + return _area; + } + + /// Get the display string option flags. + // @function DisplayString:GetFlags + // @treturn table Array of DisplayStringOption values. + sol::table ScriptDisplayString::GetFlags(sol::this_state state) const + { + auto table = sol::state_view(state).create_table(); + for (int i = 0; i < (int)_flags.size(); i++) + { + if (_flags[i]) + table.add(i); + } + return table; + } + + /// Set the text of the display string. + // @function DisplayString:SetText + // @tparam string text New text or translation key. + void ScriptDisplayString::SetText(const std::string& text) + { + _text = text; + } + + /// Set whether the text is a translation key. + // @function DisplayString:SetTranslated + // @tparam bool isTranslated True if the text is a translation key. + void ScriptDisplayString::SetTranslated(bool isTranslated) + { + _isTranslated = isTranslated; + } + + /// Set the display position in percent. + // @function DisplayString:SetPosition + // @tparam Vec2 position New display position in percent. + void ScriptDisplayString::SetPosition(const Vec2& pos) + { + _position = pos; + } + + /// Set the rotation in degrees. + // @function DisplayString:SetRotation + // @tparam float rotation New rotation in degrees. + void ScriptDisplayString::SetRotation(float rot) + { + _rotation = rot; + } + + /// Set the scale in percent. + // @function DisplayString:SetScale + // @tparam Vec2 scale New horizontal and vertical scale in percent. + void ScriptDisplayString::SetScale(const Vec2& scale) + { + _scale = scale; + } + + /// Set the color. + // @function DisplayString:SetColor + // @tparam Color color New color. + void ScriptDisplayString::SetColor(const ScriptColor& color) + { + _color = color; + } + + /// Set the text wrapping area in percent. + // Vec2(0, 0) means no wrapping. + // @function DisplayString:SetArea + // @tparam Vec2 area New wrapping area in percent. + void ScriptDisplayString::SetArea(const Vec2& area) + { + _area = area; + } + + /// Set the display string option flags. + // @function DisplayString:SetFlags + // @tparam table flags Array of DisplayStringOption values. + void ScriptDisplayString::SetFlags(const sol::table& flags) + { + _flags = {}; + for (const auto& val : flags) + { + auto i = val.second.as(); + if (i < _flags.size()) + _flags[i] = true; + } + } + + /// Set a scissor clipping rectangle for the display string. + // Clips the string to the specified rectangle when drawn. + // @function DisplayString:SetScissor + // @tparam Vec2 pos Top-left position of the scissor rectangle in percent. + // @tparam Vec2 size Width and height of the scissor rectangle in percent. + void ScriptDisplayString::SetScissor(const Vec2& pos, const Vec2& size) + { + _hasScissor = true; + _scissorPos = pos; + _scissorSize = size; + } + + /// Clear the scissor clipping rectangle from the display string. + // @function DisplayString:ClearScissor + void ScriptDisplayString::ClearScissor() + { + _hasScissor = false; + } + + /// Get the anchors of the display string. + // Returns the nine anchor points of the display string bounding box in percent. + // @function DisplayString:GetAnchors + // @tparam[opt=View.AlignMode.Center] View.AlignMode alignMode Alignment mode. + // @tparam[opt=View.ScaleMode.Fit] View.ScaleMode scaleMode Scaling mode. + // @treturn View.DisplayAnchors An object containing the anchor points of the display string bounding box. + ScriptDisplayAnchors ScriptDisplayString::GetAnchors(sol::optional alignModeOpt, sol::optional scaleModeOpt) const + { + constexpr auto DISPLAY_ASPECT = DISPLAY_SPACE_RES.x / DISPLAY_SPACE_RES.y; + constexpr auto DEFAULT_ALIGN_MODE = DisplaySpriteAlignMode::Center; + constexpr auto DEFAULT_SCALE_MODE = DisplaySpriteScaleMode::Fit; + + auto resolvedText = _isTranslated ? std::string(g_GameFlow->GetString(_text.c_str())) : _text; + + // Measure text in display space units (800x600). + auto convertedScale = Vector2(_scale.x, _scale.y) * SCALE_CONVERSION_COEFF; + auto textSize = g_Renderer.GetDisplayStringSize(resolvedText, convertedScale); + + // Text bounding box aspect ratio. + float textAspect = (textSize.y > 0.0f) ? (textSize.x / textSize.y) : 1.0f; + + // Screen aspect data. + auto screenRes = Vector2((float)g_Configuration.ScreenWidth, (float)g_Configuration.ScreenHeight); + float screenAspect = screenRes.x / screenRes.y; + float aspectCorrectionBase = screenAspect / DISPLAY_ASPECT; + + // Convert position and rotation. + auto convertedPos = Vector2(_position.x, _position.y) * POS_CONVERSION_COEFF; + short convertedRot = ANGLE(_rotation); + + // Get modes. + auto alignMode = alignModeOpt.value_or(DEFAULT_ALIGN_MODE); + auto scaleMode = scaleModeOpt.value_or(DEFAULT_SCALE_MODE); + + // Calculate layout using shared helper. + auto layout = CalculateDisplaySpriteLayout( + textAspect, convertedScale, convertedRot, + alignMode, scaleMode, screenAspect, aspectCorrectionBase); + + // Calculate final position and size. + auto position = convertedPos + layout.Offset; + auto size = layout.HalfSize * 2.0f; + + // Build vertices centered around origin (top-left, top-right, bottom-right, bottom-left). + std::array vertices = { + size / 2.0f, + Vector2(-size.x, size.y) / 2.0f, + -size / 2.0f, + Vector2(size.x, -size.y) / 2.0f + }; + + // Apply rotation + aspect correction + position offset. + auto rotMatrix = Matrix::CreateRotationZ(TO_RAD(convertedRot + ANGLE(180.0f))); + for (auto& vertex : vertices) + { + vertex = Vector2::Transform(vertex, rotMatrix); + vertex *= layout.AspectCorrection; + vertex += position; + } + + // Scale to screen resolution. + auto screenScale = screenRes / DISPLAY_SPACE_RES; + for (auto& vertex : vertices) + { + vertex.x *= screenScale.x; + vertex.y *= screenScale.y; + } + + auto toPercent = [&screenRes](const Vector2& pos) -> Vec2 + { + return Vec2(std::round((pos.x / screenRes.x) * 10000.0f) / 100.0f, std::round((pos.y / screenRes.y) * 10000.0f) / 100.0f); + }; + + // Calculate edge midpoints. + auto center = (vertices[0] + vertices[2]) / 2.0f; + auto centerTop = (vertices[0] + vertices[1]) / 2.0f; + auto centerLeft = (vertices[0] + vertices[3]) / 2.0f; + auto centerRight = (vertices[1] + vertices[2]) / 2.0f; + auto centerBottom = (vertices[2] + vertices[3]) / 2.0f; + + // Populate and return anchors. + ScriptDisplayAnchors anchors; + anchors.TOP_LEFT = toPercent(vertices[0]); + anchors.TOP_CENTER = toPercent(centerTop); + anchors.TOP_RIGHT = toPercent(vertices[1]); + anchors.CENTER_LEFT = toPercent(centerLeft); + anchors.CENTER = toPercent(center); + anchors.CENTER_RIGHT = toPercent(centerRight); + anchors.BOTTOM_RIGHT = toPercent(vertices[2]); + anchors.BOTTOM_CENTER = toPercent(centerBottom); + anchors.BOTTOM_LEFT = toPercent(vertices[3]); + + return anchors; + } + + /// Draw the display string in display space for the current frame. + // @function DisplayString:Draw + // @tparam[opt=0] int priority Draw priority. Can be thought of as a layer, with higher values having precedence. + // @tparam[opt=View.AlignMode.CENTER] View.AlignMode alignMode Horizontal alignment mode. Overrides CENTER and RIGHT flags. + // @tparam[opt=Effects.BlendID.ALPHABLEND] Effects.BlendID blendMode Blend mode. + void ScriptDisplayString::Draw(sol::optional priority, sol::optional alignMode, + sol::optional blendMode) + { + // Resolve text. + auto resolvedText = _isTranslated ? std::string(g_GameFlow->GetString(_text.c_str())) : _text; + + if (resolvedText.empty()) + return; + + // Convert percent to display space. + auto convertedPos = Vector2(_position.x, _position.y) * POS_CONVERSION_COEFF; + auto convertedArea = Vector2(_area.x, _area.y) * POS_CONVERSION_COEFF; + auto convertedScale = Vector2(_scale.x, _scale.y) * SCALE_CONVERSION_COEFF; + auto convertedColor = Vector4(_color.GetR(), _color.GetG(), _color.GetB(), _color.GetA()) / (float)UCHAR_MAX; + float convertedRotation = _rotation * (DirectX::XM_PI / 180.0f); + + // Build flags from FlagArray. + int flags = FlagArrayToFlags(_flags); + + // Override alignment with alignMode if provided. + if (alignMode.has_value()) + { + flags &= ~((int)PrintStringFlags::Center | (int)PrintStringFlags::Right); + + switch (alignMode.value()) + { + case (int)DisplaySpriteAlignMode::Center: + case (int)DisplaySpriteAlignMode::CenterTop: + case (int)DisplaySpriteAlignMode::CenterBottom: + flags |= (int)PrintStringFlags::Center; + break; + + case (int)DisplaySpriteAlignMode::CenterRight: + case (int)DisplaySpriteAlignMode::TopRight: + case (int)DisplaySpriteAlignMode::BottomRight: + flags |= (int)PrintStringFlags::Right; + break; + } + } + + if (_hasScissor) + { + auto screenRes = g_Renderer.GetScreenResolution(); + auto rect = RendererRectangle( + (int)(_scissorPos.x * screenRes.x / 100.0f), + (int)(_scissorPos.y * screenRes.y / 100.0f), + (int)((_scissorPos.x + _scissorSize.x) * screenRes.x / 100.0f), + (int)((_scissorPos.y + _scissorSize.y) * screenRes.y / 100.0f)); + SetActiveDisplayScissor(rect); + } + + g_Renderer.AddString( + resolvedText, convertedPos, convertedPos, convertedArea, + Color(convertedColor), convertedScale, convertedRotation, flags, + priority.value_or(0), + (BlendMode)blendMode.value_or((int)BlendMode::AlphaBlend)); + + if (_hasScissor) + ClearActiveDisplayScissor(); + } +} diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h new file mode 100644 index 0000000000..76b822c978 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h @@ -0,0 +1,68 @@ +#pragma once + +#include "Game/effects/DisplaySprite.h" +#include "Scripting/Internal/TEN/Strings/DisplayString/DisplayString.h" +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" +#include "Scripting/Internal/TEN/View/DisplayAnchors/ScriptDisplayAnchors.h" + +using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Scripting::Types; + +namespace TEN::Scripting::DisplayString +{ + class ScriptDisplayString + { + public: + static void Register(sol::state& state, sol::table& parent); + + private: + // Members + std::string _text = {}; + bool _isTranslated = false; + + Vec2 _position = Vec2(0.0f, 0.0f); + float _rotation = 0.0f; + Vec2 _scale = Vec2(100.0f, 100.0f); + ScriptColor _color = ScriptColor(255, 255, 255, 255); + Vec2 _area = Vec2(0.0f, 0.0f); + FlagArray _flags = {}; + + bool _hasScissor = false; + Vec2 _scissorPos = Vec2(0.0f, 0.0f); + Vec2 _scissorSize = Vec2(100.0f, 100.0f); + + public: + // Constructors + ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color, bool isTranslated); + ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color); + ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale); + ScriptDisplayString(const std::string& text, const Vec2& pos); + + // Getters + std::string GetText() const; + bool GetTranslated() const; + Vec2 GetPosition() const; + float GetRotation() const; + Vec2 GetScale() const; + ScriptColor GetColor() const; + Vec2 GetArea() const; + sol::table GetFlags(sol::this_state state) const; + + // Setters + void SetText(const std::string& text); + void SetTranslated(bool isTranslated); + void SetPosition(const Vec2& pos); + void SetRotation(float rot); + void SetScale(const Vec2& scale); + void SetColor(const ScriptColor& color); + void SetArea(const Vec2& area); + void SetFlags(const sol::table& flags); + void SetScissor(const Vec2& pos, const Vec2& size); + void ClearScissor(); + + // Utilities + ScriptDisplayAnchors GetAnchors(sol::optional alignModeOpt, sol::optional scaleModeOpt) const; + void Draw(sol::optional priority, sol::optional alignMode, sol::optional blendMode); + }; +} diff --git a/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp b/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp index 621ded9cdb..0b6291c61f 100644 --- a/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp @@ -17,8 +17,10 @@ #include "Scripting/Internal/TEN/Types/Vec3/Vec3.h" #include "Scripting/Internal/TEN/View/AlignModes.h" #include "Scripting/Internal/TEN/View/CameraTypes.h" +#include "Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h" #include "Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.h" #include "Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.h" +#include "Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h" #include "Scripting/Internal/TEN/View/ScaleModes.h" #include "Scripting/Internal/TEN/View/PostProcessEffects.h" #include "Scripting/Internal/TEN/View/DOFModes.h" @@ -27,7 +29,9 @@ #include "Specific/trutils.h" using namespace TEN::Effects::Environment; +using namespace TEN::Scripting::DisplayArea; using namespace TEN::Scripting::DisplaySprite; +using namespace TEN::Scripting::DisplayString; using namespace TEN::Scripting::DisplayItem; using namespace TEN::Scripting::View; using namespace TEN::SpotCam; @@ -428,7 +432,9 @@ namespace TEN::Scripting::View tableView.set_function("PlayFlyBy", &PlayFlyby); // Register types. + ScriptDisplayArea::Register(*state, tableView); ScriptDisplaySprite::Register(*state, tableView); + ScriptDisplayString::Register(*state, tableView); ScriptDisplayItem::Register(*state, tableView); ScriptDisplayAnchors::Register(tableView); diff --git a/TombEngine/TombEngine.vcxproj b/TombEngine/TombEngine.vcxproj index 47fed5f1b6..afa236c19f 100644 --- a/TombEngine/TombEngine.vcxproj +++ b/TombEngine/TombEngine.vcxproj @@ -936,6 +936,8 @@ if not exist "%ScriptsDir%\Strings.lua" xcopy /Y "$(SolutionDir)Scripts\Strings. + + @@ -1435,6 +1437,8 @@ if not exist "%ScriptsDir%\Strings.lua" xcopy /Y "$(SolutionDir)Scripts\Strings. + +