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 }