Tile map is not part of the engine now.

This commit is contained in:
Kapendev 2024-08-13 06:07:46 +03:00
parent 776070acb4
commit 4850a99236
5 changed files with 246 additions and 229 deletions

View file

@ -1,20 +1,17 @@
// Copyright 2024 Alexandros F. G. Kapretsos // Copyright 2024 Alexandros F. G. Kapretsos
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// TODO: Needs a lot of work. /// The `chat` module provides a simple and versatile dialogue system.
module popka.chat;
/// The `dialogue` module provides a simple and versatile dialogue system.
module popka.dialogue;
import popka.engine; import popka.engine;
public import joka; public import joka;
@safe @nogc nothrow: @safe @nogc nothrow:
enum dialogueUnitKindChars = ".#*@>|^!+-$"; enum ChatUnitKindChars = ".#*@>|^!+-$";
enum DialogueUnitKind { enum ChatUnitKind {
pause = '.', pause = '.',
comment = '#', comment = '#',
point = '*', point = '*',
@ -28,27 +25,24 @@ enum DialogueUnitKind {
command = '$', command = '$',
} }
struct DialogueUnit { struct ChatUnit {
List!char text; LStr text;
DialogueUnitKind kind; ChatUnitKind kind;
} }
struct DialogueVariable { struct ChatValue {
List!char name; LStr name;
long value; long value;
} }
alias DialogueCommandRunner = void function(const(char)[][] args); alias ChatCommandRunner = void function(IStr[] args);
struct Dialogue { struct Chat {
List!DialogueUnit units; List!ChatUnit units;
List!DialogueVariable variables; List!ChatValue values;
List!(const(char)[]) menu; IStr text;
List!(const(char)[]) command; IStr actor;
size_t unitIndex; Sz unitIndex;
size_t pointCount;
const(char)[] text;
const(char)[] actor;
@safe @nogc nothrow: @safe @nogc nothrow:
@ -56,43 +50,84 @@ struct Dialogue {
return units.length == 0; return units.length == 0;
} }
bool isKind(ChatUnitKind kind) {
return unitIndex < units.length && units[unitIndex].kind == kind;
}
bool hasChoices() { bool hasChoices() {
return menu.length != 0; return isKind(ChatUnitKind.menu);
} }
bool hasCommand() { bool hasArgs() {
return command.length != 0; return isKind(ChatUnitKind.command);
}
bool hasText() {
return unitIndex < units.length && units[unitIndex].kind != DialogueUnitKind.pause;
} }
bool hasEnded() { bool hasEnded() {
return !hasText; return isKind(ChatUnitKind.pause);
} }
const(char)[][] choices() { auto choices() {
return menu.items; struct Range {
IStr menu;
bool empty() {
return menu.length == 0;
} }
const(char)[][] args() { IStr front() {
return command.items; auto temp = menu;
return temp.skipValue(ChatUnitKind.menu).trim();
}
void popFront() {
menu.skipValue(ChatUnitKind.menu);
}
}
return hasChoices ? Range(units[unitIndex].text.items) : Range("");
}
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() { void reset() {
menu.clear();
command.clear();
unitIndex = 0;
text = ""; text = "";
actor = ""; actor = "";
unitIndex = 0;
} }
void jump(const(char)[] point) { // TODO: Make it faster.
void jump(IStr point) {
if (point.length == 0) { if (point.length == 0) {
foreach (i; unitIndex + 1 .. units.length) { foreach (i; unitIndex + 1 .. units.length) {
auto unit = units[i]; auto unit = units[i];
if (unit.kind == DialogueUnitKind.point) { if (unit.kind == ChatUnitKind.point) {
unitIndex = i; unitIndex = i;
break; break;
} }
@ -100,7 +135,7 @@ struct Dialogue {
} else { } else {
foreach (i; 0 .. units.length) { foreach (i; 0 .. units.length) {
auto unit = units[i]; auto unit = units[i];
if (unit.kind == DialogueUnitKind.point && unit.text.items == point) { if (unit.kind == ChatUnitKind.point && unit.text.items == point) {
unitIndex = i; unitIndex = i;
break; break;
} }
@ -108,10 +143,11 @@ struct Dialogue {
} }
} }
void jump(size_t i) { // TODO: Make it faster.
void jump(Sz i) {
auto currPoint = 0; auto currPoint = 0;
foreach (j, unit; units.items) { foreach (j, unit; units.items) {
if (unit.kind == DialogueUnitKind.point) { if (unit.kind == ChatUnitKind.point) {
if (currPoint == i) { if (currPoint == i) {
unitIndex = j; unitIndex = j;
break; break;
@ -121,21 +157,19 @@ struct Dialogue {
} }
} }
void skip(size_t count) { void skip(Sz count) {
foreach (i; 0 .. count) { foreach (i; 0 .. count) {
jump(""); jump("");
} }
} }
void select(size_t i) { void pick(Sz i) {
menu.clear();
skip(i + 1); skip(i + 1);
update(); update();
} }
void run(DialogueCommandRunner runner) { void run(ChatCommandRunner runner) {
runner(args); runner(args);
command.clear();
update(); update();
} }
@ -145,36 +179,24 @@ struct Dialogue {
unitIndex += 1; unitIndex += 1;
text = units[unitIndex].text.items; text = units[unitIndex].text.items;
final switch (units[unitIndex].kind) { final switch (units[unitIndex].kind) {
case DialogueUnitKind.pause, DialogueUnitKind.line: { case ChatUnitKind.line, ChatUnitKind.menu, ChatUnitKind.command, ChatUnitKind.pause: {
break; break;
} }
case DialogueUnitKind.comment, DialogueUnitKind.point: { case ChatUnitKind.comment, ChatUnitKind.point: {
update(); update();
break; break;
} }
case DialogueUnitKind.actor: { case ChatUnitKind.actor: {
actor = text; actor = text;
update(); update();
break; break;
} }
case DialogueUnitKind.jump: { case ChatUnitKind.jump: {
jump(text); jump(text);
update(); update();
break; break;
} }
case DialogueUnitKind.menu: { case ChatUnitKind.variable: {
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: {
auto variableIndex = -1; auto variableIndex = -1;
auto view = text; auto view = text;
auto name = trim(skipValue(view, '=')); auto name = trim(skipValue(view, '='));
@ -183,7 +205,7 @@ struct Dialogue {
assert(0, "TODO: An variable without a name is an error for now."); assert(0, "TODO: An variable without a name is an error for now.");
} }
// Find if variable exists. // Find if variable exists.
foreach (i, variable; variables.items) { foreach (i, variable; values.items) {
if (variable.name.items == name) { if (variable.name.items == name) {
variableIndex = cast(int) i; variableIndex = cast(int) i;
break; break;
@ -191,10 +213,10 @@ struct Dialogue {
} }
// Create variable if it does not exist. // Create variable if it does not exist.
if (variableIndex < 0) { if (variableIndex < 0) {
auto variable = DialogueVariable(); auto variable = ChatValue();
variable.name.append(name); variable.name.append(name);
variables.append(variable); values.append(variable);
variableIndex = cast(int) variables.length - 1; variableIndex = cast(int) values.length - 1;
} }
// Set variable value. // Set variable value.
if (value.length != 0) { if (value.length != 0) {
@ -203,7 +225,7 @@ struct Dialogue {
auto valueVariableIndex = -1; auto valueVariableIndex = -1;
auto valueName = value; auto valueName = value;
// Find if variable exists. // Find if variable exists.
foreach (i, variable; variables.items) { foreach (i, variable; values.items) {
if (variable.name.items == valueName) { if (variable.name.items == valueName) {
valueVariableIndex = cast(int) i; valueVariableIndex = cast(int) i;
break; break;
@ -212,20 +234,20 @@ struct Dialogue {
if (valueVariableIndex < 0) { if (valueVariableIndex < 0) {
assert(0, "TODO: A variable that doesn't exist it an error for now."); assert(0, "TODO: A variable that doesn't exist it an error for now.");
} else { } else {
variables[variableIndex].value = variables[valueVariableIndex].value; values[variableIndex].value = values[valueVariableIndex].value;
} }
} else { } else {
variables[variableIndex].value = conv.value; values[variableIndex].value = conv.value;
} }
} }
update(); update();
break; break;
} }
case DialogueUnitKind.plus, DialogueUnitKind.minus: { case ChatUnitKind.plus, ChatUnitKind.minus: {
auto variableIndex = -1; auto variableIndex = -1;
auto name = text; auto name = text;
// Find if variable exists. // Find if variable exists.
foreach (i, variable; variables.items) { foreach (i, variable; values.items) {
if (variable.name.items == name) { if (variable.name.items == name) {
variableIndex = cast(int) i; variableIndex = cast(int) i;
break; break;
@ -235,37 +257,25 @@ struct Dialogue {
if (variableIndex < 0) { if (variableIndex < 0) {
assert(0, "TODO: A variable that doesn't exist it an error for now."); assert(0, "TODO: A variable that doesn't exist it an error for now.");
} }
if (units[unitIndex].kind == DialogueUnitKind.plus) { if (units[unitIndex].kind == ChatUnitKind.plus) {
variables[variableIndex].value += 1; values[variableIndex].value += 1;
} else { } else {
variables[variableIndex].value -= 1; values[variableIndex].value -= 1;
} }
update(); update();
break; 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(); clear();
if (script.length == 0) { if (script.length == 0) {
return Fault.invalid; return Fault.invalid;
} }
units.append(DialogueUnit(List!char(), DialogueUnitKind.pause)); units.append(ChatUnit(LStr(), ChatUnitKind.pause));
auto isFirstLine = true; auto isFirstLine = true;
auto view = script; auto view = script;
while (view.length != 0) { while (view.length != 0) {
@ -277,23 +287,20 @@ struct Dialogue {
auto kind = line[0]; auto kind = line[0];
if (isFirstLine) { if (isFirstLine) {
isFirstLine = false; isFirstLine = false;
if (kind == DialogueUnitKind.pause) { if (kind == ChatUnitKind.pause) {
continue; continue;
} }
} }
if (isValidDialogueUnitKind(kind)) { if (isValidChatUnitKind(kind)) {
auto realKind = cast(DialogueUnitKind) kind; auto realKind = cast(ChatUnitKind) kind;
units.append(DialogueUnit(List!char(text), realKind)); units.append(ChatUnit(LStr(text), realKind));
if (realKind == DialogueUnitKind.point) {
pointCount += 1;
}
} else { } else {
clear(); clear();
return Fault.invalid; return Fault.invalid;
} }
} }
if (units.items[$ - 1].kind != DialogueUnitKind.pause) { if (units.items[$ - 1].kind != ChatUnitKind.pause) {
units.append(DialogueUnit(List!char(), DialogueUnitKind.pause)); units.append(ChatUnit(LStr(), ChatUnitKind.pause));
} }
return Fault.none; return Fault.none;
} }
@ -303,14 +310,11 @@ struct Dialogue {
unit.text.free(); unit.text.free();
} }
units.clear(); units.clear();
foreach (ref variable; variables) { foreach (ref variable; values) {
variable.name.free(); variable.name.free();
} }
variables.clear(); values.clear();
menu.clear();
command.clear();
reset(); reset();
pointCount = 0;
} }
void free() { void free() {
@ -318,19 +322,16 @@ struct Dialogue {
unit.text.free(); unit.text.free();
} }
units.free(); units.free();
foreach (ref variable; variables) { foreach (ref variable; values) {
variable.name.free(); variable.name.free();
} }
variables.free(); values.free();
menu.free();
command.free();
reset(); reset();
pointCount = 0;
} }
} }
bool isValidDialogueUnitKind(char c) { bool isValidChatUnitKind(char c) {
foreach (kind; dialogueUnitKindChars) { foreach (kind; ChatUnitKindChars) {
if (c == kind) { if (c == kind) {
return true; return true;
} }
@ -338,16 +339,19 @@ bool isValidDialogueUnitKind(char c) {
return false; return false;
} }
Result!Dialogue loadDialogue(IStr path) { Result!Chat toChat(IStr script) {
auto temp = loadTempText(path); auto value = Chat();
if (temp.isNone) { auto fault = value.parse(script);
return Result!Dialogue(temp.fault);
}
auto value = Dialogue();
auto fault = value.parse(temp.unwrap());
if (fault) { if (fault) {
value.free(); 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());
} }

View file

@ -1,13 +1,10 @@
// Copyright 2024 Alexandros F. G. Kapretsos // Copyright 2024 Alexandros F. G. Kapretsos
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// TODO: Needs docs!
/// The `engine` module functions as a lightweight 2D game engine. /// The `engine` module functions as a lightweight 2D game engine.
module popka.engine; module popka.engine;
import ray = popka.ray; import ray = popka.ray;
public import joka; public import joka;
public import popka.types; public import popka.types;
@ -88,15 +85,6 @@ Result!IStr loadTempText(IStr path) {
return Result!IStr(engineState.tempText.items, fault); 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. /// Loads an image file from the assets folder.
/// Can handle both forward slashes and backslashes in file paths, ensuring compatibility across operating systems. /// Can handle both forward slashes and backslashes in file paths, ensuring compatibility across operating systems.
@trusted @trusted
@ -662,38 +650,6 @@ void drawTile(Texture texture, Vec2 position, int tileID, Vec2 tileSize, DrawOpt
drawTexture(texture, position, area, options); 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 @trusted
void drawRune(Font font, Vec2 position, dchar rune, DrawOptions options = DrawOptions()) { void drawRune(Font font, Vec2 position, dchar rune, DrawOptions options = DrawOptions()) {
if (font.isEmpty) { if (font.isEmpty) {

View file

@ -3,6 +3,7 @@
module popka; module popka;
public import popka.dialogue; public import popka.chat;
public import popka.engine; public import popka.engine;
public import popka.tilemap;
public import popka.types; public import popka.types;

119
source/popka/tilemap.d Normal file
View file

@ -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);
}
}
}

View file

@ -1,14 +1,10 @@
// Copyright 2024 Alexandros F. G. Kapretsos // Copyright 2024 Alexandros F. G. Kapretsos
// SPDX-License-Identifier: MIT // 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. /// The `types` module defines all the types used within the `engine` module.
module popka.types; module popka.types;
import ray = popka.ray; import ray = popka.ray;
public import joka; public import joka;
@safe @nogc nothrow: @safe @nogc nothrow:
@ -151,6 +147,7 @@ struct EngineViewport {
int targetHeight; int targetHeight;
bool isLockResolutionQueued; bool isLockResolutionQueued;
bool isUnlockResolutionQueued; bool isUnlockResolutionQueued;
alias data this; 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 { struct Texture {
ray.Texture2D data; ray.Texture2D data;
Filter filter; Filter filter;