A game engine for top-down 2D RPG games.
rpg
game-engine
raylib
c99
1#include <keraforge.h>
2#include <raylib.h>
3#include <raymath.h>
4#include <stdio.h>
5#include <stdlib.h>
6
7
8static Camera2D cam;
9static struct kf_vec2(u32) select = { 0, 0 };
10static int selected_tile = 0;
11static enum {
12 menu_none,
13 menu_palette,
14} menu;
15static int target_fps = 60;
16static enum {
17 modal_play,
18 modal_edit,
19} modal;
20static char *modals[] = { "play", "edit" };
21static bool dirty = false;
22static bool preserve_mapdotbin = false;
23
24static kf_inputbind_t
25 inputbind_move_up,
26 inputbind_move_down,
27 inputbind_move_left,
28 inputbind_move_right,
29 inputbind_run,
30 inputbind_ui_up,
31 inputbind_ui_down,
32 inputbind_ui_left,
33 inputbind_ui_right,
34 inputbind_select,
35 inputbind_cancel,
36 inputbind_pause,
37 inputbind_palette,
38 inputbind_zoom_reset,
39 inputbind_zoom_in,
40 inputbind_zoom_out,
41 inputbind_toggle_fps_limit,
42 inputbind_toggle_editor
43;
44
45
46static
47void loadbinds()
48{
49 inputbind_move_up = kf_addinput("move_up", KEY_W, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y);
50 inputbind_move_down = kf_addinput("move_down", KEY_S, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y);
51 inputbind_move_left = kf_addinput("move_left", KEY_A, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X);
52 inputbind_move_right = kf_addinput("move_right", KEY_D, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X);
53 inputbind_run = kf_addinput("run", KEY_LEFT_SHIFT, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, GAMEPAD_AXIS_UNKNOWN);
54
55 inputbind_ui_up = kf_addinput("ui_up", KEY_W, KEY_UP, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_UP, GAMEPAD_AXIS_UNKNOWN);
56 inputbind_ui_down = kf_addinput("ui_down", KEY_S, KEY_DOWN, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_DOWN, GAMEPAD_AXIS_UNKNOWN);
57 inputbind_ui_left = kf_addinput("ui_left", KEY_A, KEY_LEFT, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_LEFT, GAMEPAD_AXIS_UNKNOWN);
58 inputbind_ui_right = kf_addinput("ui_right", KEY_D, KEY_RIGHT, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_RIGHT, GAMEPAD_AXIS_UNKNOWN);
59
60 inputbind_select = kf_addinput("select", KEY_E, KEY_ENTER, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_DOWN, GAMEPAD_AXIS_UNKNOWN);
61 inputbind_cancel = kf_addinput("cancel", KEY_Q, KEY_ESCAPE, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, GAMEPAD_AXIS_UNKNOWN);
62
63 inputbind_pause = kf_addinput("pause", KEY_ESCAPE, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_MIDDLE_RIGHT, GAMEPAD_AXIS_UNKNOWN);
64 inputbind_palette = kf_addinput("palette", KEY_TAB, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_UP, GAMEPAD_AXIS_UNKNOWN);
65
66 inputbind_zoom_reset = kf_addinput("zoom_reset", KEY_ZERO, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN);
67 inputbind_zoom_in = kf_addinput("zoom_in", KEY_EQUAL, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN);
68 inputbind_zoom_out = kf_addinput("zoom_out", KEY_MINUS, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN);
69
70 inputbind_toggle_fps_limit = kf_addinput("toggle_fps_limit", KEY_NINE, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN);
71 inputbind_toggle_editor = kf_addinput("toggle_editor", KEY_EIGHT, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN);
72}
73
74static
75void setmenu(int m)
76{
77 menu = m;
78}
79
80static
81void _player_tick_move(struct kf_actor *self)
82{
83 struct kf_vec2(f32) v = {0, 0};
84
85 /* gamepad axis movement */
86 f32 gpx = kf_getgamepadaxis(inputbind_move_left);
87 f32 gpy = kf_getgamepadaxis(inputbind_move_up);
88 if (gpx > kf_deadzone || gpx < -kf_deadzone || gpy > kf_deadzone || gpy < -kf_deadzone)
89 {
90 v.y = gpy;
91 v.x = gpx;
92
93 f32 angle = Vector2LineAngle(Vector2Zero(), (Vector2){gpx, gpy}) * RAD2DEG;
94 angle /= 90;
95 switch ((int)roundf(angle))
96 {
97 case 0: self->pointing = kf_east; break;
98 case 1: self->pointing = kf_north; break;
99 case -2: /* fallthrough */
100 case 2: self->pointing = kf_west; break;
101 case -1: self->pointing = kf_south; break;
102 }
103
104 goto done;
105 }
106
107 /* non-axis movement */
108 bool w = kf_checkinputdown(inputbind_move_up);
109 bool s = kf_checkinputdown(inputbind_move_down);
110 bool a = kf_checkinputdown(inputbind_move_left);
111 bool d = kf_checkinputdown(inputbind_move_right);
112
113 if (a && d) { v.x = 0; }
114 else if (a) { v.x = -1; self->pointing = kf_west; }
115 else if (d) { v.x = 1; self->pointing = kf_east; }
116
117 if (w && s) { v.y = 0; }
118 else if (w) { v.y = -1; self->pointing = kf_north; }
119 else if (s) { v.y = 1; self->pointing = kf_south; }
120
121 v = kf_normalize_vec2(f32)(v);
122
123done:
124 if (v.x || v.y)
125 kf_actor_addforce(self, v);
126}
127
128static
129void _player_tick(struct kf_world *world, struct kf_actor *self)
130{
131 if (menu == menu_none)
132 {
133 _player_tick_move(self);
134
135 if (kf_checkinputpress(inputbind_run))
136 {
137 self->running = true;
138 self->speedmod = 1.5;
139 }
140 else if (kf_checkinputrelease(inputbind_run))
141 {
142 self->running = false;
143 self->speedmod = 1;
144 }
145 }
146
147 kf_actor_move(world, self, kf_dts);
148}
149
150static
151void _player_draw(struct kf_world *world, struct kf_actor *self)
152{
153 (void)world;
154 kf_actor_draw(self);
155
156 cam.target.x = self->pos.x + (self->size.x / 2);
157 cam.target.y = self->pos.y + (self->size.y / 2);
158}
159
160static
161void draw_palette(int *selected)
162{
163 int px = 80, py = 80;
164 DrawRectangle(px, py, 400, 400, BLACK);
165 DrawText("tiles :3", px + 10, py + 10, 20, WHITE);
166 py += 40;
167 int x = 0, y = 0;
168 int s = KF_TILE_SIZE_PX * 2;
169 for (int i = 1 ; i <= kf_tiles.count ; i++)
170 {
171 Rectangle r = {px + x*s, py + y*s, s, s};
172 kf_drawsprite_wh(kf_tiles.sheet[i], r.x, r.y, r.width, r.height, kf_tiles.sprite[i].x + 0, kf_tiles.sprite[i].y + 3);
173
174 if (*selected == i)
175 DrawRectangleLinesEx(r, 1, GOLD);
176
177 if (CheckCollisionPointRec(GetMousePosition(), r))
178 {
179 DrawRectangleLinesEx(r, 1, WHITE);
180 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT))
181 *selected = i;
182 }
183
184 x += 1;
185 if (x >= 8)
186 {
187 x = 0;
188 y++;
189 }
190 }
191
192 if (kf_checkinputpress(inputbind_cancel))
193 setmenu(menu_none);
194};
195
196
197int main(int argc, const char *argv[])
198{
199 (void)argc;
200 (void)argv;
201
202 SetTraceLogLevel(LOG_WARNING);
203 InitWindow(800, 600, "Keraforge");
204 SetTargetFPS(target_fps);
205 SetExitKey(KEY_NULL);
206
207 loadbinds();
208
209 struct kf_spritesheet terrain = kf_loadspritesheet("data/res/img/tile/terrain.png", 16, 16);
210 KF_ADDTILE(
211 .key = "grass",
212 .mapcol = GREEN,
213 .sheet = &terrain,
214 .sprite = {0, 0},
215 );
216 KF_ADDTILE(
217 .key = "sand",
218 .mapcol = YELLOW,
219 .sheet = &terrain,
220 .sprite = {4, 0},
221 );
222 KF_ADDTILE(
223 .key = "stone",
224 .mapcol = GRAY,
225 .sheet = &terrain,
226 .sprite = {0, 4},
227 );
228 KF_ADDTILE(
229 .key = "debug",
230 .mapcol = BLUE,
231 .sheet = &terrain,
232 .sprite = {4, 4},
233 );
234 KF_ADDTILE(
235 .key = "brick",
236 .mapcol = RED,
237 .sheet = &terrain,
238 .sprite = {8, 0},
239 .collide = true,
240 );
241 KF_ADDTILE(
242 .key = "ice",
243 .mapcol = BLUE,
244 .sheet = &terrain,
245 .sprite = {8, 4},
246 .transparent = true,
247 );
248 KF_ADDTILE(
249 .key = "dirt",
250 .mapcol = BROWN,
251 .sheet = &terrain,
252 .sprite = {12, 0},
253 );
254 kf_logdbg("loaded %d tiles", kf_tiles.count);
255
256 struct kf_uiconfig *uiconfig = kf_ui_getconfig();
257 uiconfig->select = inputbind_select;
258 uiconfig->cancel = inputbind_cancel;
259 uiconfig->up = inputbind_ui_up;
260 uiconfig->down = inputbind_ui_down;
261
262 if (!DirectoryExists("data"))
263 MakeDirectory("data");
264
265 struct kf_world *world = NULL;
266 kf_world_load(&world, true);
267 if (!world)
268 KF_THROW("failed to load world: %p", world);
269
270 struct kf_actor *player = kf_actor_new(kf_actor_loadspritesheet("data/res/img/char/template.png"), 10, 10, true);
271 player->sizeoffset.y = 6;
272 player->pos.x = world->width / 4.0f * KF_TILE_SIZE_PX;
273 player->pos.y = world->height / 4.0f * KF_TILE_SIZE_PX;
274 player->tick = _player_tick;
275 player->draw = _player_draw;
276 player->controlled = true;
277
278 cam = (Camera2D){0};
279 cam.offset.x = GetScreenWidth() / 2.0f;
280 cam.offset.y = GetScreenHeight() / 2.0f;
281 cam.zoom = 2;
282
283 int running = 1;
284 while (!WindowShouldClose() && running)
285 {
286 if (IsWindowResized())
287 {
288 cam.offset.x = GetScreenWidth() / 2.0f;
289 cam.offset.y = GetScreenHeight() / 2.0f;
290 }
291
292 player->tick(world, player);
293
294 Vector2 v = GetScreenToWorld2D(GetMousePosition(), cam);
295 select.x = v.x / KF_TILE_SIZE_PX;
296 select.y = v.y / KF_TILE_SIZE_PX;
297
298 if (kf_checkinputpress(inputbind_palette) && modal == modal_edit)
299 setmenu(menu_palette);
300 else if (kf_checkinputpress(inputbind_cancel) && menu == menu_none)
301 running = 0;
302 else if (kf_checkinputpress(inputbind_zoom_reset))
303 cam.zoom = 2;
304 else if (kf_checkinputpress(inputbind_zoom_in) && cam.zoom < 3.50f)
305 cam.zoom += 0.25f;
306 else if (kf_checkinputpress(inputbind_zoom_out) && cam.zoom > 1.00f)
307 cam.zoom -= 0.25f;
308 else if (kf_checkinputpress(inputbind_toggle_fps_limit))
309 {
310 target_fps = target_fps <= 0 ? 60 : 0;
311 SetTargetFPS(target_fps);
312 }
313 else if (kf_checkinputpress(inputbind_toggle_editor))
314 {
315 if (modal == modal_edit)
316 modal = modal_play;
317 else
318 {
319 modal = modal_edit;
320 dirty = true;
321 }
322 }
323
324 BeginDrawing();
325 ClearBackground(BLACK);
326
327 BeginMode2D(cam);
328 kf_world_draw(world, cam);
329 // kf_world_drawcolliders(world, player, cam);
330 if (modal == modal_edit && menu == menu_none && select.x < world->width && select.y < world->height)
331 {
332 struct kf_tile *t = kf_world_gettile(world, select.x, select.y);
333 if (IsMouseButtonDown(MOUSE_BUTTON_LEFT))
334 {
335 t->id = (kf_tileid_t)selected_tile;
336 kf_world_updatetile(world, select.x, select.y, true);
337 }
338 else if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT))
339 t->subid = (kf_tileid_t)selected_tile;
340 else if (IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE))
341 selected_tile = t->id;
342
343 DrawRectangleLines(select.x * KF_TILE_SIZE_PX, select.y * KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, WHITE);
344 DrawText(TextFormat("%d [%d] (%d,%d)", t->id, t->data, select.x, select.y), select.x * KF_TILE_SIZE_PX, select.y * KF_TILE_SIZE_PX - 10, 10, BLACK);
345 }
346 player->draw(world, player);
347 EndMode2D();
348
349 switch (menu)
350 {
351 case menu_none:
352 break;
353 case menu_palette:
354 draw_palette(&selected_tile);
355 break;
356 }
357
358 DrawFPS(0, 0);
359 DrawText(TextFormat("%f", kf_dts), 0, 20, 20, RED);
360 DrawText(TextFormat("%f", kf_s), 0, 40, 20, RED);
361 DrawText(TextFormat("%s", modals[modal]), 0, 60, 20, ORANGE);
362
363 EndDrawing();
364
365 kf_frame++;
366 kf_dts = GetFrameTime();
367 kf_dtms = kf_dts * 1000;
368 kf_s += kf_dts;
369 }
370
371 if (world)
372 {
373 if (dirty)
374 {
375 if (!kf_writebin("data/tmp/map.bin", (u8 *)world, kf_world_getsize(world)))
376 KF_THROW("failed to save map.bin");
377 if (!kf_compress("data/tmp/map.bin", "data/map.bin.xz"))
378 KF_THROW("failed to compress map.bin into map.bin.xz");
379 if (!preserve_mapdotbin)
380 remove("data/tmp/map.bin");
381 }
382 free(world);
383 }
384 CloseWindow();
385
386 return 0;
387}