A game engine for top-down 2D RPG games.
rpg
game-engine
raylib
c99
1#include "keraforge/input.h"
2#include "keraforge/sprites.h"
3#include "keraforge/world.h"
4#include <keraforge.h>
5#include <raylib.h>
6#include <raymath.h>
7#include <stdio.h>
8#include <stdlib.h>
9
10
11static Camera2D cam;
12static struct kf_vec2(u32) select = { 0, 0 };
13static int selected_tile = 0;
14static enum {
15 menu_none,
16 menu_palette,
17} menu;
18static int target_fps = 60;
19static enum {
20 modal_play,
21 modal_edit,
22} modal;
23static char *modals[] = { "play", "edit" };
24static bool dirty = false;
25
26static kf_inputbind_t
27 inputbind_move_up,
28 inputbind_move_down,
29 inputbind_move_left,
30 inputbind_move_right,
31 inputbind_ui_up,
32 inputbind_ui_down,
33 inputbind_ui_left,
34 inputbind_ui_right,
35 inputbind_select,
36 inputbind_cancel,
37 inputbind_pause,
38 inputbind_palette,
39 inputbind_zoom_reset,
40 inputbind_zoom_in,
41 inputbind_zoom_out,
42 inputbind_toggle_fps_limit,
43 inputbind_toggle_editor
44;
45
46
47static
48void loadbinds()
49{
50 inputbind_move_up = kf_addinput("move_up", KEY_W, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y);
51 inputbind_move_down = kf_addinput("move_down", KEY_S, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y);
52 inputbind_move_left = kf_addinput("move_left", KEY_A, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X);
53 inputbind_move_right = kf_addinput("move_right", KEY_D, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X);
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:
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 _player_tick_move(self);
133
134 kf_actor_move(world, self, kf_dts);
135}
136
137static
138void _player_draw(struct kf_world *world, struct kf_actor *self)
139{
140 (void)world;
141 kf_actor_draw(self);
142
143 cam.target.x = self->pos.x + (self->size.x / 2);
144 cam.target.y = self->pos.y + (self->size.y / 2);
145}
146
147static
148void draw_palette(int *selected)
149{
150 int px = 80, py = 80;
151 DrawRectangle(px, py, 400, 400, BLACK);
152 DrawText("tiles :3", px + 10, py + 10, 20, WHITE);
153 py += 40;
154 int x = 0, y = 0;
155 int s = KF_TILE_SIZE_PX * 2;
156 for (int i = 1 ; i <= kf_tiles.count ; i++)
157 {
158 Rectangle r = {px + x*s, py + y*s, s, s};
159 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);
160
161 if (*selected == i)
162 DrawRectangleLinesEx(r, 1, GOLD);
163
164 if (CheckCollisionPointRec(GetMousePosition(), r))
165 {
166 DrawRectangleLinesEx(r, 1, WHITE);
167 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT))
168 *selected = i;
169 }
170
171 x += 1;
172 if (x >= 8)
173 {
174 x = 0;
175 y++;
176 }
177 }
178
179 if (kf_checkinputpress(inputbind_cancel))
180 setmenu(menu_none);
181};
182
183
184int main(int argc, const char *argv[])
185{
186 (void)argc;
187 (void)argv;
188
189 // SetTraceLogLevel(LOG_WARNING);
190 InitWindow(800, 600, "Keraforge");
191 SetTargetFPS(target_fps);
192 SetExitKey(KEY_NULL);
193
194 loadbinds();
195
196 struct kf_spritesheet terrain = kf_loadspritesheet("data/res/img/tile/terrain.png", 16, 16);
197 KF_ADDTILE(
198 .key = "grass",
199 .mapcol = GREEN,
200 .sheet = &terrain,
201 .sprite = {0, 0},
202 );
203 KF_ADDTILE(
204 .key = "sand",
205 .mapcol = YELLOW,
206 .sheet = &terrain,
207 .sprite = {4, 0},
208 );
209 KF_ADDTILE(
210 .key = "stone",
211 .mapcol = GRAY,
212 .sheet = &terrain,
213 .sprite = {0, 4},
214 );
215 KF_ADDTILE(
216 .key = "debug",
217 .mapcol = BLUE,
218 .sheet = &terrain,
219 .sprite = {4, 4},
220 );
221 KF_ADDTILE(
222 .key = "brick",
223 .mapcol = RED,
224 .sheet = &terrain,
225 .sprite = {8, 0},
226 .collide = true,
227 );
228 printf("loaded %d tiles\n", kf_tiles.count);
229
230 struct kf_uiconfig *uiconfig = kf_ui_getconfig();
231 uiconfig->select = inputbind_select;
232 uiconfig->cancel = inputbind_cancel;
233 uiconfig->up = inputbind_ui_up;
234 uiconfig->down = inputbind_ui_down;
235
236 if (!DirectoryExists("data"))
237 MakeDirectory("data");
238
239 struct kf_world *world = NULL;
240 if (!kf_exists("data/map.bin"))
241 {
242 printf("-> creating world\n");
243 world = kf_world_new(4096, 4096, 2);
244 printf("-> saving world\n");
245 size_t len = kf_world_getsize(world);
246 printf("-> writing of %lu bytes\n", len);
247 if (!kf_writebin("data/map.bin", (u8 *)world, len))
248 {
249 fprintf(stderr, "error creating map: failed to save map.bin\n");
250 free(world);
251 exit(1);
252 }
253 }
254 else
255 {
256 printf("-> loading world\n");
257 size_t len = 0;
258 world = (struct kf_world *)kf_readbin("data/map.bin", &len);
259 printf("-> world is %lu bytes\n", len);
260 }
261 if (!world)
262 {
263 fprintf(stderr, "error: failed to load world\n");
264 exit(1);
265 }
266
267 struct kf_actor *player = kf_actor_new(kf_actor_loadspritesheet("data/res/img/char/whom.png"), 10, 10, true);
268 player->sizeoffset.y = 6;
269 player->pos.x = world->width / 4.0f * KF_TILE_SIZE_PX;
270 player->pos.y = world->height / 4.0f * KF_TILE_SIZE_PX;
271 player->tick = _player_tick;
272 player->draw = _player_draw;
273 player->controlled = true;
274
275 cam = (Camera2D){0};
276 cam.offset.x = GetScreenWidth() / 2.0f;
277 cam.offset.y = GetScreenHeight() / 2.0f;
278 cam.zoom = 2;
279
280 int running = 1;
281 while (!WindowShouldClose() && running)
282 {
283 if (IsWindowResized())
284 {
285 cam.offset.x = GetScreenWidth() / 2.0f;
286 cam.offset.y = GetScreenHeight() / 2.0f;
287 }
288
289 player->tick(world, player);
290
291 Vector2 v = GetScreenToWorld2D(GetMousePosition(), cam);
292 select.x = v.x / KF_TILE_SIZE_PX;
293 select.y = v.y / KF_TILE_SIZE_PX;
294
295 if (kf_checkinputpress(inputbind_palette) && modal == modal_edit)
296 setmenu(menu_palette);
297 else if (kf_checkinputpress(inputbind_cancel) && menu == menu_none)
298 running = 0;
299 else if (kf_checkinputpress(inputbind_zoom_reset))
300 cam.zoom = 2;
301 else if (kf_checkinputpress(inputbind_zoom_in) && cam.zoom < 3.50f)
302 cam.zoom += 0.25f;
303 else if (kf_checkinputpress(inputbind_zoom_out) && cam.zoom > 1.00f)
304 cam.zoom -= 0.25f;
305 else if (kf_checkinputpress(inputbind_toggle_fps_limit))
306 {
307 target_fps = target_fps <= 0 ? 60 : 0;
308 SetTargetFPS(target_fps);
309 }
310 else if (kf_checkinputpress(inputbind_toggle_editor))
311 {
312 if (modal == modal_edit)
313 modal = modal_play;
314 else
315 {
316 modal = modal_edit;
317 dirty = true;
318 }
319 }
320
321 BeginDrawing();
322 ClearBackground(BLACK);
323
324 BeginMode2D(cam);
325 kf_world_draw(world, cam);
326 // kf_world_drawcolliders(world, player, cam);
327 if (modal == modal_edit && menu == menu_none && select.x < world->width && select.y < world->height)
328 {
329 struct kf_tile *t = kf_world_gettile(world, select.x, select.y);
330 if (IsMouseButtonDown(MOUSE_BUTTON_LEFT))
331 {
332 t->id = (kf_tileid_t)selected_tile;
333 kf_world_updatetile(world, select.x, select.y, true);
334 }
335 else if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT))
336 t->subid = (kf_tileid_t)selected_tile;
337 else if (IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE))
338 selected_tile = t->id;
339
340 DrawRectangleLines(select.x * KF_TILE_SIZE_PX, select.y * KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, WHITE);
341 struct kf_vec2(u32) s = kf_getspritefortilebitmask(t->data);
342 DrawText(TextFormat("%d [0x%04x] (%d,%d) {%d,%d}", t->id, t->data, select.x, select.y, s.x, s.y), select.x * KF_TILE_SIZE_PX, select.y * KF_TILE_SIZE_PX - 10, 10, BLACK);
343 }
344 player->draw(world, player);
345 EndMode2D();
346
347 switch (menu)
348 {
349 case menu_none:
350 break;
351 case menu_palette:
352 draw_palette(&selected_tile);
353 break;
354 }
355
356 DrawFPS(0, 0);
357 DrawText(TextFormat("%f", kf_dts), 0, 20, 20, RED);
358 DrawText(TextFormat("%f", kf_s), 0, 40, 20, RED);
359 DrawText(TextFormat("%s", modals[modal]), 0, 60, 20, ORANGE);
360
361 EndDrawing();
362
363 kf_frame++;
364 kf_dts = GetFrameTime();
365 kf_dtms = kf_dts * 1000;
366 kf_s += kf_dts;
367 }
368
369 if (world)
370 {
371 if (dirty && !kf_writebin("data/map.bin", (u8 *)world, kf_world_getsize(world)))
372 fprintf(stderr, "error: failed to save map.bin\n");
373 free(world);
374 }
375 CloseWindow();
376
377 return 0;
378}