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