diff --git a/source/popka/dialogue.d b/source/popka/chat.d similarity index 55% rename from source/popka/dialogue.d rename to source/popka/chat.d index 9c61b55..962c8d9 100644 --- a/source/popka/dialogue.d +++ b/source/popka/chat.d @@ -1,20 +1,17 @@ // Copyright 2024 Alexandros F. G. Kapretsos // SPDX-License-Identifier: MIT -// TODO: Needs a lot of work. - -/// The `dialogue` module provides a simple and versatile dialogue system. -module popka.dialogue; +/// The `chat` module provides a simple and versatile dialogue system. +module popka.chat; import popka.engine; - public import joka; @safe @nogc nothrow: -enum dialogueUnitKindChars = ".#*@>|^!+-$"; +enum ChatUnitKindChars = ".#*@>|^!+-$"; -enum DialogueUnitKind { +enum ChatUnitKind { pause = '.', comment = '#', point = '*', @@ -28,27 +25,24 @@ enum DialogueUnitKind { command = '$', } -struct DialogueUnit { - List!char text; - DialogueUnitKind kind; +struct ChatUnit { + LStr text; + ChatUnitKind kind; } -struct DialogueVariable { - List!char name; +struct ChatValue { + LStr name; long value; } -alias DialogueCommandRunner = void function(const(char)[][] args); +alias ChatCommandRunner = void function(IStr[] args); -struct Dialogue { - List!DialogueUnit units; - List!DialogueVariable variables; - List!(const(char)[]) menu; - List!(const(char)[]) command; - size_t unitIndex; - size_t pointCount; - const(char)[] text; - const(char)[] actor; +struct Chat { + List!ChatUnit units; + List!ChatValue values; + IStr text; + IStr actor; + Sz unitIndex; @safe @nogc nothrow: @@ -56,43 +50,84 @@ struct Dialogue { return units.length == 0; } + bool isKind(ChatUnitKind kind) { + return unitIndex < units.length && units[unitIndex].kind == kind; + } + bool hasChoices() { - return menu.length != 0; + return isKind(ChatUnitKind.menu); } - bool hasCommand() { - return command.length != 0; - } - - bool hasText() { - return unitIndex < units.length && units[unitIndex].kind != DialogueUnitKind.pause; + bool hasArgs() { + return isKind(ChatUnitKind.command); } bool hasEnded() { - return !hasText; + return isKind(ChatUnitKind.pause); } - const(char)[][] choices() { - return menu.items; + auto choices() { + struct Range { + IStr menu; + + bool empty() { + return menu.length == 0; + } + + IStr front() { + auto temp = menu; + return temp.skipValue(ChatUnitKind.menu).trim(); + } + + void popFront() { + menu.skipValue(ChatUnitKind.menu); + } + } + + return hasChoices ? Range(units[unitIndex].text.items) : Range(""); } - const(char)[][] args() { - return command.items; + IStr[] args() { + static IStr[16] buffer; + + struct Range { + IStr command; + + bool empty() { + return command.length == 0; + } + + IStr front() { + auto temp = command; + return temp.skipValue(' ').trim(); + } + + void popFront() { + command.skipValue(' '); + } + } + + auto length = 0; + auto range = hasArgs ? Range(units[unitIndex].text.items) : Range(""); + foreach (item; range) { + buffer[length] = item; + length += 1; + } + return buffer[0 .. length]; } void reset() { - menu.clear(); - command.clear(); - unitIndex = 0; text = ""; actor = ""; + unitIndex = 0; } - void jump(const(char)[] point) { + // TODO: Make it faster. + void jump(IStr point) { if (point.length == 0) { foreach (i; unitIndex + 1 .. units.length) { auto unit = units[i]; - if (unit.kind == DialogueUnitKind.point) { + if (unit.kind == ChatUnitKind.point) { unitIndex = i; break; } @@ -100,7 +135,7 @@ struct Dialogue { } else { foreach (i; 0 .. units.length) { auto unit = units[i]; - if (unit.kind == DialogueUnitKind.point && unit.text.items == point) { + if (unit.kind == ChatUnitKind.point && unit.text.items == point) { unitIndex = i; break; } @@ -108,10 +143,11 @@ struct Dialogue { } } - void jump(size_t i) { + // TODO: Make it faster. + void jump(Sz i) { auto currPoint = 0; foreach (j, unit; units.items) { - if (unit.kind == DialogueUnitKind.point) { + if (unit.kind == ChatUnitKind.point) { if (currPoint == i) { unitIndex = j; break; @@ -121,21 +157,19 @@ struct Dialogue { } } - void skip(size_t count) { + void skip(Sz count) { foreach (i; 0 .. count) { jump(""); } } - void select(size_t i) { - menu.clear(); + void pick(Sz i) { skip(i + 1); update(); } - void run(DialogueCommandRunner runner) { + void run(ChatCommandRunner runner) { runner(args); - command.clear(); update(); } @@ -145,36 +179,24 @@ struct Dialogue { unitIndex += 1; text = units[unitIndex].text.items; final switch (units[unitIndex].kind) { - case DialogueUnitKind.pause, DialogueUnitKind.line: { + case ChatUnitKind.line, ChatUnitKind.menu, ChatUnitKind.command, ChatUnitKind.pause: { break; } - case DialogueUnitKind.comment, DialogueUnitKind.point: { + case ChatUnitKind.comment, ChatUnitKind.point: { update(); break; } - case DialogueUnitKind.actor: { + case ChatUnitKind.actor: { actor = text; update(); break; } - case DialogueUnitKind.jump: { + case ChatUnitKind.jump: { jump(text); update(); break; } - case DialogueUnitKind.menu: { - if (text.length == 0) { - assert(0, "TODO: An empty menu is an error for now."); - } - menu.clear(); - auto view = text; - while (view.length != 0) { - auto option = trim(skipValue(view, DialogueUnitKind.menu)); - menu.append(option); - } - break; - } - case DialogueUnitKind.variable: { + case ChatUnitKind.variable: { auto variableIndex = -1; auto view = text; auto name = trim(skipValue(view, '=')); @@ -183,7 +205,7 @@ struct Dialogue { assert(0, "TODO: An variable without a name is an error for now."); } // Find if variable exists. - foreach (i, variable; variables.items) { + foreach (i, variable; values.items) { if (variable.name.items == name) { variableIndex = cast(int) i; break; @@ -191,10 +213,10 @@ struct Dialogue { } // Create variable if it does not exist. if (variableIndex < 0) { - auto variable = DialogueVariable(); + auto variable = ChatValue(); variable.name.append(name); - variables.append(variable); - variableIndex = cast(int) variables.length - 1; + values.append(variable); + variableIndex = cast(int) values.length - 1; } // Set variable value. if (value.length != 0) { @@ -203,7 +225,7 @@ struct Dialogue { auto valueVariableIndex = -1; auto valueName = value; // Find if variable exists. - foreach (i, variable; variables.items) { + foreach (i, variable; values.items) { if (variable.name.items == valueName) { valueVariableIndex = cast(int) i; break; @@ -212,20 +234,20 @@ struct Dialogue { if (valueVariableIndex < 0) { assert(0, "TODO: A variable that doesn't exist it an error for now."); } else { - variables[variableIndex].value = variables[valueVariableIndex].value; + values[variableIndex].value = values[valueVariableIndex].value; } } else { - variables[variableIndex].value = conv.value; + values[variableIndex].value = conv.value; } } update(); break; } - case DialogueUnitKind.plus, DialogueUnitKind.minus: { + case ChatUnitKind.plus, ChatUnitKind.minus: { auto variableIndex = -1; auto name = text; // Find if variable exists. - foreach (i, variable; variables.items) { + foreach (i, variable; values.items) { if (variable.name.items == name) { variableIndex = cast(int) i; break; @@ -235,37 +257,25 @@ struct Dialogue { if (variableIndex < 0) { assert(0, "TODO: A variable that doesn't exist it an error for now."); } - if (units[unitIndex].kind == DialogueUnitKind.plus) { - variables[variableIndex].value += 1; + if (units[unitIndex].kind == ChatUnitKind.plus) { + values[variableIndex].value += 1; } else { - variables[variableIndex].value -= 1; + values[variableIndex].value -= 1; } update(); break; } - case DialogueUnitKind.command: { - if (text.length == 0) { - assert(0, "TODO: An empty command is an error for now."); - } - command.clear(); - auto view = text; - while (view.length != 0) { - auto arg = trim(skipValue(view, ' ')); - command.append(arg); - } - break; - } } } } - Fault parse(const(char)[] script) { + Fault parse(IStr script) { clear(); if (script.length == 0) { return Fault.invalid; } - units.append(DialogueUnit(List!char(), DialogueUnitKind.pause)); + units.append(ChatUnit(LStr(), ChatUnitKind.pause)); auto isFirstLine = true; auto view = script; while (view.length != 0) { @@ -277,23 +287,20 @@ struct Dialogue { auto kind = line[0]; if (isFirstLine) { isFirstLine = false; - if (kind == DialogueUnitKind.pause) { + if (kind == ChatUnitKind.pause) { continue; } } - if (isValidDialogueUnitKind(kind)) { - auto realKind = cast(DialogueUnitKind) kind; - units.append(DialogueUnit(List!char(text), realKind)); - if (realKind == DialogueUnitKind.point) { - pointCount += 1; - } + if (isValidChatUnitKind(kind)) { + auto realKind = cast(ChatUnitKind) kind; + units.append(ChatUnit(LStr(text), realKind)); } else { clear(); return Fault.invalid; } } - if (units.items[$ - 1].kind != DialogueUnitKind.pause) { - units.append(DialogueUnit(List!char(), DialogueUnitKind.pause)); + if (units.items[$ - 1].kind != ChatUnitKind.pause) { + units.append(ChatUnit(LStr(), ChatUnitKind.pause)); } return Fault.none; } @@ -303,14 +310,11 @@ struct Dialogue { unit.text.free(); } units.clear(); - foreach (ref variable; variables) { + foreach (ref variable; values) { variable.name.free(); } - variables.clear(); - menu.clear(); - command.clear(); + values.clear(); reset(); - pointCount = 0; } void free() { @@ -318,19 +322,16 @@ struct Dialogue { unit.text.free(); } units.free(); - foreach (ref variable; variables) { + foreach (ref variable; values) { variable.name.free(); } - variables.free(); - menu.free(); - command.free(); + values.free(); reset(); - pointCount = 0; } } -bool isValidDialogueUnitKind(char c) { - foreach (kind; dialogueUnitKindChars) { +bool isValidChatUnitKind(char c) { + foreach (kind; ChatUnitKindChars) { if (c == kind) { return true; } @@ -338,16 +339,19 @@ bool isValidDialogueUnitKind(char c) { return false; } -Result!Dialogue loadDialogue(IStr path) { - auto temp = loadTempText(path); - if (temp.isNone) { - return Result!Dialogue(temp.fault); - } - - auto value = Dialogue(); - auto fault = value.parse(temp.unwrap()); +Result!Chat toChat(IStr script) { + auto value = Chat(); + auto fault = value.parse(script); if (fault) { value.free(); } - return Result!Dialogue(value, fault); + return Result!Chat(value, fault); +} + +Result!Chat loadChat(IStr path) { + auto temp = loadTempText(path); + if (temp.isNone) { + return Result!Chat(temp.fault); + } + return toChat(temp.unwrap()); } diff --git a/source/popka/engine.d b/source/popka/engine.d index d1bba46..58c9025 100644 --- a/source/popka/engine.d +++ b/source/popka/engine.d @@ -1,13 +1,10 @@ // Copyright 2024 Alexandros F. G. Kapretsos // SPDX-License-Identifier: MIT -// TODO: Needs docs! - /// The `engine` module functions as a lightweight 2D game engine. module popka.engine; import ray = popka.ray; - public import joka; public import popka.types; @@ -88,15 +85,6 @@ Result!IStr loadTempText(IStr path) { return Result!IStr(engineState.tempText.items, fault); } -Result!TileMap loadTileMap(IStr path) { - auto value = TileMap(); - auto fault = value.parse(loadTempText(path).unwrapOr()); - if (fault) { - value.free(); - } - return Result!TileMap(value, fault); -} - /// Loads an image file from the assets folder. /// Can handle both forward slashes and backslashes in file paths, ensuring compatibility across operating systems. @trusted @@ -662,38 +650,6 @@ void drawTile(Texture texture, Vec2 position, int tileID, Vec2 tileSize, DrawOpt drawTexture(texture, position, area, options); } -void drawTileMap(Texture texture, Vec2 position, TileMap tileMap, Camera camera, DrawOptions options = DrawOptions()) { - enum extraTileCount = 4; - - auto cameraArea = Rect(camera.position, resolution).area(camera.hook); - auto topLeft = cameraArea.point(Hook.topLeft); - auto bottomRight = cameraArea.point(Hook.bottomRight); - auto col1 = 0; - auto col2 = 0; - auto row1 = 0; - auto row2 = 0; - - if (camera.isAttached) { - col1 = cast(int) floor(clamp((topLeft.x - position.x) / tileMap.tileSize.x - extraTileCount, 0, tileMap.colCount)); - col2 = cast(int) floor(clamp((bottomRight.x - position.x) / tileMap.tileSize.x + extraTileCount, 0, tileMap.colCount)); - row1 = cast(int) floor(clamp((topLeft.y - position.y) / tileMap.tileSize.y - extraTileCount, 0, tileMap.rowCount)); - row2 = cast(int) floor(clamp((bottomRight.y - position.y) / tileMap.tileSize.y + extraTileCount, 0, tileMap.rowCount)); - } else { - col1 = 0; - col2 = cast(int) tileMap.colCount; - row1 = 0; - row2 = cast(int) tileMap.rowCount; - } - foreach (row; row1 .. row2) { - foreach (col; col1 .. col2) { - if (tileMap[row, col] == -1) { - continue; - } - drawTile(texture, position + Vec2(col, row) * tileMap.tileSize * options.scale, tileMap[row, col], tileMap.tileSize, options); - } - } -} - @trusted void drawRune(Font font, Vec2 position, dchar rune, DrawOptions options = DrawOptions()) { if (font.isEmpty) { diff --git a/source/popka/package.d b/source/popka/package.d index 3f23c29..2fe7904 100644 --- a/source/popka/package.d +++ b/source/popka/package.d @@ -3,6 +3,7 @@ module popka; -public import popka.dialogue; +public import popka.chat; public import popka.engine; +public import popka.tilemap; public import popka.types; diff --git a/source/popka/tilemap.d b/source/popka/tilemap.d new file mode 100644 index 0000000..0bac456 --- /dev/null +++ b/source/popka/tilemap.d @@ -0,0 +1,119 @@ +// Copyright 2024 Alexandros F. G. Kapretsos +// SPDX-License-Identifier: MIT + +/// The `tilemap` module provides a simple and fast tile map. +module popka.tilemap; + +import popka.engine; +public import joka; + +@safe @nogc nothrow: + +struct TileMap { + Grid!short data; + int tileWidth; + int tileHeight; + + alias data this; + + @safe @nogc nothrow: + + /// Returns true if the tile map has not been loaded. + bool isEmpty() { + return data.length == 0; + } + + /// Returns the tile size of the tile map. + Vec2 tileSize() { + return Vec2(tileWidth, tileHeight); + } + + /// Returns the size of the tile map. + Vec2 size() { + return tileSize * Vec2(colCount, rowCount); + } + + Fault parse(IStr csv) { + data.clear(); + if (csv.length == 0) { + return Fault.invalid; + } + + auto view = csv; + auto newRowCount = 0; + auto newColCount = 0; + while (view.length != 0) { + auto line = view.skipLine(); + newRowCount += 1; + newColCount = 0; + while (line.length != 0) { + auto value = line.skipValue(','); + newColCount += 1; + } + } + resize(newRowCount, newColCount); + + view = csv; + foreach (row; 0 .. newRowCount) { + auto line = view.skipLine(); + foreach (col; 0 .. newColCount) { + auto value = line.skipValue(',').toSigned(); + if (value.isNone) { + data.clear(); + return Fault.invalid; + } + data[row, col] = cast(short) value.unwrap(); + } + } + return Fault.none; + } +} + +Result!TileMap toTileMap(IStr csv) { + auto value = TileMap(); + auto fault = value.parse(csv); + if (fault) { + value.free(); + } + return Result!TileMap(value, fault); +} + +Result!TileMap loadTileMap(IStr path) { + auto temp = loadTempText(path); + if (temp.isNone) { + return Result!TileMap(temp.fault); + } + return toTileMap(temp.unwrap()); +} + +void drawTileMap(Texture texture, Vec2 position, TileMap tileMap, Camera camera, DrawOptions options = DrawOptions()) { + enum extraTileCount = 1; + + auto cameraArea = Rect(camera.position, resolution).area(camera.hook); + auto topLeft = cameraArea.point(Hook.topLeft); + auto bottomRight = cameraArea.point(Hook.bottomRight); + auto col1 = 0; + auto col2 = 0; + auto row1 = 0; + auto row2 = 0; + + if (camera.isAttached) { + col1 = cast(int) floor(clamp((topLeft.x - position.x) / tileMap.tileSize.x - extraTileCount, 0, tileMap.colCount)); + col2 = cast(int) floor(clamp((bottomRight.x - position.x) / tileMap.tileSize.x + extraTileCount, 0, tileMap.colCount)); + row1 = cast(int) floor(clamp((topLeft.y - position.y) / tileMap.tileSize.y - extraTileCount, 0, tileMap.rowCount)); + row2 = cast(int) floor(clamp((bottomRight.y - position.y) / tileMap.tileSize.y + extraTileCount, 0, tileMap.rowCount)); + } else { + col1 = 0; + col2 = cast(int) tileMap.colCount; + row1 = 0; + row2 = cast(int) tileMap.rowCount; + } + foreach (row; row1 .. row2) { + foreach (col; col1 .. col2) { + if (tileMap[row, col] == -1) { + continue; + } + drawTile(texture, position + Vec2(col, row) * tileMap.tileSize * options.scale, tileMap[row, col], tileMap.tileSize, options); + } + } +} diff --git a/source/popka/types.d b/source/popka/types.d index 42274e3..9f891c8 100644 --- a/source/popka/types.d +++ b/source/popka/types.d @@ -1,14 +1,10 @@ // Copyright 2024 Alexandros F. G. Kapretsos // SPDX-License-Identifier: MIT -// TODO: Needs docs! -// TODO: Move tile map to it's own module like dialogue! - /// The `types` module defines all the types used within the `engine` module. module popka.types; import ray = popka.ray; - public import joka; @safe @nogc nothrow: @@ -151,6 +147,7 @@ struct EngineViewport { int targetHeight; bool isLockResolutionQueued; bool isUnlockResolutionQueued; + alias data this; } @@ -209,66 +206,6 @@ struct Camera { } } -struct TileMap { - Grid!short data; - int tileWidth; - int tileHeight; - alias data this; - - @safe @nogc nothrow: - - /// Returns true if the tile map has not been loaded. - bool isEmpty() { - return data.length == 0; - } - - /// Returns the tile size of the tile map. - Vec2 tileSize() { - return Vec2(tileWidth, tileHeight); - } - - /// Returns the size of the tile map. - Vec2 size() { - return tileSize * Vec2(colCount, rowCount); - } - - Fault parse(IStr csv) { - data.clear(); - if (csv.length == 0) { - return Fault.invalid; - } - - auto view = csv; - auto newRowCount = 0; - auto newColCount = 0; - - while (view.length != 0) { - auto line = view.skipLine(); - newRowCount += 1; - newColCount = 0; - while (line.length != 0) { - auto value = line.skipValue(','); - newColCount += 1; - } - } - resize(newRowCount, newColCount); - - view = csv; - foreach (row; 0 .. newRowCount) { - auto line = view.skipLine(); - foreach (col; 0 .. newColCount) { - auto value = line.skipValue(',').toSigned(); - if (value.isNone) { - data.clear(); - return Fault.invalid; - } - data[row, col] = cast(short) value.unwrap(); - } - } - return Fault.none; - } -} - struct Texture { ray.Texture2D data; Filter filter;