Everything is back to normal now.

This commit is contained in:
Kapendev 2024-10-12 14:28:25 +03:00
parent 2b6dc55a95
commit 7f2b072482
14 changed files with 187 additions and 89 deletions

14
.gitignore vendored
View file

@ -2,13 +2,13 @@
docs.json
__dummy.html
docs/
/popka
popka.so
popka.dylib
popka.dll
popka.a
popka.lib
popka-test-*
/parin
parin.so
parin.dylib
parin.dll
parin.a
parin.lib
parin-test-*
*.exe
*.pdb
*.o

View file

@ -115,7 +115,7 @@ void updateSound(SoundId sound);
## Drawing
Parin provides a set of drawing functions inside the `parin.engine` module.
While drawing is not pixel-perfect by default, it can be by calling the `setIsPixelPerfect` function.
While drawing is not pixel-perfect by default, it can be by calling the `setIsPixelPerfect` or `setIsPixelSnapped` function.
```d
void drawRect(Rect area, Color color = white);

View file

@ -4,7 +4,6 @@ import parin;
// The game variables.
auto player = Rect(16, 16);
auto playerSpeed = 120;
auto coins = SparseList!Rect();
auto coinSize = Vec2(8);
auto maxCoinCount = 8;

View file

@ -1,5 +1,6 @@
/// This example shows how to use the Parin dialogue system.
import parin;
import parin.dialogue;
// The game variables.
auto dialogue = Dialogue();

View file

@ -19,38 +19,36 @@ void ready() {
}
bool update(float dt) {
// Make some options.
// Make the drawing options.
auto mapOptions = DrawOptions(Hook.center);
mapOptions.scale = Vec2(2);
auto tileOptions = mapOptions;
tileOptions.flip = tileLookDirection > 0 ? Flip.x : Flip.none;
// Move tile and camera.
// Move the tile and camera.
tile.position += wasd * Vec2(tileSpeed * dt);
camera.followPosition(tile.position, tileSpeed);
camera.position = tile.position;
if (wasd.x != 0) tileLookDirection = cast(int) wasd.normalize.round.x;
// Check for collisions.
auto colRow1 = map.firstMapPosition(camera.area.topLeftPoint, mapOptions);
auto colRow2 = map.lastMapPosition(camera.area.bottomRightPoint, mapOptions);
foreach (row; colRow1.y .. colRow2.y) {
foreach (col; colRow1.x .. colRow2.x) {
if (map[row, col] == -1) continue;
// TODO: Yeah, maybe change it to something better...
auto mapTileRect = Rect(map.worldPosition(row, col, mapOptions), Vec2(16) * mapOptions.scale);
auto myTileRect = Rect(tile.position, tile.size * mapOptions.scale).area(Hook.center);
if (mapTileRect.hasIntersection(myTileRect)) {
tile.position -= wasd * Vec2(tileSpeed * dt);
camera.followPosition(tile.position, tileSpeed);
break;
}
auto collisionArea = Rect();
foreach (gridPosition; map.gridPositions(camera.topLeftPoint, camera.bottomRightPoint, mapOptions)) {
if (map[gridPosition] == -1) continue;
auto gridTileArea = Rect(map.worldPosition(gridPosition, mapOptions), Vec2(16) * mapOptions.scale);
while (gridTileArea.hasIntersection(Rect(tile.position, tile.size * mapOptions.scale).area(tileOptions.hook))) {
tile.position -= wasd * Vec2(dt);
camera.position = tile.position;
collisionArea = gridTileArea;
}
}
// Draw game.
// Draw the game.
camera.attach();
drawTileMap(atlas, map, camera, mapOptions);
drawTile(atlas, tile, tileOptions);
drawRect(collisionArea, yellow.alpha(120));
camera.detach();
drawDebugText("Move with arrow keys.", Vec2(8));
return false;
}

View file

@ -75,7 +75,6 @@ bool update(float dt) {
drawRect(ball.centerArea);
drawRect(paddle1.centerArea);
drawRect(paddle2.centerArea);
// Draw the counter.
auto textOptions = DrawOptions(Hook.center);
textOptions.scale = Vec2(2);

View file

@ -51,7 +51,7 @@ struct Scene2 {
sceneManager.enter!Scene1();
}
drawDebugText("Press enter to change scene.", resolution * Vec2(0.5), DrawOptions(Hook.center));
drawDebugText("Press space to change scene.", resolution * Vec2(0.5), DrawOptions(Hook.center));
drawDebugText("Scene 2\nNo counter here.", Vec2(8));
return false;
}

View file

@ -40,7 +40,7 @@ struct DialogueUnit {
LStr text;
DialogueUnitKind kind;
@safe:
@safe @nogc nothrow:
void free() {
text.free();
@ -52,7 +52,7 @@ struct DialogueValue {
LStr name;
long value;
@safe:
@safe @nogc nothrow:
void free() {
name.free();
@ -60,7 +60,7 @@ struct DialogueValue {
}
}
alias DialogueCommandRunner = void function(IStr[] args);
alias DialogueCommandRunner = void function(IStr[] args) @trusted;
struct Dialogue {
List!DialogueUnit units;
@ -69,7 +69,13 @@ struct Dialogue {
IStr actor;
Sz unitIndex;
@safe:
@trusted
void run(DialogueCommandRunner runner) {
runner(args);
update();
}
@safe @nogc nothrow:
bool isEmpty() {
return units.length == 0;
@ -169,11 +175,6 @@ struct Dialogue {
update();
}
void run(DialogueCommandRunner runner) {
runner(args);
update();
}
// TODO: Remove the asserts!
void update() {
if (units.length != 0 && unitIndex < units.length - 1) {
@ -331,6 +332,8 @@ struct Dialogue {
}
}
@safe @nogc nothrow:
bool isValidDialogueUnitKind(char c) {
foreach (kind; DialogueUnitKindChars) {
if (c == kind) {

View file

@ -24,7 +24,7 @@ public import joka.faults;
public import joka.math;
public import joka.types;
@safe:
@safe @nogc nothrow:
EngineState engineState;
@ -160,7 +160,7 @@ struct DrawOptions {
Hook hook = Hook.topLeft; /// An value representing the origin point of the drawn object when origin is set to zero.
Flip flip = Flip.none; /// An value representing flipping orientations.
@safe:
@safe @nogc nothrow:
/// Initializes the options with the given scale.
this(Vec2 scale) {
@ -196,7 +196,7 @@ struct Camera {
bool isAttached; /// Indicates whether the camera is currently in use.
bool isCentered; /// Determines if the camera's origin is at the center instead of the top left.
@safe:
@safe @nogc nothrow:
/// Initializes the camera with the given position and optional centering.
this(float x, float y, bool isCentered = false) {
@ -210,11 +210,61 @@ struct Camera {
return isCentered ? Hook.center : Hook.topLeft;
}
/// Returns the origin of the camera.
Vec2 origin() {
return Rect(position, resolution).origin(hook);
}
/// Returns the area covered by the camera.
Rect area() {
return Rect(position, resolution).area(hook);
}
/// Returns the top left point of the camera.
Vec2 topLeftPoint() {
return area.topLeftPoint;
}
/// Returns the top point of the camera.
Vec2 topPoint() {
return area.topPoint;
}
/// Returns the top right point of the camera.
Vec2 topRightPoint() {
return area.topRightPoint;
}
/// Returns the left point of the camera.
Vec2 leftPoint() {
return area.leftPoint;
}
/// Returns the center point of the camera.
Vec2 centerPoint() {
return area.centerPoint;
}
/// Returns the right point of the camera.
Vec2 rightPoint() {
return area.rightPoint;
}
/// Returns the bottom left point of the camera.
Vec2 bottomLeftPoint() {
return area.bottomLeftPoint;
}
/// Returns the bottom point of the camera.
Vec2 bottomPoint() {
return area.bottomPoint;
}
/// Returns the bottom right point of the camera.
Vec2 bottomRightPoint() {
return area.bottomRightPoint;
}
/// Moves the camera to follow the target position at the specified speed.
void followPosition(Vec2 target, float speed) {
position = position.moveTo(target, Vec2(speed));
@ -243,7 +293,7 @@ struct Camera {
}
isAttached = true;
auto temp = this.toRl();
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
temp.target.x = floor(temp.target.x);
temp.target.y = floor(temp.target.y);
temp.offset.x = floor(temp.offset.x);
@ -267,7 +317,7 @@ struct TextId {
GenerationalIndex data;
alias data this;
@safe:
@safe @nogc nothrow:
/// Returns the length of the text associated with the resource identifier.
Sz length() {
@ -304,7 +354,7 @@ struct TextId {
struct Texture {
rl.Texture2D data;
@safe:
@safe @nogc nothrow:
/// Checks if the texture is not loaded.
bool isEmpty() {
@ -349,7 +399,7 @@ struct TextureId {
GenerationalIndex data;
alias data this;
@safe:
@safe @nogc nothrow:
/// Returns the width of the texture associated with the resource identifier.
int width() {
@ -398,7 +448,7 @@ struct Font {
int runeSpacing; /// The spacing between individual characters.
int lineSpacing; /// The spacing between lines of text.
@safe:
@safe @nogc nothrow:
/// Checks if the font is not loaded.
bool isEmpty() {
@ -433,7 +483,7 @@ struct FontId {
GenerationalIndex data;
alias data this;
@safe:
@safe @nogc nothrow:
/// Returns the spacing between individual characters of the font associated with the resource identifier.
int runeSpacing() {
@ -480,7 +530,7 @@ struct FontId {
struct Sound {
Variant!(rl.Sound, rl.Music) data;
@safe:
@safe @nogc nothrow:
/// Checks if the sound is not loaded.
bool isEmpty() {
@ -575,7 +625,7 @@ struct SoundId {
GenerationalIndex data;
alias data this;
@safe:
@safe @nogc nothrow:
/// Returns the current playback time of the sound associated with the resource identifier.
float time() {
@ -621,7 +671,7 @@ struct SoundId {
struct Viewport {
rl.RenderTexture2D data;
@safe:
@safe @nogc nothrow:
/// Checks if the viewport is not loaded.
bool isEmpty() {
@ -663,6 +713,7 @@ struct Viewport {
struct EngineFlags {
bool isUpdating;
bool isPixelSnapped;
bool isPixelPerfect;
bool isCursorVisible;
}
@ -678,7 +729,7 @@ struct EngineResourceGroup(T) {
GenerationalList!LStr names;
GenerationalList!Sz tags;
@safe:
@safe @nogc nothrow:
Sz length() {
return data.length;
@ -741,7 +792,7 @@ struct EngineResources {
EngineResourceGroup!Font fonts;
EngineResourceGroup!Sound sounds;
@safe:
@safe @nogc nothrow:
void free(Sz tag = 0) {
texts.free(tag);
@ -757,7 +808,7 @@ struct EngineViewport {
int targetHeight;
alias data this;
@safe:
@safe @nogc nothrow:
bool isLocking() {
return (targetWidth != 0 && targetHeight != 0) && (data.width != targetWidth && data.height != targetHeight);
@ -792,7 +843,7 @@ struct EngineState {
Filter defaultFilter;
Sz tickCount;
@safe:
@safe @nogc nothrow:
void free() {
debug {
@ -1170,8 +1221,9 @@ void openWindow(int width, int height, IStr appPath, IStr title = "Parin") {
/// You should avoid calling this function manually.
@trusted
void updateWindow(bool function(float dt) updateFunc) {
static bool function(float _dt) @trusted _updateFunc;
static bool function(float _dt) @trusted @nogc nothrow _updateFunc;
@trusted @nogc nothrow
static bool _updateWindow() {
// Begin drawing.
if (isResolutionLocked) {
@ -1193,10 +1245,9 @@ void updateWindow(bool function(float dt) updateFunc) {
auto ratio = maxSize / minSize;
auto minRatio = min(ratio.x, ratio.y);
if (isPixelPerfect) {
// TODO: Make an equals function in Joka that can change the epsilon value.
auto roundMinRatio = round(minRatio);
auto floorMinRation = floor(minRatio);
minRatio = (abs(minRatio - roundMinRatio) < 0.015f) ? roundMinRatio : floorMinRation;
minRatio = minRatio.equals(roundMinRatio, 0.015f) ? roundMinRatio : floorMinRation;
}
auto targetSize = minSize * Vec2(minRatio);
@ -1253,7 +1304,7 @@ void updateWindow(bool function(float dt) updateFunc) {
}
// Maybe bad idea, but makes life of no-attribute people easier.
_updateFunc = cast(bool function(float _dt) @trusted) updateFunc;
_updateFunc = cast(bool function(float _dt) @trusted @nogc nothrow) updateFunc;
engineState.flags.isUpdating = true;
version(WebAssembly) {
@ -1284,6 +1335,21 @@ void closeWindow() {
rl.CloseWindow();
}
/// Returns true if the drawing is snapped to pixel coordinates.
bool isPixelSnapped() {
return engineState.flags.isPixelSnapped;
}
/// Sets whether drawing should be snapped to pixel coordinates.
void setIsPixelSnapped(bool value) {
engineState.flags.isPixelSnapped = value;
}
/// Toggles whether drawing is snapped to pixel coordinates on or off.
void toggleIsPixelSnapped() {
setIsPixelSnapped(!isPixelSnapped);
}
/// Returns true if the drawing is done in a pixel perfect way.
bool isPixelPerfect() {
return engineState.flags.isPixelPerfect;
@ -1490,10 +1556,9 @@ Vec2 mouseScreenPosition() {
auto window = windowSize;
auto minRatio = min(window.x / engineState.viewport.width, window.y / engineState.viewport.height);
if (isPixelPerfect) {
// TODO: Make an equals function in Joka that can change the epsilon value.
auto roundMinRatio = round(minRatio);
auto floorMinRation = floor(minRatio);
minRatio = (abs(minRatio - roundMinRatio) < 0.015f) ? roundMinRatio : floorMinRation;
minRatio = minRatio.equals(roundMinRatio, 0.015f) ? roundMinRatio : floorMinRation;
}
auto targetSize = engineState.viewport.size * Vec2(minRatio);
// We use touch because it works on desktop, web and mobile.
@ -1781,7 +1846,7 @@ void updateSound(SoundId sound) {
/// Draws a rectangle with the specified area and color.
@trusted
void drawRect(Rect area, Color color = white) {
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.DrawRectanglePro(area.floor().toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl());
} else {
rl.DrawRectanglePro(area.toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl());
@ -1796,7 +1861,7 @@ void drawVec2(Vec2 point, float size, Color color = white) {
/// Draws a circle with the specified area and color.
@trusted
void drawCirc(Circ area, Color color = white) {
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.DrawCircleV(area.position.floor().toRl(), area.radius, color.toRl());
} else {
rl.DrawCircleV(area.position.toRl(), area.radius, color.toRl());
@ -1806,7 +1871,7 @@ void drawCirc(Circ area, Color color = white) {
/// Draws a line with the specified area, thickness, and color.
@trusted
void drawLine(Line area, float size, Color color = white) {
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.DrawLineEx(area.a.floor().toRl(), area.b.floor().toRl(), size, color.toRl());
} else {
rl.DrawLineEx(area.a.toRl(), area.b.toRl(), size, color.toRl());
@ -1839,7 +1904,7 @@ void drawTextureArea(Texture texture, Rect area, Vec2 position, DrawOptions opti
}
auto origin = options.origin == Vec2() ? target.origin(options.hook) : options.origin;
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.DrawTexturePro(
texture.data,
area.floor().toRl(),
@ -1885,14 +1950,14 @@ void drawRune(Font font, dchar rune, Vec2 position, DrawOptions options = DrawOp
auto rect = toParin(rl.GetGlyphAtlasRec(font.data, rune));
auto origin = options.origin == Vec2() ? rect.origin(options.hook) : options.origin;
rl.rlPushMatrix();
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.rlTranslatef(position.x.floor(), position.y.floor(), 0.0f);
} else {
rl.rlTranslatef(position.x, position.y, 0.0f);
}
rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f);
rl.rlScalef(options.scale.x, options.scale.y, 1.0f);
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.rlTranslatef(-origin.x.floor(), -origin.y.floor(), 0.0f);
} else {
rl.rlTranslatef(-origin.x, -origin.y, 0.0f);
@ -1916,14 +1981,14 @@ void drawText(Font font, IStr text, Vec2 position, DrawOptions options = DrawOpt
// TODO: Make it work with negative scale values.
auto origin = Rect(measureTextSize(font, text)).origin(options.hook);
rl.rlPushMatrix();
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.rlTranslatef(floor(position.x), floor(position.y), 0.0f);
} else {
rl.rlTranslatef(position.x, position.y, 0.0f);
}
rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f);
rl.rlScalef(options.scale.x, options.scale.y, 1.0f);
if (isPixelPerfect) {
if (isPixelSnapped || isPixelPerfect) {
rl.rlTranslatef(floor(-origin.x), floor(-origin.y), 0.0f);
} else {
rl.rlTranslatef(-origin.x, -origin.y, 0.0f);

View file

@ -10,7 +10,6 @@ module parin;
public import joka.ascii;
public import joka.io;
public import parin.dialogue;
public import parin.engine;
public import parin.scene;
public import parin.sprite;

View file

@ -1258,7 +1258,6 @@ void OpenURL (const(char)* url); // Open URL with default system browser (if ava
// NOTE: Following functions implemented in module [utils]
//------------------------------------------------------------------
void TraceLog (int logLevel, const(char)* text, ...); // Show trace log messages (LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERROR...)
void SetTraceLogLevel (int logLevel); // Set the current threshold (minimum) log level
void* MemAlloc (uint size); // Internal memory allocator
void* MemRealloc (void* ptr, uint size); // Internal memory reallocator
@ -1641,7 +1640,6 @@ const(char)* CodepointToUTF8 (int codepoint, int* utf8Size); // Encode one codep
int TextCopy (char* dst, const(char)* src); // Copy one string to another, returns bytes copied
bool TextIsEqual (const(char)* text1, const(char)* text2); // Check if two text string are equal
uint TextLength (const(char)* text); // Get text length, checks for '\0' ending
const(char)* TextFormat (const(char)* text, ...); // Text formatting with variables (sprintf() style)
const(char)* TextSubtext (const(char)* text, int position, int length); // Get a piece of a text string
char* TextReplace (char* text, const(char)* replace, const(char)* by); // Replace text string (WARNING: memory must be freed!)
char* TextInsert (const(char)* text, const(char)* insert, int position); // Insert text in a position (WARNING: memory must be freed!)

View file

@ -14,7 +14,7 @@ module parin.sprite;
import parin.engine;
@safe:
@safe @nogc nothrow:
struct SpriteAnimation {
ubyte frameRow;
@ -28,7 +28,7 @@ struct SpriteAnimationGroup2 {
ubyte frameSpeed = 6;
enum angleStep = 180.0f;
@safe:
@safe @nogc nothrow:
SpriteAnimation pick(float angle) {
auto id = (cast(int) round(snap(angle, angleStep) / angleStep)) % frameRows.length;
@ -42,7 +42,7 @@ struct SpriteAnimationGroup4 {
ubyte frameSpeed = 6;
enum angleStep = 90.0f;
@safe:
@safe @nogc nothrow:
SpriteAnimation pick(float angle) {
// NOTE: This is a hack to make things look better in simple cases.
@ -61,7 +61,7 @@ struct SpriteAnimationGroup8 {
ubyte frameSpeed = 6;
enum angleStep = 45.0f;
@safe:
@safe @nogc nothrow:
SpriteAnimation pick(float angle) {
auto id = (cast(int) round(snap(angle, angleStep) / angleStep)) % frameRows.length;
@ -75,7 +75,7 @@ struct SpriteAnimationGroup16 {
ubyte frameSpeed = 6;
enum angleStep = 22.5f;
@safe:
@safe @nogc nothrow:
SpriteAnimation pick(float angle) {
auto id = (cast(int) round(snap(angle, angleStep) / angleStep)) % frameRows.length;
@ -92,7 +92,7 @@ struct Sprite {
SpriteAnimation animation;
Vec2 position;
@safe:
@safe @nogc nothrow:
this(int width, int height, ushort atlasLeft, ushort atlasTop, SpriteAnimation animation = SpriteAnimation()) {
this.width = width;

View file

@ -21,7 +21,7 @@ public import joka.faults;
public import joka.math;
public import joka.types;
@safe:
@safe @nogc nothrow:
struct Tile {
Sz id;
@ -29,7 +29,7 @@ struct Tile {
int height;
Vec2 position;
@safe:
@safe @nogc nothrow:
Vec2 size() {
return Vec2(width, height);
@ -67,13 +67,13 @@ struct TileMap {
Vec2 position;
alias data this;
@safe:
@safe @nogc nothrow:
this(short value, int tileWidth, int tileHeight) {
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.estimatedMaxRowCount = data.maxRowCount;
this.estimatedMaxColCount = data.maxColCount;
this.estimatedMaxRowCount = maxRowCount;
this.estimatedMaxColCount = maxColCount;
this.data.fill(value);
}
@ -134,7 +134,7 @@ struct TileMap {
position = position.moveToWithSlowdown(target, Vec2(deltaTime), slowdown);
}
/// Returns the top left world position of a tile.
/// Returns the top left world position of a grid position.
Vec2 worldPosition(Sz row, Sz col, DrawOptions options = DrawOptions()) {
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
@ -147,7 +147,12 @@ struct TileMap {
return temp.area(options.hook).position;
}
IVec2 firstMapPosition(Vec2 topLeftWorldPosition, DrawOptions options = DrawOptions()) {
/// Returns the top left world position of a grid position.
Vec2 worldPosition(IVec2 gridPosition, DrawOptions options = DrawOptions()) {
return worldPosition(gridPosition.y, gridPosition.x, options);
}
IVec2 firstGridPosition(Vec2 topLeftWorldPosition, DrawOptions options = DrawOptions()) {
auto result = IVec2();
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
@ -156,7 +161,7 @@ struct TileMap {
return result;
}
IVec2 lastMapPosition(Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
IVec2 lastGridPosition(Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
auto result = IVec2();
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
@ -165,6 +170,37 @@ struct TileMap {
result.x = cast(int) floor(clamp((bottomRightWorldPosition.x - position.x) / targetTileWidth + extraTileCount, 0, estimatedMaxColCount));
return result;
}
auto gridPositions(Vec2 topLeftWorldPosition, Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
static struct Range {
IVec2 first;
IVec2 last;
IVec2 position;
bool empty() {
return position == last;
}
IVec2 front() {
return position;
}
void popFront() {
position.x += 1;
if (position.x >= TileMap.maxColCount) {
position.x = first.x;
position.y += 1;
}
}
}
auto result = Range(
firstGridPosition(topLeftWorldPosition, options),
lastGridPosition(bottomRightWorldPosition, options),
);
result.position = result.first;
return result;
}
}
Result!TileMap toTileMap(IStr csv, int tileWidth, int tileHeight) {
@ -185,6 +221,7 @@ Result!TileMap loadRawTileMap(IStr path, int tileWidth, int tileHeight) {
}
void drawTile(Texture texture, Tile tile, DrawOptions options = DrawOptions()) {
if (texture.isEmpty) return;
drawTextureArea(texture, tile.textureArea(texture.width / tile.width), tile.position, options);
}
@ -195,9 +232,8 @@ void drawTile(TextureId texture, Tile tile, DrawOptions options = DrawOptions())
void drawTileMap(Texture texture, TileMap map, Camera camera, DrawOptions options = DrawOptions()) {
auto targetTileWidth = cast(int) (map.tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (map.tileHeight * options.scale.y);
auto cameraArea = camera.area;
auto colRow1 = map.firstMapPosition(cameraArea.topLeftPoint, options);
auto colRow2 = map.lastMapPosition(cameraArea.bottomRightPoint, options);
auto colRow1 = map.firstGridPosition(camera.topLeftPoint, options);
auto colRow2 = map.lastGridPosition(camera.bottomRightPoint, options);
if (colRow1.x == colRow2.x || colRow1.y == colRow2.y) return;
foreach (row; colRow1.y .. colRow2.y) {

View file

@ -13,7 +13,7 @@ module parin.timer;
import joka.math;
@safe:
@safe @nogc nothrow:
struct Timer {
float time = 1.0f;
@ -22,7 +22,7 @@ struct Timer {
bool isPaused;
bool canRepeat;
@safe:
@safe @nogc nothrow:
this(float duration, bool canRepeat = false) {
this.time = duration;