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 }