From f8c0f1afda78ef63af18850734006b1ba6c91a87 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Sun, 27 Apr 2025 09:30:01 +0100 Subject: [PATCH 01/24] Update bug_report.yaml Cleaned up the bug report and streamlined the process of creating it. --- .github/ISSUE_TEMPLATE/bug_report.yaml | 155 +++++++++++-------------- 1 file changed, 70 insertions(+), 85 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e123eab748..87d8f72a88 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,99 +1,84 @@ 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. +title: "" +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: TombEngine Version + description: | + Please select the TombEngine version you are using. + options: + - v1.8.1 (latest release) + - v1.8.0 + 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: Yes + - label: No + 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 From 05949c1cb2ddd91dca0dd05d6c816c362e16f141 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Sun, 27 Apr 2025 09:31:36 +0100 Subject: [PATCH 02/24] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 87d8f72a88..b4df0b7948 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,6 +1,5 @@ name: Bug report description: Create a report to help us understand and triage your issue. -title: "" labels: - Awaiting Triage @@ -30,8 +29,8 @@ body: label: Development Version description: Are you submitting this report from a development build that has not been officially released? options: - - label: Yes - - label: No + - label: "I am using an unofficial development version." + - label: "I am using an official release." validations: required: true From cd4c9b4645167c381c1755a9a8402fde5febad19 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Sun, 27 Apr 2025 09:33:01 +0100 Subject: [PATCH 03/24] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index b4df0b7948..cbf6c1e59e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -15,7 +15,7 @@ body: - type: dropdown attributes: - label: TombEngine Version + label: Tomb Engine Version description: | Please select the TombEngine version you are using. options: From 94e095fb5342451c14d799c4d4540888201409dd Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:55:25 +0100 Subject: [PATCH 04/24] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index cbf6c1e59e..592411413b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -19,7 +19,8 @@ body: description: | Please select the TombEngine version you are using. options: - - v1.8.1 (latest release) + - v1.9.0 (Pre-release) + - v1.8.1 (latest public release) - v1.8.0 validations: required: true @@ -31,6 +32,7 @@ body: 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 From 37001e5be9aec936abb065f5e165191b8bc3c744 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Sat, 14 Jun 2025 07:57:44 +0100 Subject: [PATCH 05/24] Update README.md Added reference to skinning support. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f699f1622..76a8ebd63f 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. From ca30a69814e6f4731b727a31b9941f034e5a9556 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:45:28 +0100 Subject: [PATCH 06/24] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 592411413b..333c429a24 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -19,9 +19,9 @@ body: description: | Please select the TombEngine version you are using. options: - - v1.9.0 (Pre-release) - - v1.8.1 (latest public release) - - v1.8.0 + - Development build + - v1.9.0 (latest public release) + - v1.8.1 validations: required: true From d268f6659de17bf56a8b278005e77b0002f3d2af Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 22 Jun 2025 11:09:39 +0300 Subject: [PATCH 07/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76a8ebd63f..88b45dd54d 100644 --- a/README.md +++ b/README.md @@ -6,7 +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 skinning support for all objects. - Full diagonal geometry support. - Uncapped map size. - A streamlined player control scheme. From 6aea42463613a7d62493914745586386dfe1d8c4 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:55:52 +0100 Subject: [PATCH 08/24] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 333c429a24..755bfb370a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,8 +20,8 @@ body: Please select the TombEngine version you are using. options: - Development build - - v1.9.0 (latest public release) - - v1.8.1 + - v1.9.2 (latest public release) + - v1.9.1 validations: required: true From 50ea441f83673245a610ede060b366df1d9e1fe4 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:45:38 +0000 Subject: [PATCH 09/24] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 755bfb370a..90c42f4091 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,8 +20,8 @@ body: Please select the TombEngine version you are using. options: - Development build - - v1.9.2 (latest public release) - - v1.9.1 + - v1.10.0 (latest public release) + - v1.9.2 validations: required: true From 8e09754f1404068d4d5ab9ca8448e0602cac7532 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Sun, 23 Nov 2025 22:40:09 +0000 Subject: [PATCH 10/24] Updated Bug Report form for 1.10.1 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 90c42f4091..8c6effd1d7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,8 +20,8 @@ body: Please select the TombEngine version you are using. options: - Development build - - v1.10.0 (latest public release) - - v1.9.2 + - v1.10.1 (latest public release) + - v1.10.0 validations: required: true From 308a42c1f989fdfe669492615819452b48b77572 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 15 Mar 2026 12:04:17 +0100 Subject: [PATCH 11/24] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8c6effd1d7..7bd666036e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,8 +20,8 @@ body: Please select the TombEngine version you are using. options: - Development build - - v1.10.1 (latest public release) - - v1.10.0 + - v1.11 (latest public release) + - v1.10.1 validations: required: true From 0954dbe7908e0d8a103af6b72d32cd776f3a2ec7 Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:15:59 +0000 Subject: [PATCH 12/24] Add cross-repo dependency check workflow This workflow checks for cross-repo dependencies in pull requests and sets the commit status accordingly. This is for merging branches in the Tomb Editor and Tomb Engine repo that require both sides to be merged before the changes are completed. --- .github/workflows/cross-repo-dependency.yml | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/cross-repo-dependency.yml diff --git a/.github/workflows/cross-repo-dependency.yml b/.github/workflows/cross-repo-dependency.yml new file mode 100644 index 0000000000..a5bea6db84 --- /dev/null +++ b/.github/workflows/cross-repo-dependency.yml @@ -0,0 +1,78 @@ +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 and set status + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const body = pr.body || ""; + + // Look for: Depends on: org/repo#123 + const match = body.match(/Depends on:\s+([\w-]+)\/([\w-]+)#(\d+)/); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const sha = pr.head.sha; + + if (!match) { + // No dependency → pass + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha, + state: "success", + context: "cross-repo-dependency", + description: "No cross-repo dependency" + }); + return; + } + + const depOwner = match[1]; + const depRepo = match[2]; + const depNumber = Number(match[3]); + + // Get linked PR + const linked = await github.rest.pulls.get({ + owner: depOwner, + repo: depRepo, + pull_number: depNumber + }); + + // Get reviews to see if it has at least one APPROVED + const reviews = await github.rest.pulls.listReviews({ + owner: depOwner, + repo: depRepo, + pull_number: depNumber + }); + + const approved = reviews.data.some(r => r.state === "APPROVED"); + + // mergeable can be null while GitHub is still calculating; treat that as not ready + const mergeable = linked.data.mergeable === true; + + let state = "pending"; + let description = "Waiting for linked PR to be approved and mergeable"; + + if (approved && mergeable) { + state = "success"; + description = `Linked PR ${depOwner}/${depRepo}#${depNumber} is approved and mergeable`; + } + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha, + state, + context: "cross-repo-dependency", + description + }); From 25b81d825a566248f3d2f29fb42d199b6fdd07bd Mon Sep 17 00:00:00 2001 From: Stranger1992 <84292688+Stranger1992@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:30:41 +0000 Subject: [PATCH 13/24] Refactor dependency check workflow steps Refactor cross-repo dependency check to improve clarity and functionality. --- .github/workflows/cross-repo-dependency.yml | 79 ++++++++++++--------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/.github/workflows/cross-repo-dependency.yml b/.github/workflows/cross-repo-dependency.yml index a5bea6db84..afad434580 100644 --- a/.github/workflows/cross-repo-dependency.yml +++ b/.github/workflows/cross-repo-dependency.yml @@ -10,68 +10,77 @@ jobs: check-linked-pr: runs-on: ubuntu-latest steps: - - name: Check for dependency and set status + - name: Check for dependency + id: parse uses: actions/github-script@v7 with: script: | - const pr = context.payload.pull_request; - const body = pr.body || ""; - - // Look for: Depends on: org/repo#123 + const body = context.payload.pull_request.body || ""; const match = body.match(/Depends on:\s+([\w-]+)\/([\w-]+)#(\d+)/); - const owner = context.repo.owner; - const repo = context.repo.repo; - const sha = pr.head.sha; - if (!match) { - // No dependency → pass - await github.rest.repos.createCommitStatus({ - owner, - repo, - sha, - state: "success", - context: "cross-repo-dependency", - description: "No cross-repo dependency" - }); + // No dependency → do NOT create a status check + core.setOutput("skip", "true"); return; } - const depOwner = match[1]; - const depRepo = match[2]; - const depNumber = Number(match[3]); + 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); - // Get linked PR const linked = await github.rest.pulls.get({ - owner: depOwner, - repo: depRepo, - pull_number: depNumber + owner, + repo, + pull_number: number }); - // Get reviews to see if it has at least one APPROVED const reviews = await github.rest.pulls.listReviews({ - owner: depOwner, - repo: depRepo, - pull_number: depNumber + owner, + repo, + pull_number: number }); const approved = reviews.data.some(r => r.state === "APPROVED"); - - // mergeable can be null while GitHub is still calculating; treat that as not ready 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 ${depOwner}/${depRepo}#${depNumber} is approved and mergeable`; + description = "Linked PR is approved and mergeable"; } await github.rest.repos.createCommitStatus({ - owner, - repo, - sha, + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.payload.pull_request.head.sha, state, context: "cross-repo-dependency", description From db0e2dfe3a533ec9fcb37086b077bcab8123b16f Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:43:46 -0400 Subject: [PATCH 14/24] First pass --- Scripts/Engine/Achievements/Achievements.lua | 222 +++++++++++++ Scripts/Engine/Achievements/Block.lua | 79 +++++ Scripts/Engine/Achievements/Input.lua | 49 +++ Scripts/Engine/Achievements/List.lua | 230 ++++++++++++++ Scripts/Engine/Achievements/Popup.lua | 152 +++++++++ Scripts/Engine/Achievements/Settings.lua | 181 +++++++++++ TombEngine/Game/effects/DisplaySprite.cpp | 26 ++ TombEngine/Game/effects/DisplaySprite.h | 9 + TombEngine/Renderer/Renderer.h | 6 + TombEngine/Renderer/RendererDraw2D.cpp | 44 ++- TombEngine/Renderer/RendererString.cpp | 95 +++++- .../Renderer/Structures/RendererSprite2D.h | 4 + .../Structures/RendererStringToDraw.h | 11 +- .../Scripting/Internal/ReservedScriptNames.h | 13 + .../View/DisplayArea/ScriptDisplayArea.cpp | 205 ++++++++++++ .../TEN/View/DisplayArea/ScriptDisplayArea.h | 44 +++ .../DisplayString/ScriptDisplayString.cpp | 300 ++++++++++++++++++ .../View/DisplayString/ScriptDisplayString.h | 59 ++++ .../Internal/TEN/View/ViewHandler.cpp | 6 + TombEngine/TombEngine.vcxproj | 4 + 20 files changed, 1725 insertions(+), 14 deletions(-) create mode 100644 Scripts/Engine/Achievements/Achievements.lua create mode 100644 Scripts/Engine/Achievements/Block.lua create mode 100644 Scripts/Engine/Achievements/Input.lua create mode 100644 Scripts/Engine/Achievements/List.lua create mode 100644 Scripts/Engine/Achievements/Popup.lua create mode 100644 Scripts/Engine/Achievements/Settings.lua create mode 100644 TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp create mode 100644 TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h create mode 100644 TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp create mode 100644 TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h 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/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.h b/TombEngine/Renderer/Renderer.h index 030f91b5fb..6b5666c15c 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -430,6 +430,9 @@ namespace TEN::Renderer void InitializeMenuBars(int y); void InitializeSky(); void DrawAllStrings(); + 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 PrepareDynamicLight(RendererLight& light); void PrepareLaserBarriers(RenderView& view); void PrepareSingleLaserBeam(RenderView& view); @@ -741,6 +744,9 @@ 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 = 0, BlendMode blendMode = BlendMode::AlphaBlend); 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); diff --git a/TombEngine/Renderer/RendererDraw2D.cpp b/TombEngine/Renderer/RendererDraw2D.cpp index 8b67fd15fa..41cb094e03 100644 --- a/TombEngine/Renderer/RendererDraw2D.cpp +++ b/TombEngine/Renderer/RendererDraw2D.cpp @@ -341,11 +341,22 @@ namespace TEN::Renderer return; Texture2D* 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); @@ -353,14 +364,32 @@ namespace TEN::Renderer _context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); _context->IASetInputLayout(_inputLayout.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); @@ -408,6 +437,10 @@ namespace TEN::Renderer if (texture2DPtr != nullptr) _primitiveBatch->End(); + + // Reset scissor if it was active. + if (currentHasScissor) + ResetScissor(); } void Renderer::DrawFullScreenQuad(ID3D11ShaderResourceView* texture, Vector3 color, bool fit, float customAspect) @@ -570,6 +603,7 @@ namespace TEN::Renderer spriteToDraw.Priority = priority; spriteToDraw.BlendMode = blendMode; spriteToDraw.AspectCorrection = aspectCorrection; + spriteToDraw.HasScissor = false; renderView.DisplaySpritesToDraw.push_back(spriteToDraw); } @@ -612,6 +646,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/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index a064e7c947..819825ddf3 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -1,9 +1,14 @@ #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; + namespace TEN::Renderer { void Renderer::AddDebugString(const std::string& string, const Vector2& pos, const Color& color, float scale, RendererDebugPage page) @@ -34,7 +39,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; @@ -49,8 +68,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(L" ")).x * stringScale; + auto stringScale = Vector2(uiScale * fontScale) * scale; + float baseScale = stringScale.y; + float spaceWidth = Vector3(_gameFont->MeasureString(L" ")).x * baseScale; std::vector stringLines; @@ -74,7 +94,7 @@ namespace TEN::Renderer for (const auto& word : words) { - float wordWidth = Vector3(_gameFont->MeasureString(word.c_str())).x * stringScale; + float wordWidth = Vector3(_gameFont->MeasureString(word.c_str())).x * baseScale; if (!currentLine.empty() && (currentLineWidth + wordWidth + spaceWidth > area.x * factor.x)) { @@ -107,9 +127,9 @@ namespace TEN::Renderer for (const auto& line : stringLines) { if (line.empty()) - totalHeight += fontSpacing * stringScale; + totalHeight += fontSpacing * baseScale; else - totalHeight += Vector2(_gameFont->MeasureString(line.c_str())).y * stringScale; + totalHeight += Vector2(_gameFont->MeasureString(line.c_str())).y * baseScale; } // Calculate maximum textbox height. @@ -142,9 +162,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.c_str())) * rString.Scale; + auto stringSize = line.empty() ? Vector2(0, fontSpacing * baseScale) : Vector2(_gameFont->MeasureString(line.c_str())) * baseScale; // If height clipping enabled, stop drawing when exceeding maxHeight. if (maxHeight > 0.0f && (yOffset + stringSize.y) > maxHeight) @@ -164,7 +190,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 * baseScale; rString.Position.x = pos.x * factor.x + indent; rString.PrevPosition.x = prevPos.x * factor.x + indent; @@ -194,35 +220,80 @@ 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(); + auto currentBlend = BlendMode::AlphaBlend; + SetBlendMode(currentBlend); + + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + for (const auto& rString : _stringsToDraw) { + // Switch blend mode per string if needed. + if (rString.Blend != currentBlend) + { + _spriteBatch->End(); + currentBlend = rString.Blend; + SetBlendMode(currentBlend); + _spriteBatch->Begin(); + } + + // 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(); + } + 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.c_str(), - Vector2(drawPos.x + shadowOffset * rString.Scale, drawPos.y + shadowOffset * rString.Scale), + shadowPos, (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, - 0.0f, Vector4::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale); } // Draw string. _gameFont->DrawString( _spriteBatch.get(), rString.String.c_str(), - Vector2(drawPos.x, drawPos.y), + drawPos, (rString.Color * rString.Color.w) * ScreenFadeCurrent, - 0.0f, Vector4::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale); } _spriteBatch->End(); + + // Reset scissor if it was active. + if (currentHasScissor) + ResetScissor(); } } diff --git a/TombEngine/Renderer/Structures/RendererSprite2D.h b/TombEngine/Renderer/Structures/RendererSprite2D.h index 50c4ecb011..9b1e849844 100644 --- a/TombEngine/Renderer/Structures/RendererSprite2D.h +++ b/TombEngine/Renderer/Structures/RendererSprite2D.h @@ -1,5 +1,6 @@ #pragma once +#include "Renderer/Structures/RendererRectangle.h" #include "Renderer/Structures/RendererSprite.h" #include "Renderer/RendererEnums.h" @@ -18,5 +19,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 379dba178d..809142f6a8 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::wstring 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 2afab69f34..a8a3f182c5 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,14 @@ 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_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..edd3596de1 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp @@ -0,0 +1,205 @@ +#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_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()) + result = drawFunc(item, sol::as_args(args.as())); + else + result = drawFunc(item); + + if (!result.valid()) + { + sol::error err = result; + TENLog(std::string("DisplayArea: Error drawing item: ") + err.what(), LogLevel::Warning); + } + } + + ClearActiveDisplayScissor(); + } +} 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..c782cfa85f --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h @@ -0,0 +1,44 @@ +#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(); + }; +} 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..89b051df60 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp @@ -0,0 +1,300 @@ +#include "framework.h" +#include "Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h" + +#include "Game/effects/DisplaySprite.h" +#include "Renderer/Renderer.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" + +using namespace TEN::Effects::DisplaySprite; +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_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. + FlagArray ScriptDisplayString::GetFlags() const + { + return _flags; + } + + /// 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 FlagArray& flags) + { + _flags = flags; + } + + /// 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; + } + } + + g_Renderer.AddString( + resolvedText, convertedPos, convertedPos, convertedArea, + Color(convertedColor), convertedScale, convertedRotation, flags, + priority.value_or(0), + (BlendMode)blendMode.value_or((int)BlendMode::AlphaBlend)); + } +} 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..281a1b4b8f --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h @@ -0,0 +1,59 @@ +#pragma once + +#include "Scripting/Internal/TEN/Strings/DisplayString/DisplayString.h" +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" + +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 = {}; + + 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; + FlagArray GetFlags() 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 FlagArray& flags); + + // Utilities + 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 4a1b8c4af8..df0c419f56 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 "Specific/clock.h" @@ -26,7 +28,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::Utils; @@ -369,7 +373,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 91cbb9a3b1..6ad05f1f0b 100644 --- a/TombEngine/TombEngine.vcxproj +++ b/TombEngine/TombEngine.vcxproj @@ -895,6 +895,8 @@ if not exist "%ScriptsDir%\Strings.lua" xcopy /Y "$(SolutionDir)Scripts\Strings. + + @@ -1370,6 +1372,8 @@ if not exist "%ScriptsDir%\Strings.lua" xcopy /Y "$(SolutionDir)Scripts\Strings. + + From 9016541c4ab86745532fe74fb78489b6610463e2 Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:13:12 -0400 Subject: [PATCH 15/24] Fix GetFlags; SetFlags --- .../DisplayString/ScriptDisplayString.cpp | 20 +++++++++++++++---- .../View/DisplayString/ScriptDisplayString.h | 4 ++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp index 89b051df60..4f38f9f6e0 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp @@ -176,9 +176,15 @@ namespace TEN::Scripting::DisplayString /// Get the display string option flags. // @function DisplayString:GetFlags // @treturn table Array of DisplayStringOption values. - FlagArray ScriptDisplayString::GetFlags() const + sol::table ScriptDisplayString::GetFlags(sol::this_state state) const { - return _flags; + 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. @@ -241,9 +247,15 @@ namespace TEN::Scripting::DisplayString /// Set the display string option flags. // @function DisplayString:SetFlags // @tparam table flags Array of DisplayStringOption values. - void ScriptDisplayString::SetFlags(const FlagArray& flags) + void ScriptDisplayString::SetFlags(const sol::table& flags) { - _flags = flags; + _flags = {}; + for (const auto& val : flags) + { + auto i = val.second.as(); + if (i < _flags.size()) + _flags[i] = true; + } } /// Draw the display string in display space for the current frame. diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h index 281a1b4b8f..5364087d19 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h @@ -40,7 +40,7 @@ namespace TEN::Scripting::DisplayString Vec2 GetScale() const; ScriptColor GetColor() const; Vec2 GetArea() const; - FlagArray GetFlags() const; + sol::table GetFlags(sol::this_state state) const; // Setters void SetText(const std::string& text); @@ -50,7 +50,7 @@ namespace TEN::Scripting::DisplayString void SetScale(const Vec2& scale); void SetColor(const ScriptColor& color); void SetArea(const Vec2& area); - void SetFlags(const FlagArray& flags); + void SetFlags(const sol::table& flags); // Utilities void Draw(sol::optional priority, sol::optional alignMode, From fdb21ae9dc262bda060cf2122894e3c1b5cab210 Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:33:11 -0400 Subject: [PATCH 16/24] fix scissor --- TombEngine/Renderer/Renderer.h | 3 ++ TombEngine/Renderer/RendererDraw.cpp | 2 + TombEngine/Renderer/RendererDraw2D.cpp | 22 +++++++++++ TombEngine/Renderer/RendererString.cpp | 7 ++-- .../Scripting/Internal/ReservedScriptNames.h | 1 + .../View/DisplayArea/ScriptDisplayArea.cpp | 37 ++++++++++++++++++- .../TEN/View/DisplayArea/ScriptDisplayArea.h | 1 + 7 files changed, 69 insertions(+), 4 deletions(-) diff --git a/TombEngine/Renderer/Renderer.h b/TombEngine/Renderer/Renderer.h index 6b5666c15c..e021f6f877 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -252,6 +252,7 @@ namespace TEN::Renderer std::vector _lines2DToDraw = {}; std::vector _lines3DToDraw = {}; std::vector _triangles3DToDraw = {}; + std::vector> _debugDisplayRects = {}; // Textures, objects and sprites @@ -430,6 +431,7 @@ namespace TEN::Renderer void InitializeMenuBars(int y); void InitializeSky(); void DrawAllStrings(); + void DrawDebugDisplayRects(); 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); @@ -775,6 +777,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(LPCSTR msg, va_list args); void PrintDebugMessage(LPCSTR msg, ...); diff --git a/TombEngine/Renderer/RendererDraw.cpp b/TombEngine/Renderer/RendererDraw.cpp index 34a7743628..5439c4122e 100644 --- a/TombEngine/Renderer/RendererDraw.cpp +++ b/TombEngine/Renderer/RendererDraw.cpp @@ -1758,6 +1758,7 @@ namespace TEN::Renderer _lines3DToDraw.clear(); _triangles3DToDraw.clear(); _stringsToDraw.clear(); + _debugDisplayRects.clear(); _currentCausticsFrame++; _currentCausticsFrame %= 32; @@ -2034,6 +2035,7 @@ namespace TEN::Renderer DrawDisplaySprites(view, false); DrawDebugRenderTargets(view); + DrawDebugDisplayRects(); DrawAllStrings(); DrawDisplaySprites(view, true); } diff --git a/TombEngine/Renderer/RendererDraw2D.cpp b/TombEngine/Renderer/RendererDraw2D.cpp index 41cb094e03..bf9a92c98e 100644 --- a/TombEngine/Renderer/RendererDraw2D.cpp +++ b/TombEngine/Renderer/RendererDraw2D.cpp @@ -333,6 +333,28 @@ 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(SpriteSortMode_Deferred, _renderStates->NonPremultiplied(), nullptr, nullptr, _cullNoneRasterizerState.Get()); + + for (const auto& [rect, color] : _debugDisplayRects) + { + auto destRect = RECT{ rect.Left, rect.Top, rect.Right, rect.Bottom }; + _spriteBatch->Draw(_whiteTexture.ShaderResourceView.Get(), destRect, DirectX::XMLoadFloat4(&color)); + } + + _spriteBatch->End(); + } + void Renderer::DrawDisplaySprites(RenderView& renderView, bool negativePriority) { constexpr auto VERTEX_COUNT = 4; diff --git a/TombEngine/Renderer/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index 819825ddf3..c69200e3be 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -227,7 +227,8 @@ namespace TEN::Renderer float shadowOffset = 1.5f / (REFERENCE_FONT_SIZE / _gameFont->GetLineSpacing()); auto shadowColor = (Vector4)g_GameFlow->GetSettings()->UI.ShadowTextColor; - _spriteBatch->Begin(); + ResetScissor(); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); auto currentBlend = BlendMode::AlphaBlend; SetBlendMode(currentBlend); @@ -243,7 +244,7 @@ namespace TEN::Renderer _spriteBatch->End(); currentBlend = rString.Blend; SetBlendMode(currentBlend); - _spriteBatch->Begin(); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); } // Handle scissor rect changes. @@ -265,7 +266,7 @@ namespace TEN::Renderer currentHasScissor = rString.HasScissor; currentScissor = rString.ScissorRect; - _spriteBatch->Begin(); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); } auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor()); diff --git a/TombEngine/Scripting/Internal/ReservedScriptNames.h b/TombEngine/Scripting/Internal/ReservedScriptNames.h index a8a3f182c5..20965ecdd4 100644 --- a/TombEngine/Scripting/Internal/ReservedScriptNames.h +++ b/TombEngine/Scripting/Internal/ReservedScriptNames.h @@ -77,6 +77,7 @@ 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_EndReasonDeath[] = "DEATH"; static constexpr char ScriptReserved_EndReasonExitToTitle[] = "EXIT_TO_TITLE"; diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp index edd3596de1..d5bebbdda3 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp @@ -40,6 +40,7 @@ namespace TEN::Scripting::DisplayArea ScriptReserved_AddItem, &ScriptDisplayArea::AddItem, ScriptReserved_RemoveItem, &ScriptDisplayArea::RemoveItem, ScriptReserved_Clear, &ScriptDisplayArea::Clear, + ScriptReserved_Debug, &ScriptDisplayArea::Debug, ScriptReserved_DisplaySpriteDraw, &ScriptDisplayArea::Draw); } @@ -189,9 +190,21 @@ namespace TEN::Scripting::DisplayArea sol::protected_function_result result; if (args.is()) - result = drawFunc(item, sol::as_args(args.as())); + { + 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()) { @@ -202,4 +215,26 @@ namespace TEN::Scripting::DisplayArea 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 index c782cfa85f..8cf150f149 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h @@ -40,5 +40,6 @@ namespace TEN::Scripting::DisplayArea // Utilities void Draw(); + void Debug(sol::optional color); }; } From 68427dcad839492ff028fb97a846cd4756cc5cd6 Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:02:22 -0400 Subject: [PATCH 17/24] Allow freeze mode debug --- TombEngine/Renderer/RendererDrawMenu.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/TombEngine/Renderer/RendererDrawMenu.cpp b/TombEngine/Renderer/RendererDrawMenu.cpp index c6f47c185e..417cd723ab 100644 --- a/TombEngine/Renderer/RendererDrawMenu.cpp +++ b/TombEngine/Renderer/RendererDrawMenu.cpp @@ -1407,6 +1407,7 @@ namespace TEN::Renderer CollectDisplaySprites(_gameCamera); DrawDisplaySprites(_gameCamera, false); DrawDisplayItems(); + DrawDebugDisplayRects(); DrawDisplaySprites(_gameCamera, true); DrawAllStrings(); From 44f2a79984d89447d33993b5ac65fb6da6930e64 Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:07:24 -0400 Subject: [PATCH 18/24] WIP --- TombEngine/Renderer/Renderer.h | 2 + TombEngine/Renderer/RendererDraw.cpp | 4 +- TombEngine/Renderer/RendererDraw2D.cpp | 212 ++++++++++++++++++ TombEngine/Renderer/RendererDrawMenu.cpp | 7 +- TombEngine/Renderer/RendererString.cpp | 20 ++ .../DisplayString/ScriptDisplayString.cpp | 99 ++++++++ .../View/DisplayString/ScriptDisplayString.h | 5 +- 7 files changed, 339 insertions(+), 10 deletions(-) diff --git a/TombEngine/Renderer/Renderer.h b/TombEngine/Renderer/Renderer.h index e021f6f877..02f01fce77 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -465,6 +465,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); @@ -750,6 +751,7 @@ namespace TEN::Renderer const Color& color, const Vector2& scale, float rotation, int flags, int priority = 0, BlendMode blendMode = BlendMode::AlphaBlend); void AddDebugString(const std::string& string, const Vector2& pos, const Color& color, float scale, RendererDebugPage page = RendererDebugPage::None); + Vector2 GetDisplayStringSize(const std::string& text, const Vector2& scale) const; void FreeRendererData(); void AddDynamicPointLight(const Vector3& pos, float radius, const Color& color, bool castShadows, int hash = 0); void AddDynamicFogBulb(const Vector3& pos, float radius, float density, const Color& color, int hash = 0); diff --git a/TombEngine/Renderer/RendererDraw.cpp b/TombEngine/Renderer/RendererDraw.cpp index 5439c4122e..11746552b5 100644 --- a/TombEngine/Renderer/RendererDraw.cpp +++ b/TombEngine/Renderer/RendererDraw.cpp @@ -2032,12 +2032,10 @@ namespace TEN::Renderer if (renderMode == SceneRenderMode::Full && g_GameFlow->LastGameStatus == GameStatus::Normal) { CollectDisplaySprites(view); - DrawDisplaySprites(view, false); + DrawAllDisplayLayers(view); DrawDebugRenderTargets(view); DrawDebugDisplayRects(); - DrawAllStrings(); - DrawDisplaySprites(view, true); } time2 = std::chrono::high_resolution_clock::now(); diff --git a/TombEngine/Renderer/RendererDraw2D.cpp b/TombEngine/Renderer/RendererDraw2D.cpp index bf9a92c98e..f0968db21d 100644 --- a/TombEngine/Renderer/RendererDraw2D.cpp +++ b/TombEngine/Renderer/RendererDraw2D.cpp @@ -630,6 +630,218 @@ namespace TEN::Renderer 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. + { + Texture2D* 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); + _context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + _context->IASetInputLayout(_inputLayout.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_TempToVector4(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(); + SetBlendMode(currentBlend); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + + while (stringIdx < (int)_stringsToDraw.size() && _stringsToDraw[stringIdx].Priority == priority) + { + const auto& rString = _stringsToDraw[stringIdx++]; + + if (rString.Blend != currentBlend) + { + _spriteBatch->End(); + currentBlend = rString.Blend; + SetBlendMode(currentBlend); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + + 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(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + + 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.c_str(), + shadowPos, + (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, + rString.Rotation, Vector2::Zero, rString.Scale); + } + + _gameFont->DrawString( + _spriteBatch.get(), rString.String.c_str(), + drawPos, + (rString.Color * rString.Color.w) * ScreenFadeCurrent, + rString.Rotation, Vector2::Zero, rString.Scale); + } + + _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; diff --git a/TombEngine/Renderer/RendererDrawMenu.cpp b/TombEngine/Renderer/RendererDrawMenu.cpp index 417cd723ab..31ea902d95 100644 --- a/TombEngine/Renderer/RendererDrawMenu.cpp +++ b/TombEngine/Renderer/RendererDrawMenu.cpp @@ -1403,13 +1403,10 @@ namespace TEN::Renderer RenderScene(&_backBuffer, _gameCamera, SceneRenderMode::NoHud); } - // Draw display sprites sorted by priority. + // Draw display sprites and strings sorted by priority. CollectDisplaySprites(_gameCamera); - DrawDisplaySprites(_gameCamera, false); - DrawDisplayItems(); + DrawAllDisplayLayers(_gameCamera); DrawDebugDisplayRects(); - DrawDisplaySprites(_gameCamera, true); - DrawAllStrings(); if (staticBackground) { diff --git a/TombEngine/Renderer/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index c69200e3be..a8b31c45b0 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -29,6 +29,26 @@ 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 wtext = TEN::Utils::ToWString(text); + auto measured = Vector2(_gameFont->MeasureString(wtext.c_str())) * 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); diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp index 4f38f9f6e0..ec0ee9fb7f 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp @@ -2,12 +2,15 @@ #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::Scripting::Types; @@ -79,6 +82,7 @@ namespace TEN::Scripting::DisplayString ScriptReserved_SetArea, &ScriptDisplayString::SetArea, ScriptReserved_GetFlags, &ScriptDisplayString::GetFlags, ScriptReserved_SetFlags, &ScriptDisplayString::SetFlags, + ScriptReserved_DisplayStringGetAnchors, &ScriptDisplayString::GetAnchors, ScriptReserved_DisplaySpriteDraw, &ScriptDisplayString::Draw); } @@ -258,6 +262,101 @@ namespace TEN::Scripting::DisplayString } } + /// 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. diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h index 5364087d19..1f7207e610 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h @@ -3,6 +3,7 @@ #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::Scripting::Types; @@ -53,7 +54,7 @@ namespace TEN::Scripting::DisplayString void SetFlags(const sol::table& flags); // Utilities - void Draw(sol::optional priority, sol::optional alignMode, - sol::optional blendMode); + ScriptDisplayAnchors GetAnchors(sol::optional alignModeOpt, sol::optional scaleModeOpt) const; + void Draw(sol::optional priority, sol::optional alignMode, sol::optional blendMode); }; } From 697c4d920de62a278c1abc7c0802560e88c11b9e Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:26:20 -0400 Subject: [PATCH 19/24] FIx bugs --- TombEngine/Renderer/RendererDraw.cpp | 6 ++++++ TombEngine/Renderer/RendererDrawMenu.cpp | 3 +++ .../Internal/TEN/View/DisplayString/ScriptDisplayString.h | 2 ++ 3 files changed, 11 insertions(+) diff --git a/TombEngine/Renderer/RendererDraw.cpp b/TombEngine/Renderer/RendererDraw.cpp index 11746552b5..94ead20458 100644 --- a/TombEngine/Renderer/RendererDraw.cpp +++ b/TombEngine/Renderer/RendererDraw.cpp @@ -2329,6 +2329,9 @@ namespace TEN::Renderer ClearScene(); _context->ClearState(); + _lastBlendMode = BlendMode::Unknown; + _lastCullMode = CullMode::Unknown; + _lastDepthState = DepthState::Unknown; _swapChain->Present(1, 0); } @@ -3368,6 +3371,9 @@ namespace TEN::Renderer RenderScene(&_backBuffer, _gameCamera); _context->ClearState(); + _lastBlendMode = BlendMode::Unknown; + _lastCullMode = CullMode::Unknown; + _lastDepthState = DepthState::Unknown; _swapChain->Present(1, 0); } diff --git a/TombEngine/Renderer/RendererDrawMenu.cpp b/TombEngine/Renderer/RendererDrawMenu.cpp index 31ea902d95..476c37bbbd 100644 --- a/TombEngine/Renderer/RendererDrawMenu.cpp +++ b/TombEngine/Renderer/RendererDrawMenu.cpp @@ -1421,6 +1421,9 @@ namespace TEN::Renderer ClearScene(); _context->ClearState(); + _lastBlendMode = BlendMode::Unknown; + _lastCullMode = CullMode::Unknown; + _lastDepthState = DepthState::Unknown; _swapChain->Present(1, 0); } diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h index 1f7207e610..b8b8e4bdd1 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h @@ -1,10 +1,12 @@ #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 From 28306e3e3f5ebea8c19a3006b7b9169407874258 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:07:38 +0000 Subject: [PATCH 20/24] Fix merge conflicts: Update RendererString for Vector2 Scale and new features Agent-Logs-Url: https://github.com/TrainWrack/TombEngine/sessions/eeb30453-a1e0-423e-9279-9fc2a1199c56 Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- TombEngine/Game/effects/DisplaySprite.cpp | 26 ++++ TombEngine/Game/effects/DisplaySprite.h | 9 ++ TombEngine/Renderer/Renderer.h | 3 + TombEngine/Renderer/RendererString.cpp | 119 ++++++++++++++++-- .../Structures/RendererStringToDraw.h | 11 +- 5 files changed, 154 insertions(+), 14 deletions(-) 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.h b/TombEngine/Renderer/Renderer.h index 030f91b5fb..81d073aeac 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -429,6 +429,7 @@ 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 PrepareDynamicLight(RendererLight& light); void PrepareLaserBarriers(RenderView& view); @@ -741,6 +742,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); diff --git a/TombEngine/Renderer/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index a064e7c947..6777db3e6e 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -1,9 +1,15 @@ #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 { void Renderer::AddDebugString(const std::string& string, const Vector2& pos, const Color& color, float scale, RendererDebugPage page) @@ -24,6 +30,26 @@ 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 wtext = TEN::Utils::ToWString(text); + auto measured = Vector2(_gameFont->MeasureString(wtext.c_str())) * 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); @@ -34,7 +60,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; @@ -49,8 +89,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(L" ")).x * stringScale; + auto stringScale = Vector2(uiScale * fontScale) * scale; + float baseScale = stringScale.y; + float spaceWidth = Vector3(_gameFont->MeasureString(L" ")).x * baseScale; std::vector stringLines; @@ -74,7 +115,7 @@ namespace TEN::Renderer for (const auto& word : words) { - float wordWidth = Vector3(_gameFont->MeasureString(word.c_str())).x * stringScale; + float wordWidth = Vector3(_gameFont->MeasureString(word.c_str())).x * baseScale; if (!currentLine.empty() && (currentLineWidth + wordWidth + spaceWidth > area.x * factor.x)) { @@ -107,9 +148,9 @@ namespace TEN::Renderer for (const auto& line : stringLines) { if (line.empty()) - totalHeight += fontSpacing * stringScale; + totalHeight += fontSpacing * baseScale; else - totalHeight += Vector2(_gameFont->MeasureString(line.c_str())).y * stringScale; + totalHeight += Vector2(_gameFont->MeasureString(line.c_str())).y * stringScale.y; } // Calculate maximum textbox height. @@ -142,9 +183,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.c_str())) * rString.Scale; + auto stringSize = line.empty() ? Vector2(0, fontSpacing * rString.Scale.y) : Vector2(_gameFont->MeasureString(line.c_str())) * rString.Scale.y; // If height clipping enabled, stop drawing when exceeding maxHeight. if (maxHeight > 0.0f && (yOffset + stringSize.y) > maxHeight) @@ -164,7 +211,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; @@ -194,35 +241,81 @@ 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(); + ResetScissor(); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + + auto currentBlend = BlendMode::AlphaBlend; + SetBlendMode(currentBlend); + + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; for (const auto& rString : _stringsToDraw) { + // Switch blend mode per string if needed. + if (rString.Blend != currentBlend) + { + _spriteBatch->End(); + currentBlend = rString.Blend; + SetBlendMode(currentBlend); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + + // 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(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + 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.c_str(), - Vector2(drawPos.x + shadowOffset * rString.Scale, drawPos.y + shadowOffset * rString.Scale), + shadowPos, (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, - 0.0f, Vector4::Zero, rString.Scale); + 0.0f, Vector2::Zero, rString.Scale.y); } // Draw string. _gameFont->DrawString( _spriteBatch.get(), rString.String.c_str(), - Vector2(drawPos.x, drawPos.y), + drawPos, (rString.Color * rString.Color.w) * ScreenFadeCurrent, - 0.0f, Vector4::Zero, rString.Scale); + 0.0f, Vector2::Zero, rString.Scale.y); } _spriteBatch->End(); + + // Reset scissor if it was active. + if (currentHasScissor) + ResetScissor(); } } diff --git a/TombEngine/Renderer/Structures/RendererStringToDraw.h b/TombEngine/Renderer/Structures/RendererStringToDraw.h index 379dba178d..93a28572d8 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::wstring String; Vector4 Color; - float Scale; + Vector2 Scale; + float Rotation = 0.0f; + int Priority = 0; + BlendMode Blend = BlendMode::AlphaBlend; + + bool HasScissor = false; + RendererRectangle ScissorRect = {}; }; } From 35934cafb258c2353ebc4fae807800ee6b5e4843 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:08:35 +0000 Subject: [PATCH 21/24] Fix DrawString calls to use Vector2 scale and rotation Agent-Logs-Url: https://github.com/TrainWrack/TombEngine/sessions/eeb30453-a1e0-423e-9279-9fc2a1199c56 Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- TombEngine/Renderer/RendererString.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TombEngine/Renderer/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index 6777db3e6e..949dbd3450 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -301,7 +301,7 @@ namespace TEN::Renderer _spriteBatch.get(), rString.String.c_str(), shadowPos, (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, - 0.0f, Vector2::Zero, rString.Scale.y); + rString.Rotation, Vector2::Zero, rString.Scale); } // Draw string. @@ -309,7 +309,7 @@ namespace TEN::Renderer _spriteBatch.get(), rString.String.c_str(), drawPos, (rString.Color * rString.Color.w) * ScreenFadeCurrent, - 0.0f, Vector2::Zero, rString.Scale.y); + rString.Rotation, Vector2::Zero, rString.Scale); } _spriteBatch->End(); From 8661bc851fb5cb7ec8f4996ed483dc77ee70baec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:09:31 +0000 Subject: [PATCH 22/24] Add scissor support to RendererSprite2D structure Agent-Logs-Url: https://github.com/TrainWrack/TombEngine/sessions/eeb30453-a1e0-423e-9279-9fc2a1199c56 Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- TombEngine/Renderer/Structures/RendererSprite2D.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TombEngine/Renderer/Structures/RendererSprite2D.h b/TombEngine/Renderer/Structures/RendererSprite2D.h index 50c4ecb011..ed4389dbd2 100644 --- a/TombEngine/Renderer/Structures/RendererSprite2D.h +++ b/TombEngine/Renderer/Structures/RendererSprite2D.h @@ -2,6 +2,7 @@ #include "Renderer/Structures/RendererSprite.h" #include "Renderer/RendererEnums.h" +#include "Renderer/Structures/RendererRectangle.h" namespace TEN::Renderer::Structures { @@ -18,5 +19,8 @@ namespace TEN::Renderer::Structures BlendMode BlendMode = BlendMode::AlphaBlend; Vector2 AspectCorrection = Vector2::One; + + bool HasScissor = false; + RendererRectangle ScissorRect = {}; }; } From 74fb93de6d0296300c37f9fb16feffd62b4d671f Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Thu, 21 May 2026 21:57:41 -0400 Subject: [PATCH 23/24] WIP --- TombEngine/Renderer/Renderer.h | 4 --- TombEngine/Renderer/RendererDraw2D.cpp | 34 +++++++++++-------------- TombEngine/Renderer/RendererString.cpp | 35 ++++++++++---------------- 3 files changed, 27 insertions(+), 46 deletions(-) diff --git a/TombEngine/Renderer/Renderer.h b/TombEngine/Renderer/Renderer.h index 41be820cf5..f6159c492d 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -386,9 +386,6 @@ namespace TEN::Renderer 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 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 PrepareDynamicLight(RendererLight& light); void PrepareLaserBarriers(RenderView& view); void PrepareSingleLaserBeam(RenderView& view); @@ -708,7 +705,6 @@ namespace TEN::Renderer 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); - Vector2 GetDisplayStringSize(const std::string& text, const Vector2& scale) const; void FreeRendererData(); void AddDynamicPointLight(const Vector3& pos, float radius, const Color& color, bool castShadows, int hash = 0); void AddDynamicFogBulb(const Vector3& pos, float radius, float density, const Color& color, int hash = 0); diff --git a/TombEngine/Renderer/RendererDraw2D.cpp b/TombEngine/Renderer/RendererDraw2D.cpp index 0b9436e47b..6276e880a6 100644 --- a/TombEngine/Renderer/RendererDraw2D.cpp +++ b/TombEngine/Renderer/RendererDraw2D.cpp @@ -339,13 +339,10 @@ namespace TEN::Renderer return; ResetScissor(); - _spriteBatch->Begin(SpriteSortMode_Deferred, _renderStates->NonPremultiplied(), nullptr, nullptr, _cullNoneRasterizerState.Get()); + _spriteBatch->Begin(SpriteSortingMode::Deferred, BlendMode::AlphaBlend); for (const auto& [rect, color] : _debugDisplayRects) - { - auto destRect = RECT{ rect.Left, rect.Top, rect.Right, rect.Bottom }; - _spriteBatch->Draw(_whiteTexture.ShaderResourceView.Get(), destRect, DirectX::XMLoadFloat4(&color)); - } + _spriteBatch->Draw(_whiteTexture.get(), rect, color); _spriteBatch->End(); } @@ -357,11 +354,10 @@ namespace TEN::Renderer if (renderView.DisplaySpritesToDraw.empty()) return; - Texture2D* texture2DPtr = nullptr; + ITexture2D* texture2DPtr = nullptr; bool currentHasScissor = false; auto currentScissor = RendererRectangle{}; - ITexture2D* texture2DPtr = nullptr; for (const auto& spriteToDraw : renderView.DisplaySpritesToDraw) { if ((spriteToDraw.Priority >= 0) == negativePriority) @@ -659,7 +655,7 @@ namespace TEN::Renderer // Draw sprites at this priority level. { - Texture2D* texture2DPtr = nullptr; + ITexture2D* texture2DPtr = nullptr; bool currentHasScissor = false; auto currentScissor = RendererRectangle{}; @@ -678,8 +674,8 @@ namespace TEN::Renderer if (texture2DPtr == nullptr) { _shaders.Bind(Shader::FullScreenQuad); - _context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - _context->IASetInputLayout(_inputLayout.Get()); + _graphicsDevice->SetPrimitiveType(PrimitiveType::TriangleList); + _graphicsDevice->SetInputLayout(_vertexInputLayout.get()); if (spriteToDraw.HasScissor) SetScissor(spriteToDraw.ScissorRect); @@ -734,7 +730,7 @@ namespace TEN::Renderer { rVertices[i].Position = Vector3(vertices[i]); rVertices[i].UV = spriteToDraw.SpritePtr->UV[i]; - rVertices[i].Color = VectorColorToRGBA_TempToVector4(Vector4( + rVertices[i].Color = VectorColorToRGBA(Vector4( spriteToDraw.Color.x, spriteToDraw.Color.y, spriteToDraw.Color.z, spriteToDraw.Color.w)); } @@ -760,8 +756,7 @@ namespace TEN::Renderer auto currentScissor = RendererRectangle{}; ResetScissor(); - SetBlendMode(currentBlend); - _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); while (stringIdx < (int)_stringsToDraw.size() && _stringsToDraw[stringIdx].Priority == priority) { @@ -771,8 +766,7 @@ namespace TEN::Renderer { _spriteBatch->End(); currentBlend = rString.Blend; - SetBlendMode(currentBlend); - _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); } bool scissorChanged = @@ -794,7 +788,7 @@ namespace TEN::Renderer currentHasScissor = rString.HasScissor; currentScissor = rString.ScissorRect; - _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); } auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor()); @@ -803,17 +797,17 @@ namespace TEN::Renderer { auto shadowPos = Vector2(drawPos.x + shadowOffset * rString.Scale.y, drawPos.y + shadowOffset * rString.Scale.y); _gameFont->DrawString( - _spriteBatch.get(), rString.String.c_str(), + _spriteBatch.get(), rString.String, shadowPos, (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, - rString.Rotation, Vector2::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale.x); } _gameFont->DrawString( - _spriteBatch.get(), rString.String.c_str(), + _spriteBatch.get(), rString.String, drawPos, (rString.Color * rString.Color.w) * ScreenFadeCurrent, - rString.Rotation, Vector2::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale.x); } _spriteBatch->End(); diff --git a/TombEngine/Renderer/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index 33189e59c3..9fdc2de905 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -43,8 +43,7 @@ namespace TEN::Renderer auto stringScale = Vector2(uiScale * fontScale) * scale; float baseScale = stringScale.y; - auto wtext = TEN::Utils::ToWString(text); - auto measured = Vector2(_gameFont->MeasureString(wtext.c_str())) * baseScale; + 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); @@ -91,7 +90,7 @@ namespace TEN::Renderer float fontScale = REFERENCE_FONT_SIZE / fontSpacing; auto stringScale = Vector2(uiScale * fontScale) * scale; float baseScale = stringScale.y; - float spaceWidth = Vector3(_gameFont->MeasureString(L" ")).x * baseScale; + float spaceWidth = Vector3(_gameFont->MeasureString(" ")).x * baseScale; std::vector stringLines; @@ -115,7 +114,7 @@ namespace TEN::Renderer for (const auto& word : words) { - float wordWidth = Vector3(_gameFont->MeasureString(word.c_str())).x * baseScale; + float wordWidth = Vector3(_gameFont->MeasureString(word)).x * baseScale; if (!currentLine.empty() && (currentLineWidth + wordWidth + spaceWidth > area.x * factor.x)) { @@ -191,7 +190,7 @@ namespace TEN::Renderer rString.ScissorRect = GetActiveDisplayScissor(); // Measure string. - auto stringSize = line.empty() ? Vector2(0, fontSpacing * rString.Scale.y) : Vector2(_gameFont->MeasureString(line.c_str())) * rString.Scale.y; + 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) @@ -211,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.y; + 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; @@ -249,14 +248,13 @@ namespace TEN::Renderer auto shadowColor = (Vector4)g_GameFlow->GetSettings()->UI.ShadowTextColor; ResetScissor(); - _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); auto currentBlend = BlendMode::AlphaBlend; - SetBlendMode(currentBlend); - bool currentHasScissor = false; auto currentScissor = RendererRectangle{}; + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); + for (const auto& rString : _stringsToDraw) { // Switch blend mode per string if needed. @@ -264,8 +262,7 @@ namespace TEN::Renderer { _spriteBatch->End(); currentBlend = rString.Blend; - SetBlendMode(currentBlend); - _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); } // Handle scissor rect changes. @@ -287,15 +284,10 @@ namespace TEN::Renderer currentHasScissor = rString.HasScissor; currentScissor = rString.ScissorRect; - _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + _spriteBatch->Begin(SpriteSortingMode::Deferred, currentBlend); } auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor()); - _spriteBatch->Begin(SpriteSortingMode::Deferred, BlendMode::PremultipliedAlphaBlend); - - for (const auto& rString : _stringsToDraw) - { - auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor(true)); // Draw shadow. if (rString.Flags & (int)PrintStringFlags::Outline) @@ -303,23 +295,22 @@ namespace TEN::Renderer auto shadowPos = Vector2(drawPos.x + shadowOffset * rString.Scale.y, drawPos.y + shadowOffset * rString.Scale.y); _gameFont->DrawString( - _spriteBatch.get(), rString.String.c_str(), + _spriteBatch.get(), rString.String, shadowPos, (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, - rString.Rotation, Vector2::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale.x); } // Draw string. _gameFont->DrawString( - _spriteBatch.get(), rString.String.c_str(), + _spriteBatch.get(), rString.String, drawPos, (rString.Color * rString.Color.w) * ScreenFadeCurrent, - rString.Rotation, Vector2::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale.x); } _spriteBatch->End(); - // Reset scissor if it was active. if (currentHasScissor) ResetScissor(); } From 4b2d3a744a8e6d88afccecbd5e3f5290c430559d Mon Sep 17 00:00:00 2001 From: TrainWrack <120750885+TrainWrack@users.noreply.github.com> Date: Sun, 31 May 2026 10:29:17 -0400 Subject: [PATCH 24/24] WIP Add scissors for items --- TombEngine/Game/Hud/DrawItems/DisplayItem.cpp | 27 ++++++++++++++ TombEngine/Game/Hud/DrawItems/DisplayItem.h | 5 +++ TombEngine/Game/Hud/DrawItems/DrawItems.cpp | 19 ++++++++++ TombEngine/Renderer/Renderer.cpp | 3 ++ TombEngine/Renderer/Renderer.h | 2 + .../Scripting/Internal/ReservedScriptNames.h | 2 + .../View/DisplayItem/ScriptDisplayItem.cpp | 21 +++++++++++ .../TEN/View/DisplayItem/ScriptDisplayItem.h | 2 + .../DisplaySprite/ScriptDisplaySprite.cpp | 37 +++++++++++++++++++ .../View/DisplaySprite/ScriptDisplaySprite.h | 6 +++ .../DisplayString/ScriptDisplayString.cpp | 36 ++++++++++++++++++ .../View/DisplayString/ScriptDisplayString.h | 6 +++ 12 files changed, 166 insertions(+) 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/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 f6159c492d..e1d4c5160a 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -770,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/Scripting/Internal/ReservedScriptNames.h b/TombEngine/Scripting/Internal/ReservedScriptNames.h index fe9cc72adb..68f4655320 100644 --- a/TombEngine/Scripting/Internal/ReservedScriptNames.h +++ b/TombEngine/Scripting/Internal/ReservedScriptNames.h @@ -78,6 +78,8 @@ 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"; 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 index ec0ee9fb7f..e0da94f093 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp @@ -13,6 +13,7 @@ #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; @@ -82,6 +83,8 @@ namespace TEN::Scripting::DisplayString 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); } @@ -262,6 +265,25 @@ namespace TEN::Scripting::DisplayString } } + /// 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 @@ -402,10 +424,24 @@ namespace TEN::Scripting::DisplayString } } + 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 index b8b8e4bdd1..76b822c978 100644 --- a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h @@ -28,6 +28,10 @@ namespace TEN::Scripting::DisplayString 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); @@ -54,6 +58,8 @@ namespace TEN::Scripting::DisplayString 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;