Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
The dates are in European standard format where date is presented as **YYYY-MM-DD**.
TombEngine releases are located in this repository (alongside with Tomb Editor): https://github.com/TombEngine/TombEditorReleases

## [Version 2.0]

### New features
* Added ease-in and ease-out to flyby camera movement when the "Freeze camera" flag is set.
* Added gamma correction setting.

### Bug fixes
* Fixed incorrect dynamic range for vertex colors, ambient light, dynamic lights and particle effects.
* Fixed flyby camera jitter by converting the spline type to floating-point.

### Lua API changes
* Added `GlobalVars` namespace for globally persistent variables across game sessions, including the title level.

## [Version 1.11.1]

### Bug fixes
Expand Down
222 changes: 222 additions & 0 deletions Scripts/Engine/Achievements/Achievements.lua
Original file line number Diff line number Diff line change
@@ -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
79 changes: 79 additions & 0 deletions Scripts/Engine/Achievements/Block.lua
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions Scripts/Engine/Achievements/Input.lua
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading