diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e123eab748..7bd666036e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,99 +1,85 @@ name: Bug report -description: Create a report to help us understand and diagnose your issue. Your contribution is welcomed and valued! It is highly recommended that you are using the latest version before submitting a bug report. -title: "[ bug Report ]" -labels: Awaiting Triage +description: Create a report to help us understand and triage your issue. +labels: + - Awaiting Triage body: + - type: markdown + attributes: + value: | + ## Bug Report -- type: markdown - attributes: - value: | - Please follow this document in order to report a bug. It is highly recommended that you are using the latest version before submitting a bug report. + Please follow this document carefully to report a bug. -- type: dropdown - attributes: - label: TombEngine version - description: | - Please select the TombEngine Version from the dropdown list. - options: - - Development Build - - v1.8.0 (latest version) - - v1.7.1 - - v1.7.0 - - v1.5 - validations: - required: true + > **Important**: It is highly recommended that you use the latest version before submitting a bug report. -- type: dropdown - attributes: - label: Tomb Editor version - description: | - Please select the Tomb Editor version used from the dropdown list. - options: - - Development Build - - v1.8.0 (latest version) - - v1.7.2 - - v1.7.1 - - v1.7.0 - validations: - required: true + - type: dropdown + attributes: + label: Tomb Engine Version + description: | + Please select the TombEngine version you are using. + options: + - Development build + - v1.11 (latest public release) + - v1.10.1 + validations: + required: true -- type: textarea - attributes: - label: Describe the bug - description: | - Please provide A clear and concise description of what the bug is. - placeholder: | - Your bug report here. - validations: - required: true + - type: checkboxes + attributes: + label: Development Version + description: Are you submitting this report from a development build that has not been officially released? + options: + - label: "I am using an unofficial development version." + - label: "I am using an official release." + - label: "I am using an official pre-release." + validations: + required: true -- type: textarea - attributes: - label: To Reproduce - description: | - To reproduce the behaviour, please provide detailed steps for the development team to follow. This can be done through screenshots or a written guide + - type: textarea + attributes: + label: Describe the Bug + description: | + Please provide a clear and concise description of what the issue is. + placeholder: | + Your bug report here. + validations: + required: true - **If the bug cannot be reproduced, and if the issue is not adequately explained, it will be closed without further investigation** - placeholder: | - Provide detailed reproducible steps here. - validations: - required: true + - type: textarea + attributes: + label: To Reproduce + description: | + Please provide detailed steps to reproduce the issue. + + **Note**: If the bug cannot be reproduced or the issue is not clearly explained, it may be closed without further investigation. + placeholder: | + Step-by-step reproduction instructions here. + validations: + required: true -- type: textarea - attributes: - label: Expected Behaviour - description: | - A clear and concise description of what you expected to happen. - placeholder: | - A description of what should happen here. - validations: - required: true + - type: textarea + attributes: + label: Expected Behaviour + description: | + What did you expect to happen? -- type: textarea - attributes: - label: Additional Content - description: | - Add any other context about the problem here. + **Note**: If the bug cannot be reproduced or the issue is not clearly explained, it may be closed without further investigation. + placeholder: | + A description of what should happen here. + validations: + required: true - * Are you testing an build of a TombEngine that has not yet been released? If so please give some context. - * Did you get any asset from the TombEngine website that has presented a bug? - placeholder: | - A description of any additional content here. - validations: - required: false - -- type: textarea - attributes: - label: Minimal reproduction project - description: | - **Please upload a .zip file containing your level and all assets needed to compile the level and a cut-down version of your level where the bug presents itself** - The project can be uploaded as a zip file (10 mb max) or provide a link from google drive, dropbox etc. - **Note** if you do not provide this, your issue may be rejected - placeholder: | - Download link to your project - validations: - required: true - + - type: textarea + attributes: + label: Minimal Reproduction Project + description: | + Please upload a .zip file (10 MB max) containing your level and all assets needed to compile the level, including a minimal version where the bug occurs. + Alternatively, provide a download link from a cloud storage service (e.g., Google Drive, Dropbox). + > **Important**: If you do not provide a minimal reproduction project, your issue may be rejected. + placeholder: | + Download link to your project or attach a .zip file. + validations: + required: true diff --git a/.github/workflows/cross-repo-dependency.yml b/.github/workflows/cross-repo-dependency.yml new file mode 100644 index 0000000000..afad434580 --- /dev/null +++ b/.github/workflows/cross-repo-dependency.yml @@ -0,0 +1,87 @@ +name: Cross repo dependency + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + pull_request_review: + types: [submitted] + +jobs: + check-linked-pr: + runs-on: ubuntu-latest + steps: + - name: Check for dependency + id: parse + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ""; + const match = body.match(/Depends on:\s+([\w-]+)\/([\w-]+)#(\d+)/); + + if (!match) { + // No dependency → do NOT create a status check + core.setOutput("skip", "true"); + return; + } + + core.setOutput("skip", "false"); + core.setOutput("depOwner", match[1]); + core.setOutput("depRepo", match[2]); + core.setOutput("depNumber", match[3]); + + - name: Exit early if no dependency + if: steps.parse.outputs.skip == 'true' + run: echo "No dependency found — skipping." + + - name: Check linked PR status + if: steps.parse.outputs.skip == 'false' + id: check + uses: actions/github-script@v7 + with: + script: | + const owner = steps.parse.outputs.depOwner; + const repo = steps.parse.outputs.depRepo; + const number = Number(steps.parse.outputs.depNumber); + + const linked = await github.rest.pulls.get({ + owner, + repo, + pull_number: number + }); + + const reviews = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: number + }); + + const approved = reviews.data.some(r => r.state === "APPROVED"); + const mergeable = linked.data.mergeable === true; + + core.setOutput("approved", approved); + core.setOutput("mergeable", mergeable); + + - name: Set dependency status + if: steps.parse.outputs.skip == 'false' + uses: actions/github-script@v7 + with: + script: | + const approved = steps.check.outputs.approved === 'true'; + const mergeable = steps.check.outputs.mergeable === 'true'; + + let state = "pending"; + let description = "Waiting for linked PR to be approved and mergeable"; + + if (approved && mergeable) { + state = "success"; + description = "Linked PR is approved and mergeable"; + } + + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.payload.pull_request.head.sha, + state, + context: "cross-repo-dependency", + description + }); diff --git a/README.md b/README.md index 5f699f1622..88b45dd54d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - *Lua* as the native scripting language. - Many objects from the original series (1-5). - Support for high framerate, antialiasing, mipmapping, and SSAO. +- Full skinning support for all objects. - Full diagonal geometry support. - Uncapped map size. - A streamlined player control scheme. @@ -38,4 +39,4 @@ Once done, you should be able to build a level with *Tomb Editor* and run it in Contributions are welcome. If you would like to participate in development to any degree, whether that be through suggestions, bug reports, or code, join our [Discord server](https://discord.gg/h5tUYFmres). # Disclaimer -Tomb Engine uses modified MIT license for non-commercial use only. For more information, see [license](https://github.com/TombEngine/TombEngine?tab=License-1-ov-file#readme). Tomb Engine is unaffiliated with the Crystal Dynamics group of companies or Embracer Group AB. *Tomb Raider* is a trademark of the Crystal Dynamics group of companies. Tomb Engine team is not responsible for illegal use of this source code and built binaries alone or in combination with third-party assets or components. This source code is released as-is and continues to be maintained by non-paid contributors in their free time. \ No newline at end of file +Tomb Engine uses modified MIT license for non-commercial use only. For more information, see [license](https://github.com/TombEngine/TombEngine?tab=License-1-ov-file#readme). Tomb Engine is unaffiliated with the Crystal Dynamics group of companies or Embracer Group AB. *Tomb Raider* is a trademark of the Crystal Dynamics group of companies. Tomb Engine team is not responsible for illegal use of this source code and built binaries alone or in combination with third-party assets or components. This source code is released as-is and continues to be maintained by non-paid contributors in their free time. diff --git a/TombEngine/Game/control/box.cpp b/TombEngine/Game/control/box.cpp index a7a122c498..00c0745595 100644 --- a/TombEngine/Game/control/box.cpp +++ b/TombEngine/Game/control/box.cpp @@ -903,6 +903,15 @@ bool CreaturePathfind(ItemInfo* item, Vector3i prevPos, short angle, short tilt) else item->Pose.Orientation.y += BIFF_AVOID_TURN; + // Update floor height to prevent sinking through geometry when nudged by another creature. + if (LOT->Fly == NO_FLYING) + { + floor = GetFloor(item->Pose.Position.x, item->Pose.Position.y, item->Pose.Position.z, &roomNumber); + item->Floor = GetFloorHeight(floor, item->Pose.Position.x, item->Pose.Position.y, item->Pose.Position.z); + if (item->Pose.Position.y > item->Floor) + item->Pose.Position.y = item->Floor; + } + return true; } @@ -1086,8 +1095,11 @@ bool CreaturePathfind(ItemInfo* item, Vector3i prevPos, short angle, short tilt) floor = GetFloor(item->Pose.Position.x, item->Pose.Position.y, item->Pose.Position.z, &roomNumber); item->Floor = GetFloorHeight(floor, item->Pose.Position.x, item->Pose.Position.y, item->Pose.Position.z); - // Snap to floor or smoothly descend. - if (item->Pose.Position.y > item->Floor) + // Snap to floor or smoothly ascend/descend. + int heightDiff = item->Pose.Position.y - item->Floor; + if (heightDiff > CLICK(0.25f) && heightDiff <= CLICK(1)) + item->Pose.Position.y -= CLICK(0.25f); + else if (item->Pose.Position.y > item->Floor) item->Pose.Position.y = item->Floor; else if (item->Floor - item->Pose.Position.y > CLICK(0.25f)) item->Pose.Position.y += CLICK(0.25f); @@ -2144,6 +2156,31 @@ int CreatureVault(short itemNumber, short angle, int vault, int shift) auto* item = &g_Level.Items[itemNumber]; auto* creature = GetCreatureInfo(item); + // Apply a forward pivot offset when stepping up so the vault triggers when the + // creature's front half enters the next sector rather than its body centre. + int pivotOffsetX = 0; + int pivotOffsetZ = 0; + if (item->BoxNumber != NO_VALUE) + { + int nextBox = creature->LOT.Node[item->BoxNumber].exitBox; + if (nextBox != NO_VALUE && + g_Level.PathfindingBoxes[nextBox].height < g_Level.PathfindingBoxes[item->BoxNumber].height) + { + int stepHeight = g_Level.PathfindingBoxes[item->BoxNumber].height - g_Level.PathfindingBoxes[nextBox].height; + if (stepHeight <= CLICK(1)) + { + int forwardExtent = GameBoundingBox(item->ObjectNumber, item->Animation.AnimNumber, item->Animation.FrameNumber).Z2 / 2; + if (forwardExtent > 0) + { + pivotOffsetX = (int)(phd_sin(item->Pose.Orientation.y) * forwardExtent); + pivotOffsetZ = (int)(phd_cos(item->Pose.Orientation.y) * forwardExtent); + item->Pose.Position.x += pivotOffsetX; + item->Pose.Position.z += pivotOffsetZ; + } + } + } + } + int xBlock = item->Pose.Position.x / BLOCK(1); int zBlock = item->Pose.Position.z / BLOCK(1); int y = item->Pose.Position.y; @@ -2169,6 +2206,8 @@ int CreatureVault(short itemNumber, short angle, int vault, int shift) } else if (item->Pose.Position.y > (y - CLICK(1.5f))) { + item->Pose.Position.x -= pivotOffsetX; + item->Pose.Position.z -= pivotOffsetZ; return 0; } else if (item->Pose.Position.y > (y - CLICK(2.5f))) @@ -2191,7 +2230,11 @@ int CreatureVault(short itemNumber, short angle, int vault, int shift) if (zBlock == newZblock) { if (xBlock == newXblock) + { + item->Pose.Position.x -= pivotOffsetX; + item->Pose.Position.z -= pivotOffsetZ; return 0; + } if (xBlock < newXblock) { diff --git a/TombEngine/Objects/TR3/Entity/tr3_monkey.cpp b/TombEngine/Objects/TR3/Entity/tr3_monkey.cpp index b1eee50f53..8615c5c679 100644 --- a/TombEngine/Objects/TR3/Entity/tr3_monkey.cpp +++ b/TombEngine/Objects/TR3/Entity/tr3_monkey.cpp @@ -20,6 +20,11 @@ namespace TEN::Entities::Creatures::TR3 // TODO: Work out damage constants. constexpr auto MONKEY_SWIPE_ATTACK_PLAYER_DAMAGE = 40; constexpr auto MONKEY_SWIPE_ATTACK_CREATURE_DAMAGE = 20; + constexpr auto MONKEY_PICKUP_FRAME = 12; + constexpr auto MONKEY_MESH_NORMAL = ALL_JOINT_BITS; + constexpr auto MONKEY_MESH_MEDIPACK = 0xFFFFFEFF; + constexpr auto MONKEY_MESH_KEY = 0xFFFF6E6F; + constexpr auto MONKEY_MESH_KEY_EMPTY = 0xFFFF6F6F; // TODO: Range constants. @@ -85,6 +90,73 @@ namespace TEN::Entities::Creatures::TR3 MONKEY_ANIM_WALK_FORWARD_TO_IDLE = 30 }; + bool IsMonkeyPickupTarget(ItemInfo* target, GAME_OBJECT_ID objectNumber, CreatureInfo* creature) + { + if (target->ObjectNumber != objectNumber) + return false; + + if (target->RoomNumber == NO_VALUE || target->AIBits) + return false; + + if (target->Status == ITEM_INVISIBLE || target->Flags & IFLAG_CLEAR_BODY) + return false; + + return SameZone(creature, target); + } + + bool IsMonkeyPickupInSameBox(ItemInfo* item, CreatureInfo* creature) + { + auto* enemy = creature->Enemy; + if (enemy == nullptr) + return false; + + return item->BoxNumber == enemy->BoxNumber; + } + + void UpdateMonkeyPickupTarget(ItemInfo* item, CreatureInfo* creature) + { + if (item->CarriedItem != NO_VALUE) + return; + + auto targetObjectNumber = (item->AIBits == MODIFY) ? ID_KEY_ITEM4 : ID_SMALLMEDI_ITEM; + + if (creature->Enemy && IsMonkeyPickupTarget(creature->Enemy, targetObjectNumber, creature)) + return; + + for (int i = 0; i < g_Level.NumItems; i++) + { + auto* target = &g_Level.Items[i]; + + if (IsMonkeyPickupTarget(target, targetObjectNumber, creature)) + { + creature->Enemy = target; + return; + } + } + } + + void ApplyMonkeyMeshSwap(ItemInfo* item, unsigned int swapMask) + { + auto meshSwapObjectNumber = (item->AIBits == MODIFY) ? + ID_MESHSWAP_MONKEY_KEY : + ID_MESHSWAP_MONKEY_MEDIPACK; + + const auto& meshSwapObject = Objects[meshSwapObjectNumber]; + if (!meshSwapObject.loaded) + { + item->ResetModelToDefault(); + return; + } + + for (int i = 0; i < item->Model.MeshIndex.size(); i++) + { + if (swapMask & (1 << i)) + item->Model.MeshIndex[i] = item->Model.BaseMesh + i; + else + item->Model.MeshIndex[i] = meshSwapObject.meshIndex + i; + } + } + void InitializeMonkey(short itemNumber) { auto* item = &g_Level.Items[itemNumber]; @@ -110,61 +182,36 @@ namespace TEN::Entities::Creatures::TR3 { if (item->Animation.ActiveState != MONKEY_STATE_DEATH) { + item->ResetModelToDefault(); SetAnimation(item, MONKEY_ANIM_DEATH); item->MeshBits = ALL_JOINT_BITS; } } else { - GetAITarget(creature); + if (item->AIBits & AMBUSH) + { + if (creature->Enemy == nullptr || creature->Enemy->ObjectNumber != ID_AI_AMBUSH) + FindAITargetObject(creature, ID_AI_AMBUSH, 0, true); + } + else + { + GetAITarget(creature); + } if (creature->HurtByLara) creature->Enemy = LaraItem; else - { - creature->Enemy = nullptr; - int minDistance = INT_MAX; - - for (auto creatureIndex : ActiveCreatures) - { - auto* currentCreature = GetCreatureInfo(&g_Level.Items[creatureIndex]); - - if (currentCreature->ItemNumber == NO_VALUE || currentCreature->ItemNumber == itemNumber) - continue; - - auto* target = &g_Level.Items[currentCreature->ItemNumber]; - if (target->ObjectNumber == ID_LARA || target->ObjectNumber == ID_MONKEY) - continue; - - if (target->ObjectNumber == ID_SMALLMEDI_ITEM) - { - int x = target->Pose.Position.x - item->Pose.Position.x; - int z = target->Pose.Position.z - item->Pose.Position.z; - int distance = pow(x, 2) + pow(z, 2); + UpdateMonkeyPickupTarget(item, creature); - if (distance < minDistance) - { - creature->Enemy = target; - minDistance = distance; - } - } - } - } + auto swapMask = MONKEY_MESH_NORMAL; + if (item->AIBits == MODIFY) + swapMask = (item->CarriedItem != NO_VALUE) ? MONKEY_MESH_KEY : MONKEY_MESH_KEY_EMPTY; + else if (item->CarriedItem != NO_VALUE) + swapMask = MONKEY_MESH_MEDIPACK; - if (item->AIBits != MODIFY) - { - if (item->CarriedItem != NO_VALUE) - item->MeshBits = 0xFFFFFEFF; - else - item->MeshBits = ALL_JOINT_BITS; - } - else - { - if (item->CarriedItem != NO_VALUE) - item->MeshBits = 0xFFFF6E6F; - else - item->MeshBits = 0xFFFF6F6F; - } + item->MeshBits = ALL_JOINT_BITS; + ApplyMonkeyMeshSwap(item, swapMask); AI_INFO AI; CreatureAIInfo(item, &AI); @@ -186,7 +233,7 @@ namespace TEN::Entities::Creatures::TR3 int dx = LaraItem->Pose.Position.x - item->Pose.Position.x; int dz = LaraItem->Pose.Position.z - item->Pose.Position.z; - laraAI.angle = phd_atan(dz, dz) - item->Pose.Orientation.y; + laraAI.angle = phd_atan(dz, dx) - item->Pose.Orientation.y; laraAI.distance = pow(dx, 2) + pow(dz, 2); } @@ -349,11 +396,11 @@ namespace TEN::Entities::Creatures::TR3 break; else if ((creature->Enemy->ObjectNumber == ID_SMALLMEDI_ITEM || creature->Enemy->ObjectNumber == ID_KEY_ITEM4) && - item->Animation.FrameNumber == 12) + item->Animation.FrameNumber == MONKEY_PICKUP_FRAME) { if (creature->Enemy->RoomNumber == NO_VALUE || creature->Enemy->Status == ITEM_INVISIBLE || - creature->Enemy->Flags & -32768) + creature->Enemy->Flags & IFLAG_CLEAR_BODY) { creature->Enemy = nullptr; } @@ -386,7 +433,8 @@ namespace TEN::Entities::Creatures::TR3 } } else if (creature->Enemy->ObjectNumber == ID_AI_AMBUSH && - item->Animation.FrameNumber == 12) + item->Animation.FrameNumber == MONKEY_PICKUP_FRAME && + item->CarriedItem != NO_VALUE) { item->AIBits = 0; @@ -430,9 +478,11 @@ namespace TEN::Entities::Creatures::TR3 if (Random::TestProbability(1 / 128.0f)) item->Animation.TargetState = MONKEY_STATE_SIT; } + else if (IsMonkeyPickupInSameBox(item, creature)) + item->Animation.TargetState = MONKEY_STATE_IDLE; else if (AI.bite && AI.distance < pow(682, 2)) item->Animation.TargetState = MONKEY_STATE_IDLE; - + break; case MONKEY_STATE_RUN_FORWARD: diff --git a/TombEngine/Objects/TR3/tr3_objects.cpp b/TombEngine/Objects/TR3/tr3_objects.cpp index 99ea154209..fc971d0ac9 100644 --- a/TombEngine/Objects/TR3/tr3_objects.cpp +++ b/TombEngine/Objects/TR3/tr3_objects.cpp @@ -230,6 +230,7 @@ static void StartEntity(ObjectInfo* obj) obj->HitPoints = 8; obj->radius = 102; obj->intelligent = true; + obj->LotType = LotType::Human; obj->pivotLength = 0; obj->SetBoneRotationFlags(0, ROT_X | ROT_Y); obj->SetBoneRotationFlags(7, ROT_Y);