diff --git a/CHANGELOG.md b/CHANGELOG.md index b2189ff1e..45f329a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ - renamed the project from Tomb1Main to TR1X in an effort to establish our own unique identity, while respectfully disassociating from TR2Main. - added Linux builds and toolchain - added an option to allow Lara to roll while underwater, similar to TR2+ (#993) -- fixed baddies dropping duplicate guns (only affects mods) (#1000) - added the bonus level type for custom levels that unlocks if all main game secrets are found (#645) - added detection for animation commands to play SFX on land, water or both (#999) +- added support for customizable enemy item drops via the gameflow (#967) +- fixed baddies dropping duplicate guns (only affects mods) (#1000) - improved frame scheduling to use less CPU (#985) ## [2.16](https://github.com/LostArtefacts/TR1X/compare/2.15.3...2.16) - 2023-09-20 diff --git a/README.md b/README.md index 31d5bb926..50d9f7bd6 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,7 @@ Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for det - you can change the main menu backdrop - you can specify the anchor room for Bacon Lara - you can add bonus levels that unlock if all main game secrets are found + - you can specify which enemies drop items, and the number and types of those items - added automatic calculation of secret counts (no longer having to fiddle with the .exe to get correct secret stats) - added save game crystals game mode (enabled via gameflow) - added per-level customizable water color (with customizable blue component) diff --git a/bin/cfg/TR1X_gameflow.json5 b/bin/cfg/TR1X_gameflow.json5 index ef9d3641b..b86617eed 100644 --- a/bin/cfg/TR1X_gameflow.json5 +++ b/bin/cfg/TR1X_gameflow.json5 @@ -53,6 +53,11 @@ "data/uzi_sfx.bin", ], + // When enabled and an enemy is configured to drop a gun, if Lara already has + // that gun, then the equivalent ammo will instead be dropped. Otherwise, the + // gun will always be dropped. + "convert_dropped_guns": false, + // List of levels "levels": [ // Level 0 @@ -305,6 +310,9 @@ "data/tihocan_itemrots.bin", "data/tihocan_textures.bin", ], + "item_drops": [ + {"enemy_num": 82, "object_ids": [86, 144, 129]}, + ], "sequence": [ {"type": "start_game"}, {"type": "loop_game"}, @@ -407,6 +415,11 @@ "data/mines_textures.bin", "data/skateboardkid_textures.bin" ], + "item_drops": [ + {"enemy_num": 17, "object_ids": [86]}, + {"enemy_num": 50, "object_ids": [87]}, + {"enemy_num": 75, "object_ids": [85]}, + ], "sequence": [ {"type": "play_fmv", "fmv_path": "fmv/canyon.avi"}, {"type": "remove_guns"}, diff --git a/bin/cfg/TR1X_gameflow_ub.json5 b/bin/cfg/TR1X_gameflow_ub.json5 index d988b0612..a4eed20e4 100644 --- a/bin/cfg/TR1X_gameflow_ub.json5 +++ b/bin/cfg/TR1X_gameflow_ub.json5 @@ -17,6 +17,7 @@ "data/lara_jumping.bin", "data/uzi_sfx.bin", ], + "convert_dropped_guns": false, "levels": [ // Level 0 diff --git a/meson.build b/meson.build index 89ff81f5f..061340497 100644 --- a/meson.build +++ b/meson.build @@ -86,6 +86,7 @@ sources = [ 'src/filesystem.c', 'src/game/box.c', 'src/game/camera.c', + 'src/game/carrier.c', 'src/game/clock.c', 'src/game/collide.c', 'src/game/creature.c', diff --git a/src/game/carrier.c b/src/game/carrier.c new file mode 100644 index 000000000..861786307 --- /dev/null +++ b/src/game/carrier.c @@ -0,0 +1,241 @@ +#include "game/carrier.h" + +#include "game/gamebuf.h" +#include "game/gameflow.h" +#include "game/inventory.h" +#include "game/items.h" +#include "global/const.h" +#include "global/types.h" +#include "global/vars.h" +#include "log.h" + +#include +#include + +#define NO_OBJECT O_NUMBER_OF + +typedef struct GAME_OBJECT_PAIR { + const GAME_OBJECT_ID key_id; + const GAME_OBJECT_ID value_id; +} GAME_OBJECT_PAIR; + +static ITEM_INFO *Carrier_GetCarrier(int16_t item_num); +static bool Carrier_IsObjectType( + GAME_OBJECT_ID object_id, const GAME_OBJECT_ID *test_arr); +static GAME_OBJECT_ID Carrier_GetCognate( + GAME_OBJECT_ID key_id, const GAME_OBJECT_PAIR *test_map); + +static const GAME_OBJECT_ID m_CarrierObjects[] = { + O_WOLF, O_BEAR, O_BAT, O_LION, O_LIONESS, O_PUMA, O_APE, + O_TREX, O_RAPTOR, O_CENTAUR, O_MUMMY, O_LARSON, O_PIERRE, O_SKATEKID, + O_COWBOY, O_BALDY, O_NATLA, O_TORSO, NO_OBJECT +}; + +static const GAME_OBJECT_ID m_PlaceholderObjects[] = { O_STATUE, O_PODS, + O_BIG_POD, NO_OBJECT }; + +static const GAME_OBJECT_ID m_DropObjects[] = { + O_GUN_ITEM, O_SHOTGUN_ITEM, O_MAGNUM_ITEM, O_UZI_ITEM, + O_SG_AMMO_ITEM, O_MAG_AMMO_ITEM, O_UZI_AMMO_ITEM, O_MEDI_ITEM, + O_BIGMEDI_ITEM, O_PUZZLE_ITEM1, O_PUZZLE_ITEM2, O_PUZZLE_ITEM3, + O_PUZZLE_ITEM4, O_KEY_ITEM1, O_KEY_ITEM2, O_KEY_ITEM3, + O_KEY_ITEM4, O_PICKUP_ITEM1, O_PICKUP_ITEM2, O_LEADBAR_ITEM, + O_SCION_ITEM2, NO_OBJECT +}; + +static const GAME_OBJECT_ID m_GunObjects[] = { O_SHOTGUN_ITEM, O_MAGNUM_ITEM, + O_UZI_ITEM, NO_OBJECT }; + +static const GAME_OBJECT_PAIR m_GunAmmoMap[] = { + { O_SHOTGUN_ITEM, O_SG_AMMO_ITEM }, + { O_MAGNUM_ITEM, O_MAG_AMMO_ITEM }, + { O_UZI_ITEM, O_UZI_AMMO_ITEM }, + { NO_OBJECT, NO_OBJECT }, +}; + +static const GAME_OBJECT_PAIR m_LegacyMap[] = { + { O_PIERRE, O_SCION_ITEM2 }, { O_COWBOY, O_MAGNUM_ITEM }, + { O_SKATEKID, O_UZI_ITEM }, { O_BALDY, O_SHOTGUN_ITEM }, + { NO_OBJECT, NO_OBJECT }, +}; + +void Carrier_InitialiseLevel(int32_t level_num) +{ + int32_t total_item_count = g_LevelItemCount; + GAMEFLOW_LEVEL level = g_GameFlow.levels[level_num]; + for (int i = 0; i < level.item_drops.count; i++) { + GAMEFLOW_DROP_ITEM_DATA *data = &level.item_drops.data[i]; + + ITEM_INFO *item = Carrier_GetCarrier(data->enemy_num); + if (!item) { + LOG_WARNING("%d does not refer to a loaded item", data->enemy_num); + continue; + } + + if (total_item_count + data->count > MAX_ITEMS) { + LOG_WARNING("Too many items being loaded"); + return; + } + + if (item->carried_item) { + LOG_WARNING("Item %d is already carrying", data->enemy_num); + continue; + } + + if (!Carrier_IsObjectType(item->object_number, m_CarrierObjects)) { + LOG_WARNING( + "Item %d of type %d cannot carry items", data->enemy_num, + item->object_number); + continue; + } + + item->carried_item = + GameBuf_Alloc(sizeof(CARRIED_ITEM) * data->count, GBUF_ITEMS); + CARRIED_ITEM *drop = item->carried_item; + for (int i = 0; i < data->count; i++) { + drop->object_id = data->object_ids[i]; + drop->spawn_number = NO_ITEM; + + if (Carrier_IsObjectType(drop->object_id, m_DropObjects)) { + drop->status = IS_NOT_ACTIVE; + total_item_count++; + } else { + LOG_WARNING( + "Items of type %d cannot be carried", drop->object_id); + drop->object_id = NO_OBJECT; + drop->status = IS_INVISIBLE; + } + + if (i < data->count - 1) { + drop->next_item = drop + 1; + drop++; + } else { + drop->next_item = NULL; + } + } + } +} + +static ITEM_INFO *Carrier_GetCarrier(int16_t item_num) +{ + if (item_num < 0 || item_num >= g_LevelItemCount) { + return NULL; + } + + // Allow carried items to be allocated to holder objects (pods/statues), + // but then have those items dropped by the actual creatures within. + ITEM_INFO *item = &g_Items[item_num]; + if (Carrier_IsObjectType(item->object_number, m_PlaceholderObjects)) { + int16_t child_item_num = *(int16_t *)item->data; + item = &g_Items[child_item_num]; + } + + if (!g_Objects[item->object_number].loaded) { + return NULL; + } + + return item; +} + +static bool Carrier_IsObjectType( + GAME_OBJECT_ID object_id, const GAME_OBJECT_ID *test_arr) +{ + for (int i = 0; test_arr[i] != NO_OBJECT; i++) { + if (test_arr[i] == object_id) { + return true; + } + } + return false; +} + +static GAME_OBJECT_ID Carrier_GetCognate( + GAME_OBJECT_ID key_id, const GAME_OBJECT_PAIR *test_map) +{ + const GAME_OBJECT_PAIR *pair = &test_map[0]; + while (pair->key_id != NO_OBJECT) { + if (pair->key_id == key_id) { + return pair->value_id; + } + pair++; + } + + return NO_OBJECT; +} + +int32_t Carrier_GetItemCount(int16_t item_num) +{ + ITEM_INFO *carrier = Carrier_GetCarrier(item_num); + if (!carrier) { + return 0; + } + + CARRIED_ITEM *item = carrier->carried_item; + int32_t count = 0; + while (item) { + if (item->object_id != NO_OBJECT) { + count++; + } + item = item->next_item; + } + + return count; +} + +void Carrier_TestItemDrops(int16_t item_num) +{ + ITEM_INFO *carrier = &g_Items[item_num]; + CARRIED_ITEM *item = carrier->carried_item; + if (carrier->status != IS_DEACTIVATED || !item + || (carrier->object_number == O_PIERRE + && !(carrier->flags & IF_ONESHOT))) { + return; + } + + // The enemy is killed (plus is not runaway) and is carrying at + // least one item. Ensure that each item has not already spawned, + // convert guns to ammo if applicable, and spawn the items. + do { + if (item->status == IS_INVISIBLE) { + continue; + } + + GAME_OBJECT_ID object_id = item->object_id; + if (g_GameFlow.convert_dropped_guns + && Carrier_IsObjectType(object_id, m_GunObjects) + && Inv_RequestItem(object_id)) { + object_id = Carrier_GetCognate(object_id, m_GunAmmoMap); + } + + item->spawn_number = Item_Spawn(carrier, object_id); + item->status = IS_ACTIVE; + } while ((item = item->next_item)); +} + +void Carrier_TestLegacyDrops(int16_t item_num) +{ + ITEM_INFO *carrier = &g_Items[item_num]; + if (carrier->status != IS_DEACTIVATED) { + return; + } + + // Handle cases where legacy saves have been loaded. Ensure that + // the OG enemy will still spawn items if Lara hasn't yet collected + // them by using a test cognate in each case. Ensure also that + // collected items do not re-spawn now or in future saves. + GAME_OBJECT_ID test_id = + Carrier_GetCognate(carrier->object_number, m_LegacyMap); + if (test_id == NO_OBJECT) { + return; + } + + if (!Inv_RequestItem(test_id)) { + Carrier_TestItemDrops(item_num); + } else { + CARRIED_ITEM *item = carrier->carried_item; + while (item) { + // Simulate Lara having picked up the item. + item->status = IS_INVISIBLE; + item = item->next_item; + } + } +} diff --git a/src/game/carrier.h b/src/game/carrier.h new file mode 100644 index 000000000..43587f2ca --- /dev/null +++ b/src/game/carrier.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +void Carrier_InitialiseLevel(int32_t level_num); +int32_t Carrier_GetItemCount(int16_t item_num); +void Carrier_TestItemDrops(int16_t item_num); +void Carrier_TestLegacyDrops(int16_t item_num); diff --git a/src/game/creature.c b/src/game/creature.c index 17d43ed03..89e3a287f 100644 --- a/src/game/creature.c +++ b/src/game/creature.c @@ -1,6 +1,7 @@ #include "game/creature.h" #include "game/box.h" +#include "game/carrier.h" #include "game/collide.h" #include "game/effects.h" #include "game/effects/gunshot.h" @@ -418,6 +419,7 @@ bool Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt) item->hit_points = DONT_TARGET; LOT_DisableBaddieAI(item_num); Item_RemoveActive(item_num); + Carrier_TestItemDrops(item_num); return false; } diff --git a/src/game/gameflow.c b/src/game/gameflow.c index 34429a01e..d67aea8cd 100644 --- a/src/game/gameflow.c +++ b/src/game/gameflow.c @@ -292,6 +292,9 @@ static bool GameFlow_LoadScriptMeta(struct json_object_s *obj) g_GameFlow.injections.length = 0; } + g_GameFlow.convert_dropped_guns = + json_object_get_bool(obj, "convert_dropped_guns", false); + return true; } @@ -934,6 +937,53 @@ static bool GameFlow_LoadScriptLevels(struct json_object_s *obj) } cur->lara_type = (GAME_OBJECT_ID)tmp_i; + tmp_arr = json_object_get_array(jlvl_obj, "item_drops"); + if (tmp_arr) { + cur->item_drops.count = (signed)tmp_arr->length; + cur->item_drops.data = Memory_Alloc( + sizeof(GAMEFLOW_DROP_ITEM_DATA) * (signed)tmp_arr->length); + + for (int i = 0; i < cur->item_drops.count; i++) { + GAMEFLOW_DROP_ITEM_DATA *data = &cur->item_drops.data[i]; + struct json_object_s *jlvl_data = + json_array_get_object(tmp_arr, i); + + data->enemy_num = json_object_get_int( + jlvl_data, "enemy_num", JSON_INVALID_NUMBER); + if (data->enemy_num == JSON_INVALID_NUMBER) { + LOG_ERROR( + "level %d, item drop %d: 'enemy_num' must be a number", + level_num, i); + return false; + } + + struct json_array_s *object_arr = + json_object_get_array(jlvl_data, "object_ids"); + if (!object_arr) { + LOG_ERROR( + "level %d, item drop %d: 'object_ids' must be an array", + level_num, i); + return false; + } + + data->count = (signed)object_arr->length; + data->object_ids = Memory_Alloc(sizeof(int16_t) * data->count); + for (int j = 0; j < data->count; j++) { + int id = json_array_get_int(object_arr, j, -1); + if (id < 0 || id >= O_NUMBER_OF) { + LOG_ERROR( + "level %d, item drop %d, index %d: 'object_id' " + "must be a valid object id", + level_num, i, j); + return false; + } + data->object_ids[j] = (int16_t)id; + } + } + } else { + cur->item_drops.count = 0; + } + if (!GameFlow_LoadLevelSequence(jlvl_obj, level_num)) { return false; } @@ -1030,6 +1080,15 @@ void GameFlow_Shutdown(void) &g_GameFlow.levels[i].injections.data_paths[j]); } + if (g_GameFlow.levels[i].item_drops.count) { + for (int j = 0; j < g_GameFlow.levels[i].item_drops.count; + j++) { + Memory_FreePointer( + &g_GameFlow.levels[i].item_drops.data[j].object_ids); + } + Memory_FreePointer(&g_GameFlow.levels[i].item_drops.data); + } + GAMEFLOW_SEQUENCE *seq = g_GameFlow.levels[i].sequence; if (seq) { while (seq->type != GFS_END) { diff --git a/src/game/gameflow.h b/src/game/gameflow.h index 2797008b1..e07897a7e 100644 --- a/src/game/gameflow.h +++ b/src/game/gameflow.h @@ -15,6 +15,12 @@ typedef struct GAMEFLOW_SEQUENCE { void *data; } GAMEFLOW_SEQUENCE; +typedef struct GAMEFLOW_DROP_ITEM_DATA { + int32_t enemy_num; + int32_t count; + int16_t *object_ids; +} GAMEFLOW_DROP_ITEM_DATA; + typedef struct GAMEFLOW_LEVEL { GAMEFLOW_LEVEL_TYPE level_type; int16_t music; @@ -52,6 +58,10 @@ typedef struct GAMEFLOW_LEVEL { int length; char **data_paths; } injections; + struct { + int count; + GAMEFLOW_DROP_ITEM_DATA *data; + } item_drops; GAME_OBJECT_ID lara_type; } GAMEFLOW_LEVEL; @@ -77,6 +87,7 @@ typedef struct GAMEFLOW { int length; char **data_paths; } injections; + bool convert_dropped_guns; } GAMEFLOW; extern GAMEFLOW g_GameFlow; diff --git a/src/game/items.c b/src/game/items.c index 01678d074..cfb4da73b 100644 --- a/src/game/items.c +++ b/src/game/items.c @@ -132,6 +132,7 @@ void Item_Initialise(int16_t item_num) item->touch_bits = 0; item->data = NULL; item->priv = NULL; + item->carried_item = NULL; if (item->flags & IF_NOT_VISIBLE) { item->status = IS_INVISIBLE; diff --git a/src/game/level.c b/src/game/level.c index 36ce85859..02537a6b1 100644 --- a/src/game/level.c +++ b/src/game/level.c @@ -2,6 +2,7 @@ #include "config.h" #include "filesystem.h" +#include "game/carrier.h" #include "game/effects.h" #include "game/gamebuf.h" #include "game/gameflow.h" @@ -48,7 +49,7 @@ static bool Level_LoadSamples(MYFILE *fp); static bool Level_LoadTexturePages(MYFILE *fp); static bool Level_LoadFromFile(const char *filename, int32_t level_num); -static void Level_CompleteSetup(void); +static void Level_CompleteSetup(int32_t level_num); static bool Level_LoadFromFile(const char *filename, int32_t level_num) { @@ -659,7 +660,7 @@ static bool Level_LoadTexturePages(MYFILE *fp) return true; } -static void Level_CompleteSetup(void) +static void Level_CompleteSetup(int32_t level_num) { Inject_AllInjections(&m_LevelInfo); @@ -675,6 +676,9 @@ static void Level_CompleteSetup(void) Item_Initialise(i); } + // Configure enemies who carry and drop items + Carrier_InitialiseLevel(level_num); + // Move the prepared texture pages into g_TexturePagePtrs. uint8_t *base = GameBuf_Alloc( m_LevelInfo.texture_page_count * PAGE_SIZE, GBUF_TEXTURE_PAGES); @@ -723,7 +727,7 @@ bool Level_Load(int level_num) Level_LoadFromFile(g_GameFlow.levels[level_num].level_file, level_num); if (ret) { - Level_CompleteSetup(); + Level_CompleteSetup(level_num); } Inject_Cleanup(); diff --git a/src/game/objects/creatures/baldy.c b/src/game/objects/creatures/baldy.c index df4719ee4..07eff5976 100644 --- a/src/game/objects/creatures/baldy.c +++ b/src/game/objects/creatures/baldy.c @@ -1,7 +1,6 @@ #include "game/objects/creatures/baldy.h" #include "game/creature.h" -#include "game/inventory.h" #include "game/items.h" #include "game/lot.h" #include "global/const.h" @@ -77,11 +76,6 @@ void Baldy_Control(int16_t item_num) if (item->current_anim_state != BALDY_DEATH) { item->current_anim_state = BALDY_DEATH; Item_SwitchToAnim(item, BALDY_DIE_ANIM, 0); - if (Inv_RequestItem(O_SHOTGUN_ITEM)) { - Item_Spawn(item, O_SG_AMMO_ITEM); - } else { - Item_Spawn(item, O_SHOTGUN_ITEM); - } } } else { AI_INFO info; diff --git a/src/game/objects/creatures/cowboy.c b/src/game/objects/creatures/cowboy.c index 1640a1e86..408a8791c 100644 --- a/src/game/objects/creatures/cowboy.c +++ b/src/game/objects/creatures/cowboy.c @@ -3,7 +3,6 @@ #include "game/creature.h" #include "game/effects.h" #include "game/effects/gunshot.h" -#include "game/inventory.h" #include "game/items.h" #include "game/lot.h" #include "global/const.h" @@ -74,11 +73,6 @@ void Cowboy_Control(int16_t item_num) if (item->current_anim_state != COWBOY_DEATH) { item->current_anim_state = COWBOY_DEATH; Item_SwitchToAnim(item, COWBOY_DIE_ANIM, 0); - if (Inv_RequestItem(O_MAGNUM_ITEM)) { - Item_Spawn(item, O_MAG_AMMO_ITEM); - } else { - Item_Spawn(item, O_MAGNUM_ITEM); - } } } else { AI_INFO info; diff --git a/src/game/objects/creatures/mummy.c b/src/game/objects/creatures/mummy.c index 9211ad389..4c1cde462 100644 --- a/src/game/objects/creatures/mummy.c +++ b/src/game/objects/creatures/mummy.c @@ -1,5 +1,6 @@ #include "game/objects/creatures/mummy.h" +#include "game/carrier.h" #include "game/creature.h" #include "game/gamebuf.h" #include "game/items.h" @@ -67,6 +68,9 @@ void Mummy_Control(int16_t item_num) g_GameInfo.current[g_CurrentLevel].stats.kill_count++; } Item_RemoveActive(item_num); + if (item->hit_points != DONT_TARGET) { + Carrier_TestItemDrops(item_num); + } item->hit_points = DONT_TARGET; } } diff --git a/src/game/objects/creatures/pierre.c b/src/game/objects/creatures/pierre.c index 29bea31c3..e9034a890 100644 --- a/src/game/objects/creatures/pierre.c +++ b/src/game/objects/creatures/pierre.c @@ -2,7 +2,6 @@ #include "config.h" #include "game/creature.h" -#include "game/inventory.h" #include "game/items.h" #include "game/los.h" #include "game/lot.h" @@ -114,13 +113,6 @@ void Pierre_Control(int16_t item_num) if (item->current_anim_state != PIERRE_DEATH) { item->current_anim_state = PIERRE_DEATH; Item_SwitchToAnim(item, PIERRE_DIE_ANIM, 0); - if (Inv_RequestItem(O_MAGNUM_ITEM)) { - Item_Spawn(item, O_MAG_AMMO_ITEM); - } else { - Item_Spawn(item, O_MAGNUM_ITEM); - } - Item_Spawn(item, O_SCION_ITEM2); - Item_Spawn(item, O_KEY_ITEM1); } } else { AI_INFO info; diff --git a/src/game/objects/creatures/skate_kid.c b/src/game/objects/creatures/skate_kid.c index f93ec7dcf..b5dfa1e39 100644 --- a/src/game/objects/creatures/skate_kid.c +++ b/src/game/objects/creatures/skate_kid.c @@ -1,7 +1,6 @@ #include "game/objects/creatures/skate_kid.h" #include "game/creature.h" -#include "game/inventory.h" #include "game/items.h" #include "game/lot.h" #include "game/music.h" @@ -86,11 +85,6 @@ void SkateKid_Control(int16_t item_num) if (item->current_anim_state != SKATE_KID_DEATH) { item->current_anim_state = SKATE_KID_DEATH; Item_SwitchToAnim(item, SKATE_KID_DIE_ANIM, 0); - if (Inv_RequestItem(O_UZI_ITEM)) { - Item_Spawn(item, O_UZI_AMMO_ITEM); - } else { - Item_Spawn(item, O_UZI_ITEM); - } } } else { AI_INFO info; diff --git a/src/game/savegame/savegame.c b/src/game/savegame/savegame.c index 8111092c7..badbf61cb 100644 --- a/src/game/savegame/savegame.c +++ b/src/game/savegame/savegame.c @@ -126,31 +126,14 @@ static void Savegame_LoadPostprocess(void) if (item->object_number == O_PIERRE && item->hit_points <= 0 && (item->flags & IF_ONESHOT)) { - if (Inv_RequestItem(O_SCION_ITEM) == 1) { - Item_Spawn(item, O_MAGNUM_ITEM); - Item_Spawn(item, O_SCION_ITEM2); - Item_Spawn(item, O_KEY_ITEM1); - } g_MusicTrackFlags[MX_PIERRE_SPEECH] |= IF_ONESHOT; } - if (item->object_number == O_SKATEKID && item->hit_points <= 0) { - if (!Inv_RequestItem(O_UZI_ITEM)) { - Item_Spawn(item, O_UZI_ITEM); - } - } - if (item->object_number == O_COWBOY && item->hit_points <= 0) { - if (!Inv_RequestItem(O_MAGNUM_ITEM)) { - Item_Spawn(item, O_MAGNUM_ITEM); - } g_MusicTrackFlags[MX_COWBOY_SPEECH] |= IF_ONESHOT; } if (item->object_number == O_BALDY && item->hit_points <= 0) { - if (!Inv_RequestItem(O_SHOTGUN_ITEM)) { - Item_Spawn(item, O_SHOTGUN_ITEM); - } g_MusicTrackFlags[MX_BALDY_SPEECH] |= IF_ONESHOT; } diff --git a/src/game/savegame/savegame_bson.c b/src/game/savegame/savegame_bson.c index cd1af627c..e21502092 100644 --- a/src/game/savegame/savegame_bson.c +++ b/src/game/savegame/savegame_bson.c @@ -1,6 +1,7 @@ #include "game/savegame/savegame_bson.h" #include "config.h" +#include "game/carrier.h" #include "game/effects.h" #include "game/gameflow.h" #include "game/inventory.h" @@ -586,6 +587,32 @@ static bool Savegame_BSON_LoadItems( } } } + + struct json_array_s *carried_items = + json_object_get_array(item_obj, "carried_items"); + if (carried_items) { + CARRIED_ITEM *carried_item = item->carried_item; + for (int j = 0; j < (signed)carried_items->length; j++) { + if (!carried_item) { + LOG_ERROR("Malformed save: carried item mismatch"); + return false; + } + + struct json_object_s *carried_item_obj = + json_array_get_object(carried_items, j); + + carried_item->object_id = json_object_get_int( + carried_item_obj, "object_id", carried_item->object_id); + carried_item->status = json_object_get_int( + carried_item_obj, "status", carried_item->status); + + carried_item = carried_item->next_item; + } + + Carrier_TestItemDrops(i); + } else if (header_version < VERSION_4) { + Carrier_TestLegacyDrops(i); + } } return true; @@ -1050,6 +1077,28 @@ static struct json_array_s *Savegame_BSON_DumpItems(void) } } + struct json_array_s *carried_items_arr = json_array_new(); + + CARRIED_ITEM *drop_item = item->carried_item; + while (drop_item) { + struct json_object_s *drop_obj = json_object_new(); + json_object_append_int(drop_obj, "object_id", drop_item->object_id); + + ITEM_STATUS status = drop_item->status; + if (status == IS_ACTIVE) { + ITEM_INFO *drop = &g_Items[drop_item->spawn_number]; + if (drop->status == IS_INVISIBLE) { + status = IS_INVISIBLE; + } + } + json_object_append_int(drop_obj, "status", status); + + json_array_append_object(carried_items_arr, drop_obj); + drop_item = drop_item->next_item; + } + + json_object_append_array(item_obj, "carried_items", carried_items_arr); + json_array_append_object(items_arr, item_obj); } return items_arr; diff --git a/src/game/savegame/savegame_legacy.c b/src/game/savegame/savegame_legacy.c index c67a3e13d..15824fcbe 100644 --- a/src/game/savegame/savegame_legacy.c +++ b/src/game/savegame/savegame_legacy.c @@ -1,5 +1,6 @@ #include "game/savegame/savegame_legacy.h" +#include "game/carrier.h" #include "game/effects.h" #include "game/gameflow.h" #include "game/inventory.h" @@ -600,6 +601,8 @@ bool Savegame_Legacy_LoadFromFile(MYFILE *fp, GAME_INFO *game_info) item->data = NULL; } } + + Carrier_TestLegacyDrops(i); } Savegame_Legacy_ReadLara(&g_Lara); diff --git a/src/game/stats.c b/src/game/stats.c index 37624106d..28b320811 100644 --- a/src/game/stats.c +++ b/src/game/stats.c @@ -1,6 +1,7 @@ #include "game/stats.h" #include "config.h" +#include "game/carrier.h" #include "game/clock.h" #include "game/game.h" #include "game/gamebuf.h" @@ -19,11 +20,6 @@ #include #include -#define PIERRE_ITEMS 3 -#define SKATEKID_ITEMS 1 -#define COWBOY_ITEMS 1 -#define BALDY_ITEMS 1 - typedef struct TOTAL_STATS { uint32_t timer; uint32_t death_count; @@ -70,6 +66,7 @@ static void Stats_TraverseFloor(void); static void Stats_CheckTriggers( ROOM_INFO *r, int room_num, int x_floor, int y_floor); static bool Stats_IsObjectKillable(int32_t obj_num); +static void Stats_IncludeKillableItem(int16_t item_num); static void Stats_TraverseFloor(void) { @@ -148,9 +145,7 @@ static void Stats_CheckTriggers( // Add Pierre pickup and kills if oneshot if (item->object_number == O_PIERRE && trig_flags & IF_ONESHOT) { - m_KillableItems[idx] = true; - m_LevelPickups += PIERRE_ITEMS; - m_LevelKillables += 1; + Stats_IncludeKillableItem(idx); } // Check for only valid pods @@ -160,26 +155,13 @@ static void Stats_CheckTriggers( int16_t bug_item_num = *(int16_t *)item->data; const ITEM_INFO *bug_item = &g_Items[bug_item_num]; if (g_Objects[bug_item->object_number].loaded) { - m_KillableItems[idx] = true; - m_LevelKillables += 1; + Stats_IncludeKillableItem(idx); } } // Add killable if object triggered if (Stats_IsObjectKillable(item->object_number)) { - m_KillableItems[idx] = true; - m_LevelKillables += 1; - - // Add mercenary pickups - if (item->object_number == O_SKATEKID) { - m_LevelPickups += SKATEKID_ITEMS; - } - if (item->object_number == O_COWBOY) { - m_LevelPickups += COWBOY_ITEMS; - } - if (item->object_number == O_BALDY) { - m_LevelPickups += BALDY_ITEMS; - } + Stats_IncludeKillableItem(idx); } } } while (!(trigger & END_BIT)); @@ -198,6 +180,13 @@ static bool Stats_IsObjectKillable(int32_t obj_num) return false; } +static void Stats_IncludeKillableItem(int16_t item_num) +{ + m_KillableItems[item_num] = true; + m_LevelKillables += 1; + m_LevelPickups += Carrier_GetItemCount(item_num); +} + static void Stats_ComputeTotal(GAMEFLOW_LEVEL_TYPE level_type) { memset(&m_TotalStats, 0, sizeof(TOTAL_STATS)); diff --git a/src/global/types.h b/src/global/types.h index 9e6126719..38de300be 100644 --- a/src/global/types.h +++ b/src/global/types.h @@ -1369,6 +1369,13 @@ typedef struct ROOM_INFO { uint16_t flags; } ROOM_INFO; +typedef struct CARRIED_ITEM { + GAME_OBJECT_ID object_id; + int16_t spawn_number; + enum ITEM_STATUS status; + struct CARRIED_ITEM *next_item; +} CARRIED_ITEM; + typedef struct ITEM_INFO { int32_t floor; uint32_t touch_bits; @@ -1391,6 +1398,7 @@ typedef struct ITEM_INFO { int16_t shade; void *data; void *priv; + CARRIED_ITEM *carried_item; PHD_3DPOS pos; uint16_t active : 1; uint16_t status : 2;