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