phantasia

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

world.c (29931B)


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