smoke.c (28589B)
1 #include "engine/area.h" 2 #include "engine/world.h" 3 #include "engine/battle.h" 4 #include "engine/audio.h" 5 #include "engine/save.h" 6 #include "game/content.h" 7 #include "config.h" 8 9 #include <stdio.h> 10 #include <limits.h> 11 #include <string.h> 12 13 #define LEN(a) (sizeof(a) / sizeof(0[a])) 14 15 static int 16 ph_event_overflow_test(void) 17 { 18 PhWorld world = { 0 }; 19 PhWorldEvent event; 20 int i; 21 22 for (i = 0; i < PH_MAX_WORLD_EVENTS; ++i) 23 if (ph_world_emit_event(&world, PH_WORLD_EVENT_MOVE, (PhVec2){ 0 }, i) < 0) return 1; 24 if (world.event_dropped || ph_world_emit_event(&world, PH_WORLD_EVENT_MOVE, 25 (PhVec2){ 0 }, -1) != -1 || world.event_dropped != 1) return 1; 26 for (i = 0; i < PH_MAX_WORLD_EVENTS; ++i) 27 if (!ph_world_take_event(&world, &event) || event.entity_index != i) return 1; 28 if (world.event_dropped != 1 || ph_world_take_event(&world, &event)) return 1; 29 if (ph_world_emit_event(&world, PH_WORLD_EVENT_MOVE, (PhVec2){ 0 }, 17) < 0 || 30 !ph_world_take_event(&world, &event) || event.entity_index != 17) return 1; 31 world.event_count = PH_MAX_WORLD_EVENTS; 32 world.event_dropped = UINT_MAX; 33 ph_world_emit_event(&world, PH_WORLD_EVENT_MOVE, (PhVec2){ 0 }, -1); 34 return world.event_dropped != UINT_MAX; 35 } 36 37 static unsigned int 38 ph_world_event_mask(PhWorld *world) 39 { 40 PhWorldEvent event; 41 unsigned int mask = 0; 42 43 while (ph_world_take_event(world, &event)) mask |= 1u << event.kind; 44 return mask; 45 } 46 47 static int 48 ph_area_catalog_test(void) 49 { 50 unsigned char tiles[] = { 0, 0, 0, 0 }; 51 PhAreaMarker markers[] = { 52 { '@', 0, 0 }, 53 { 'a', 1, 0 }, 54 { '1', 0, 1 }, 55 }; 56 const PhMarkerDef marker_defs[] = { 57 { 7, '@', '.', PH_MARKER_PLAYER, 10, 0, 0, 0, 0 }, 58 { 7, 'a', '.', PH_MARKER_ENTITY, 20, 0, 3, 0, 0 }, 59 { 7, '1', '.', PH_MARKER_ITEM, 30, 2, 0, 0, 0 }, 60 }; 61 const PhAreaCatalog catalog = { 62 .markers = marker_defs, 63 .marker_count = (int)LEN(marker_defs), 64 }; 65 const PhEntityDef entity_defs[] = { 66 { .id = 10, .stats = { .max_hp = 10 }, .kind = PH_ENTITY_PLAYER }, 67 { .id = 20, .stats = { .max_hp = 5 }, .kind = PH_ENTITY_MONSTER }, 68 }; 69 const PhItemDef item_defs[] = { { .id = 30 } }; 70 const PhWorldCatalog content = { 71 .entities = entity_defs, 72 .items = item_defs, 73 .entity_count = (int)LEN(entity_defs), 74 .item_count = (int)LEN(item_defs), 75 }; 76 PhArea area = { 77 .name = "Catalog", 78 .width = 2, 79 .height = 2, 80 .tiles = tiles, 81 .markers = markers, 82 .marker_count = (int)LEN(markers), 83 }; 84 PhWorld world; 85 int player = -1; 86 int entity_count; 87 int item_count; 88 89 if (ph_world_init(&world, area, 7, content) < 0 || 90 ph_world_prepare_area(&world, &area, &catalog, 7, &player) < 0) 91 return 1; 92 if (player < 0 || world.entity_count != 2 || world.ground_item_count != 1 || 93 world.entities[1].behavior.territory_radius != 3 || 94 world.ground_items[0].amount != 2 || world.initialized_area_count != 1) 95 return 1; 96 entity_count = world.entity_count; 97 item_count = world.ground_item_count; 98 if (ph_world_prepare_area(&world, &area, &catalog, 7, &player) < 0 || 99 world.entity_count != entity_count || world.ground_item_count != item_count) 100 return 1; 101 ph_world_destroy(&world); 102 return 0; 103 } 104 105 static int 106 ph_movement_test(int open, int blocked) 107 { 108 enum { 109 MAP_W = 9, 110 MAP_H = 5, 111 ROW = 2, 112 PLAYER_COL = 1, 113 MOB_COL = 6, 114 WALL_COL = 5, 115 TERRITORY = 2, 116 WANDER_ACTIONS = 20, 117 }; 118 unsigned char tiles[MAP_W * MAP_H]; 119 PhEntityDef entity_defs[] = { 120 { .id = 1, .stats = { .max_hp = 10 }, .move_speed = 2, 121 .animation_frames = 4, .kind = PH_ENTITY_PLAYER }, 122 { .id = 2, .stats = { .max_hp = 10 }, .move_speed = 1, 123 .vision = 8, .aggressive = 1, .blocks_movement = 1, 124 .kind = PH_ENTITY_MONSTER }, 125 }; 126 const PhWorldCatalog content = { 127 .entities = entity_defs, 128 .entity_count = (int)LEN(entity_defs), 129 }; 130 PhArea area = { .name = "Movement", .width = MAP_W, .height = MAP_H, 131 .tiles = tiles, .tile_defs = ph_tile_defs, 132 .tile_def_count = (int)LEN(ph_tile_defs) }; 133 PhVec2 player_start = { PH_TILE_CENTER(PLAYER_COL), PH_TILE_CENTER(ROW) }; 134 PhVec2 mob_start = { PH_TILE_CENTER(MOB_COL), PH_TILE_CENTER(ROW) }; 135 float action_seconds = (float)PH_WORLD_ACTION_MILLISECONDS / 1000.0f; 136 const float frame_seconds = 0.05f; 137 PhWorld world; 138 PhWorldEvent event; 139 PhEntity *mob; 140 int player_index; 141 int mob_index; 142 int moved = 0; 143 int x; 144 int y; 145 int i; 146 147 memset(tiles, open, sizeof(tiles)); 148 for (y = 0; y < area.height; ++y) 149 for (x = 0; x < area.width; ++x) 150 if (!x || !y || x == area.width - 1 || y == area.height - 1) 151 tiles[y * area.width + x] = (unsigned char)blocked; 152 if (ph_world_init(&world, area, 1, content) < 0 || 153 (player_index = ph_world_spawn_entity(&world, 1, player_start, 0, 1)) < 0 || 154 (mob_index = ph_world_spawn_entity(&world, 2, mob_start, 155 TERRITORY, 1)) < 0) return 1; 156 ph_world_set_player(&world, player_index); 157 mob = &world.entities[mob_index]; 158 ph_world_tick(&world, (PhInput){ 0 }, action_seconds); 159 if (!mob->motion.moving || mob->motion.target.x != PH_TILE_CENTER(MOB_COL - 1) || 160 mob->motion.target.y != mob_start.y || 161 !ph_world_take_event(&world, &event) || 162 event.kind != PH_WORLD_EVENT_MOVE || event.entity_index != mob_index) 163 return 1; 164 ph_world_tick(&world, (PhInput){ 0 }, frame_seconds); 165 if (mob->pos.x >= mob_start.x) return 1; 166 ph_world_tick(&world, (PhInput){ .move_x = 1 }, frame_seconds); 167 if (!world.entities[player_index].motion.moving || 168 world.entities[player_index].motion.target.x != 169 PH_TILE_CENTER(PLAYER_COL + 1) || 170 !ph_world_take_event(&world, &event) || 171 event.kind != PH_WORLD_EVENT_MOVE || event.entity_index != player_index || 172 ph_entity_animation_frame(&world.content.entities[0], 173 &world.entities[player_index]) != 0) return 1; 174 ph_world_tick(&world, (PhInput){ 0 }, frame_seconds); 175 if (ph_entity_animation_frame(&world.content.entities[0], 176 &world.entities[player_index]) <= 0) return 1; 177 world.entities[player_index].pos = 178 world.entities[player_index].motion.target = player_start; 179 world.entities[player_index].motion.moving = 0; 180 mob->pos = mob->motion.target = (PhVec2){ 181 PH_TILE_CENTER(PLAYER_COL + 1), player_start.y }; 182 mob->motion.moving = 0; 183 mob->motion.credit = 0.0f; 184 ph_world_tick(&world, (PhInput){ 0 }, action_seconds); 185 if (world.encounter_index != mob_index) return 1; 186 187 world.entities[player_index].pos = player_start; 188 mob->pos = mob->motion.target = mob_start; 189 mob->behavior.last_seen = player_start; 190 mob->motion.moving = 0; 191 mob->motion.credit = 0.0f; 192 mob->behavior.chase_turns = 2; 193 world.encounter_index = -1; 194 tiles[ROW * area.width + WALL_COL] = (unsigned char)blocked; 195 ph_world_tick(&world, (PhInput){ 0 }, action_seconds); 196 if (mob->motion.moving || mob->behavior.chase_turns != 1) return 1; 197 198 tiles[ROW * area.width + WALL_COL] = (unsigned char)open; 199 entity_defs[1].aggressive = 0; 200 mob->behavior.home = mob->pos = mob->motion.target = mob_start; 201 mob->behavior.territory_radius = 1; 202 mob->behavior.chase_turns = 0; 203 mob->motion.credit = 0.0f; 204 for (i = 0; i < WANDER_ACTIONS; ++i) { 205 float dx; 206 float dy; 207 float radius = (float)(mob->behavior.territory_radius * PH_TILE_SIZE); 208 209 ph_world_tick(&world, (PhInput){ 0 }, action_seconds); 210 dx = mob->pos.x - mob->behavior.home.x; 211 dy = mob->pos.y - mob->behavior.home.y; 212 if (dx != 0.0f || dy != 0.0f) moved = 1; 213 if (dx * dx + dy * dy > radius * radius) return 1; 214 } 215 return moved ? 0 : 1; 216 } 217 218 static int 219 ph_area_state_scaling_test(void) 220 { 221 enum { AREA_COUNT = 512 }; 222 const PhEntityDef player_def = { 223 .id = 1, .stats = { .max_hp = 10 }, .kind = PH_ENTITY_PLAYER, 224 }; 225 const PhWorldCatalog content = { .entities = &player_def, .entity_count = 1 }; 226 PhWorld world; 227 PhArea area = { 0 }; 228 int player; 229 int i; 230 int result = 1; 231 232 if (ph_world_init(&world, area, 1, content) < 0 || 233 (player = ph_world_spawn_entity(&world, 1, (PhVec2){ 0 }, 0, 1)) < 0) 234 return 1; 235 ph_world_set_player(&world, player); 236 for (i = AREA_COUNT; i > 0; --i) { 237 world.area_id = i; 238 world.entities[world.player_index].area_id = i; 239 if (ph_world_capture_area(&world) < 0) goto done; 240 } 241 if (world.initialized_area_count != AREA_COUNT || 242 world.area_state_capacity < AREA_COUNT) goto done; 243 for (i = 0; i < AREA_COUNT; ++i) 244 if (world.area_states[i].area_id != i + 1) goto done; 245 result = 0; 246 done: 247 ph_world_destroy(&world); 248 return result; 249 } 250 251 static int 252 ph_flash_delay_test(void) 253 { 254 const PhEntityDef entity_defs[] = { 255 { .id = 1, .stats = { .max_hp = 200, .max_mp = 100, .magic = 8, 256 .agility = 10, .accuracy = 100 }, .kind = PH_ENTITY_PLAYER }, 257 { .id = 2, .stats = { .max_hp = 200, .strength = 2, .agility = 10, 258 .accuracy = 100 }, .kind = PH_ENTITY_MONSTER }, 259 }; 260 const PhWorldCatalog content = { 261 .entities = entity_defs, .entity_count = (int)LEN(entity_defs), 262 }; 263 PhWorld world; 264 PhBattle battle; 265 PhArea area = { 0 }; 266 int order[PH_BATTLE_ORDER_COUNT]; 267 int player; 268 int enemy; 269 int cast; 270 int expected_next; 271 272 if (ph_world_init(&world, area, 1, content) < 0 || 273 (player = ph_world_spawn_entity(&world, 1, (PhVec2){ 0 }, 0, 1)) < 0 || 274 (enemy = ph_world_spawn_entity(&world, 2, (PhVec2){ 16, 0 }, 0, 1)) < 0) 275 return 1; 276 ph_world_set_player(&world, player); 277 ph_battle_begin(&battle, &world, enemy, ph_player_spellbook); 278 if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) != 279 PH_BATTLE_READY) return 1; 280 for (cast = 0; cast < 2; ++cast) { 281 expected_next = cast == 0 ? player : enemy; 282 battle.choice.command = PH_BATTLE_MAGICK; 283 ph_battle_select(&battle, &world); 284 ph_battle_cast(&battle, &world); 285 ph_battle_order(&battle, &world, order); 286 if (battle.phase != PH_BATTLE_TARGET || order[0] != player || 287 order[1] != expected_next) return 1; 288 ph_battle_confirm_target(&battle, &world); 289 if (battle.turn.action_delay != 50) return 1; 290 if (ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS, 0) != 291 PH_BATTLE_CONTINUE || battle.turn.current_actor != expected_next || 292 battle.phase != (cast == 0 ? PH_BATTLE_COMMAND : 293 PH_BATTLE_ENEMY_ACTION)) return 1; 294 if (cast == 0) { 295 battle.choice.command = PH_BATTLE_FIGHT; 296 ph_battle_select(&battle, &world); 297 ph_battle_confirm_target(&battle, &world); 298 if (ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS, 0) != 299 PH_BATTLE_CONTINUE || battle.phase != PH_BATTLE_ENEMY_ACTION || 300 battle.turn.current_actor != enemy) return 1; 301 if (ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS, 0) != 302 PH_BATTLE_CONTINUE || battle.phase != PH_BATTLE_COMMAND || 303 battle.turn.current_actor != player) return 1; 304 } 305 } 306 ph_world_destroy(&world); 307 return 0; 308 } 309 310 static int 311 ph_logic_test(int open, int blocked) 312 { 313 enum { MAP_W = 4, MAP_H = 3, ROW = 1, LEFT_COL = 1, RIGHT_COL = 2 }; 314 unsigned char tiles[MAP_W * MAP_H]; 315 const PhEntityDef entity_defs[] = { 316 { 317 .id = 1, .name = "Tester", 318 .stats = { .max_hp = 5, .max_mp = 5, .strength = 4, 319 .magic = 3, .accuracy = 100, .agility = 1 }, 320 .move_speed = 1, .kind = PH_ENTITY_PLAYER, 321 }, 322 { 323 .id = 4, .name = "Target", 324 .stats = { .max_hp = 8, .strength = 2, .defense = 1, 325 .accuracy = 100 }, 326 .reward = { 7, 2, 1 }, 327 .blocks_movement = 1, .kind = PH_ENTITY_MONSTER, 328 }, 329 }; 330 const PhItemDef item_defs[] = { 331 { .id = 1, .bonuses = { .max_hp = 2, .strength = 2 }, 332 .equip_slot = PH_EQUIP_WEAPON }, 333 { .id = 2, .use = PH_ITEM_USE_HEAL, .use_power = 3 }, 334 { .id = 3, .equip_slot = PH_EQUIP_WEAPON }, 335 }; 336 const PhWorldCatalog content = { 337 .entities = entity_defs, 338 .items = item_defs, 339 .entity_count = (int)LEN(entity_defs), 340 .item_count = (int)LEN(item_defs), 341 }; 342 PhArea area = { .name = "Smoke", .width = MAP_W, .height = MAP_H, 343 .tiles = tiles, .tile_defs = ph_tile_defs, 344 .tile_def_count = (int)LEN(ph_tile_defs) }; 345 PhVec2 player_start = { PH_TILE_CENTER(LEFT_COL), PH_TILE_CENTER(ROW) }; 346 PhVec2 enemy_start = { PH_TILE_CENTER(MAP_W + 1), PH_TILE_CENTER(MAP_H + 1) }; 347 float crossing_seconds = (float)PH_WORLD_STEP_MILLISECONDS / 1000.0f; 348 PhWorld world; 349 PhBattle battle; 350 const PhEntity *player; 351 PhStats stats; 352 int order[PH_BATTLE_ORDER_COUNT]; 353 int player_index; 354 int enemy_index; 355 int ally_index; 356 unsigned int events; 357 358 memset(tiles, blocked, sizeof(tiles)); 359 tiles[ROW * area.width + LEFT_COL] = (unsigned char)open; 360 tiles[ROW * area.width + RIGHT_COL] = (unsigned char)open; 361 if (ph_world_init(&world, area, 1, content) < 0 || 362 (player_index = ph_world_spawn_entity(&world, 1, player_start, 0, 1)) < 0 || 363 (enemy_index = ph_world_spawn_entity(&world, 4, enemy_start, 0, 1)) < 0 || 364 ph_world_drop_item(&world, 1, player_start, 1, 1) < 0 || 365 ph_world_drop_item(&world, 2, player_start, 2, 1) < 0 || 366 ph_world_drop_item(&world, 1, 367 (PhVec2){ PH_TILE_CENTER(RIGHT_COL), PH_TILE_CENTER(ROW) }, 368 1, 1) < 0) return 1; 369 ph_world_set_player(&world, player_index); 370 if (ph_world_equip_item(&world, player_index, 3) < 0) return 1; 371 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 372 if (!world.ground_items[2].active) return 1; 373 if (ph_world_equip_inventory_item(&world, player_index, 1) < 0) return 1; 374 world.entities[player_index].vitals.hp = 1; 375 if (ph_world_use_inventory_item(&world, player_index, 2) < 0 || 376 ph_world_drop_inventory_item(&world, player_index, 2) < 0) return 1; 377 events = ph_world_event_mask(&world); 378 if (!(events & (1u << PH_WORLD_EVENT_ITEM_PICKUP)) || 379 !(events & (1u << PH_WORLD_EVENT_ITEM_DROP))) return 1; 380 381 ph_world_tick(&world, (PhInput){ .move_x = 1 }, crossing_seconds); 382 ph_world_tick(&world, (PhInput){ .move_x = 1, .move_y = 1 }, crossing_seconds); 383 player = ph_world_player(&world); 384 stats = ph_world_entity_stats(&world, player); 385 if (!player || (int)(player->pos.x / PH_TILE_SIZE) != RIGHT_COL || 386 (int)(player->pos.y / PH_TILE_SIZE) != ROW || 387 player->motion.facing_x != 0 || player->motion.facing_y != 1 || 388 ph_world_item_amount(&world, 1) != 0 || 389 ph_world_item_amount(&world, 2) != 0 || 390 ph_world_item_amount(&world, 3) != 1 || player->vitals.hp != 4 || 391 stats.max_hp != 7 || stats.strength != 6 || 392 player->equipment[PH_EQUIP_WEAPON] != 1 || 393 !world.ground_items[0].active || world.ground_items[0].item_id != 2) 394 return 1; 395 world.entities[enemy_index].pos = (PhVec2){ 396 player->pos.x - PH_TILE_SIZE, player->pos.y }; 397 ph_world_tick(&world, (PhInput){ .move_x = -1 }, crossing_seconds); 398 ph_battle_begin(&battle, &world, enemy_index, ph_player_spellbook); 399 if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) != 400 PH_BATTLE_READY) return 1; 401 battle.choice.command = PH_BATTLE_FIGHT; 402 ph_battle_order(&battle, &world, order); 403 if (order[0] != player_index || order[1] != enemy_index) return 1; 404 battle.choice.command = PH_BATTLE_MAGICK; 405 ph_battle_order(&battle, &world, order); 406 if (order[0] != player_index || order[1] != enemy_index) return 1; 407 ph_battle_select(&battle, &world); 408 ph_battle_order(&battle, &world, order); 409 if (battle.phase != PH_BATTLE_MAGIC || order[0] != player_index || 410 order[1] != player_index || world.entities[player_index].vitals.mp != 5 || 411 world.encounter_index != enemy_index || 412 ph_world_apply_accuracy_penalty(&world, enemy_index, 20, 3) < 0) 413 return 1; 414 world.entities[player_index].vitals.mp = 20; 415 ph_battle_cast(&battle, &world); 416 if (battle.phase != PH_BATTLE_TARGET || 417 battle.choice.target_index != enemy_index || 418 ph_battle_target_step(&battle, &world, 1) != player_index || 419 ph_battle_target_step(&battle, &world, 1) != enemy_index) return 1; 420 ph_battle_cancel(&battle); 421 battle.choice.spell = 1; 422 ph_battle_cast(&battle, &world); 423 ally_index = ph_world_spawn_entity(&world, 1, player->pos, 0, 1); 424 if (battle.phase != PH_BATTLE_TARGET || 425 battle.choice.target_index != player_index || 426 ally_index < 0 || 427 ph_battle_target_step(&battle, &world, 1) != enemy_index || 428 ph_battle_target_step(&battle, &world, 1) != ally_index || 429 ph_battle_target_step(&battle, &world, 1) != player_index) return 1; 430 ph_battle_cancel(&battle); 431 world.entities[player_index].vitals.mp = 5; 432 ph_battle_cancel(&battle); 433 ph_battle_order(&battle, &world, order); 434 if (battle.phase != PH_BATTLE_COMMAND || order[0] != player_index || 435 order[1] != enemy_index) return 1; 436 ph_world_advance_effects(&world, enemy_index); 437 ph_world_advance_effects(&world, enemy_index); 438 ph_world_advance_effects(&world, enemy_index); 439 if (world.entities[enemy_index].accuracy.value != 0 || 440 world.entities[enemy_index].accuracy.turns != 0 || 441 ph_world_physical_attack(&world, player_index, enemy_index) <= 0 || 442 ph_world_magic_attack(&world, player_index, enemy_index, 2, 4) <= 0 || 443 world.entities[enemy_index].vitals.hp != 0 || 444 ph_world_finish_encounter(&world, enemy_index) < 0 || 445 player->xp != 7 || player->vitals.mp != 3 || 446 ph_world_item_amount(&world, 2) != 1 || world.encounter_index != -1) 447 return 1; 448 printf("smoke hp=%d equipment=%d xp=%d\n", player->vitals.hp, 449 player->equipment[PH_EQUIP_WEAPON], player->xp); 450 return 0; 451 } 452 453 static int 454 ph_enter_portal(PhWorld *world, int destination_area_id) 455 { 456 PhEntity *player = &world->entities[world->player_index]; 457 int i; 458 459 for (i = 0; i < world->area.marker_count; ++i) { 460 const PhAreaMarker *marker = &world->area.markers[i]; 461 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 462 world->area_id, marker->symbol); 463 const PhTileDef *tile; 464 465 if (!def || def->kind != PH_MARKER_PORTAL || 466 def->destination_area_id != destination_area_id) continue; 467 tile = ph_area_tile_def(&world->area, marker->tile_x, marker->tile_y); 468 if (!tile || tile->blocks_movement) return -1; 469 player->pos = (PhVec2){ PH_TILE_CENTER(marker->tile_x), 470 PH_TILE_CENTER(marker->tile_y) }; 471 return ph_world_update_portal(world, ph_game_world.areas); 472 } 473 return -1; 474 } 475 476 static int 477 ph_area_shop_test(void) 478 { 479 PhWorld world; 480 PhEntity *player; 481 const PhItemDef *potion; 482 const PhItemDef *ether; 483 PhSpellBook spells = ph_player_spellbook; 484 int potion_before; 485 int slime_found = 0; 486 int shop_id = -1; 487 int i; 488 unsigned int events; 489 490 if (ph_world_start(&world, &ph_game_world) < 0) return 1; 491 player = &world.entities[world.player_index]; 492 potion = ph_world_item_def(&world, PH_ITEM_HEALING_POTION); 493 ether = ph_world_item_def(&world, PH_ITEM_ETHER); 494 if (!potion || !ether || world.area_id != PH_AREA_MEADOW || player->gold != 40 || 495 world.initialized_area_count != 1) 496 goto fail; 497 player->vitals.hp = 10; 498 if (ph_spellbook_use(&spells, &world, world.player_index, 499 PH_SPELL_FLASH) >= 0 || 500 ph_spellbook_use(&spells, &world, world.player_index, 501 PH_SPELL_HEAL) != 18 || player->vitals.hp != 28 || 502 player->vitals.mp != 40) goto fail; 503 player->vitals.hp = ph_world_entity_stats(&world, player).max_hp; 504 player->vitals.mp = ph_world_entity_stats(&world, player).max_mp; 505 if (!ph_area_tile_blocked(&world.area, 26, 1) || 506 !ph_area_tile_blocked(&world.area, 27, 3) || 507 ph_area_tile_blocked(&world.area, 28, 3)) goto fail; 508 if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 || 509 world.area_id != PH_AREA_STONEHOLLOW || 510 world.initialized_area_count != 2) goto fail; 511 if (ph_area_tile_blocked(&world.area, 3, 2) || 512 ph_area_tile_blocked(&world.area, 8, 2) || 513 !ph_area_tile_blocked(&world.area, 13, 2) || 514 ph_area_tile_blocked(&world.area, 4, 4) || 515 ph_area_tile_blocked(&world.area, 6, 6)) goto fail; 516 player->pos = (PhVec2){ PH_TILE_CENTER(13), PH_TILE_CENTER(3) }; 517 player->motion.facing_x = 0; 518 player->motion.facing_y = -1; 519 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 520 if (strcmp(world.notice.text, "The door is locked.") != 0) goto fail; 521 if (ph_enter_portal(&world, PH_AREA_MIRA_SHOP) != 1 || 522 world.area_id != PH_AREA_MIRA_SHOP || 523 world.initialized_area_count != 3) goto fail; 524 if (ph_area_tile_blocked(&world.area, 0, 0) || 525 ph_area_tile_blocked(&world.area, 0, 1) || 526 !ph_area_tile_blocked(&world.area, 0, world.area.height - 1) || 527 !ph_area_tile_blocked(&world.area, world.area.width - 1, 528 world.area.height - 1) || 529 ph_area_tile_blocked(&world.area, 10, world.area.height - 1) || 530 !ph_area_tile_blocked(&world.area, -1, 0) || 531 !ph_area_tile_blocked(&world.area, world.area.width, 0)) goto fail; 532 for (i = 0; i < world.area.marker_count; ++i) { 533 const PhMarkerDef *def = ph_area_catalog_marker(ph_game_world.areas, 534 world.area_id, world.area.markers[i].symbol); 535 536 if (def && def->kind == PH_MARKER_PORTAL && 537 def->destination_area_id == PH_AREA_STONEHOLLOW) break; 538 } 539 if (i == world.area.marker_count || 540 world.area.markers[i].tile_x != 541 (int)(player->pos.x / (float)PH_TILE_SIZE)) goto fail; 542 for (i = 0; i < world.entity_count; ++i) { 543 const PhEntityDef *def = ph_world_entity_def(&world, 544 world.entities[i].type_id); 545 const PhInteractionDef *interaction = def ? 546 ph_game_interaction_def(def->interaction_id) : NULL; 547 548 if (interaction && interaction->kind == PH_INTERACTION_SHOP) { 549 shop_id = interaction->content_id; 550 player->pos = (PhVec2){ world.entities[i].pos.x, 551 world.entities[i].pos.y + PH_TILE_SIZE }; 552 player->motion.facing_x = 0; 553 player->motion.facing_y = -1; 554 break; 555 } 556 } 557 if (i == world.entity_count) goto fail; 558 ph_world_event_mask(&world); 559 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 560 { 561 const PhInteractionDef *interaction = 562 ph_game_interaction_def(world.interaction_id); 563 564 if (!interaction || interaction->kind != PH_INTERACTION_SHOP || 565 interaction->content_id != shop_id) goto fail; 566 } 567 events = ph_world_event_mask(&world); 568 if (!(events & (1u << PH_WORLD_EVENT_INTERACT))) goto fail; 569 potion_before = ph_world_item_amount(&world, potion->id); 570 if (ph_world_buy_item(&world, world.player_index, potion->id) < 0 || 571 player->gold != 40 - potion->buy_value || 572 ph_world_item_amount(&world, potion->id) != potion_before + 1 || 573 ph_world_sell_item(&world, world.player_index, potion->id) < 0 || 574 player->gold != 40 - potion->buy_value + potion->sell_value) goto fail; 575 events = ph_world_event_mask(&world); 576 if (!(events & (1u << PH_WORLD_EVENT_BUY)) || 577 !(events & (1u << PH_WORLD_EVENT_SELL))) goto fail; 578 if (ph_world_buy_item(&world, world.player_index, ether->id) < 0) goto fail; 579 player->vitals.mp = 0; 580 if (ph_world_use_inventory_item(&world, world.player_index, ether->id) < 0 || 581 player->vitals.mp != ether->use_power) goto fail; 582 if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 || 583 world.area_id != PH_AREA_STONEHOLLOW || 584 world.initialized_area_count != 3 || 585 ph_enter_portal(&world, PH_AREA_MAGICK_SHOP) != 1 || 586 world.area_id != PH_AREA_MAGICK_SHOP || 587 world.initialized_area_count != 4) goto fail; 588 for (i = 0; i < world.entity_count; ++i) { 589 const PhEntityDef *def = ph_world_entity_def(&world, 590 world.entities[i].type_id); 591 const PhInteractionDef *interaction = def ? 592 ph_game_interaction_def(def->interaction_id) : NULL; 593 594 if (!interaction || interaction->kind != PH_INTERACTION_SHOP || 595 interaction->content_id != PH_SHOP_MAGICK || 596 world.entities[i].area_id != PH_AREA_MAGICK_SHOP) continue; 597 player->pos = (PhVec2){ world.entities[i].pos.x, 598 world.entities[i].pos.y + PH_TILE_SIZE }; 599 player->motion.facing_x = 0; 600 player->motion.facing_y = -1; 601 break; 602 } 603 if (i == world.entity_count) goto fail; 604 ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f); 605 { 606 const PhInteractionDef *interaction = 607 ph_game_interaction_def(world.interaction_id); 608 609 if (!interaction || interaction->content_id != PH_SHOP_MAGICK) goto fail; 610 } 611 if (!ph_spellbook_has(&spells, PH_SPELL_FLASH) || 612 ph_spellbook_has(&spells, PH_SPELL_EMBER) || 613 ph_spellbook_buy(&spells, &world, world.player_index, 614 PH_SPELL_EMBER) < 0 || 615 !ph_spellbook_has(&spells, PH_SPELL_EMBER) || player->gold != 0 || 616 ph_spellbook_buy(&spells, &world, world.player_index, 617 PH_SPELL_EMBER) == 0 || 618 ph_spellbook_buy(&spells, &world, world.player_index, 619 PH_SPELL_METEOR) == 0) goto fail; 620 if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 || 621 world.area_id != PH_AREA_STONEHOLLOW || 622 world.initialized_area_count != 4 || 623 ph_enter_portal(&world, PH_AREA_MEADOW) != 1 || 624 world.area_id != PH_AREA_MEADOW || world.initialized_area_count != 4) 625 goto fail; 626 for (i = 0; i < world.entity_count; ++i) { 627 const PhEntityDef *def = ph_world_entity_def(&world, 628 world.entities[i].type_id); 629 630 if (def && def->kind == PH_ENTITY_MONSTER && 631 world.entities[i].area_id == PH_AREA_MEADOW && 632 world.entities[i].active) slime_found = 1; 633 } 634 ph_world_destroy(&world); 635 return slime_found ? 0 : 1; 636 637 fail: 638 ph_world_destroy(&world); 639 return 1; 640 } 641 642 static int 643 ph_save_test(void) 644 { 645 const char *path = "obj/test-save.phsave"; 646 PhWorld world; 647 PhWorld loaded; 648 PhWorld checkpoint; 649 PhBattle checkpoint_battle; 650 PhSpellBook spells = ph_player_spellbook; 651 PhSpellBook loaded_spells = ph_player_spellbook; 652 PhEntity *player; 653 FILE *corrupt; 654 int enemy_hp; 655 int result = 1; 656 657 remove(path); 658 remove("obj/test-save.phsave.tmp"); 659 if (ph_save_load(path, &loaded, &ph_game_world, &loaded_spells) != 0 || 660 ph_world_start(&world, &ph_game_world) < 0) return 1; 661 player = &world.entities[world.player_index]; 662 player->vitals.hp = 23; 663 player->xp = 91; 664 player->gold = 137; 665 world.event_dropped = 3; 666 world.inventory[3] = 4; 667 if (ph_spellbook_learn(&spells, PH_SPELL_EMBER) < 0) goto done_world; 668 if (ph_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1 || 669 world.entity_count != 1) goto done_world; 670 if (ph_save_write(path, &world, &spells) < 0) goto done_world; 671 if (ph_save_load(path, &loaded, &ph_game_world, &loaded_spells) != 1) { 672 fprintf(stderr, "save round trip failed to load\n"); 673 goto done_world; 674 } 675 player = &loaded.entities[loaded.player_index]; 676 if (loaded.area_id != PH_AREA_STONEHOLLOW || loaded.entity_count != 1 || 677 loaded.initialized_area_count != 2 || loaded.event_dropped != 0 || player->vitals.hp != 23 || 678 player->xp != 91 || player->gold != 137 || 679 ph_world_item_amount(&loaded, PH_ITEM_HEALING_POTION) != 4 || 680 !ph_spellbook_has(&loaded_spells, PH_SPELL_FLASH) || 681 !ph_spellbook_has(&loaded_spells, PH_SPELL_HEAL) || 682 !ph_spellbook_has(&loaded_spells, PH_SPELL_EMBER)) { 683 fprintf(stderr, "save round trip restored incorrect global state\n"); 684 goto done_loaded; 685 } 686 if (ph_enter_portal(&loaded, PH_AREA_MEADOW) != 1 || 687 loaded.entity_count != 2 || loaded.ground_item_count != 2) { 688 fprintf(stderr, "save round trip restored incorrect area state: area=%d entities=%d items=%d\n", 689 loaded.area_id, loaded.entity_count, loaded.ground_item_count); 690 goto done_loaded; 691 } 692 if (ph_save_write(path, &loaded, &loaded_spells) < 0) goto done_loaded; 693 player = &loaded.entities[1]; 694 enemy_hp = player->vitals.hp; 695 ph_battle_begin(&checkpoint_battle, &loaded, 1, loaded_spells); 696 --loaded.entities[1].vitals.hp; 697 if (ph_save_write(path, &loaded, &loaded_spells) != -1 || 698 ph_save_load(path, &checkpoint, &ph_game_world, &loaded_spells) != 1) goto done_loaded; 699 if (checkpoint.entity_count != 2 || 700 checkpoint.entities[1].vitals.hp != enemy_hp) { 701 ph_world_destroy(&checkpoint); 702 fprintf(stderr, "mid-combat save overwrote stable checkpoint\n"); 703 goto done_loaded; 704 } 705 ph_world_destroy(&checkpoint); 706 corrupt = fopen(path, "wb"); 707 if (!corrupt) goto done_loaded; 708 if (fwrite("bad", 1, 3, corrupt) != 3) { 709 fclose(corrupt); 710 goto done_loaded; 711 } 712 if (fclose(corrupt) != 0) goto done_loaded; 713 if (ph_save_load(path, &(PhWorld){ 0 }, &ph_game_world, &loaded_spells) != -1) goto done_loaded; 714 result = 0; 715 716 done_loaded: 717 ph_world_destroy(&loaded); 718 done_world: 719 ph_world_destroy(&world); 720 remove(path); 721 remove("obj/test-save.phsave.tmp"); 722 return result; 723 } 724 725 int 726 main(void) 727 { 728 int open = -1; 729 int blocked = -1; 730 size_t tile; 731 732 for (tile = 0; tile < LEN(ph_tile_defs); ++tile) { 733 if (ph_tile_defs[tile].blocks_movement) blocked = (int)tile; 734 else open = (int)tile; 735 } 736 if (open < 0 || blocked < 0) { 737 fprintf(stderr, "smoke test failed: tile configuration\n"); 738 return 1; 739 } 740 if (ph_event_overflow_test()) { 741 fprintf(stderr, "smoke test failed: event overflow diagnostics\n"); 742 return 1; 743 } 744 if (ph_area_catalog_test()) { 745 fprintf(stderr, "smoke test failed: engine area catalog\n"); 746 return 1; 747 } 748 if (ph_movement_test(open, blocked)) { 749 fprintf(stderr, "smoke test failed: world movement\n"); 750 return 1; 751 } 752 if (ph_area_state_scaling_test()) { 753 fprintf(stderr, "smoke test failed: scalable area state storage\n"); 754 return 1; 755 } 756 if (ph_flash_delay_test()) { 757 fprintf(stderr, "smoke test failed: repeat Flash delay\n"); 758 return 1; 759 } 760 if (ph_logic_test(open, blocked)) { 761 fprintf(stderr, "smoke test failed: world or battle logic\n"); 762 return 1; 763 } 764 if (ph_area_shop_test()) { 765 fprintf(stderr, "smoke test failed: area or shop logic\n"); 766 return 1; 767 } 768 if (ph_save_test()) { 769 fprintf(stderr, "smoke test failed: save persistence\n"); 770 return 1; 771 } 772 return 0; 773 }