changeset 250:8ef7fb7f14ad

rpg: add support for collisions with actions While here, minimal cleanup in maps.
author David Demelier <markand@malikania.fr>
date Tue, 01 Dec 2020 19:24:11 +0100
parents f4dc208aa1e3
children a6a850e65d23
files doc/docs/specs/map.md examples/assets/maps/map-world.json examples/example-map/main.c libmlk-core/core/maths.c libmlk-rpg/rpg/map-file.c libmlk-rpg/rpg/map-file.h libmlk-rpg/rpg/map.c libmlk-rpg/rpg/map.h tools/map/main.c
diffstat 9 files changed, 204 insertions(+), 137 deletions(-) [+]
line wrap: on
line diff
--- a/doc/docs/specs/map.md	Tue Dec 01 17:18:12 2020 +0100
+++ b/doc/docs/specs/map.md	Tue Dec 01 19:24:11 2020 +0100
@@ -45,7 +45,7 @@
     <int> 
     ...
     layer|actions
-    <int>|<int>|<unsigned int>|<unsigned int>|<const char *>
+    <int>|<int>|<unsigned int>|<unsigned int>|<int>|<const char *>
 
 Description of directives:
 
@@ -72,12 +72,17 @@
     4 columns and 6 the number of tiles must not exceed 24.
 
     For the layer action, defines an object to be implemented in the API. The
-    first 4 integers define the ``x``, ``y``, ``width`` and ``height`` dimensions
-    of the action rectangle respectively. The final string is an argument to pass
-    to the action.
+    first 4 integers define the `x`, `y`, `width`, `height` dimensions of the
+    action rectangle respectively. The fifth integer argument should be set to 1
+    if it must collide with the player or 0 otherwise. The final string is an
+    argument to pass to the action and may be omitted.
 
 ## Examples
 
+Some examples of valid maps.
+
+### Example of basic map
+
 The following file is a map with 4 columns and 2 rows (it is probably small for
 a real game but at least it is readable for this example). The player starts at
 10, 10, the background is using the same tile while the foreground is using
@@ -105,3 +110,18 @@
     0
     3
     3
+
+### Example of actions
+
+!!! note
+    For simplicity, we only focus on the `layer|actions` and assume that the
+    beginning of the map is valid.
+
+This layer of actions contains two blocking regions and one non-blocking action
+that is named "game-over".
+
+    layer|actions
+    100|100|50|50|1
+    200|200|50|50|1
+    1680|2855|48|48|0|game-over
+
--- a/examples/assets/maps/map-world.json	Tue Dec 01 17:18:12 2020 +0100
+++ b/examples/assets/maps/map-world.json	Tue Dec 01 19:24:11 2020 +0100
@@ -44,6 +44,11 @@
                  "name":"",
                  "properties":[
                         {
+                         "name":"block",
+                         "type":"bool",
+                         "value":false
+                        }, 
+                        {
                          "name":"exec",
                          "type":"string",
                          "value":"teleport:map-rebonai"
--- a/examples/example-map/main.c	Tue Dec 01 17:18:12 2020 +0100
+++ b/examples/example-map/main.c	Tue Dec 01 19:24:11 2020 +0100
@@ -44,7 +44,7 @@
 #define W       (1280)
 #define H       (720)
 
-static struct action *load_action(struct map *, int, int, int, int, const char *);
+static void load_action(struct map *, int, int, int, int, const char *);
 
 struct map_state {
 	struct map_file loader;
@@ -192,7 +192,7 @@
 	game.inhibit = INHIBIT_STATE_INPUT;
 
 	teleport_start(&fx->tp);
-	action_stack_add(&current_map->actions, &fx->act);
+	action_stack_add(&current_map->astack_par, &fx->act);
 }
 
 struct teleport_touch {
@@ -260,13 +260,11 @@
 	return &touch->act;
 }
 
-static struct action *
+static void
 load_action(struct map *map, int x, int y, int w, int h, const char *id)
 {
 	if (strncmp(id, "teleport:", 9) == 0)
-		return teleport_touch_new(map, x, y, w, h, id + 9);
-
-	return NULL;
+		action_stack_add(&map->astack_par, teleport_touch_new(map, x, y, w, h, id + 9));
 }
 
 static void
--- a/libmlk-core/core/maths.c	Tue Dec 01 17:18:12 2020 +0100
+++ b/libmlk-core/core/maths.c	Tue Dec 01 19:24:11 2020 +0100
@@ -21,8 +21,8 @@
 bool
 maths_is_boxed(int x, int y, unsigned int w, unsigned int h, int px, int py)
 {
-	return px >= x &&
-	       py >= y &&
-	       px <= x + (int)w &&
-	       py <= y + (int)h;
+	return px > x &&
+	       py > y &&
+	       px < x + (int)w &&
+	       py < y + (int)h;
 }
--- a/libmlk-rpg/rpg/map-file.c	Tue Dec 01 17:18:12 2020 +0100
+++ b/libmlk-rpg/rpg/map-file.c	Tue Dec 01 19:24:11 2020 +0100
@@ -82,21 +82,41 @@
 parse_actions(struct context *ctx)
 {
 	char exec[128 + 1];
-	int x = 0, y = 0;
+	int x = 0, y = 0, block = 0;
 	unsigned int w = 0, h = 0;
 
-	while (fscanf(ctx->fp, "%d|%d|%u|%u|%128[^\n]\n", &x, &y, &w, &h, exec) == 5) {
-		struct action *act;
+	while (fscanf(ctx->fp, "%d|%d|%u|%u|%d|%128[^\n]\n", &x, &y, &w, &h, &block, exec) >= 5) {
+		struct map_block *reg;
 
 		if (!ctx->mf->load_action) {
-			tracef(_("ignoring action %d,%d,%u,%u,%s"), x, y, w, h, exec);
+			tracef(_("ignoring action %d,%d,%u,%u,%d,%s"), x, y, w, h, block, exec);
 			continue;
 		}
 
-		if ((act = ctx->mf->load_action(ctx->map, x, y, w, h, exec)))
-			action_stack_add(&ctx->map->actions, act);
+		ctx->mf->load_action(ctx->map, x, y, w, h, exec);
+
+		/*
+		 * Actions do not have concept of collisions because they are
+		 * not only used on maps. The map structure has its very own
+		 * object to manage collisions but the .map file use the same
+		 * directive for simplicity. So create a block region if the
+		 * directive has one.
+		 */
+		if (block) {
+			if (!(reg = alloc_pool_new(&ctx->mf->blocks)))
+				return false;
+
+			reg->x = x;
+			reg->y = y;
+			reg->w = w;
+			reg->h = h;
+		}
 	}
 
+	/* Reference the blocks array from map_file. */
+	ctx->map->blocks = ctx->mf->blocks.data;
+	ctx->map->blocksz = ctx->mf->blocks.size;
+
 	return true;
 }
 
@@ -257,6 +277,9 @@
 
 	memset(map, 0, sizeof (*map));
 
+	if (!alloc_pool_init(&file->blocks, sizeof (*map->blocks), NULL))
+		return false;
+
 	if (!(ctx.fp = fopen(path, "r")))
 		return errorf("%s", strerror(errno));
 
@@ -280,6 +303,7 @@
 	free(file->layers[2].tiles);
 
 	tileset_file_finish(&file->tileset_file);
+	alloc_pool_finish(&file->blocks);
 
 	memset(file, 0, sizeof (*file));
 }
--- a/libmlk-rpg/rpg/map-file.h	Tue Dec 01 17:18:12 2020 +0100
+++ b/libmlk-rpg/rpg/map-file.h	Tue Dec 01 19:24:11 2020 +0100
@@ -39,6 +39,7 @@
 
 #include <stdbool.h>
 
+#include <core/alloc.h>
 #include <core/sprite.h>
 #include <core/texture.h>
 
@@ -65,32 +66,14 @@
 	/**
 	 * (+?) User function to create an action when it is listed in the map
 	 * definition.
-	 *
-	 * The returned value is owned by the user and is never free'ed from the
-	 * map file itself. If the function is present and return a non-NULL
-	 * action it is added into map.
 	 */
-	struct action *(*load_action)(struct map *map, int x, int y, int w, int h, const char *exec);
-
-	/**
-	 * (*) Map title loaded from file.
-	 */
-	char title[MAP_FILE_TITLE_MAX];
+	void (*load_action)(struct map *map, int x, int y, int w, int h, const char *exec);
 
-	/**
-	 * (*) Map layers stored dynamically here.
-	 */
-	struct map_layer layers[MAP_LAYER_TYPE_NUM];
-
-	/**
-	 * (*) Tileset file loader.
-	 */
-	struct tileset_file tileset_file;
-
-	/**
-	 * (*) Tileset referenced from the map.
-	 */
-	struct tileset tileset;
+	char title[MAP_FILE_TITLE_MAX];                 /*!< \private */
+	struct map_layer layers[MAP_LAYER_TYPE_NUM];    /*!< \private */
+	struct tileset_file tileset_file;               /*!< \private */
+	struct tileset tileset;                         /*!< \private */
+	struct alloc_pool blocks;                       /*!< \private */
 };
 
 /**
--- a/libmlk-rpg/rpg/map.c	Tue Dec 01 17:18:12 2020 +0100
+++ b/libmlk-rpg/rpg/map.c	Tue Dec 01 19:24:11 2020 +0100
@@ -42,7 +42,7 @@
  * SPEED represents the number of pixels it must move per SEC.
  * SEC simply represends the number of milliseconds in one second.
  */
-#define SPEED 220
+#define SPEED 150
 #define SEC   1000
 
 /*
@@ -92,15 +92,16 @@
 	[0xC] = 5
 };
 
-struct collision {
-	int x;
-	int y;
-	unsigned int w;
-	unsigned int h;
-};
-
+/*
+ * Check if this block is usable for collision detection. For example if the
+ * player is moving upwards but the collision shape is below it is unnecessary
+ * to check.
+ */
 static bool
-is_collision_out(const struct map *map, struct collision *block, int drow, int dcol)
+is_block_relevant(const struct map *map,
+                    const struct map_block *block,
+                    int drow,
+                    int dcol)
 {
 	if (drow) {
 		/* Object outside of left-right bounds. */
@@ -125,6 +126,23 @@
 	return true;
 }
 
+/*
+ * Determine if this collision shape is "closer" to the player by checking the
+ * new block coordinates with the previous one.
+ */
+static bool
+is_block_better(const struct map_block *now,
+                const struct map_block *new,
+                int drow,
+                int dcol)
+{
+	return ((drow < 0 && new->y + new->h > now->y + now->h) ||
+	        (drow > 0 && new->y < now->y) ||
+	        (dcol < 0 && new->x + new->w > now->x + now->w) ||
+		(dcol > 0 && new->x < now->x));
+		
+}
+
 static void
 center(struct map *map)
 {
@@ -263,7 +281,7 @@
 
 static void
 find_block_iterate(const struct map *map,
-                   struct collision *block,
+                   struct map_block *block,
                    int rowstart,
                    int rowend,
                    int colstart,
@@ -274,10 +292,11 @@
 	assert(map);
 	assert(block);
 
+	/* First, check with tiledefs. */
 	for (int r = rowstart; r <= rowend; ++r) {
 		for (int c = colstart; c <= colend; ++c) {
 			struct tileset_tiledef *td;
-			struct collision tmp;
+			struct map_block tmp;
 
 			if (!(td = find_tiledef_by_row_column(map, r, c)))
 				continue;
@@ -289,13 +308,10 @@
 			tmp.h = td->h;
 
 			/* This tiledef is out of context. */
-			if (!is_collision_out(map, &tmp, drow, dcol))
+			if (!is_block_relevant(map, &tmp, drow, dcol))
 				continue;
 
-			if ((drow < 0 && tmp.y + tmp.h > block->y + block->h) ||
-			    (drow > 0 && tmp.y < block->y) ||
-			    (dcol < 0 && tmp.x + tmp.w > block->x + block->w) ||
-			    (dcol > 0 && tmp.x < block->x)) {
+			if (is_block_better(block, &tmp, drow, dcol)) {
 				block->x = tmp.x;
 				block->y = tmp.y;
 				block->w = tmp.w;
@@ -303,10 +319,23 @@
 			}
 		}
 	}
+
+	/* Now check if there are objects closer than tiledefs. */
+	for (size_t i = 0; i < map->blocksz; ++i) {
+		const struct map_block *new = &map->blocks[i];
+
+		if (is_block_relevant(map, new, drow, dcol) &&
+		    is_block_better(block, new, drow, dcol)) {
+			block->x = new->x;
+			block->y = new->y;
+			block->w = new->w;
+			block->h = new->h;
+		}
+	}
 }
 
 static void
-find_collision(const struct map *map, struct collision *block, int drow, int dcolumn)
+find_collision(const struct map *map, struct map_block *block, int drow, int dcolumn)
 {
 	assert((drow && !dcolumn) || (dcolumn && !drow));
 
@@ -360,7 +389,7 @@
 static void
 move_x(struct map *map, int delta)
 {
-	struct collision block;
+	struct map_block block;
 
 	find_collision(map, &block, 0, delta < 0 ? -1 : +1);
 
@@ -384,7 +413,7 @@
 static void
 move_y(struct map *map, int delta)
 {
-	struct collision block;
+	struct map_block block;
 
 	find_collision(map, &block, delta < 0 ? -1 : +1, 0);
 
@@ -562,10 +591,48 @@
 {
 	assert(map);
 
-	action_stack_update(&map->actions, ticks);
+	action_stack_update(&map->astack_par, ticks);
+	action_stack_update(&map->astack_seq, ticks);
 
 	tileset_update(map->tileset, ticks);
-	move(map, ticks);
+
+	/* No movements if the sequential actions are running. */
+	if (action_stack_completed(&map->astack_seq))
+		move(map, ticks);
+}
+
+static void
+draw_collide(const struct map *map)
+{
+	struct texture box = {0};
+
+	if (map->flags & MAP_FLAGS_SHOW_COLLIDE && texture_new(&box, 64, 64)) {
+		/* Draw collide box around player if requested. */
+		texture_set_alpha_mod(&box, 100);
+		texture_set_blend_mode(&box, TEXTURE_BLEND_BLEND);
+		PAINTER_BEGIN(&box);
+		painter_set_color(0x4f8fbaff);
+		painter_clear();
+		PAINTER_END();
+		texture_scale(&box, 0, 0, 64, 64,
+		    map->player_x - map->view_x, map->player_y - map->view_y,
+			      map->player_sprite->cellw, map->player_sprite->cellh, 0.f);
+
+		/* Do the same for every objects. */
+		PAINTER_BEGIN(&box);
+		painter_set_color(0xa8ca58ff);
+		painter_clear();
+		PAINTER_END();
+
+		for (size_t i = 0; i < map->blocksz; ++i) {
+			texture_scale(&box, 0, 0, 64, 64,
+			    map->blocks[i].x - map->view_x, map->blocks[i].y - map->view_y,
+			    map->blocks[i].w, map->blocks[i].h,
+			    0.f);
+		}
+
+		texture_finish(&box);
+	}
 }
 
 void
@@ -573,8 +640,6 @@
 {
 	assert(map);
 
-	struct texture box = {0};
-
 	/* Draw the texture about background/foreground. */
 	draw_layer(map, &map->layers[MAP_LAYER_TYPE_BACKGROUND]);
 	draw_layer(map, &map->layers[MAP_LAYER_TYPE_FOREGROUND]);
@@ -586,21 +651,10 @@
 		map->player_y - map->view_y);
 
 	draw_layer(map, &map->layers[MAP_LAYER_TYPE_ABOVE]);
-
-	action_stack_draw(&map->actions);
+	draw_collide(map);
 
-	/* Draw collide box around player if requested. */
-	if (map->flags & MAP_FLAGS_SHOW_COLLIDE &&
-	    texture_new(&box, map->player_sprite->cellw, map->player_sprite->cellh)) {
-		texture_set_alpha_mod(&box, 100);
-		texture_set_blend_mode(&box, TEXTURE_BLEND_BLEND);
-		PAINTER_BEGIN(&box);
-		painter_set_color(0x4f8fbaff);
-		painter_clear();
-		PAINTER_END();
-		texture_draw(&box, map->player_x - map->view_x, map->player_y - map->view_y);
-		texture_finish(&box);
-	}
+	action_stack_draw(&map->astack_par);
+	action_stack_draw(&map->astack_seq);
 }
 
 void
@@ -608,7 +662,8 @@
 {
 	assert(map);
 
-	action_stack_finish(&map->actions);
+	action_stack_finish(&map->astack_par);
+	action_stack_finish(&map->astack_seq);
 
 	memset(map, 0, sizeof (*map));
 }
--- a/libmlk-rpg/rpg/map.h	Tue Dec 01 17:18:12 2020 +0100
+++ b/libmlk-rpg/rpg/map.h	Tue Dec 01 19:24:11 2020 +0100
@@ -61,6 +61,16 @@
 };
 
 /**
+ * \brief Map collision block.
+ */
+struct map_block {
+	int x;                                  /*!< (+) Position in x. */
+	int y;                                  /*!< (+) Position in y. */
+	unsigned int w;                         /*!< (+) Width. */
+	unsigned int h;                         /*!< (+) Height. */
+};
+
+/**
  * \brief Map object.
  *
  * The map object is used to move a player within the map according to the
@@ -80,8 +90,13 @@
 	/* View options. */
 	enum map_flags flags;           /*!< (+) View options. */
 
+	/* Extra collisions blocks. */
+	struct map_block *blocks;       /*!< (+&?) Extra collisions. */
+	size_t blocksz;                 /*!< (+) Number of collisions. */
+
 	/* List of actions. */
-	struct action_stack actions;    /*!< (+) */
+	struct action_stack astack_par; /*!< (+) Parallel actions. */
+	struct action_stack astack_seq; /*!< (+) Blocking actions. */
 
 	/* Player. */
 	struct sprite *player_sprite;   /*!< (+) The sprite to use */
--- a/tools/map/main.c	Tue Dec 01 17:18:12 2020 +0100
+++ b/tools/map/main.c	Tue Dec 01 19:24:11 2020 +0100
@@ -54,43 +54,17 @@
 static const json_t *
 find_property(const json_t *props, const char *which)
 {
-	json_t *value;
+	const json_t *obj;
 	size_t index;
 
-	json_array_foreach(props, index, value) {
-		if (!json_is_object(value))
+	json_array_foreach(props, index, obj) {
+		if (!json_is_object(obj))
 			continue;
 
-		const json_t *key = json_object_get(value, "name");
-
-		if (json_is_string(key) && strcmp(json_string_value(key), which) == 0)
-			return value;
-	}
-
-	return NULL;
-}
-
-static const json_t *
-find_action_exec(const json_t *props)
-{
-	assert(json_is_array(props));
+		const json_t *key = json_object_get(obj, "name");
+		const json_t *value = json_object_get(obj, "value");
 
-	json_t *prop, *name, *value;
-	size_t i;
-
-	json_array_foreach(props, i, prop) {
-		if (!json_is_object(prop))
-			die("invalid property in object\n");
-
-		name = json_object_get(prop, "name");
-		value = json_object_get(prop, "value");
-
-		if (!name || !json_is_string(name))
-			die("invalid 'name' property in object properties");
-		if (!value || !json_is_string(value))
-			die("invalid 'value' property in object properties");
-
-		if (strcmp(json_string_value(name), "exec") == 0)
+		if (json_is_string(key) && value && strcmp(json_string_value(key), which) == 0)
 			return value;
 	}
 
@@ -102,13 +76,8 @@
 {
 	const json_t *prop_title = find_property(props, "title");
 
-	if (!prop_title)
-		return;
-
-	const json_t *title = json_object_get(prop_title, "value");
-
-	if (title && json_is_string(title))
-		printf("title|%s\n", json_string_value(title));
+	if (prop_title && json_is_string(prop_title))
+		printf("title|%s\n", json_string_value(prop_title));
 }
 
 static void
@@ -117,19 +86,13 @@
 	const json_t *prop_origin_x = find_property(props, "origin-x");
 	const json_t *prop_origin_y = find_property(props, "origin-y");
 
-	if (!prop_origin_x || !prop_origin_y)
-		return;
-
-	const json_t *origin_x = json_object_get(prop_origin_x, "value");
-	const json_t *origin_y = json_object_get(prop_origin_y, "value");
-
-	if (!origin_x || !json_is_integer(origin_x) ||
-	    !origin_y || !json_is_integer(origin_y))
+	if (!prop_origin_x || !json_is_integer(prop_origin_x) ||
+	    !prop_origin_y || !json_is_integer(prop_origin_y))
 		return;
 
 	printf("origin|%d|%d\n",
-	    (int)json_integer_value(origin_x),
-	    (int)json_integer_value(origin_y));
+	    (int)json_integer_value(prop_origin_x),
+	    (int)json_integer_value(prop_origin_y));
 }
 
 static void
@@ -164,7 +127,7 @@
 	json_t *width = json_object_get(object, "width");
 	json_t *height = json_object_get(object, "height");
 	json_t *props = json_object_get(object, "properties");
-	const json_t *exec;
+	const json_t *exec, *block;
 
 	if (!x || !json_is_number(x))
 		die("invalid 'x' property in object\n");
@@ -175,16 +138,20 @@
 	if (!height || !json_is_number(height))
 		die("invalid 'height' property in object\n");
 
+	/* This is optional and set to 0 if not present. */
+	block = find_property(props, "block");
+
 	/* In tiled, those properties are float but we only use ints in MA */
-	printf("%d|%d|%d|%d|",
+	printf("%d|%d|%d|%d|%d",
 	    (int)json_integer_value(x),
 	    (int)json_integer_value(y),
 	    (int)json_integer_value(width),
-	    (int)json_integer_value(height)
+	    (int)json_integer_value(height),
+	    (int)json_is_true(block)
 	);
 
-	if ((exec = find_action_exec(props)))
-		printf("%s", json_string_value(exec));
+	if ((exec = find_property(props, "exec")) && json_is_string(exec))
+		printf("|%s", json_string_value(exec));
 
 	printf("\n");
 }