diff options
| -rw-r--r-- | src/main.c | 2 | ||||
| -rw-r--r-- | src/text.c | 32 | ||||
| -rw-r--r-- | template/DIALOG_STATE_PATTERN.md | 316 |
3 files changed, 338 insertions, 12 deletions
@@ -142,7 +142,7 @@ int main( int argn, const char** argc ) { splashInit(); bool splashDone = false; - while ( !splashDone && !WindowShouldClose() ) { + while ( !splashDone ) { float delta = GetFrameTime(); splashDone = splashUpdate( delta ); splashDraw(); @@ -185,7 +185,9 @@ Load font from file into GPU memory (VRAM) */ int ltextLoadFont( lua_State* L ) { if ( FileExists( luaL_checkstring( L, 1 ) ) ) { - uluaPushFont( L, LoadFont( lua_tostring( L, 1 ) ) ); + Font font = LoadFont( lua_tostring( L, 1 ) ); + SetTextureFilter( font.texture, TEXTURE_FILTER_POINT ); + uluaPushFont( L, font ); return 1; } @@ -207,16 +209,19 @@ int ltextLoadFontEx( lua_State* L ) { int fontSize = luaL_checkinteger( L, 2 ); if ( FileExists( luaL_checkstring( L, 1 ) ) ) { + Font font; if ( lua_istable( L, 3 ) ) { int codepointCount = uluaGetTableLen( L, 3 ); int codepoints[ codepointCount ]; getCodepoints( L, codepoints, 3 ); - uluaPushFont( L, LoadFontEx( lua_tostring( L, 1 ), fontSize, codepoints, codepointCount ) ); - - return 1; + font = LoadFontEx( lua_tostring( L, 1 ), fontSize, codepoints, codepointCount ); + } + else { + font = LoadFontEx( lua_tostring( L, 1 ), fontSize, NULL, 0 ); } - uluaPushFont( L, LoadFontEx( lua_tostring( L, 1 ), fontSize, NULL, 0 ) ); + SetTextureFilter( font.texture, TEXTURE_FILTER_POINT ); + uluaPushFont( L, font ); return 1; } @@ -238,7 +243,9 @@ int ltextLoadFontFromImage( lua_State* L ) { Color key = uluaGetColor( L, 2 ); int firstChar = luaL_checkinteger( L, 3 ); - uluaPushFont( L, LoadFontFromImage( *image, key, firstChar ) ); + Font font = LoadFontFromImage( *image, key, firstChar ); + SetTextureFilter( font.texture, TEXTURE_FILTER_POINT ); + uluaPushFont( L, font ); return 1; } @@ -255,17 +262,20 @@ int ltextLoadFontFromMemory( lua_State* L ) { Buffer* fileData = uluaGetBuffer( L, 2 ); int fontSize = luaL_checkinteger( L, 3 ); + Font font; if ( lua_istable( L, 4 ) ) { int codepointCount = uluaGetTableLen( L, 4 ); int codepoints[ codepointCount ]; getCodepoints( L, codepoints, 4 ); - uluaPushFont( L, LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, codepoints, codepointCount ) ); - - return 1; + font = LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, codepoints, codepointCount ); } - /* If no codepoints provided. */ - uluaPushFont( L, LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, NULL, 0 ) ); + else { + /* If no codepoints provided. */ + font = LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, NULL, 0 ); + } + SetTextureFilter( font.texture, TEXTURE_FILTER_POINT ); + uluaPushFont( L, font ); return 1; } diff --git a/template/DIALOG_STATE_PATTERN.md b/template/DIALOG_STATE_PATTERN.md new file mode 100644 index 0000000..08f34b2 --- /dev/null +++ b/template/DIALOG_STATE_PATTERN.md @@ -0,0 +1,316 @@ +# Dialog State Pattern + +This document explains how to implement Zelda-style dialog systems using the GameState push/pop stack pattern. + +## Overview + +The GameState system supports a **state stack** that allows you to temporarily push a new state (like a dialog) on top of the current game state, then pop back when done. This is perfect for: + +- Dialog systems (Zelda-style text boxes) +- Pause menus +- Inventory screens +- Any UI that needs exclusive input control + +## How It Works + +```lua +GameState.push(newState) -- Pushes current state to stack, switches to new state +GameState.pop() -- Returns to previous state from stack +``` + +When you `push()` a state: +- Current state's `leave()` is called +- New state's `enter()` is called +- Current state remains in memory on the stack + +When you `pop()`: +- Current state's `leave()` is called +- Previous state's `resume()` is called (not `enter()`) +- Previous state gets control back + +## Example: Dialog System + +### Step 1: Create Dialog State + +Create `states/dialog.lua`: + +```lua +local Object = require("lib.classic") +local GameState = require("lib.gamestate") +local DialogState = Object:extend() + +function DialogState:new(dialogData) + self.texts = dialogData.texts or {"Default dialog text"} + self.currentIndex = 1 + self.characterName = dialogData.name or "" + self.portrait = dialogData.portrait or nil + self.textSpeed = dialogData.textSpeed or 50 -- chars per second + self.currentCharIndex = 0 + self.displayedText = "" + self.isComplete = false +end + +function DialogState:enter(previous) + print("Dialog opened") + self.currentCharIndex = 0 + self.displayedText = "" + self.isComplete = false +end + +function DialogState:update(dt) + local currentText = self.texts[self.currentIndex] + + -- Animate text reveal + if not self.isComplete then + self.currentCharIndex = self.currentCharIndex + self.textSpeed * dt + if self.currentCharIndex >= #currentText then + self.currentCharIndex = #currentText + self.isComplete = true + end + self.displayedText = currentText:sub(1, math.floor(self.currentCharIndex)) + end + + -- Handle input + if RL.IsKeyPressed(RL.KEY_ENTER) or RL.IsKeyPressed(RL.KEY_SPACE) then + if not self.isComplete then + -- Skip to end of current text + self.currentCharIndex = #currentText + self.displayedText = currentText + self.isComplete = true + else + -- Move to next text or close dialog + if self.currentIndex < #self.texts then + self.currentIndex = self.currentIndex + 1 + self.currentCharIndex = 0 + self.displayedText = "" + self.isComplete = false + else + -- Dialog finished, return to game + GameState.pop() + end + end + end +end + +function DialogState:draw() + local screenSize = RL.GetScreenSize() + local boxHeight = 200 + local boxY = screenSize[2] - boxHeight - 20 + local padding = 20 + + -- Draw dialog box + RL.DrawRectangle({0, boxY, screenSize[1], boxHeight}, {20, 20, 30, 240}) + RL.DrawRectangleLines({0, boxY, screenSize[1], boxHeight}, 3, RL.WHITE) + + -- Draw character name if present + if self.characterName ~= "" then + local nameBoxWidth = 200 + local nameBoxHeight = 40 + RL.DrawRectangle({padding, boxY - nameBoxHeight + 5, nameBoxWidth, nameBoxHeight}, {40, 40, 50, 255}) + RL.DrawRectangleLines({padding, boxY - nameBoxHeight + 5, nameBoxWidth, nameBoxHeight}, 2, RL.WHITE) + RL.DrawText(self.characterName, {padding + 15, boxY - nameBoxHeight + 15}, 20, RL.WHITE) + end + + -- Draw portrait if present + local textX = padding + 10 + if self.portrait then + RL.DrawTexture(self.portrait, {padding + 10, boxY + padding}, RL.WHITE) + textX = textX + self.portrait.width + 20 + end + + -- Draw text + RL.DrawText(self.displayedText, {textX, boxY + padding}, 20, RL.WHITE) + + -- Draw continue indicator + if self.isComplete then + local indicator = "▼" + if self.currentIndex < #self.texts then + indicator = "Press ENTER to continue" + else + indicator = "Press ENTER to close" + end + local indicatorSize = 16 + local indicatorWidth = RL.MeasureText(indicator, indicatorSize) + RL.DrawText(indicator, {screenSize[1] - indicatorWidth - padding, boxY + boxHeight - padding - 20}, indicatorSize, RL.LIGHTGRAY) + end +end + +function DialogState:leave() + print("Dialog closed") +end + +return DialogState +``` + +### Step 2: Use Dialog in Game State + +Modify `states/game.lua`: + +```lua +local Object = require("lib.classic") +local GameState = require("lib.gamestate") +local Animation = require("lib.animation") +local DialogState = require("states.dialog") +local GameState_Class = Object:extend() + +-- ... (Player class code) ... + +function GameState_Class:new() + self.player = nil + self.paused = false +end + +function GameState_Class:enter(previous) + print("Entered game state") + + local screenSize = RL.GetScreenSize() + self.player = Player(screenSize[1] / 2 - 16, screenSize[2] / 2 - 16) + + -- Example: Show dialog when entering game (for testing) + -- Remove this after testing + local welcomeDialog = DialogState({ + name = "System", + texts = { + "Welcome to the game!", + "Use WASD or Arrow keys to move around.", + "Press ENTER to continue through dialogs." + }, + textSpeed = 30 + }) + GameState.push(welcomeDialog) +end + +function GameState_Class:update(dt) + -- ESC pauses game + if RL.IsKeyPressed(RL.KEY_ESCAPE) then + self.paused = not self.paused + end + + if self.paused then + return + end + + -- Example: Press T to trigger dialog (for testing) + if RL.IsKeyPressed(RL.KEY_T) then + local testDialog = DialogState({ + name = "NPC", + texts = { + "Hello traveler!", + "This is an example dialog system.", + "You can have multiple text boxes.", + "Press ENTER to continue!" + }, + textSpeed = 40 + }) + GameState.push(testDialog) + end + + -- Update game objects (only when not in dialog) + if self.player then + self.player:update(dt) + end +end + +function GameState_Class:resume(previous) + -- Called when returning from dialog + print("Resumed game state from: " .. tostring(previous)) + -- You can handle post-dialog logic here + -- Example: Give item, update quest state, etc. +end + +function GameState_Class:draw() + RL.ClearBackground({50, 50, 50, 255}) + + -- Draw game objects + if self.player then + self.player:draw() + end + + -- Draw pause overlay + if self.paused then + local screenSize = RL.GetScreenSize() + local centerX = screenSize[1] / 2 + local centerY = screenSize[2] / 2 + + RL.DrawRectangle({0, 0, screenSize[1], screenSize[2]}, {0, 0, 0, 128}) + + local text = "PAUSED" + local size = 40 + local width = RL.MeasureText(text, size) + RL.DrawText(text, {centerX - width / 2, centerY - 20}, size, RL.WHITE) + end + + -- Draw controls hint + local hint = "WASD/ARROWS: Move | T: Test Dialog | ESC: Pause" + local hintSize = 16 + local screenSize = RL.GetScreenSize() + RL.DrawText(hint, {10, screenSize[2] - 30}, hintSize, RL.LIGHTGRAY) +end + +function GameState_Class:leave() + print("Left game state") +end + +return GameState_Class +``` + +## Key Benefits + +✅ **Clean Separation**: Dialog has its own file and logic +✅ **Input Control**: Dialog gets exclusive control when active +✅ **No Coupling**: Game doesn't need to know about dialog internals +✅ **Automatic Pause**: Game automatically stops updating when dialog is pushed +✅ **Easy Extension**: Add more dialog types (shops, menus) using same pattern +✅ **Post-Dialog Logic**: Use `resume()` callback to handle what happens after dialog closes + +## Advanced: Passing Data Back to Game + +You can pass data when popping: + +```lua +-- In dialog state +function DialogState:update(dt) + if playerMadeChoice then + GameState.pop(choiceData) -- Pass choice back to game + end +end + +-- In game state +function GameState_Class:resume(previous, choiceData) + if choiceData then + print("Player chose: " .. choiceData.choice) + -- Handle the choice + end +end +``` + +## State Stack Visual + +``` +Initial: [Game State] + +After push: [Game State] -> [Dialog State] (Dialog has control) + +After pop: [Game State] (Game has control back) +``` + +## When to Use Push/Pop vs Flags + +**Use Push/Pop for:** +- Dialog systems +- Pause menus +- Shop interfaces +- Inventory screens +- Any state that needs exclusive control + +**Use Flags (self.paused, etc.) for:** +- Simple on/off toggles +- Quick state checks +- Non-blocking overlays +- Debug info displays + +## See Also + +- `lib/gamestate.lua` - Full GameState implementation +- `states/game.lua` - Example game state +- `states/menu.lua` - Example menu state |
