Skip to content

Commit

Permalink
tr1/carrier: add support for TR2-style item drops (#1719)
Browse files Browse the repository at this point in the history
This allows item drops to be defined in the level data in the same
fashion as TR2, so any pickup item that is in the same position as an
enemy will be carried by that enemy. The gameflow approach is retained
for OG levels, and the provided gameflow setting will default to this.

Resolves #1713.
  • Loading branch information
lahm86 authored Oct 16, 2024
1 parent 8b01389 commit 9f4b6fa
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 23 deletions.
1 change: 1 addition & 0 deletions data/tr1/ship/cfg/TR1X_gameflow.json5
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"data/injections/uzi_sfx.bin",
"data/injections/explosion.bin",
],
"enable_tr2_item_drops": false,
"convert_dropped_guns": false,

"levels": [
Expand Down
1 change: 1 addition & 0 deletions data/tr1/ship/cfg/TR1X_gameflow_demo_pc.json5
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"data/injections/uzi_sfx.bin",
"data/injections/explosion.bin",
],
"enable_tr2_item_drops": false,
"convert_dropped_guns": false,

"levels": [
Expand Down
1 change: 1 addition & 0 deletions data/tr1/ship/cfg/TR1X_gameflow_ub.json5
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"data/injections/uzi_sfx.bin",
"data/injections/explosion.bin",
],
"enable_tr2_item_drops": false,
"convert_dropped_guns": false,

"levels": [
Expand Down
1 change: 1 addition & 0 deletions docs/tr1/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr1-4.5.1...develop) - ××××-××-××
- fixed missing pushblock SFX in Natla's Mines (#1714)
- improved enemy item drops by supporting the TR2+ approach of having drops defined in level data (#1713)

## [4.5.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.5...tr1-4.5.1) - 2024-10-14
- fixed mac builds missing embedded resources (#1710, regression from 4.5)
Expand Down
26 changes: 22 additions & 4 deletions docs/tr1/GAMEFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ various pieces of global behaviour.
</ul>
</td>
</tr>
<tr valign="top">
<td>
<a name="enable-tr2-item-drops"></a>
<code>enable_tr2_item_drops</code>
</td>
<td>Boolean</td>
<td>No</td>
<td>
Forces enemies who are placed in the same position as pickup items to
carry those items and drop them when killed, similar to TR2+. See
<a href="#item-drops">Item drops</a> for full details.
</td>
</tr>
<tr valign="top">
<td>
<code>force_game_modes</code>
Expand Down Expand Up @@ -997,10 +1010,15 @@ to allow the _majority_ of enemy types to carry and drop items. Note that this
also means by default that the original enemies who did drop items will not do
so unless the gameflow has been configured as such.

Item drops are defined in the `item_drops` section of a level's definition by
creating objects with the following parameter structure. You can define at most
one entry per enemy, but that definition can have as many drop items as
necessary (within the engine's overall item limit).
Item drops can be defined in two ways. If `enable_tr2_item_drops` is `true`,
then custom level builders can add items directly to the level file, setting
their position to be the same as the enemies who should drop them.

For the original levels, `enable_tr2_item_drops` is `false`. Item drops are
instead defined in the `item_drops` section of a level's definition by creating
objects with the following parameter structure. You can define at most one entry
per enemy, but that definition can have as many drop items as necessary (within
the engine's overall item limit).

<details>
<summary>Show example setup</summary>
Expand Down
109 changes: 102 additions & 7 deletions src/tr1/game/carrier.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
#include "global/const.h"
#include "global/types.h"
#include "global/vars.h"
#include "math/math_misc.h"

#include <libtrx/log.h>
#include <libtrx/vector.h>

#include <stdbool.h>
#include <stddef.h>
Expand All @@ -23,7 +25,10 @@
static int16_t m_AnimatingCount = 0;

static ITEM *M_GetCarrier(int16_t item_num);
static CARRIED_ITEM *M_GetFirstDropItem(const ITEM *carrier);
static void M_AnimateDrop(CARRIED_ITEM *item);
static void M_InitialiseDataDrops(void);
static void M_InitialiseGameflowDrops(int32_t level_num);

static const GAME_OBJECT_PAIR m_LegacyMap[] = {
{ O_PIERRE, O_SCION_ITEM_2 }, { O_COWBOY, O_MAGNUM_ITEM },
Expand Down Expand Up @@ -53,6 +58,19 @@ static ITEM *M_GetCarrier(const int16_t item_num)
return item;
}

static CARRIED_ITEM *M_GetFirstDropItem(const ITEM *const carrier)
{
if (carrier->hit_points > 0 && carrier->object_id != O_MUMMY) {
return NULL;
}

if (carrier->object_id == O_PIERRE && !(carrier->flags & IF_ONE_SHOT)) {
return NULL;
}

return carrier->carried_item;
}

static void M_AnimateDrop(CARRIED_ITEM *const item)
{
if (item->status != DS_FALLING) {
Expand Down Expand Up @@ -96,10 +114,60 @@ static void M_AnimateDrop(CARRIED_ITEM *const item)
item->fall_speed = pickup->fall_speed;
}

void Carrier_InitialiseLevel(const int32_t level_num)
static void M_InitialiseDataDrops(void)
{
m_AnimatingCount = 0;
VECTOR *const pickups = Vector_Create(sizeof(int16_t));

for (int32_t i = 0; i < g_LevelItemCount; i++) {
ITEM *const carrier = M_GetCarrier(i);
if (carrier == NULL
|| !Object_IsObjectType(carrier->object_id, g_EnemyObjects)) {
continue;
}

const ROOM *const room = Room_Get(carrier->room_num);
int16_t pickup_num = room->item_num;
do {
ITEM *const pickup = Item_Get(pickup_num);
if (Object_IsObjectType(pickup->object_id, g_PickupObjects)
&& XYZ_32_AreEquivalent(&pickup->pos, &carrier->pos)) {
Vector_Add(pickups, (void *)&pickup_num);
Item_RemoveDrawn(pickup_num);
pickup->room_num = NO_ROOM;
}

pickup_num = pickup->next_item;
} while (pickup_num != NO_ITEM);

if (pickups->count == 0) {
continue;
}

carrier->carried_item =
GameBuf_Alloc(sizeof(CARRIED_ITEM) * pickups->count, GBUF_ITEMS);
CARRIED_ITEM *drop = carrier->carried_item;
for (int32_t j = 0; j < pickups->count; j++) {
drop->spawn_num = *(const int16_t *)Vector_Get(pickups, j);
drop->room_num = NO_ROOM;
drop->fall_speed = 0;
drop->status = DS_CARRIED;

if (j < pickups->count - 1) {
drop->next_item = drop + 1;
drop++;
} else {
drop->next_item = NULL;
}
}

Vector_Clear(pickups);
}

Vector_Free(pickups);
}

static void M_InitialiseGameflowDrops(const int32_t level_num)
{
int32_t total_item_count = g_LevelItemCount;
const GAMEFLOW_LEVEL level = g_GameFlow.levels[level_num];
for (int32_t i = 0; i < level.item_drops.count; i++) {
Expand Down Expand Up @@ -157,6 +225,16 @@ void Carrier_InitialiseLevel(const int32_t level_num)
}
}

void Carrier_InitialiseLevel(const int32_t level_num)
{
m_AnimatingCount = 0;
if (g_GameFlow.enable_tr2_item_drops) {
M_InitialiseDataDrops();
} else {
M_InitialiseGameflowDrops(level_num);
}
}

int32_t Carrier_GetItemCount(const int16_t item_num)
{
const ITEM *const carrier = M_GetCarrier(item_num);
Expand All @@ -176,6 +254,14 @@ int32_t Carrier_GetItemCount(const int16_t item_num)
return count;
}

bool Carrier_IsItemCarried(const int16_t item_num)
{
// This only applies to TR2-style drops; gameflow drop item numbers are not
// assigned until they are dropped, so this would always logically be false.
const ITEM *item = Item_Get(item_num);
return item->room_num == NO_ROOM;
}

DROP_STATUS Carrier_GetSaveStatus(const CARRIED_ITEM *item)
{
// This allows us to save drops as still being carried to allow accurate
Expand All @@ -193,10 +279,8 @@ DROP_STATUS Carrier_GetSaveStatus(const CARRIED_ITEM *item)
void Carrier_TestItemDrops(const int16_t item_num)
{
const ITEM *const carrier = Item_Get(item_num);
CARRIED_ITEM *item = carrier->carried_item;
if (carrier->hit_points > 0 || !item
|| (carrier->object_id == O_PIERRE
&& !(carrier->flags & IF_ONE_SHOT))) {
CARRIED_ITEM *item = M_GetFirstDropItem(carrier);
if (item == NULL) {
return;
}

Expand All @@ -215,7 +299,18 @@ void Carrier_TestItemDrops(const int16_t item_num)
object_id = Object_GetCognate(object_id, g_GunAmmoObjectMap);
}

item->spawn_num = Item_Spawn(carrier, object_id);
if (item->spawn_num == NO_ITEM) {
// This is a gameflow-defined drop, so a spawn number is required.
item->spawn_num = Item_Spawn(carrier, object_id);
} else {
// TR2-style item drops will already have a spawn number.
Item_NewRoom(item->spawn_num, carrier->room_num);
ITEM *const pickup = Item_Get(item->spawn_num);
pickup->pos = carrier->pos;
pickup->rot = carrier->rot;
pickup->status = IS_INACTIVE;
}

item->status = DS_FALLING;
m_AnimatingCount++;

Expand Down
1 change: 1 addition & 0 deletions src/tr1/game/carrier.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

void Carrier_InitialiseLevel(int32_t level_num);
int32_t Carrier_GetItemCount(int16_t item_num);
bool Carrier_IsItemCarried(int16_t item_num);
void Carrier_TestItemDrops(int16_t item_num);
void Carrier_TestLegacyDrops(int16_t item_num);
DROP_STATUS Carrier_GetSaveStatus(const CARRIED_ITEM *item);
Expand Down
12 changes: 9 additions & 3 deletions src/tr1/game/gameflow.c
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ static bool M_LoadScriptMeta(JSON_OBJECT *obj)
g_GameFlow.injections.length = 0;
}

g_GameFlow.enable_tr2_item_drops =
JSON_ObjectGetBool(obj, "enable_tr2_item_drops", false);
g_GameFlow.convert_dropped_guns =
JSON_ObjectGetBool(obj, "convert_dropped_guns", false);

Expand Down Expand Up @@ -808,7 +810,13 @@ static bool M_LoadScriptLevels(JSON_OBJECT *obj)
cur->lara_type = (GAME_OBJECT_ID)tmp_i;

tmp_arr = JSON_ObjectGetArray(jlvl_obj, "item_drops");
if (tmp_arr) {
cur->item_drops.count = 0;
if (tmp_arr && g_GameFlow.enable_tr2_item_drops) {
LOG_WARNING(
"TR2 item drops are enabled: gameflow-defined drops for level "
"%d will be ignored",
level_num);
} else 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);
Expand Down Expand Up @@ -849,8 +857,6 @@ static bool M_LoadScriptLevels(JSON_OBJECT *obj)
data->object_ids[j] = (int16_t)id;
}
}
} else {
cur->item_drops.count = 0;
}

if (!M_LoadLevelSequence(jlvl_obj, level_num)) {
Expand Down
1 change: 1 addition & 0 deletions src/tr1/game/gameflow.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ typedef struct {
int length;
char **data_paths;
} injections;
bool enable_tr2_item_drops;
bool convert_dropped_guns;
} GAMEFLOW;

Expand Down
18 changes: 10 additions & 8 deletions src/tr1/game/items.c
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,16 @@ void Item_NewRoom(int16_t item_num, int16_t room_num)
ITEM *item = &g_Items[item_num];
ROOM *r = &g_RoomInfo[item->room_num];

int16_t linknum = r->item_num;
if (linknum == item_num) {
r->item_num = item->next_item;
} else {
for (; linknum != NO_ITEM; linknum = g_Items[linknum].next_item) {
if (g_Items[linknum].next_item == item_num) {
g_Items[linknum].next_item = item->next_item;
break;
if (item->room_num != NO_ROOM) {
int16_t linknum = r->item_num;
if (linknum == item_num) {
r->item_num = item->next_item;
} else {
for (; linknum != NO_ITEM; linknum = g_Items[linknum].next_item) {
if (g_Items[linknum].next_item == item_num) {
g_Items[linknum].next_item = item->next_item;
break;
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/tr1/game/stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ void Stats_CalculateStats(void)
continue;
}

if (Object_IsObjectType(item->object_id, g_PickupObjects)) {
if (Object_IsObjectType(item->object_id, g_PickupObjects)
&& !Carrier_IsItemCarried(i)) {
m_LevelPickups++;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/tr1/math/math_misc.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,8 @@ int32_t XYZ_32_GetDistance(const XYZ_32 *const pos1, const XYZ_32 *const pos2)
);
// clang-format on
}

bool XYZ_32_AreEquivalent(const XYZ_32 *const pos1, const XYZ_32 *const pos2)
{
return pos1->x == pos2->x && pos1->y == pos2->y && pos1->z == pos2->z;
}
1 change: 1 addition & 0 deletions src/tr1/math/math_misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ void Math_GetVectorAngles(int32_t x, int32_t y, int32_t z, int16_t *dest);
int32_t Math_AngleInCone(int32_t angle1, int32_t angle2, int32_t cone);
int32_t Math_AngleMean(int32_t angle1, int32_t angle2, double ratio);
int32_t XYZ_32_GetDistance(const XYZ_32 *pos1, const XYZ_32 *pos2);
bool XYZ_32_AreEquivalent(const XYZ_32 *pos1, const XYZ_32 *pos2);

0 comments on commit 9f4b6fa

Please sign in to comment.