phantasia

Phantasia - 2D SDL3 RPG prototype.
git clone git://git.beep.wimdupont.com/phantasia.git
Log | Files | Refs | README | LICENSE

world.c (30790B)


      1 #include "engine/world.h"
      2 #include "config.h"
      3 
      4 #include <stdint.h>
      5 #include <limits.h>
      6 #include <stdio.h>
      7 #include <stdlib.h>
      8 #include <math.h>
      9 #include <string.h>
     10 
     11 #if PH_ACCURACY_LUCK_DIVISOR <= 0 || PH_DAMAGE_LUCK_DIVISOR <= 0 || \
     12 	PH_PHYSICAL_DEFENSE_DIVISOR <= 0 || PH_MAGIC_DEFENSE_DIVISOR <= 0
     13 #error "combat formula divisors must be positive"
     14 #endif
     15 
     16 #if PH_WORLD_STEP_MILLISECONDS <= 0 || PH_WORLD_ANIMATION_PIXELS_PER_FRAME <= 0 || \
     17 	PH_WORLD_ACTION_MILLISECONDS <= 0 || \
     18 	PH_MOB_MEMORY_TURNS < 0 || \
     19 	PH_MOB_WANDER_PERCENT < 0 || PH_MOB_WANDER_PERCENT > 100
     20 #error "invalid world movement configuration"
     21 #endif
     22 
     23 typedef struct {
     24 	uint32_t magic;
     25 	uint32_t version;
     26 	uint32_t width;
     27 	uint32_t height;
     28 	uint32_t marker_count;
     29 	char name[PH_AREA_NAME_MAX];
     30 } PhMapHeader;
     31 
     32 typedef struct {
     33 	uint32_t symbol;
     34 	uint32_t tile_x;
     35 	uint32_t tile_y;
     36 } PhMapMarker;
     37 
     38 static float
     39 ph_clampf(float value, float min, float max)
     40 {
     41 	if (value < min) return min;
     42 	if (value > max) return max;
     43 	return value;
     44 }
     45 
     46 static int
     47 ph_clampi(int value, int min, int max)
     48 {
     49 	if (value < min) return min;
     50 	if (value > max) return max;
     51 	return value;
     52 }
     53 
     54 static unsigned int
     55 ph_world_random(PhWorld *world)
     56 {
     57 	world->rng_state = world->rng_state * 1664525u + 1013904223u;
     58 	return world->rng_state;
     59 }
     60 
     61 static float
     62 ph_dist2(PhVec2 a, PhVec2 b)
     63 {
     64 	float dx = a.x - b.x;
     65 	float dy = a.y - b.y;
     66 	return dx * dx + dy * dy;
     67 }
     68 
     69 static int
     70 ph_entity_def_index(const PhWorld *world, int type_id)
     71 {
     72 	int i;
     73 
     74 	for (i = 0; i < world->content.entity_count; ++i)
     75 		if (world->content.entities[i].id == type_id) return i;
     76 	return -1;
     77 }
     78 
     79 static int
     80 ph_item_def_index(const PhWorld *world, int item_id)
     81 {
     82 	int i;
     83 
     84 	for (i = 0; i < world->content.item_count; ++i)
     85 		if (world->content.items[i].id == item_id) return i;
     86 	return -1;
     87 }
     88 
     89 static void
     90 ph_camera_follow_player(PhWorld *world)
     91 {
     92 	const PhEntity *player;
     93 	float max_x;
     94 	float max_y;
     95 
     96 	player = ph_world_player(world);
     97 	if (!player) return;
     98 
     99 	max_x = (float)(world->area.width * PH_TILE_SIZE) - world->camera.viewport_w;
    100 	max_y = (float)(world->area.height * PH_TILE_SIZE) - world->camera.viewport_h;
    101 	if (max_x < 0.0f) max_x = 0.0f;
    102 	if (max_y < 0.0f) max_y = 0.0f;
    103 
    104 	world->camera.pos.x = ph_clampf(player->pos.x - world->camera.viewport_w * 0.5f, 0.0f, max_x);
    105 	world->camera.pos.y = ph_clampf(player->pos.y - world->camera.viewport_h * 0.5f, 0.0f, max_y);
    106 }
    107 
    108 static int
    109 ph_position_blocked(const PhWorld *world, PhVec2 pos)
    110 {
    111 	int tx = (int)floorf(pos.x / (float)PH_TILE_SIZE);
    112 	int ty = (int)floorf(pos.y / (float)PH_TILE_SIZE);
    113 
    114 	return ph_area_tile_blocked(&world->area, tx, ty);
    115 }
    116 
    117 static void
    118 ph_world_set_notice(PhWorld *world, const char *text)
    119 {
    120 	snprintf(world->notice.text, sizeof(world->notice.text), "%s", text);
    121 	world->notice.seconds = PH_NOTICE_SECONDS;
    122 }
    123 
    124 int
    125 ph_world_emit_event(PhWorld *world, PhWorldEventKind kind, PhVec2 pos,
    126 	int entity_index)
    127 {
    128 	int tail;
    129 
    130 	if (!world || kind <= PH_WORLD_EVENT_NONE ||
    131 			world->event_count >= PH_MAX_WORLD_EVENTS) return -1;
    132 	tail = (world->event_head + world->event_count) % PH_MAX_WORLD_EVENTS;
    133 	world->events[tail] = (PhWorldEvent){ kind, pos, entity_index };
    134 	++world->event_count;
    135 	return 0;
    136 }
    137 
    138 int
    139 ph_world_take_event(PhWorld *world, PhWorldEvent *event)
    140 {
    141 	if (!world || !event || world->event_count <= 0) return 0;
    142 	*event = world->events[world->event_head];
    143 	world->event_head = (world->event_head + 1) % PH_MAX_WORLD_EVENTS;
    144 	--world->event_count;
    145 	return 1;
    146 }
    147 
    148 static const PhEntity *
    149 ph_world_blocking_entity(const PhWorld *world, int self_index, PhVec2 pos)
    150 {
    151 	int i;
    152 
    153 	for (i = 0; i < world->entity_count; ++i) {
    154 		const PhEntity *entity = &world->entities[i];
    155 		const PhEntityDef *def;
    156 
    157 		if (i == self_index || !entity->active || entity->area_id != world->area_id) {
    158 			continue;
    159 		}
    160 
    161 		def = ph_world_entity_def(world, entity->type_id);
    162 		if (!def || (!def->blocks_movement && i != world->player_index)) {
    163 			continue;
    164 		}
    165 
    166 		if (ph_dist2(entity->pos, pos) <
    167 				(float)(PH_ENTITY_BLOCK_RADIUS * PH_ENTITY_BLOCK_RADIUS)) {
    168 			return entity;
    169 		}
    170 		if (entity->motion.moving && ph_dist2(entity->motion.target, pos) <
    171 				(float)(PH_ENTITY_BLOCK_RADIUS * PH_ENTITY_BLOCK_RADIUS)) {
    172 			return entity;
    173 		}
    174 	}
    175 
    176 	return NULL;
    177 }
    178 
    179 static int
    180 ph_entity_step_blocked(PhWorld *world, int entity_index, PhVec2 pos)
    181 {
    182 	const PhEntity *blocker;
    183 	const PhEntityDef *mover;
    184 	const PhEntityDef *def;
    185 
    186 	if (ph_position_blocked(world, pos)) {
    187 		int tx = (int)floorf(pos.x / (float)PH_TILE_SIZE);
    188 		int ty = (int)floorf(pos.y / (float)PH_TILE_SIZE);
    189 		const PhTileDef *tile = ph_area_tile_def(&world->area, tx, ty);
    190 
    191 		mover = ph_world_entity_def(world, world->entities[entity_index].type_id);
    192 		if (mover && mover->kind == PH_ENTITY_PLAYER && tile &&
    193 				tile->interaction_notice)
    194 			ph_world_set_notice(world, tile->interaction_notice);
    195 		return 1;
    196 	}
    197 
    198 	blocker = ph_world_blocking_entity(world, entity_index, pos);
    199 	if (!blocker) {
    200 		return 0;
    201 	}
    202 
    203 	mover = ph_world_entity_def(world, world->entities[entity_index].type_id);
    204 	def = ph_world_entity_def(world, blocker->type_id);
    205 	if (mover && def && mover->kind == PH_ENTITY_PLAYER &&
    206 			def->kind == PH_ENTITY_MONSTER) {
    207 		world->encounter_index = (int)(blocker - world->entities);
    208 		return 1;
    209 	}
    210 	if (mover && def && mover->kind == PH_ENTITY_MONSTER &&
    211 			def->kind == PH_ENTITY_PLAYER) {
    212 		world->encounter_index = entity_index;
    213 		return 1;
    214 	}
    215 	if (mover && mover->kind == PH_ENTITY_PLAYER && def && def->name) {
    216 		char text[PH_NOTICE_SIZE];
    217 
    218 		snprintf(text, sizeof(text), "%s blocks your path.", def->name);
    219 		ph_world_set_notice(world, text);
    220 	}
    221 	return 1;
    222 }
    223 
    224 static void
    225 ph_try_pickup(PhWorld *world, PhEntity *entity)
    226 {
    227 	int i;
    228 
    229 	if (!entity || !entity->active) {
    230 		return;
    231 	}
    232 
    233 	for (i = 0; i < world->ground_item_count; ++i) {
    234 		PhGroundItem *drop = &world->ground_items[i];
    235 		int item_index;
    236 
    237 		if (!drop->active || drop->area_id != world->area_id) {
    238 			continue;
    239 		}
    240 		if ((int)floorf(entity->pos.x / PH_TILE_SIZE) !=
    241 				(int)floorf(drop->pos.x / PH_TILE_SIZE) ||
    242 				(int)floorf(entity->pos.y / PH_TILE_SIZE) !=
    243 				(int)floorf(drop->pos.y / PH_TILE_SIZE)) {
    244 			continue;
    245 		}
    246 
    247 		item_index = ph_item_def_index(world, drop->item_id);
    248 		if (item_index >= 0) {
    249 			world->inventory[item_index] += drop->amount;
    250 			ph_world_emit_event(world, PH_WORLD_EVENT_ITEM_PICKUP,
    251 				drop->pos, world->player_index);
    252 		}
    253 		drop->active = 0;
    254 	}
    255 }
    256 
    257 static void
    258 ph_try_interact(PhWorld *world, const PhEntity *player)
    259 {
    260 	PhVec2 target = {
    261 		player->pos.x + (float)(player->motion.facing_x * PH_TILE_SIZE),
    262 		player->pos.y + (float)(player->motion.facing_y * PH_TILE_SIZE),
    263 	};
    264 	float closest = PH_INTERACT_RADIUS * PH_INTERACT_RADIUS;
    265 	PhVec2 interaction_pos = player->pos;
    266 	int i;
    267 
    268 	for (i = 0; i < world->entity_count; ++i) {
    269 		const PhEntity *entity = &world->entities[i];
    270 		const PhEntityDef *def;
    271 		float distance;
    272 
    273 		if (i == world->player_index || !entity->active ||
    274 				entity->area_id != world->area_id) continue;
    275 		def = ph_world_entity_def(world, entity->type_id);
    276 		if (!def || def->interaction_id <= 0) continue;
    277 		distance = ph_dist2(entity->pos, target);
    278 		if (distance > closest) continue;
    279 		closest = distance;
    280 		world->interaction_id = def->interaction_id;
    281 		interaction_pos = entity->pos;
    282 	}
    283 	if (world->interaction_id)
    284 		ph_world_emit_event(world, PH_WORLD_EVENT_INTERACT, interaction_pos,
    285 			world->player_index);
    286 	if (!world->interaction_id) {
    287 		int tx = (int)floorf(target.x / (float)PH_TILE_SIZE);
    288 		int ty = (int)floorf(target.y / (float)PH_TILE_SIZE);
    289 		const PhTileDef *tile = ph_area_tile_def(&world->area, tx, ty);
    290 
    291 		if (tile && tile->interaction_notice)
    292 			ph_world_set_notice(world, tile->interaction_notice);
    293 	}
    294 }
    295 
    296 static PhInput
    297 ph_orthogonal_input(PhInput input)
    298 {
    299 	if (input.move_y != 0) {
    300 		input.move_x = 0;
    301 	}
    302 	return input;
    303 }
    304 
    305 static int
    306 ph_area_line_clear(const PhArea *area, PhVec2 from, PhVec2 to)
    307 {
    308 	int x = (int)floorf(from.x / PH_TILE_SIZE);
    309 	int y = (int)floorf(from.y / PH_TILE_SIZE);
    310 	int end_x = (int)floorf(to.x / PH_TILE_SIZE);
    311 	int end_y = (int)floorf(to.y / PH_TILE_SIZE);
    312 	int dx = abs(end_x - x);
    313 	int dy = -abs(end_y - y);
    314 	int step_x = x < end_x ? 1 : -1;
    315 	int step_y = y < end_y ? 1 : -1;
    316 	int error = dx + dy;
    317 
    318 	for (;;) {
    319 		if (ph_area_tile_blocked(area, x, y)) return 0;
    320 		if (x == end_x && y == end_y) return 1;
    321 		if (error * 2 >= dy) {
    322 			error += dy;
    323 			x += step_x;
    324 		}
    325 		if (error * 2 <= dx) {
    326 			error += dx;
    327 			y += step_y;
    328 		}
    329 	}
    330 }
    331 
    332 static int
    333 ph_actor_sees_player(const PhWorld *world, const PhEntity *actor,
    334 	const PhEntityDef *def)
    335 {
    336 	const PhEntity *player = ph_world_player(world);
    337 	float range = (float)(def->vision * PH_TILE_SIZE);
    338 
    339 	return def->aggressive && def->vision > 0 && player && player->active &&
    340 		ph_dist2(actor->pos, player->pos) <= range * range &&
    341 		ph_area_line_clear(&world->area, actor->pos, player->pos);
    342 }
    343 
    344 static int
    345 ph_entity_begin_step(PhWorld *world, int entity_index, int dx, int dy,
    346 	int stay_in_territory)
    347 {
    348 	PhEntity *entity = &world->entities[entity_index];
    349 	PhVec2 target = {
    350 		entity->pos.x + (float)(dx * PH_TILE_SIZE),
    351 		entity->pos.y + (float)(dy * PH_TILE_SIZE),
    352 	};
    353 	float radius = (float)(entity->behavior.territory_radius * PH_TILE_SIZE);
    354 
    355 	if ((!dx && !dy) || entity->motion.moving) return 0;
    356 	entity->motion.facing_x = dx;
    357 	entity->motion.facing_y = dy;
    358 	if (stay_in_territory && entity->behavior.territory_radius > 0 &&
    359 			ph_dist2(target, entity->behavior.home) > radius * radius) return 0;
    360 	if (ph_entity_step_blocked(world, entity_index, target)) return 0;
    361 	entity->motion.target = target;
    362 	entity->motion.moving = 1;
    363 	ph_world_emit_event(world, PH_WORLD_EVENT_MOVE, entity->pos, entity_index);
    364 	return 1;
    365 }
    366 
    367 static int
    368 ph_entity_step_toward(PhWorld *world, int entity_index, PhVec2 target)
    369 {
    370 	PhEntity *entity = &world->entities[entity_index];
    371 	float dx = target.x - entity->pos.x;
    372 	float dy = target.y - entity->pos.y;
    373 	int sx = dx < 0.0f ? -1 : dx > 0.0f ? 1 : 0;
    374 	int sy = dy < 0.0f ? -1 : dy > 0.0f ? 1 : 0;
    375 
    376 	if (fabsf(dx) >= fabsf(dy)) {
    377 		if (ph_entity_begin_step(world, entity_index, sx, 0, 0)) return 1;
    378 		return ph_entity_begin_step(world, entity_index, 0, sy, 0);
    379 	}
    380 	if (ph_entity_begin_step(world, entity_index, 0, sy, 0)) return 1;
    381 	return ph_entity_begin_step(world, entity_index, sx, 0, 0);
    382 }
    383 
    384 static void
    385 ph_world_actor_action(PhWorld *world, int entity_index)
    386 {
    387 	PhEntity *actor = &world->entities[entity_index];
    388 	const PhEntityDef *def = ph_world_entity_def(world, actor->type_id);
    389 	int direction;
    390 
    391 	if (!def || def->kind == PH_ENTITY_PLAYER || !actor->active || actor->vitals.hp <= 0)
    392 		return;
    393 	if (ph_actor_sees_player(world, actor, def)) {
    394 		actor->behavior.last_seen = ph_world_player(world)->pos;
    395 		actor->behavior.chase_turns = PH_MOB_MEMORY_TURNS;
    396 		ph_entity_step_toward(world, entity_index, actor->behavior.last_seen);
    397 		return;
    398 	}
    399 	if (actor->behavior.chase_turns > 0) {
    400 		--actor->behavior.chase_turns;
    401 		ph_entity_step_toward(world, entity_index, actor->behavior.last_seen);
    402 		return;
    403 	}
    404 	if (actor->behavior.territory_radius > 0) {
    405 		float radius = (float)(actor->behavior.territory_radius * PH_TILE_SIZE);
    406 
    407 		if (ph_dist2(actor->pos, actor->behavior.home) > radius * radius) {
    408 			ph_entity_step_toward(world, entity_index, actor->behavior.home);
    409 			return;
    410 		}
    411 	}
    412 	if ((int)(ph_world_random(world) % 100u) >= PH_MOB_WANDER_PERCENT) return;
    413 	direction = (int)(ph_world_random(world) % 4u);
    414 	ph_entity_begin_step(world, entity_index,
    415 		direction == 0 ? -1 : direction == 1 ? 1 : 0,
    416 		direction == 2 ? -1 : direction == 3 ? 1 : 0, 1);
    417 }
    418 
    419 static void
    420 ph_world_advance_movers(PhWorld *world, float dt)
    421 {
    422 	float step_seconds = (float)PH_WORLD_STEP_MILLISECONDS / 1000.0f;
    423 	int i;
    424 
    425 	for (i = 0; i < world->entity_count; ++i) {
    426 		PhEntity *entity = &world->entities[i];
    427 		const PhEntityDef *def;
    428 		float dx;
    429 		float dy;
    430 		float distance;
    431 		float travel;
    432 		float moved;
    433 		float animation_cycle;
    434 		int animation_frames;
    435 
    436 		if (!entity->active || !entity->motion.moving ||
    437 				entity->area_id != world->area_id) continue;
    438 		def = ph_world_entity_def(world, entity->type_id);
    439 		if (!def || def->move_speed <= 0) continue;
    440 		dx = entity->motion.target.x - entity->pos.x;
    441 		dy = entity->motion.target.y - entity->pos.y;
    442 		distance = sqrtf(dx * dx + dy * dy);
    443 		travel = (float)(PH_TILE_SIZE * def->move_speed) * dt / step_seconds;
    444 		moved = distance < travel ? distance : travel;
    445 		entity->motion.animation_distance += moved;
    446 		animation_frames = def->animation_frames > 0 &&
    447 			def->animation_frames <= PH_ANIMATION_FRAMES ?
    448 			def->animation_frames : 1;
    449 		animation_cycle = (float)(animation_frames *
    450 			PH_WORLD_ANIMATION_PIXELS_PER_FRAME);
    451 		if (entity->motion.animation_distance >= animation_cycle)
    452 			entity->motion.animation_distance = fmodf(entity->motion.animation_distance,
    453 				animation_cycle);
    454 		if (distance <= travel || distance <= 0.001f) {
    455 			entity->pos = entity->motion.target;
    456 			entity->motion.moving = 0;
    457 		} else {
    458 			entity->pos.x += dx / distance * travel;
    459 			entity->pos.y += dy / distance * travel;
    460 		}
    461 	}
    462 }
    463 
    464 static void
    465 ph_world_update_actors(PhWorld *world, float dt)
    466 {
    467 	float action_seconds = (float)PH_WORLD_ACTION_MILLISECONDS / 1000.0f;
    468 	int i;
    469 
    470 	for (i = 0; i < world->entity_count && world->encounter_index < 0; ++i) {
    471 		PhEntity *entity = &world->entities[i];
    472 		const PhEntityDef *def = ph_world_entity_def(world, entity->type_id);
    473 
    474 		if (!def || def->kind == PH_ENTITY_PLAYER || def->move_speed <= 0 ||
    475 				!entity->active || entity->area_id != world->area_id) continue;
    476 		entity->motion.credit += (float)def->move_speed * dt;
    477 		if (entity->motion.credit > action_seconds) entity->motion.credit = action_seconds;
    478 		if (entity->motion.moving || entity->motion.credit < action_seconds) continue;
    479 		entity->motion.credit -= action_seconds;
    480 		ph_world_actor_action(world, i);
    481 	}
    482 }
    483 
    484 int
    485 ph_world_init(PhWorld *world, PhArea area, int area_id,
    486 	PhWorldCatalog content, float viewport_w, float viewport_h)
    487 {
    488 	if (content.entity_count < 0 || content.entity_count > PH_MAX_ENTITY_TYPES ||
    489 			content.item_count < 0 || content.item_count > PH_MAX_ITEM_TYPES ||
    490 			(content.entity_count && !content.entities) ||
    491 			(content.item_count && !content.items)) return -1;
    492 	memset(world, 0, sizeof(*world));
    493 	world->area = area;
    494 	world->area_id = area_id;
    495 	world->content = content;
    496 	world->camera.viewport_w = viewport_w;
    497 	world->camera.viewport_h = viewport_h;
    498 	world->player_index = -1;
    499 	world->encounter_index = -1;
    500 	world->rng_state = 0x5048414eu;
    501 	return 0;
    502 }
    503 
    504 void
    505 ph_world_enter_area(PhWorld *world, PhArea area, int area_id, PhVec2 player_pos)
    506 {
    507 	PhEntity *player;
    508 
    509 	ph_area_free(&world->area);
    510 	world->area = area;
    511 	world->area_id = area_id;
    512 	world->encounter_index = -1;
    513 	world->interaction_id = 0;
    514 	player = world->player_index >= 0 && world->player_index < world->entity_count ?
    515 		&world->entities[world->player_index] : NULL;
    516 	if (!player) return;
    517 	player->area_id = area_id;
    518 	player->pos = player->motion.target = player->behavior.home = player_pos;
    519 	player->motion.moving = 0;
    520 	ph_camera_follow_player(world);
    521 }
    522 
    523 int
    524 ph_area_load(PhArea *area, const char *path)
    525 {
    526 	FILE *fp;
    527 	PhMapHeader header;
    528 	size_t tile_count;
    529 	size_t name_len;
    530 	unsigned char *tiles;
    531 	PhAreaMarker *markers = NULL;
    532 	char *name;
    533 	uint32_t i;
    534 
    535 	memset(area, 0, sizeof(*area));
    536 
    537 	fp = fopen(path, "rb");
    538 	if (!fp) {
    539 		return -1;
    540 	}
    541 
    542 	if (fread(&header, sizeof(header), 1, fp) != 1 ||
    543 			header.magic != PH_MAP_MAGIC ||
    544 			header.version != PH_MAP_VERSION ||
    545 			header.width == 0 ||
    546 			header.height == 0 || header.width > INT_MAX ||
    547 			header.height > INT_MAX || header.marker_count > INT_MAX ||
    548 			(size_t)header.width > SIZE_MAX / header.height) {
    549 		fclose(fp);
    550 		return -1;
    551 	}
    552 
    553 	tile_count = (size_t)header.width * (size_t)header.height;
    554 	if (header.marker_count > tile_count) {
    555 		fclose(fp);
    556 		return -1;
    557 	}
    558 	tiles = malloc(tile_count);
    559 	if (!tiles) {
    560 		fclose(fp);
    561 		return -1;
    562 	}
    563 
    564 	if (fread(tiles, 1, tile_count, fp) != tile_count) {
    565 		free(tiles);
    566 		fclose(fp);
    567 		return -1;
    568 	}
    569 	if (header.marker_count > 0) {
    570 		markers = calloc(header.marker_count, sizeof(*markers));
    571 		if (!markers) {
    572 			free(tiles);
    573 			fclose(fp);
    574 			return -1;
    575 		}
    576 		for (i = 0; i < header.marker_count; ++i) {
    577 			PhMapMarker marker;
    578 
    579 			if (fread(&marker, sizeof(marker), 1, fp) != 1 ||
    580 					marker.symbol > 255 || marker.tile_x >= header.width ||
    581 					marker.tile_y >= header.height) {
    582 				free(markers);
    583 				free(tiles);
    584 				fclose(fp);
    585 				return -1;
    586 			}
    587 			markers[i] = (PhAreaMarker){ (unsigned char)marker.symbol,
    588 				(int)marker.tile_x, (int)marker.tile_y };
    589 		}
    590 	}
    591 	fclose(fp);
    592 
    593 	header.name[PH_AREA_NAME_MAX - 1] = '\0';
    594 	name_len = strlen(header.name) + 1;
    595 	name = malloc(name_len);
    596 	if (!name) {
    597 		free(markers);
    598 		free(tiles);
    599 		return -1;
    600 	}
    601 	memcpy(name, header.name, name_len);
    602 
    603 	area->name = name;
    604 	area->width = (int)header.width;
    605 	area->height = (int)header.height;
    606 	area->tiles = tiles;
    607 	area->markers = markers;
    608 	area->marker_count = (int)header.marker_count;
    609 	area->owns_tiles = 1;
    610 	return 0;
    611 }
    612 
    613 void
    614 ph_area_free(PhArea *area)
    615 {
    616 	if (area->owns_tiles) {
    617 		free(area->tiles);
    618 		free(area->markers);
    619 		free((char *)area->name);
    620 	}
    621 	memset(area, 0, sizeof(*area));
    622 }
    623 
    624 void
    625 ph_world_destroy(PhWorld *world)
    626 {
    627 	int i;
    628 
    629 	if (!world) return;
    630 	ph_area_free(&world->area);
    631 	for (i = 0; i < world->initialized_area_count; ++i) {
    632 		free(world->area_states[i].entities);
    633 		free(world->area_states[i].ground_items);
    634 	}
    635 	free(world->area_states);
    636 	memset(world, 0, sizeof(*world));
    637 }
    638 
    639 int
    640 ph_world_spawn_entity(PhWorld *world, int type_id, PhVec2 pos,
    641 	int territory_radius, int area_id)
    642 {
    643 	PhEntity *entity;
    644 	const PhEntityDef *def;
    645 
    646 	if (world->entity_count >= PH_MAX_ENTITIES) {
    647 		return -1;
    648 	}
    649 
    650 	def = ph_world_entity_def(world, type_id);
    651 	if (!def) {
    652 		return -1;
    653 	}
    654 
    655 	entity = &world->entities[world->entity_count];
    656 	memset(entity, 0, sizeof(*entity));
    657 	entity->type_id = type_id;
    658 	entity->area_id = area_id;
    659 	entity->pos = pos;
    660 	entity->motion.target = pos;
    661 	entity->behavior.home = pos;
    662 	entity->behavior.last_seen = pos;
    663 	entity->vitals.hp = def->stats.max_hp;
    664 	entity->vitals.mp = def->stats.max_mp;
    665 	entity->gold = def->starting_gold;
    666 	entity->motion.facing_x = 0;
    667 	entity->motion.facing_y = 1;
    668 	entity->behavior.territory_radius = territory_radius > 0 ? territory_radius : 0;
    669 	entity->active = 1;
    670 
    671 	return world->entity_count++;
    672 }
    673 
    674 int
    675 ph_world_drop_item(PhWorld *world, int item_id, PhVec2 pos, int amount,
    676 	int area_id)
    677 {
    678 	PhGroundItem *drop = NULL;
    679 	int i;
    680 
    681 	if (amount <= 0 || !ph_world_item_def(world, item_id)) {
    682 		return -1;
    683 	}
    684 	for (i = 0; i < world->ground_item_count; ++i)
    685 		if (!world->ground_items[i].active) {
    686 			drop = &world->ground_items[i];
    687 			break;
    688 		}
    689 	if (!drop) {
    690 		if (world->ground_item_count >= PH_MAX_GROUND_ITEMS) return -1;
    691 		drop = &world->ground_items[world->ground_item_count++];
    692 	}
    693 
    694 	drop->item_id = item_id;
    695 	drop->area_id = area_id;
    696 	drop->pos = pos;
    697 	drop->amount = amount;
    698 	drop->active = 1;
    699 
    700 	return (int)(drop - world->ground_items);
    701 }
    702 
    703 void
    704 ph_world_set_player(PhWorld *world, int entity_index)
    705 {
    706 	if (entity_index >= 0 && entity_index < world->entity_count) {
    707 		world->player_index = entity_index;
    708 		ph_camera_follow_player(world);
    709 	}
    710 }
    711 
    712 int
    713 ph_world_equip_item(PhWorld *world, int entity_index, int item_id)
    714 {
    715 	PhEntity *entity;
    716 	const PhItemDef *item;
    717 	PhStats before;
    718 	PhStats after;
    719 	int slot;
    720 
    721 	if (entity_index < 0 || entity_index >= world->entity_count) return -1;
    722 	item = ph_world_item_def(world, item_id);
    723 	if (!item) return -1;
    724 	slot = (int)item->equip_slot;
    725 	if (slot <= PH_EQUIP_NONE || slot >= PH_EQUIP_COUNT) return -1;
    726 	entity = &world->entities[entity_index];
    727 	before = ph_world_entity_stats(world, entity);
    728 	entity->equipment[slot] = item_id;
    729 	after = ph_world_entity_stats(world, entity);
    730 	entity->vitals.hp += after.max_hp - before.max_hp;
    731 	entity->vitals.mp += after.max_mp - before.max_mp;
    732 	if (entity->vitals.hp < 0) entity->vitals.hp = 0;
    733 	if (entity->vitals.hp > after.max_hp) entity->vitals.hp = after.max_hp;
    734 	if (entity->vitals.mp < 0) entity->vitals.mp = 0;
    735 	if (entity->vitals.mp > after.max_mp) entity->vitals.mp = after.max_mp;
    736 	return 0;
    737 }
    738 
    739 int
    740 ph_world_equip_inventory_item(PhWorld *world, int entity_index, int item_id)
    741 {
    742 	const PhItemDef *item = ph_world_item_def(world, item_id);
    743 	PhEntity *entity;
    744 	int item_index;
    745 	int old_index;
    746 	int old_item;
    747 
    748 	if (!item || entity_index < 0 || entity_index >= world->entity_count) return -1;
    749 	item_index = ph_item_def_index(world, item_id);
    750 	if (world->inventory[item_index] <= 0 || item->equip_slot <= PH_EQUIP_NONE ||
    751 			item->equip_slot >= PH_EQUIP_COUNT) return -1;
    752 	entity = &world->entities[entity_index];
    753 	old_item = entity->equipment[item->equip_slot];
    754 	if (ph_world_equip_item(world, entity_index, item_id) < 0) return -1;
    755 	--world->inventory[item_index];
    756 	old_index = ph_item_def_index(world, old_item);
    757 	if (old_index >= 0) ++world->inventory[old_index];
    758 	return 0;
    759 }
    760 
    761 int
    762 ph_world_use_inventory_item(PhWorld *world, int entity_index, int item_id)
    763 {
    764 	const PhItemDef *item = ph_world_item_def(world, item_id);
    765 	PhEntity *entity;
    766 	PhStats stats;
    767 	int item_index;
    768 
    769 	if (!item || entity_index < 0 || entity_index >= world->entity_count) return -1;
    770 	item_index = ph_item_def_index(world, item_id);
    771 	if (world->inventory[item_index] <= 0) return -1;
    772 	entity = &world->entities[entity_index];
    773 	stats = ph_world_entity_stats(world, entity);
    774 	if (item->use_power <= 0) return -1;
    775 	if (item->use == PH_ITEM_USE_HEAL && entity->vitals.hp < stats.max_hp) {
    776 		entity->vitals.hp += item->use_power;
    777 		if (entity->vitals.hp > stats.max_hp) entity->vitals.hp = stats.max_hp;
    778 	} else if (item->use == PH_ITEM_USE_RESTORE_MP && entity->vitals.mp < stats.max_mp) {
    779 		entity->vitals.mp += item->use_power;
    780 		if (entity->vitals.mp > stats.max_mp) entity->vitals.mp = stats.max_mp;
    781 	} else {
    782 		return -1;
    783 	}
    784 	--world->inventory[item_index];
    785 	return 0;
    786 }
    787 
    788 int
    789 ph_world_drop_inventory_item(PhWorld *world, int entity_index, int item_id)
    790 {
    791 	int item_index;
    792 
    793 	if (entity_index < 0 || entity_index >= world->entity_count) return -1;
    794 	item_index = ph_item_def_index(world, item_id);
    795 	if (item_index < 0 || world->inventory[item_index] <= 0) return -1;
    796 	if (ph_world_drop_item(world, item_id, world->entities[entity_index].pos, 1,
    797 			world->area_id) < 0)
    798 		return -1;
    799 	--world->inventory[item_index];
    800 	ph_world_emit_event(world, PH_WORLD_EVENT_ITEM_DROP,
    801 		world->entities[entity_index].pos, entity_index);
    802 	return 0;
    803 }
    804 
    805 int
    806 ph_world_buy_item(PhWorld *world, int entity_index, int item_id)
    807 {
    808 	PhEntity *entity;
    809 	const PhItemDef *item;
    810 	int item_index;
    811 
    812 	if (entity_index < 0 || entity_index >= world->entity_count) return -1;
    813 	item = ph_world_item_def(world, item_id);
    814 	item_index = ph_item_def_index(world, item_id);
    815 	if (!item || item_index < 0 || item->buy_value <= 0) return -1;
    816 	entity = &world->entities[entity_index];
    817 	if (entity->gold < item->buy_value) return -1;
    818 	entity->gold -= item->buy_value;
    819 	++world->inventory[item_index];
    820 	ph_world_emit_event(world, PH_WORLD_EVENT_BUY, entity->pos, entity_index);
    821 	return 0;
    822 }
    823 
    824 int
    825 ph_world_sell_item(PhWorld *world, int entity_index, int item_id)
    826 {
    827 	PhEntity *entity;
    828 	const PhItemDef *item;
    829 	int item_index;
    830 
    831 	if (entity_index < 0 || entity_index >= world->entity_count) return -1;
    832 	item = ph_world_item_def(world, item_id);
    833 	item_index = ph_item_def_index(world, item_id);
    834 	if (!item || item_index < 0 || item->sell_value <= 0 ||
    835 			world->inventory[item_index] <= 0) return -1;
    836 	entity = &world->entities[entity_index];
    837 	--world->inventory[item_index];
    838 	entity->gold += item->sell_value;
    839 	ph_world_emit_event(world, PH_WORLD_EVENT_SELL, entity->pos, entity_index);
    840 	return 0;
    841 }
    842 
    843 int
    844 ph_world_physical_attack(PhWorld *world, int attacker_index, int target_index)
    845 {
    846 	PhEntity *attacker;
    847 	PhEntity *target;
    848 	PhStats attack;
    849 	PhStats defense;
    850 	int accuracy;
    851 	int chance;
    852 	int damage;
    853 
    854 	if (attacker_index < 0 || attacker_index >= world->entity_count ||
    855 			target_index < 0 || target_index >= world->entity_count) return -1;
    856 	attacker = &world->entities[attacker_index];
    857 	target = &world->entities[target_index];
    858 	if (!attacker->active || !target->active || attacker->vitals.hp <= 0 || target->vitals.hp <= 0)
    859 		return -1;
    860 	attack = ph_world_entity_stats(world, attacker);
    861 	defense = ph_world_entity_stats(world, target);
    862 	accuracy = attack.accuracy * (100 -
    863 		ph_clampi(attacker->accuracy.value, 0, 100)) / 100;
    864 	chance = ph_clampi(accuracy + attack.luck / PH_ACCURACY_LUCK_DIVISOR -
    865 		defense.evasion, PH_HIT_CHANCE_MIN, PH_HIT_CHANCE_MAX);
    866 	if ((int)(ph_world_random(world) % 100u) >= chance) return 0;
    867 	damage = attack.strength + attack.luck / PH_DAMAGE_LUCK_DIVISOR -
    868 		defense.defense / PH_PHYSICAL_DEFENSE_DIVISOR;
    869 	damage = damage > 0 ? damage : 1;
    870 	target->vitals.hp = ph_clampi(target->vitals.hp - damage, 0, defense.max_hp);
    871 	return damage;
    872 }
    873 
    874 int
    875 ph_world_apply_accuracy_penalty(PhWorld *world, int entity_index,
    876 	int percent, int turns)
    877 {
    878 	PhEntity *entity;
    879 
    880 	if (entity_index < 0 || entity_index >= world->entity_count ||
    881 			percent <= 0 || turns <= 0) return -1;
    882 	entity = &world->entities[entity_index];
    883 	if (!entity->active) return -1;
    884 	entity->accuracy.value = ph_clampi(percent, 0, 100);
    885 	entity->accuracy.turns = turns;
    886 	return 0;
    887 }
    888 
    889 void
    890 ph_world_advance_effects(PhWorld *world, int entity_index)
    891 {
    892 	PhEntity *entity;
    893 
    894 	if (entity_index < 0 || entity_index >= world->entity_count) return;
    895 	entity = &world->entities[entity_index];
    896 	if (entity->accuracy.turns > 0 && --entity->accuracy.turns == 0)
    897 		entity->accuracy.value = 0;
    898 }
    899 
    900 int
    901 ph_world_magic_attack(PhWorld *world, int attacker_index, int target_index,
    902 	int mp_cost, int power)
    903 {
    904 	PhEntity *attacker;
    905 	PhEntity *target;
    906 	PhStats defense;
    907 	int damage;
    908 
    909 	if (attacker_index < 0 || attacker_index >= world->entity_count ||
    910 			target_index < 0 || target_index >= world->entity_count ||
    911 			mp_cost < 0 || power < 0) return -1;
    912 	attacker = &world->entities[attacker_index];
    913 	target = &world->entities[target_index];
    914 	if (!attacker->active || !target->active || attacker->vitals.hp <= 0 ||
    915 			target->vitals.hp <= 0 || attacker->vitals.mp < mp_cost) return -1;
    916 	defense = ph_world_entity_stats(world, target);
    917 	damage = ph_world_entity_stats(world, attacker).magic + power -
    918 		defense.magic_defense / PH_MAGIC_DEFENSE_DIVISOR;
    919 	damage = damage > 0 ? damage : 1;
    920 	attacker->vitals.mp -= mp_cost;
    921 	target->vitals.hp = ph_clampi(target->vitals.hp - damage, 0, defense.max_hp);
    922 	return damage;
    923 }
    924 
    925 int
    926 ph_world_finish_encounter(PhWorld *world, int enemy_index)
    927 {
    928 	PhEntity *player;
    929 	PhEntity *enemy;
    930 	const PhEntityDef *def;
    931 	int item_index = -1;
    932 
    933 	if (enemy_index < 0 || enemy_index >= world->entity_count ||
    934 			world->player_index < 0 || world->player_index >= world->entity_count)
    935 		return -1;
    936 	player = &world->entities[world->player_index];
    937 	enemy = &world->entities[enemy_index];
    938 	def = ph_world_entity_def(world, enemy->type_id);
    939 	if (!def || def->kind != PH_ENTITY_MONSTER || enemy->vitals.hp > 0) return -1;
    940 	if (def->reward.item_id && def->reward.amount > 0) {
    941 		item_index = ph_item_def_index(world, def->reward.item_id);
    942 		if (item_index < 0) return -1;
    943 	}
    944 	player->xp += def->reward.xp;
    945 	if (item_index >= 0) world->inventory[item_index] += def->reward.amount;
    946 	enemy->active = 0;
    947 	world->encounter_index = -1;
    948 	world->combat_active = 0;
    949 	return 0;
    950 }
    951 
    952 void
    953 ph_world_tick(PhWorld *world, PhInput input, float dt)
    954 {
    955 	PhEntity *player;
    956 	const PhEntityDef *def;
    957 	int was_moving;
    958 
    959 	if (world->player_index < 0 || world->player_index >= world->entity_count) {
    960 		return;
    961 	}
    962 
    963 	player = &world->entities[world->player_index];
    964 	def = ph_world_entity_def(world, player->type_id);
    965 	if (!player->active || !def) {
    966 		return;
    967 	}
    968 	if (world->notice.seconds > 0.0f) {
    969 		world->notice.seconds -= dt;
    970 		if (world->notice.seconds <= 0.0f) {
    971 			world->notice.seconds = 0.0f;
    972 			world->notice.text[0] = '\0';
    973 		}
    974 	}
    975 
    976 	input = ph_orthogonal_input(input);
    977 	world->interaction_id = 0;
    978 	was_moving = player->motion.moving;
    979 	if (!player->motion.moving && def->move_speed > 0)
    980 		ph_entity_begin_step(world, world->player_index,
    981 			input.move_x, input.move_y, 0);
    982 	ph_world_advance_movers(world, dt);
    983 	if (was_moving && !player->motion.moving && world->encounter_index < 0 &&
    984 			def->move_speed > 0)
    985 		ph_entity_begin_step(world, world->player_index,
    986 			input.move_x, input.move_y, 0);
    987 
    988 	if (input.interact) {
    989 		ph_try_pickup(world, player);
    990 		ph_try_interact(world, player);
    991 	}
    992 
    993 	ph_world_update_actors(world, dt);
    994 	ph_camera_follow_player(world);
    995 }
    996 
    997 int
    998 ph_entity_animation_frame(const PhEntityDef *def, const PhEntity *entity)
    999 {
   1000 	int frames;
   1001 
   1002 	if (!def || !entity || !entity->motion.moving) return 0;
   1003 	frames = def->animation_frames > 0 &&
   1004 		def->animation_frames <= PH_ANIMATION_FRAMES ? def->animation_frames : 1;
   1005 	return (int)(entity->motion.animation_distance /
   1006 		PH_WORLD_ANIMATION_PIXELS_PER_FRAME) % frames;
   1007 }
   1008 
   1009 const PhEntity *
   1010 ph_world_player(const PhWorld *world)
   1011 {
   1012 	if (world->player_index < 0 || world->player_index >= world->entity_count) {
   1013 		return NULL;
   1014 	}
   1015 	return &world->entities[world->player_index];
   1016 }
   1017 
   1018 const PhEntityDef *
   1019 ph_world_entity_def(const PhWorld *world, int type_id)
   1020 {
   1021 	int i = ph_entity_def_index(world, type_id);
   1022 	return i >= 0 ? &world->content.entities[i] : NULL;
   1023 }
   1024 
   1025 const PhItemDef *
   1026 ph_world_item_def(const PhWorld *world, int item_id)
   1027 {
   1028 	int i = ph_item_def_index(world, item_id);
   1029 	return i >= 0 ? &world->content.items[i] : NULL;
   1030 }
   1031 
   1032 int
   1033 ph_world_item_amount(const PhWorld *world, int item_id)
   1034 {
   1035 	int i = ph_item_def_index(world, item_id);
   1036 	return i >= 0 ? world->inventory[i] : 0;
   1037 }
   1038 
   1039 PhStats
   1040 ph_world_entity_stats(const PhWorld *world, const PhEntity *entity)
   1041 {
   1042 	const PhEntityDef *def;
   1043 	PhStats stats = { 0 };
   1044 	int slot;
   1045 
   1046 	if (!entity || !(def = ph_world_entity_def(world, entity->type_id))) return stats;
   1047 	stats = def->stats;
   1048 	for (slot = PH_EQUIP_WEAPON; slot < PH_EQUIP_COUNT; ++slot) {
   1049 		const PhItemDef *item = ph_world_item_def(world, entity->equipment[slot]);
   1050 		PhStats bonus;
   1051 
   1052 		if (!item) continue;
   1053 		bonus = item->bonuses;
   1054 		stats.max_hp += bonus.max_hp;
   1055 		stats.max_mp += bonus.max_mp;
   1056 		stats.strength += bonus.strength;
   1057 		stats.defense += bonus.defense;
   1058 		stats.magic += bonus.magic;
   1059 		stats.magic_defense += bonus.magic_defense;
   1060 		stats.agility += bonus.agility;
   1061 		stats.luck += bonus.luck;
   1062 		stats.evasion += bonus.evasion;
   1063 		stats.accuracy += bonus.accuracy;
   1064 	}
   1065 	return stats;
   1066 }
   1067 
   1068 const PhTileDef *
   1069 ph_area_tile_def(const PhArea *area, int tx, int ty)
   1070 {
   1071 	unsigned char tile;
   1072 
   1073 	if (tx < 0 || ty < 0 || tx >= area->width || ty >= area->height) {
   1074 		return NULL;
   1075 	}
   1076 	tile = area->tiles[(size_t)ty * (size_t)area->width + (size_t)tx];
   1077 	return tile < area->tile_def_count ? &area->tile_defs[tile] : NULL;
   1078 }
   1079 
   1080 int
   1081 ph_area_tile_blocked(const PhArea *area, int tx, int ty)
   1082 {
   1083 	const PhTileDef *def = ph_area_tile_def(area, tx, ty);
   1084 	return !def || def->blocks_movement;
   1085 }