diff --git a/files.cmake b/files.cmake index 0834dc9f0d..9f10b09e29 100644 --- a/files.cmake +++ b/files.cmake @@ -1513,6 +1513,9 @@ set(DUSK_FILES src/dusk/mods/manifest.cpp src/dusk/mods/manifest.hpp src/dusk/mods/svc/actor.cpp + src/dusk/mods/svc/actor_attribute.cpp + src/dusk/mods/svc/actor_attribute.hpp + src/dusk/mods/svc/actor_attribute_helpers.hpp src/dusk/mods/svc/audio_res/audio_res.hpp src/dusk/mods/svc/audio_res/audio_res.cpp src/dusk/mods/svc/audio_res/bst.cpp diff --git a/sdk/include/mods/svc/actor_attribute.h b/sdk/include/mods/svc/actor_attribute.h new file mode 100644 index 0000000000..d52fda8656 --- /dev/null +++ b/sdk/include/mods/svc/actor_attribute.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define ACTOR_ATTRIBUTE_SERVICE_ID "dev.twilitrealm.dusklight.actor_attribute" +#define ACTOR_ATTRIBUTE_SERVICE_MAJOR 1u +#define ACTOR_ATTRIBUTE_SERVICE_MINOR 5u + +/* 0 is never a valid handle. */ +typedef uint64_t ActorAttributeResolverHandle; + +/* + * Semantic actor values exposed by the host. A game integration should pass a + * freshly calculated vanilla value immediately before it is consumed. Mods + * may then transform that value without replacing the owning actor function. + */ +typedef enum ActorAttribute { + ACTOR_ATTRIBUTE_MOVEMENT_SPEED = 0, + ACTOR_ATTRIBUTE_SIZE = 1, + ACTOR_ATTRIBUTE_HEALTH = 2, + ACTOR_ATTRIBUTE_ATTACK_DAMAGE = 3, + /* Per-frame downward acceleration magnitude, not a stored actor field. */ + ACTOR_ATTRIBUTE_GRAVITY = 4, + /* Enemy awareness/aggro distance, separate from physical reach. */ + ACTOR_ATTRIBUTE_NOTICE_RANGE = 5, + /* Horizontal distance Link is pushed when this actor damages him. */ + ACTOR_ATTRIBUTE_PLAYER_KNOCKBACK = 6, + /* Duration of a genuine living-enemy stun/vulnerability window. */ + ACTOR_ATTRIBUTE_STUN_DURATION = 7, +} ActorAttribute; + +/* Host-owned callback data, valid only for the duration of the callback. */ +typedef struct ActorAttributeInfo { + const void* actor; /* const fopAc_ac_c* */ + ActorAttribute attribute; + float vanilla_value; + float current_value; +} ActorAttributeInfo; + +/* + * Return true and write out_value to replace current_value, or return false to + * leave it unchanged. Resolvers are chained in mod load order. + */ +typedef bool (*ActorAttributeResolveFn)(ModContext* ctx, const ActorAttributeInfo* info, + float* out_value, void* user_data); + +typedef struct ActorAttributeService { + ServiceHeader header; + + ModResult (*register_resolver)(ModContext* ctx, ActorAttributeResolveFn fn, void* user_data, + ActorAttributeResolverHandle* out_handle); + + ModResult (*unregister_resolver)(ModContext* ctx, ActorAttributeResolverHandle handle); + + /* An actor-specific mod hook asks for the currently resolved semantic + * value where it is actually used. No actor-source switch is required. */ + float (*resolve)(const void* actor, ActorAttribute attribute, float vanilla_value); +} ActorAttributeService; + +MOD_DECLARE_SERVICE(ActorAttributeService, svc_actor_attribute, ACTOR_ATTRIBUTE_SERVICE_ID, + ACTOR_ATTRIBUTE_SERVICE_MAJOR, ACTOR_ATTRIBUTE_SERVICE_MINOR); diff --git a/src/d/actor/d_a_L7op_demo_dr.cpp b/src/d/actor/d_a_L7op_demo_dr.cpp index 3b3cb0a2db..6d44c7e464 100644 --- a/src/d/actor/d_a_L7op_demo_dr.cpp +++ b/src/d/actor/d_a_L7op_demo_dr.cpp @@ -6,12 +6,75 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep #include "d/actor/d_a_L7op_demo_dr.h" +#if TARGET_PC // additional actor attribute integration +#include "dusk/mods/svc/actor_attribute_helpers.hpp" +#endif #include "d/actor/d_a_player.h" #include "d/d_com_inf_game.h" #include "d/d_s_play.h" #include "d/actor/d_a_npc4.h" #include "f_op/f_op_camera_mng.h" #include "f_op/f_op_msg_mng.h" +#if TARGET_PC // enemy attribute integration + +namespace actor_attr = dusk::mods::svc::actor_attr; + +namespace dusk::mods::svc::actor_attr { + +/* The opening dragon is a temporary child of the real Argorok actor. */ +template <> +struct EnemyAttributeOwner { + static fopAc_ac_c* get(daL7ODR_c* i_this) { + + fopAc_ac_c* parent = fopAcM_SearchByID(i_this->parentActorID); + if (parent != NULL && fopAcM_GetName(parent) == fpcNm_B_DR_e) { + return parent; + } + + return i_this; + +} +}; + +} // namespace dusk::mods::svc::actor_attr + +/* Preserve the demo's one-frame timer milestone while advancing byte timers + on the boss action clock. This avoids u8 overflow at very slow speeds. */ +static void l7_tick_demo_timer(u8& i_timer, int i_ticks) { + + if (i_timer == 0 || i_ticks <= 0) { + return; + } + + if (i_timer == 1) { + i_timer = 0; + } else if (i_ticks >= i_timer) { + i_timer = 1; + } else { + i_timer -= i_ticks; + } +} + +/* field_0x8b7 bit 0 is the vanilla "intro BGM started" flag. Bit 1 is free + * and remembers that a fast dragon crossed the scripted release half-plane + * while the independent 10-frame release timer was still running. */ +static constexpr u8 L7_FLY_RELEASE_SIDE_SEEN = 1 << 1; + +static void l7_record_fly_release_crossing(daL7ODR_c* i_this, const cXyz& i_orbitCenter, const u16 i_tangentBeforeMove) { + + if (actor_attr::enemy_action_time_speed(i_this) == 1.0f) { + return; + } + + const u16 tangentAfterMove = static_cast(cLib_targetAngleY(&i_this->current.pos, &i_orbitCenter) + 0x4000); + if (i_tangentBeforeMove < 0x8000 || tangentAfterMove < 0x8000) { + if ((i_this->field_0x8b7 & L7_FLY_RELEASE_SIDE_SEEN) == 0) { + OS_REPORT("[Dusklight randomizer] L7ODR latched orbit release side " "(timer %u, pre %04x, post %04x)\n", static_cast(i_this->field_0x8b5), static_cast(i_tangentBeforeMove), static_cast(tangentAfterMove)); + } + i_this->field_0x8b7 |= L7_FLY_RELEASE_SIDE_SEEN; + } +} +#endif void daL7ODR_c::setAction(daL7ODR_c::actionFn i_action) { if (mAction != NULL) { @@ -53,7 +116,7 @@ void daL7ODR_c::mtx_set() { mDoMtx_stack_c::transS(current.pos); mDoMtx_stack_c::ZXYrotM(current.angle); - model->setBaseScale(scale); + model->setBaseScale(DUSK_IF_ELSE(actor_attr::enemy_size_multiplier(this, 1.0f), scale)); model->setBaseTRMtx(mDoMtx_stack_c::get()); mpModelMorf->modelCalc(); @@ -72,8 +135,14 @@ int daL7ODR_c::draw() { mpModelMorf->entryDL(); cXyz sp8; +#if TARGET_PC // enemy attribute integration + const f32 sizeMultiplier = actor_attr::enemy_size_multiplier(this); + sp8.set(current.pos.x, 10.0f * sizeMultiplier + current.pos.y, current.pos.z); + mShadowKey = dComIfGd_setShadow(mShadowKey, 0, model, &sp8, (6000.0f + BREG_F(19)) * sizeMultiplier, 0.0f, current.pos.y, mAcch.GetGroundH(), mAcch.m_gnd, &tevStr, 0, 1.0f, dDlst_shadowControl_c::getSimpleTex()); +#else sp8.set(current.pos.x, 10.0f + current.pos.y, current.pos.z); mShadowKey = dComIfGd_setShadow(mShadowKey, 0, model, &sp8, 6000.0f + BREG_F(19), 0.0f, current.pos.y, mAcch.GetGroundH(), mAcch.m_gnd, &tevStr, 0, 1.0f, dDlst_shadowControl_c::getSimpleTex()); +#endif return 1; } @@ -155,9 +224,9 @@ void daL7ODR_c::pl_walk() { } else if (field_0x888 == 2) { if (field_0x8b5 != 0) { if (field_0x8b5 <= 10) { - cLib_chaseF(&field_0x898, 0.0f, 0.2f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_float(this, &field_0x898, 0.0f, 0.2f), cLib_chaseF(&field_0x898, 0.0f, 0.2f)); } - mDemoCamCenter.y += field_0x898; + mDemoCamCenter.y += DUSK_IF_ELSE(actor_attr::enemy_action_step(this, field_0x898), field_0x898); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye, 64.0f, 0); } else { field_0x8b5 = 60; @@ -222,7 +291,7 @@ void daL7ODR_c::pl_walk() { } else if (field_0x8b6 == 0) { cXyz player_pos(player->current.pos); if (player_pos.x) { - cLib_chaseF(&player_pos.x, 0.0f, 1.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_float(this, &player_pos.x, 0.0f, 1.0f), cLib_chaseF(&player_pos.x, 0.0f, 1.0f)); player->setPlayerPosAndAngle(&player_pos, -0x8000, 0); player->changeDemoMoveAngle(-0x8000); player->changeDemoPos0(&player_pos); @@ -253,7 +322,7 @@ void daL7ODR_c::dr_wait() { current.pos.set(5000.0f * cM_ssin(angle), 7000.0f, 5000.0f * cM_scos(angle)); current.angle.y = cLib_targetAngleY(¤t.pos, &sp8) + 0x4000; - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x2F), 2, 3.0f, 1.0f, 0.0f, -1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x2F), 2, 3.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x2F), 2, 3.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x2F; mpModelMorf->setFrame(20.0f); mpModelMorf->setPlaySpeed(0.0f); @@ -274,7 +343,7 @@ void daL7ODR_c::dr_fly() { if (field_0x88c == 0) { field_0x8b4 = 0; - mpModelMorf->setPlaySpeed(1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation_play_speed(this, mpModelMorf, 1.0f), mpModelMorf->setPlaySpeed(1.0f)); field_0x898 = 600.0f; mDemoCamCenter = player->current.pos; @@ -293,11 +362,11 @@ void daL7ODR_c::dr_fly() { field_0x88c++; } else if (field_0x88c != -1) { if (field_0x88c == 1) { - cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0x800, 0x10), cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10)); current.angle.y = cLib_targetAngleY(¤t.pos, &sp264) + 0x4000; speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); mDemoCamCenter = player->current.pos; mDemoCamCenter.y += 160.0f; @@ -310,7 +379,7 @@ void daL7ODR_c::dr_fly() { setZoomOutCamPos(mDemoCamEye, mDemoCamCenter, field_0x898); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); - if (8.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(8.0f), 8.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } @@ -319,11 +388,11 @@ void daL7ODR_c::dr_fly() { field_0x88c++; } } else if (field_0x88c == 2) { - cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0x800, 0x10), cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10)); current.angle.y = cLib_targetAngleY(¤t.pos, &sp264) + 0x4000; speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); mDemoCamCenter = player->current.pos; mDemoCamCenter.y += 160.0f; @@ -334,12 +403,17 @@ void daL7ODR_c::dr_fly() { mDemoCamEye.y += 400.0f; f32 temp_f30 = mDemoCamCenter.abs(mDemoCamEye) - 1800.0f; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x89c, 140.0f, 2.0f); + actor_attr::enemy_chase_action_float(this, &field_0x898, temp_f30, field_0x89c); +#else cLib_chaseF(&field_0x89c, 140.0f, 2.0f); cLib_chaseF(&field_0x898, temp_f30, field_0x89c); +#endif setZoomOutCamPos(mDemoCamEye, mDemoCamCenter, field_0x898); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); - if (8.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(8.0f), 8.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } @@ -351,11 +425,11 @@ void daL7ODR_c::dr_fly() { field_0x88c++; } else if (field_0x88c == 3) { cXyz sp258; - cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0x800, 0x10), cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10)); current.angle.y = cLib_targetAngleY(¤t.pos, &sp264) + 0x4000; speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); sp258 = player->current.pos; sp258.y += 160.0f; @@ -366,13 +440,18 @@ void daL7ODR_c::dr_fly() { mDoMtx_stack_c::multVecZero(&mDemoCamEye); mDemoCamEye.y += 400.0f; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x89c, 0.0f, 4.0f); + field_0x898 += actor_attr::enemy_action_step(this, field_0x89c); +#else cLib_chaseF(&field_0x89c, 0.0f, 4.0f); field_0x898 += field_0x89c; +#endif setZoomOutCamPos(mDemoCamEye, sp258, field_0x898); - cLib_chasePos(&mDemoCamCenter, field_0x830, 100.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mDemoCamCenter, field_0x830, 100.0f), cLib_chasePos(&mDemoCamCenter, field_0x830, 100.0f)); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); - if (8.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(8.0f), 8.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } @@ -384,16 +463,22 @@ void daL7ODR_c::dr_fly() { field_0x88c++; } } else if (field_0x88c == 4) { - cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0x800, 0x10), cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10)); current.angle.y = cLib_targetAngleY(¤t.pos, &sp264) + 0x4000; - cLib_chaseF(&speedF, 80.0f, 0.5f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_float(this, &speedF, 80.0f, 0.5f), cLib_chaseF(&speedF, 80.0f, 0.5f)); speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_pos(this, &mDemoCamCenter, field_0x830, 100.0f); + actor_attr::enemy_chase_action_float(this, &field_0x8a0, 100.0f, 0.5f); + actor_attr::enemy_chase_action_float(this, &field_0x89c, -1400.0f, field_0x8a0); +#else cLib_chasePos(&mDemoCamCenter, field_0x830, 100.0f); cLib_chaseF(&field_0x8a0, 100.0f, 0.5f); cLib_chaseF(&field_0x89c, -1400.0f, field_0x8a0); +#endif cXyz sp24C; mDoMtx_stack_c::transS(current.pos); @@ -402,11 +487,16 @@ void daL7ODR_c::dr_fly() { mDoMtx_stack_c::multVecZero(&sp24C); sp24C.y += 400.0f; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x898, 100.0f, 1.0f); + actor_attr::enemy_chase_action_pos(this, &mDemoCamEye, sp24C, field_0x898); +#else cLib_chaseF(&field_0x898, 100.0f, 1.0f); cLib_chasePos(&mDemoCamEye, sp24C, field_0x898); +#endif camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); - if (8.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(8.0f), 8.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } @@ -414,15 +504,26 @@ void daL7ODR_c::dr_fly() { field_0x88c++; } } else if (field_0x88c == 5) { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0x800, 0x10); + actor_attr::enemy_chase_action_angle(this, ¤t.angle.y, 0, 0x80); + actor_attr::enemy_chase_action_float(this, &speedF, 80.0f, 0.5f); +#else cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10); cLib_chaseAngleS(¤t.angle.y, 0, 0x80); cLib_chaseF(&speedF, 80.0f, 0.5f); +#endif mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); mDemoCamCenter = field_0x830; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x8a0, 100.0f, 0.5f); + actor_attr::enemy_chase_action_float(this, &field_0x89c, -1400.0f, field_0x8a0); +#else cLib_chaseF(&field_0x8a0, 100.0f, 0.5f); cLib_chaseF(&field_0x89c, -1400.0f, field_0x8a0); +#endif cXyz sp240; mDoMtx_stack_c::transS(current.pos); @@ -431,29 +532,40 @@ void daL7ODR_c::dr_fly() { mDoMtx_stack_c::multVecZero(&sp240); sp240.y += 400.0f; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x898, 100.0f, 1.0f); + actor_attr::enemy_chase_action_pos(this, &mDemoCamEye, sp240, field_0x898); +#else cLib_chaseF(&field_0x898, 100.0f, 1.0f); cLib_chasePos(&mDemoCamEye, sp240, field_0x898); +#endif camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); - if (8.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(8.0f), 8.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } - if (mpModelMorf->getFrame() >= 33.0f) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x30), 0, 3.0f, 1.0f, 0.0f, -1.0f); + // Fast playback can wrap past frame 33 between cutscene updates. + if (DUSK_IF_ELSE(mpModelMorf->getFrame() >= 33.0f || (actor_attr::enemy_action_time_speed(this) > 1.0f && mpModelMorf->checkFrame(33.0f)), mpModelMorf->getFrame() >= 33.0f)) { + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x30), 0, 3.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x30), 0, 3.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x30; current.angle.y = 0; field_0x88c++; } } else if (field_0x88c == 6) { - cLib_chaseAngleS(¤t.angle.x, 0, 0x20); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0, 0x20), cLib_chaseAngleS(¤t.angle.x, 0, 0x20)); speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); mDemoCamCenter = field_0x830; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x8a0, 100.0f, 0.5f); + actor_attr::enemy_chase_action_float(this, &field_0x89c, -1400.0f, field_0x8a0); +#else cLib_chaseF(&field_0x8a0, 100.0f, 0.5f); cLib_chaseF(&field_0x89c, -1400.0f, field_0x8a0); +#endif cXyz sp234; mDoMtx_stack_c::transS(current.pos); @@ -462,15 +574,20 @@ void daL7ODR_c::dr_fly() { mDoMtx_stack_c::multVecZero(&sp234); sp234.y += 400.0f; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x898, 100.0f, 1.0f); + actor_attr::enemy_chase_action_pos(this, &mDemoCamEye, sp234, field_0x898); +#else cLib_chaseF(&field_0x898, 100.0f, 1.0f); cLib_chasePos(&mDemoCamEye, sp234, field_0x898); +#endif camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); if (field_0x890 == 0x30) { if (mpModelMorf->isStop()) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x31), 2, 3.0f, 1.0f, 0.0f, -1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x31), 2, 3.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x31), 2, 3.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x31; - } else if (8.0f == mpModelMorf->getFrame()) { + } else if (DUSK_IF_ELSE(mpModelMorf->checkFrame(8.0f), 8.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } } else if (field_0x890 == 0x31) { @@ -479,37 +596,54 @@ void daL7ODR_c::dr_fly() { if (current.pos.z >= 3000.0f) { field_0x8b5 = 10; + IF_DUSK(field_0x8b7 &= ~L7_FLY_RELEASE_SIDE_SEEN;) field_0x88c++; } } else if (field_0x88c == 7) { cXyz sp228(2674.0f, 0.0f, 3038.0f); +#if TARGET_PC // enemy attribute integration + const cXyz flyWaypoint(sp228); +#endif if (field_0x890 == 0x30) { if (mpModelMorf->isStop()) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x31), 2, 3.0f, 1.0f, 0.0f, -1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x31), 2, 3.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x31), 2, 3.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x31; - } else if (8.0f == mpModelMorf->getFrame()) { + } else if (DUSK_IF_ELSE(mpModelMorf->checkFrame(8.0f), 8.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } } else if (field_0x890 == 0x31) { mSound.startCreatureSoundLevel(Z2SE_EN_DR_GLIDE, 0, -1); } - cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0x800, 0x10), cLib_chaseAngleS(¤t.angle.x, 0x800, 0x10)); sp228 += current.pos; sp228 /= 2.0f; if ((u16)current.angle.y > 0xE000) { - cLib_addCalcAngleS(¤t.angle.z, 0x2000, 30, 0x800, 0x40); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, ¤t.angle.z, 0x2000, 30, 0x800, 0x40), cLib_addCalcAngleS(¤t.angle.z, 0x2000, 30, 0x800, 0x40)); } else { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.z, 0, 60, 0x800, 0x40); + actor_attr::enemy_chase_action_float(this, &speedF, 240.0f, 1.0f); +#else cLib_addCalcAngleS(¤t.angle.z, 0, 60, 0x800, 0x40); cLib_chaseF(&speedF, 240.0f, 1.0f); +#endif } current.angle.y = cLib_targetAngleY(¤t.pos, &sp228) + 0x4000; +#if TARGET_PC // enemy attribute integration + const u16 tangentBeforeMove = static_cast(current.angle.y); +#endif speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_action_pos_move_f(this, NULL); + l7_record_fly_release_crossing(this, flyWaypoint, tangentBeforeMove); +#else fopAcM_posMoveF(this, NULL); +#endif mDemoCamCenter = field_0x830; cXyz sp21C; @@ -519,11 +653,16 @@ void daL7ODR_c::dr_fly() { mDoMtx_stack_c::multVecZero(&sp21C); sp21C.y += 400.0f; - cLib_chasePos(&mDemoCamEye, sp21C, 90.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mDemoCamEye, sp21C, 90.0f), cLib_chasePos(&mDemoCamEye, sp21C, 90.0f)); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); mSound.startCreatureSoundLevel(Z2SE_EN_DR_GLIDE, 0, -1); +#if TARGET_PC // enemy attribute integration + if (field_0x8b5 == 0 && ((u16)current.angle.y < 0x8000 || (field_0x8b7 & L7_FLY_RELEASE_SIDE_SEEN) != 0)) { + OS_REPORT("[Dusklight randomizer] L7ODR advancing after orbit " "release (yaw %04x, latch %u)\n", static_cast(static_cast(current.angle.y)), static_cast((field_0x8b7 & L7_FLY_RELEASE_SIDE_SEEN) != 0)); +#else if (field_0x8b5 == 0 && (u16)current.angle.y < 0x8000) { +#endif cXyz sp210; current.angle.y = -0x8000; field_0x8b4 = 1; @@ -544,20 +683,25 @@ void daL7ODR_c::dr_fly() { } else if (field_0x88c == 8) { cXyz sp204(0.0f, 220.0f, 1200.0f); if (mDemoCamEye.z > 1800.0f) { - cLib_chaseF(&field_0x898, 80.0f, 2.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_float(this, &field_0x898, 80.0f, 2.0f), cLib_chaseF(&field_0x898, 80.0f, 2.0f)); } else { - cLib_chaseF(&field_0x898, 0.0f, 4.5f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_float(this, &field_0x898, 0.0f, 4.5f), cLib_chaseF(&field_0x898, 0.0f, 4.5f)); } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_pos(this, &mDemoCamEye, sp204, field_0x898); + mDemoCamCenter.z -= actor_attr::enemy_action_step(this, 85.0f); +#else cLib_chasePos(&mDemoCamEye, sp204, field_0x898); mDemoCamCenter.z -= 85.0f; +#endif camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); mSound.startCreatureSoundLevel(Z2SE_EN_DR_GLIDE, 0, -1); if (mDemoCamEye.z <= sp204.z && !field_0x898) { cXyz sp1F8; field_0x8b4 = 0; - mpModelMorf->setPlaySpeed(1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation_play_speed(this, mpModelMorf, 1.0f), mpModelMorf->setPlaySpeed(1.0f)); field_0x8b5 = 0; sp1F8.set(0.0f, 0.0f, 1400.0f); @@ -573,14 +717,22 @@ void daL7ODR_c::dr_fly() { } } else if (field_0x88c == 9) { if (field_0x8b5 == 0) { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_set_animation_play_speed(this, mpModelMorf, 1.0f); + actor_attr::enemy_add_action_angle(this, ¤t.angle.z, 0, 0x20, 0x800, 0x40); + actor_attr::enemy_chase_action_float(this, &field_0x898, 128.0f, 4.0f); + actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, 0x200, field_0x898); + actor_attr::enemy_chase_action_float(this, &speedF, 240.0f, 1.0f); +#else mpModelMorf->setPlaySpeed(1.0f); cLib_addCalcAngleS(¤t.angle.z, 0, 0x20, 0x800, 0x40); cLib_chaseF(&field_0x898, 128.0f, 4.0f); cLib_chaseAngleS(¤t.angle.x, 0x200, field_0x898); cLib_chaseF(&speedF, 240.0f, 1.0f); +#endif speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); } mDemoCamCenter.set(0.0f, -140.0f, -930.0f); @@ -589,25 +741,32 @@ void daL7ODR_c::dr_fly() { mSound.startCreatureSoundLevel(Z2SE_EN_DR_GLIDE, 0, -1); if (1000.0f > current.pos.z) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x45), 0, 20.0f, 1.0f, 0.0f, -1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x45), 0, 20.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x45), 0, 20.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x45; field_0x898 = 0.0f; field_0x89c = 0.0f; field_0x88c++; } } else if (field_0x88c == 10) { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x898, 3072.0f, 96.0f); + actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, -0x4000, field_0x898); + actor_attr::enemy_chase_action_float(this, &field_0x89c, 8.0f, 0.3f); + actor_attr::enemy_chase_action_float(this, &speedF, 0.0f, field_0x89c); +#else cLib_chaseF(&field_0x898, 3072.0f, 96.0f); cLib_chaseAngleS(¤t.angle.x, -0x4000, field_0x898); cLib_chaseF(&field_0x89c, 8.0f, 0.3f); cLib_chaseF(&speedF, 0.0f, field_0x89c); +#endif speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); if (field_0x830.z > -400.0f) { mDemoCamCenter.set(0.0f, -140.0f, -930.0f); } else { - cLib_chasePos(&mDemoCamCenter, current.pos, 100.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mDemoCamCenter, current.pos, 100.0f), cLib_chasePos(&mDemoCamCenter, current.pos, 100.0f)); } mDemoCamEye.set(0.0f, 220.0f, 1200.0f); @@ -615,89 +774,104 @@ void daL7ODR_c::dr_fly() { mSound.startCreatureSoundLevel(Z2SE_EN_DR_GLIDE, 0, -1); if (current.angle.x < -0x2000) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x42), 0, 3.0f, 1.0f, 0.0f, -1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x42), 0, 3.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x42), 0, 3.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x42; field_0x88c++; } } else if (field_0x88c == 11) { if (speedF < 40.0f) { - cLib_addCalcAngleS(¤t.angle.x, 0, 0x20, 0x800, 0x200); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, ¤t.angle.x, 0, 0x20, 0x800, 0x200), cLib_addCalcAngleS(¤t.angle.x, 0, 0x20, 0x800, 0x200)); } else { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x898, 3072.0f, 96.0f); + actor_attr::enemy_chase_action_angle(this, ¤t.angle.x, -0x4000, field_0x898); +#else cLib_chaseF(&field_0x898, 3072.0f, 96.0f); cLib_chaseAngleS(¤t.angle.x, -0x4000, field_0x898); +#endif } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_chase_action_float(this, &field_0x89c, 8.0f, 0.3f); + actor_attr::enemy_chase_action_float(this, &speedF, 0.0f, field_0x89c); +#else cLib_chaseF(&field_0x89c, 8.0f, 0.3f); cLib_chaseF(&speedF, 0.0f, field_0x89c); +#endif speed.y = -speedF * cM_ssin(current.angle.x); mAcch.CrrPos(dComIfG_Bgsp()); s16 sp8 = current.angle.y; current.angle.y = -0x8000; - fopAcM_posMoveF(this, NULL); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, NULL), fopAcM_posMoveF(this, NULL)); current.angle.y = sp8; - cLib_chasePos(&mDemoCamCenter, current.pos, 100.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mDemoCamCenter, current.pos, 100.0f), cLib_chasePos(&mDemoCamCenter, current.pos, 100.0f)); mDemoCamEye.set(0.0f, 220.0f, 1200.0f); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); - if (5.0f == mpModelMorf->getFrame() || 35.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(5.0f) || mpModelMorf->checkFrame(35.0f), 5.0f == mpModelMorf->getFrame() || 35.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } if (mpModelMorf->isStop()) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x39), 0, 0.0f, 1.0f, 0.0f, -1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x39), 0, 0.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x39), 0, 0.0f, 1.0f, 0.0f, -1.0f)); fopMsgM_messageSetDemo(0x485); field_0x890 = 0x39; current.angle.y = 0; field_0x88c++; } } else if (field_0x88c == 12) { - cLib_chasePos(&mDemoCamCenter, field_0x830, 100.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mDemoCamCenter, field_0x830, 100.0f), cLib_chasePos(&mDemoCamCenter, field_0x830, 100.0f)); mDemoCamEye.set(0.0f, 1700.0f, -680.0f); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); +#if TARGET_PC // enemy attribute integration + if (mpModelMorf->checkFrame(22.0f) || mpModelMorf->checkFrame(59.0f) || mpModelMorf->checkFrame(92.0f) || mpModelMorf->checkFrame(125.0f)) { +#else f32 temp_f1 = mpModelMorf->getFrame(); if (22.0f == temp_f1 || 59.0f == temp_f1 || 92.0f == temp_f1 || 125.0f == temp_f1) { +#endif mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); - } else if (29.0f == temp_f1) { + } else if (DUSK_IF_ELSE(mpModelMorf->checkFrame(29.0f ), 29.0f == temp_f1)) { mSound.startCreatureVoice(Z2SE_EN_DR_V_SCREAM, -1); - } else if (33.0f == temp_f1) { + } else if (DUSK_IF_ELSE(mpModelMorf->checkFrame(33.0f ), 33.0f == temp_f1)) { dComIfGp_getVibration().StartQuake(VIBMODE_Q_POWER4, 0x1F, cXyz(0.0f, 1.0f, 0.0f)); - } else if (132.0f == temp_f1) { + } else if (DUSK_IF_ELSE(mpModelMorf->checkFrame(132.0f ), 132.0f == temp_f1)) { dComIfGp_getVibration().StopQuake(0x1F); } if (mpModelMorf->isStop()) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x35), 2, 3.0f, 1.0f, 0.0f, -1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x35), 2, 3.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x35), 2, 3.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x35; field_0x8b5 = 120; field_0x88c++; } } else if (field_0x88c == 13) { cXyz sp1EC; - cLib_chasePos(&mDemoCamCenter, field_0x830, 100.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mDemoCamCenter, field_0x830, 100.0f), cLib_chasePos(&mDemoCamCenter, field_0x830, 100.0f)); sp1EC = player->current.pos; sp1EC.y += 200.0f; sp1EC.z += 200.0f; - cLib_chasePos(&mDemoCamEye, sp1EC, 10.0f); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mDemoCamEye, sp1EC, 10.0f), cLib_chasePos(&mDemoCamEye, sp1EC, 10.0f)); camera->mCamera.Set(mDemoCamCenter, mDemoCamEye); - if (7.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(7.0f), 7.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); } if (field_0x8b5 == 0) { - if (mpModelMorf->getFrame() >= 33.0f) { - mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x34), 0, 3.0f, 1.0f, 0.0f, -1.0f); + // Fast playback can wrap past frame 33 between cutscene updates. + if (DUSK_IF_ELSE(mpModelMorf->getFrame() >= 33.0f || (actor_attr::enemy_action_time_speed(this) > 1.0f && mpModelMorf->checkFrame(33.0f)), mpModelMorf->getFrame() >= 33.0f)) { + DUSK_IF_ELSE(actor_attr::enemy_set_animation(this, mpModelMorf, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x34), 0, 3.0f, 1.0f, 0.0f, -1.0f), mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x34), 0, 3.0f, 1.0f, 0.0f, -1.0f)); field_0x890 = 0x34; field_0x88c++; } } } else { - if (7.0f == mpModelMorf->getFrame() || 37.0f == mpModelMorf->getFrame()) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(7.0f) || mpModelMorf->checkFrame(37.0f), 7.0f == mpModelMorf->getFrame() || 37.0f == mpModelMorf->getFrame())) { mSound.startCreatureSound(Z2SE_EN_DR_WING, 0, -1); - } else if (62.0f == mpModelMorf->getFrame()) { + } else if (DUSK_IF_ELSE(mpModelMorf->checkFrame(62.0f), 62.0f == mpModelMorf->getFrame())) { argument = 1; } @@ -715,13 +889,23 @@ void daL7ODR_c::dr_fly() { } int daL7ODR_c::execute() { +#if !TARGET_PC // enemy attribute integration if (field_0x8b5 != 0) { field_0x8b5--; } - +#endif + +#if TARGET_PC // enemy attribute integration + field_0x8a4 += actor_attr::enemy_action_time_speed(this); + const int timerTicks = static_cast(field_0x8a4); + field_0x8a4 -= timerTicks; + l7_tick_demo_timer(field_0x8b5, timerTicks); + l7_tick_demo_timer(field_0x8b6, timerTicks); +#else if (field_0x8b6 != 0) { field_0x8b6--; } +#endif action(); dr_action(); @@ -761,7 +945,7 @@ static int daL7ODR_Delete(daL7ODR_c* i_this) { int daL7ODR_c::CreateHeap() { J3DModelData* modelData = (J3DModelData*)dComIfG_getObjectRes("B_dr", 0x49); JUT_ASSERT(1146, modelData != NULL); - mpModelMorf = JKR_NEW mDoExt_McaMorfSO(modelData, NULL, NULL, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x2F), 2, 1.0f, 0, -1, &mSound, 0, 0x11000084); + mpModelMorf = JKR_NEW mDoExt_McaMorfSO(modelData, NULL, NULL, (J3DAnmTransform*)dComIfG_getObjectRes("B_dr", 0x2F), 2, DUSK_IF_ELSE(actor_attr::enemy_action_time_speed(this), 1.0f), 0, -1, &mSound, 0, 0x11000084); field_0x890 = 0x2F; if (mpModelMorf == NULL || mpModelMorf->getModel() == NULL) { return 0; @@ -811,7 +995,11 @@ int daL7ODR_c::create() { field_0x560 = health = 1; +#if TARGET_PC // enemy attribute integration + scale= actor_attr::enemy_size_multiplier(this, 1.0f); +#else scale.setall(1.0f); +#endif attention_info.distances[fopAc_attn_BATTLE_e] = 0; attention_info.flags &= ~fopAc_AttnFlag_BATTLE_e; @@ -820,7 +1008,14 @@ int daL7ODR_c::create() { fopAcM_OffStatus(this, 0); mAcch.Set(fopAcM_GetPosition_p(this), fopAcM_GetOldPosition_p(this), this, 1, &mAcchCir, fopAcM_GetSpeed_p(this), NULL, NULL); +#if TARGET_PC // enemy attribute integration + const f32 sizeMultiplier = actor_attr::enemy_size_multiplier(this); + mAcchCir.SetWall(20.0f * sizeMultiplier, 40.0f * sizeMultiplier); + + field_0x8a4 = 0.0f; +#else mAcchCir.SetWall(20.0f, 40.0f); +#endif setAction(&daL7ODR_c::wait); setDrAction(&daL7ODR_c::dr_wait); diff --git a/src/d/actor/d_a_b_dr.cpp b/src/d/actor/d_a_b_dr.cpp index 7e1cc8c0d5..62f81d7bdc 100644 --- a/src/d/actor/d_a_b_dr.cpp +++ b/src/d/actor/d_a_b_dr.cpp @@ -6,6 +6,9 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep #include "d/actor/d_a_b_dr.h" +#if TARGET_PC // additional actor attribute integration +#include "dusk/mods/svc/actor_attribute_helpers.hpp" +#endif #include "d/d_com_inf_game.h" #include "d/actor/d_a_player.h" #include "d/d_menu_window.h" @@ -16,6 +19,21 @@ #include "c/c_damagereaction.h" #include "f_op/f_op_camera_mng.h" #include "Z2AudioLib/Z2Instances.h" +#if TARGET_PC // additional actor attribute integration + +namespace actor_attr = dusk::mods::svc::actor_attr; + +namespace dusk::mods::svc::actor_attr { +template <> struct EnemyAttributeOwner { + static fopAc_ac_c* get(daB_DR_c* i_this) { + + fopAc_ac_c* parent = fopAcM_SearchByID(i_this->parentActorID); + return parent != NULL && fopAcM_GetName(parent) == fpcNm_B_DR_e ? parent : i_this; + +} +}; +} +#endif class daB_DR_HIO_c : public JORReflexible { @@ -290,6 +308,33 @@ daB_DR_HIO_c::daB_DR_HIO_c() { static u8 hio_set; static daB_DR_HIO_c l_HIO; +#if TARGET_PC // enemy attribute integration + +// Argorok's mounted-back sequence has the same nested damage-budget problem +// as Morpheel: a native number of sword hits per mount sits inside the boss's +// overall randomized health budget. Split the health multiplier across those +// dimensions with sqrt(), so higher/lower health changes both how many hits +// fit in one mount and, naturally through Argorok's real HP, how many mounts +// the fight takes without applying the full health multiplier twice. +static f32 dragon_health_budget_scale(daB_DR_c* i_this) { + + return JMAFastSqrt(actor_attr::enemy_health_amount(i_this, 1.0f)); +} + +static s16 dragon_hits_per_mount(daB_DR_c* i_this) { + + return actor_attr::positive_s16(4.0f * dragon_health_budget_scale(i_this)); +} + +// This is a mount/attack opportunity window, not a stun. Keep it independent +// of the randomized stun-duration attribute, and expand/contract it in the +// same proportion as the number of sword hits allowed per mount. +// Vanilla: 4 hits / 150 frames. +static s16 dragon_mount_window_for_hits(daB_DR_c* i_this, f32 vanillaFrames) { + + return actor_attr::positive_s16(vanillaFrames * ((f32)dragon_hits_per_mount(i_this) / 4.0f)); +} +#endif int daB_DR_c::ctrlJoint(J3DJoint* i_joint, J3DModel* i_model) { cXyz unused_sp20; @@ -478,6 +523,12 @@ static int daB_DR_Draw(daB_DR_c* i_this) { } void daB_DR_c::setBck(int i_anm, u8 i_mode, f32 i_morf, f32 i_speed) { +#if TARGET_PC // enemy attribute integration + + const actor_attr::ActionAnimationParams actorAttributeAnimation = actor_attr::enemy_animation_params(this, i_morf, i_speed); + i_morf = actorAttributeAnimation.morph; + i_speed = actorAttributeAnimation.playbackSpeed; +#endif mpModelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_DR", i_anm), i_mode, i_morf, i_speed, 0.0f, -1.0f); mAnm = i_anm; } @@ -511,13 +562,19 @@ void daB_DR_c::mBoot_c_tranceSet(bool param_0) { mae.z = 500.0f; MtxPosition(&mae, &ato); +#if TARGET_PC // enemy attribute integration + mBoot_c_trance.x += actor_attr::enemy_action_step(this, ato.x / 45.0f); + mBoot_c_trance.y += actor_attr::enemy_action_step(this, ato.y / 45.0f); + mBoot_c_trance.z += actor_attr::enemy_action_step(this, ato.z / 45.0f); +#else mBoot_c_trance.x += ato.x / 45.0f; mBoot_c_trance.y += ato.y / 45.0f; mBoot_c_trance.z += ato.z / 45.0f; +#endif } } else { ato.zero(); - cLib_addCalcPos(&mBoot_c_trance, ato, 0.8f, 20.0f, 5.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, &mBoot_c_trance, ato, 0.8f, 20.0f, 5.0f), cLib_addCalcPos(&mBoot_c_trance, ato, 0.8f, 20.0f, 5.0f)); } } @@ -804,6 +861,9 @@ void daB_DR_c::chkPartCreate(bool param_0) { int part_no = 0; int var_r27 = 0; int part_max = 0; +#if TARGET_PC // enemy attribute integration + const int parts_per_frame = std::max(1, static_cast(std::ceil(actor_attr::enemy_action_step(this, 4.0f)))); +#endif if (param_0 || field_0x7d1 != 2) { int break_no = mBreakPartsNo; @@ -830,7 +890,7 @@ void daB_DR_c::chkPartCreate(bool param_0) { return; } - for (; part_no < part_max && var_r27 < 4; var_r27++) { + for (; part_no < part_max && var_r27 < DUSK_IF_ELSE(parts_per_frame, 4); var_r27++) { mDoMtx_stack_c::copy(mpModelMorf->getModel()->getAnmMtx(BREAK_PARTS_DT[part_no].jnt_no)); mDoMtx_stack_c::multVecZero(&pos); mDoMtx_MtxToRot(mDoMtx_stack_c::get(), &rot); @@ -842,7 +902,7 @@ void daB_DR_c::chkPartCreate(bool param_0) { prm |= (BREAK_PARTS_DT[part_no].prm << 8); size.x = size.y = size.z = BREAK_PARTS_DT[part_no].size; - fopAcM_create(fpcNm_B_DR_e, prm, &pos, fopAcM_GetRoomNo(this), &rot, &size, -1); + DUSK_IF_ELSE(fopAcM_createChild(fpcNm_B_DR_e, fopAcM_GetID(this), prm, &pos, fopAcM_GetRoomNo(this), &rot, &size, -1, NULL), fopAcM_create(fpcNm_B_DR_e, prm, &pos, fopAcM_GetRoomNo(this), &rot, &size, -1)); field_0x7e0++; part_no++; @@ -866,12 +926,12 @@ void daB_DR_c::mHabatakiAnmSet(int param_0) { setBck(ANM_DR_BRAKE_TO_HOVER, 0, 3.0f, 1.0f); break; case ANM_DR_FLY: - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f))) { setBck(ANM_DR_BRAKE_TO_HOVER, 0, l_HIO.unk_0x2c, 1.0f); } break; case ANM_DR_FLAP: - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f))) { if (param_0 != 5 && param_0 != 6) { setBck(ANM_DR_FLAP_TO_FLY, 0, 3.0f, 1.0f); } else { @@ -897,7 +957,7 @@ void daB_DR_c::mHabatakiAnmSet(int param_0) { setBck(ANM_DR_HOVERING, 2, 3.0f, 1.0f); /* fallthrough */ case ANM_DR_HOVERING: - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f) && param_0 == 4) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) && param_0 == 4) { setBck(ANM_DR_FLAP, 2, 3.0f, 1.0f); } break; @@ -916,13 +976,13 @@ void daB_DR_c::mGlider_AniSet(bool param_0) { if (cLib_calcTimer(&mTimer[0]) == 0) { switch (mAnm) { case ANM_DR_WIND_ATTACKB: - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f))) { setBck(ANM_DR_WIND_ATTACKBTOFLY, 2, 3.0f, 1.0f); } break; case ANM_DR_HOVERING: case ANM_DR_WIND_ATTACKBTOFLY: - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f))) { if (mAnm == ANM_DR_WIND_ATTACKBTOFLY) { setBck(ANM_DR_FLY, 2, 5.0f, 1.0f); } else { @@ -931,19 +991,19 @@ void daB_DR_c::mGlider_AniSet(bool param_0) { } break; case ANM_DR_FLY: - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f))) { setBck(ANM_DR_FLY_TO_GLIDE, 0, 3.0f, 1.0f); } break; case ANM_DR_FLY_TO_GLIDE: if (mpModelMorf->isStop()) { setBck(ANM_DR_GLIDE, 2, 3.0f, 1.0f); - mTimer[0] = cM_rndF(50.0f) + 50.0f; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, cM_rndF(50.0f) + 50.0f), cM_rndF(50.0f) + 50.0f); } break; case ANM_DR_GLIDE: if (!param_0) { - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f))) { setBck(ANM_DR_GLIDE_TO_FLY, 0, 3.0f, 1.0f); } } @@ -952,7 +1012,7 @@ void daB_DR_c::mGlider_AniSet(bool param_0) { case ANM_DR_GLIDE_TO_FLY: if (mpModelMorf->isStop()) { setBck(ANM_DR_FLY, 2, 3.0f, 1.0f); - mTimer[0] = cM_rndF(34.0f) + 34.0f; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, cM_rndF(34.0f) + 34.0f), cM_rndF(34.0f) + 34.0f); } break; } @@ -971,7 +1031,7 @@ void daB_DR_c::mGliderAtAniSet() { setBck(ANM_DR_WIND_ATTACKATOB, 0, 3.0f, 1.0f); break; case ANM_DR_FLY: - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f))) { setBck(ANM_DR_WIND_ATTACKATOB, 0, 3.0f, 1.0f); } break; @@ -1066,11 +1126,21 @@ void daB_DR_c::mHeadAngleSet() { target_angle_x = -0x1000; } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, &mHeadAngle.y, target_angle_y, 8, 0x500); + actor_attr::enemy_add_action_angle(this, &mHeadAngle.x, target_angle_x, 8, 0x500); +#else cLib_addCalcAngleS2(&mHeadAngle.y, target_angle_y, 8, 0x500); cLib_addCalcAngleS2(&mHeadAngle.x, target_angle_x, 8, 0x500); +#endif } else { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, &mHeadAngle.y, 0, 8, 0x500); + actor_attr::enemy_add_action_angle(this, &mHeadAngle.x, 0, 8, 0x500); +#else cLib_addCalcAngleS2(&mHeadAngle.y, 0, 8, 0x500); cLib_addCalcAngleS2(&mHeadAngle.x, 0, 8, 0x500); +#endif } } @@ -1095,10 +1165,10 @@ bool daB_DR_c::flapMove(bool param_0) { field_0x724 = 20.0f; } - cLib_addCalc2(¤t.pos.y, target_y, 0.8f, field_0x724); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, ¤t.pos.y, target_y, 0.8f, field_0x724), cLib_addCalc2(¤t.pos.y, target_y, 0.8f, field_0x724)); if (!param_0) { - cLib_addCalc2(&field_0x724, 4.0f, 0.8f, 2.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &field_0x724, 4.0f, 0.8f, 2.0f), cLib_addCalc2(&field_0x724, 4.0f, 0.8f, 2.0f)); } if (fabsf(current.pos.y - target_y) < 100.0f) { @@ -1122,7 +1192,7 @@ bool daB_DR_c::revolutionMove() { arg0 = 1; if (fabsf(current.pos.y - mTargetHeight) > 100.0f) { - cLib_addCalc2(¤t.pos.y, mTargetHeight, 0.8f, 50.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, ¤t.pos.y, mTargetHeight, 0.8f, 50.0f), cLib_addCalc2(¤t.pos.y, mTargetHeight, 0.8f, 50.0f)); mae = current.pos - home.pos; mae.y = 0.0f; field_0x750 = 200; @@ -1130,7 +1200,7 @@ bool daB_DR_c::revolutionMove() { return false; } - cLib_addCalc0(&speed.y, 0.7f, 5.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc0(this, &speed.y, 0.7f, 5.0f), cLib_addCalc0(&speed.y, 0.7f, 5.0f)); cMtx_YrotS(*calc_mtx, field_0x74c); mae.x = 0.0f; mae.y = 0.0f; @@ -1142,8 +1212,13 @@ bool daB_DR_c::revolutionMove() { mae = field_0x7a8 - current.pos; mae.y = 0.0f; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, (s16)mae.atan2sX_Z(), field_0x750, 0x400); + actor_attr::enemy_add_action_angle(this, &field_0x750, 8, 10, 40); +#else cLib_addCalcAngleS2(¤t.angle.y, (s16)mae.atan2sX_Z(), field_0x750, 0x400); cLib_addCalcAngleS2(&field_0x750, 8, 10, 40); +#endif shape_angle.y = current.angle.y; if (abs((s16)((s16)mae.atan2sX_Z() - shape_angle.y)) > 0x1000) { @@ -1191,17 +1266,32 @@ bool daB_DR_c::mBgFallGroundCheck() { chkpos.y = home.pos.y; } +#if TARGET_PC // enemy attribute integration + const f32 body_clearance = actor_attr::enemy_size_value(this, 80.0f); + if (!mAcch.ChkGroundHit() && !(body_clearance + chkpos.y > current.pos.y)) { +#else if (!mAcch.ChkGroundHit() && !(80.0f + chkpos.y > current.pos.y)) { +#endif return false; } speed.y = 0.0f; +#if TARGET_PC // enemy attribute integration + if (current.pos.y < body_clearance + home.pos.y) { + current.pos.y = body_clearance + home.pos.y; +#else if (current.pos.y < 80.0f + home.pos.y) { current.pos.y = 80.0f + home.pos.y; +#endif } +#if TARGET_PC // enemy attribute integration + if (current.pos.y < body_clearance + chkpos.y) { + current.pos.y = body_clearance + chkpos.y; +#else if (current.pos.y < 80.0f + chkpos.y) { current.pos.y = 80.0f + chkpos.y; +#endif } gravity = 0.0f; @@ -1235,8 +1325,13 @@ void daB_DR_c::normalHoverMove() { target_pos += home.pos; target_pos.y = current.pos.y; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, target_pos, 0.4f, 5.0f, field_0x744); + actor_attr::enemy_add_action_calc2(this, &field_0x744, 1.0f, 0.3f, 0.1f); +#else cLib_addCalcPos(¤t.pos, target_pos, 0.4f, 5.0f, field_0x744); cLib_addCalc2(&field_0x744, 1.0f, 0.3f, 0.1f); +#endif target_vec = target_pos - current.pos; target_vec.y = 0.0f; @@ -1262,19 +1357,25 @@ void daB_DR_c::executeWait() { switch (mMoveMode) { case 0: mAllClr(); - mTimer[2] = l_HIO.no_attack_time; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.no_attack_time), l_HIO.no_attack_time); field_0x750 = JREG_S(8) + 200; mMoveMode++; /* fallthrough */ case 1: sp20.set(home.pos); sp20.y = current.pos.y; - cLib_addCalcPos(¤t.pos, sp20, 0.8f, 20.0f, 5.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, sp20, 0.8f, 20.0f, 5.0f), cLib_addCalcPos(¤t.pos, sp20, 0.8f, 20.0f, 5.0f)); sp14 = sp20 - current.pos; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, fopAcM_searchPlayerAngleY(this), 10, 0x800); + actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, field_0x750, 0x400); + actor_attr::enemy_add_action_angle(this, &field_0x750, 8, 8, 20); +#else cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), 10, 0x800); cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, 0x400); cLib_addCalcAngleS2(&field_0x750, 8, 8, 20); +#endif if (daPy_getPlayerActorClass()->getDamageWaitTimer() == 0 && temp_r27 && sp14.abs() < 100.0f) { if (mBreakPartsNo == 0) { @@ -1298,8 +1399,13 @@ void daB_DR_c::executeWait() { } } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc0(this, &speedF, 0.8f, 10.0f); + actor_attr::enemy_add_action_calc0(this, &speed.y, 0.8f, 10.0f); +#else cLib_addCalc0(&speedF, 0.8f, 10.0f); cLib_addCalc0(&speed.y, 0.8f, 10.0f); +#endif if (mMoveMode == 2 && (cLib_calcTimer(&mTimer[2]) == 0 || dComIfGp_checkPlayerStatus0(0, 0x100))) { if (mPlayerHighCheck()) { @@ -1326,7 +1432,7 @@ void daB_DR_c::executeFlyWait() { } #endif - cLib_addCalc0(&speedF, 0.8f, 10.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc0(this, &speedF, 0.8f, 10.0f), cLib_addCalc0(&speedF, 0.8f, 10.0f)); mHabatakiAnmSet(0); switch (mMoveMode) { @@ -1338,7 +1444,7 @@ void daB_DR_c::executeFlyWait() { field_0x724 = 0.0f; if (revolutionMove()) { field_0x750 = JREG_S(8) + 200; - mTimer[2] = l_HIO.no_attack_no_armor_time; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.no_attack_no_armor_time), l_HIO.no_attack_no_armor_time); mMoveMode = 2; } else { mMoveMode = 1; @@ -1350,18 +1456,23 @@ void daB_DR_c::executeFlyWait() { } field_0x750 = JREG_S(8) + 200; +#if TARGET_PC // enemy attribute integration + mTimer[2] = actor_attr::enemy_sync_timer(this, l_HIO.no_attack_no_armor_time); + actor_attr::enemy_set_animation_play_speed(this, mpModelMorf, 1.0f); +#else mTimer[2] = l_HIO.no_attack_no_armor_time; mpModelMorf->setPlaySpeed(1.0f); +#endif mMoveMode = 2; // fallthrough case 2: field_0x7d6 = 0; normalHoverMove(); - cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), field_0x750, 0x200); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, ¤t.angle.y, fopAcM_searchPlayerAngleY(this), field_0x750, 0x200), cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), field_0x750, 0x200)); sp8.set(home.pos); sp8.y = current.pos.y; - cLib_addCalcPos(¤t.pos, sp8, 0.8f, 20.0f, 5.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, sp8, 0.8f, 20.0f, 5.0f), cLib_addCalcPos(¤t.pos, sp8, 0.8f, 20.0f, 5.0f)); if (mTargetHeight < player->current.pos.y || mPlayerHighCheck()) { setActionMode(ACTION_BREATH_ATTACK, 0); @@ -1374,12 +1485,12 @@ void daB_DR_c::executeFlyWait() { } if (mMoveMode < 10) { - cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, 0x400); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, field_0x750, 0x400), cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, 0x400)); } else { - cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, 0x200); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, field_0x750, 0x200), cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, 0x200)); } - cLib_addCalcAngleS2(&field_0x750, 8, 10, 20); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &field_0x750, 8, 10, 20), cLib_addCalcAngleS2(&field_0x750, 8, 10, 20)); } void daB_DR_c::executeTailHit() { @@ -1399,7 +1510,7 @@ void daB_DR_c::executeTailHit() { #endif bool sp10 = mPlayerHighCheck(); - if (daPy_getPlayerActorClass()->getDamageWaitTimer() != 0) { + if (DUSK_IF_ELSE(daPy_getPlayerActorClass()->getDamageWaitTimer() != 0 && mMoveMode < 10, daPy_getPlayerActorClass()->getDamageWaitTimer() != 0)) { dMw_onPauseWindow(); if (field_0x7d1 != 2) { field_0x724 = 50.0f; @@ -1411,8 +1522,60 @@ void daB_DR_c::executeTailHit() { } if ((mMoveMode == 0 || mMoveMode == 1) && current.pos.y < (1000.0f + l_HIO.breath_attack_threshold)) { - cLib_addCalc2(¤t.pos.y, 1000.0f + l_HIO.breath_attack_threshold, 0.7f, 20.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, ¤t.pos.y, 1000.0f + l_HIO.breath_attack_threshold, 0.7f, 20.0f), cLib_addCalc2(¤t.pos.y, 1000.0f + l_HIO.breath_attack_threshold, 0.7f, 20.0f)); } +#if TARGET_PC // enemy attribute integration + + // Compute the world-space Y where Argorok should actually be considered + // landed for this pose/size. This is shared by the BOOT_A synchronization + // below and the normal BOOT_B fallback fall. + auto tail_fall_landing_y = [&](f32* out_landing_ground) -> f32 { + const f32 landing_ground = std::max(field_0x734, home.pos.y); + if (out_landing_ground != NULL) { + *out_landing_ground = landing_ground; + } + + const f32 sizeMultiplier = actor_attr::enemy_size_multiplier(this); + f32 landing_clearance = 80.0f; + + if (sizeMultiplier != 1.0f) { + const f32 scaledVanillaClearance = actor_attr::enemy_size_value(this, 80.0f); + const f32 tailBottomY = mTailCc.GetC().y - mTailCc.GetR(); + const f32 tailClearance = current.pos.y - tailBottomY; + + landing_clearance = std::max(scaledVanillaClearance, tailClearance); + } + + return landing_ground + landing_clearance; + }; + + // Normal post-animation fallback. field_0x728 is the accumulated fall + // acceleration/max Y step; do not size-scale it here. + auto advance_tail_fall_position = [&](f32* out_landing_ground) -> bool { + field_0x728 += actor_attr::enemy_action_step(this, l_HIO.fall_accel); + if (field_0x728 >= l_HIO.fall_accel_max) { + field_0x728 = l_HIO.fall_accel_max; + } + + actor_attr::enemy_add_action_calc2(this, ¤t.pos.y, field_0x734, 0.8f, field_0x728); + actor_attr::enemy_add_action_calc2(this, ¤t.pos.x, home.pos.x, 0.8f, 8.0f); + actor_attr::enemy_add_action_calc2(this, ¤t.pos.z, home.pos.z, 0.8f, 8.0f); + + mAcch.SetWallNone(); + mAcch.SetGroundUpY(75.0f); + mAcch.CrrPos(dComIfG_Bgsp()); + + const f32 landing_y = tail_fall_landing_y(out_landing_ground); + + const bool boss_landed = current.pos.y <= landing_y || (mAcch.ChkGroundHit() && current.pos.y <= landing_y + actor_attr::enemy_size_value(this, 100.0f)); + + if (boss_landed) { + current.pos.y = landing_y; + } + + return boss_landed; + }; +#endif switch (mMoveMode) { case 0: @@ -1426,7 +1589,7 @@ void daB_DR_c::executeTailHit() { if (dComIfGp_checkPlayerStatus1(0, 0x10000)) { field_0x728 = 0.0f; - mTimer[0] = l_HIO.tail_hit_chance_time; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_stun_timer(this, l_HIO.tail_hit_chance_time), l_HIO.tail_hit_chance_time); mMoveMode++; } break; @@ -1510,11 +1673,66 @@ void daB_DR_c::executeTailHit() { } break; case 10: +#if TARGET_PC // enemy attribute integration + { +#endif mStatusONOFF(0); dComIfGs_onZoneSwitch(22, fopAcM_GetRoomNo(this)); if (mBreakPartsNo == 0) { dCam_getBody()->SetTrimTypeForce(2); +#if TARGET_PC // enemy attribute integration + } + + const f32 sizeMultiplier = actor_attr::enemy_size_multiplier(this); + + if (sizeMultiplier > 1.0f) { + // BOOT_A visually pitches/falls Argorok forward. On a large model + // that visual displacement is much larger, so the actor origin must + // descend faster during this one animation too. + // + // Keep field_0x728 as the fall acceleration/max-step value, but use + // a size-scaled fall step here. Also calculate the minimum catch-up + // step needed to reach the real landing Y by the end of BOOT_A. + // This keeps the physical fall matched to the animation instead of + // finishing the pose in mid-air and then free-falling afterward. + field_0x728 += actor_attr::enemy_action_step(this, l_HIO.fall_accel); + if (field_0x728 >= l_HIO.fall_accel_max) { + field_0x728 = l_HIO.fall_accel_max; + } + + actor_attr::enemy_add_action_calc2(this, ¤t.pos.x, home.pos.x, 0.8f, 8.0f); + actor_attr::enemy_add_action_calc2(this, ¤t.pos.z, home.pos.z, 0.8f, 8.0f); + + mAcch.SetWallNone(); + mAcch.SetGroundUpY(75.0f); + mAcch.CrrPos(dComIfG_Bgsp()); + + f32 unused_landing_ground; + const f32 landing_y = tail_fall_landing_y(&unused_landing_ground); + + if (current.pos.y > landing_y) { + const f32 remainingY = current.pos.y - landing_y; + const f32 frame = mpModelMorf->getFrame(); + const f32 endFrame = mpModelMorf->getEndFrame(); + const f32 frameStep = std::max(fabsf(mpModelMorf->getPlaySpeed()), 0.001f); + const f32 remainingFrames = std::max(endFrame - frame, frameStep); + + const f32 sizeScaledFallStep = field_0x728 * sizeMultiplier; + const f32 catchUpStep = remainingY * std::min(frameStep / remainingFrames, 1.0f); + + const f32 fallStep = std::max(sizeScaledFallStep, catchUpStep); + + current.pos.y -= std::min(remainingY, fallStep); + } + + // action() checks this before mpModelMorf->play() advances the + // animation for the current update. If BOOT_A is already stopped + // here, make the endpoint exact before changing to BOOT_B. + if (mpModelMorf->isStop()) { + current.pos.y = landing_y; + } +#endif } if (!mpModelMorf->isStop()) { @@ -1524,25 +1742,66 @@ void daB_DR_c::executeTailHit() { setBck(ANM_DR_BOOT_B, 2, 3.0f, 1.0f); mBgFallGroundCheck(); mMoveMode++; +#if TARGET_PC // enemy attribute integration + /* fallthrough */ + } +#endif case 11: +#if TARGET_PC // enemy attribute integration + { +#endif if (mBreakPartsNo == 0) { dCam_getBody()->SetTrimTypeForce(2); } +#if TARGET_PC // enemy attribute integration + f32 landing_ground; + const bool boss_landed = advance_tail_fall_position(&landing_ground); + + if (player->current.pos.y < landing_ground) { + // The original fallback treated any position below the sampled + // floor as a successful landing. At a slow boss action rate Link + // can instead remain hook-carried through the arena floor. Release + // that carry, restore the player to the surface, and give player + // collision one update before this sequence is allowed to advance. + fopAcM_cancelHookCarryNow(this); + cXyz player_landing_pos(player->current.pos); + player_landing_pos.y = landing_ground; + player->setPlayerPosAndAngle(&player_landing_pos, player->shape_angle.y, 1); + return; +#else field_0x728 += l_HIO.fall_accel; if (field_0x728 >= l_HIO.fall_accel_max) { field_0x728 = l_HIO.fall_accel_max; +#endif } +#if TARGET_PC // enemy attribute integration + const bool player_landed = player->getFootOnGround() || fabsf(player->current.pos.y - landing_ground) < 10.0f; +#else cLib_addCalc2(¤t.pos.y, field_0x734, 0.8f, field_0x728); cLib_addCalc2(¤t.pos.x, home.pos.x, 0.8f, 8.0f); cLib_addCalc2(¤t.pos.z, home.pos.z, 0.8f, 8.0f); +#endif +#if TARGET_PC // enemy attribute integration + // At high action speeds Argorok can reach his size-aware tail landing + // point one update before Link's hook-carried position reaches the + // floor. Once the boss/tail is safely landed, stop carrying Link so + // he can fall the remaining small distance naturally. Returning here + // also avoids re-applying the hook carry offset at the end of this + // function while we wait for Link to touch down. + if (boss_landed && !player_landed) { + fopAcM_cancelHookCarryNow(this); + return; + } +#else mAcch.SetWallNone(); mAcch.SetGroundUpY(75.0f); mAcch.CrrPos(dComIfG_Bgsp()); +#endif - if (player->getFootOnGround() || player->current.pos.y < 10.0f + field_0x734) { + if (DUSK_IF_ELSE(player_landed && boss_landed, player->getFootOnGround() || player->current.pos.y < 10.0f + field_0x734)) { mSound.startCreatureSound(Z2SE_EN_DR_LAND, 0, -1); if (mBreakPartsNo != 0) { @@ -1569,11 +1828,15 @@ void daB_DR_c::executeTailHit() { mMoveMode++; } break; +#if TARGET_PC // enemy attribute integration + } +#endif case 12: if (startDemoCheck()) { shape_angle.y = current.angle.y = home.angle.y; current.pos.x = home.pos.x; current.pos.z = home.pos.z; + IF_DUSK(field_0x7e0 = 0;) setBck(ANM_DR_IMPACT, 0, 3.0f, 1.0f); mMoveMode++; @@ -1582,12 +1845,12 @@ void daB_DR_c::executeTailHit() { case 13: if (mAnm == ANM_DR_BOOT_C) { if ((int)mpModelMorf->getFrame() >= 34) { - cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f), cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f)); } if (mpModelMorf->isStop()) { setBck(ANM_DR_DOWN_WAIT, 2, 3.0f, 1.0f); - mTimer[0] = 35; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 35), 35); } } @@ -1605,7 +1868,7 @@ void daB_DR_c::executeTailHit() { case 14: if (mAnm == ANM_DR_BOOT_C) { if ((int)mpModelMorf->getFrame() >= 34) { - cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f), cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f)); } if (!mpModelMorf->isStop()) { break; @@ -1614,7 +1877,7 @@ void daB_DR_c::executeTailHit() { if (mAnm != ANM_DR_DOWN_WAIT) { setBck(ANM_DR_DOWN_WAIT, 2, 3.0f, 1.0f); - mTimer[0] = 35; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 35), 35); } if (cLib_calcTimer(&mTimer[0]) != 0) { @@ -1629,7 +1892,7 @@ void daB_DR_c::executeTailHit() { Z2GetAudioMgr()->subBgmStart(Z2BGM_DRAGON_D02); dComIfGs_onZoneSwitch(23, fopAcM_GetRoomNo(this)); field_0x7e0 = 0; - mTimer[0] = BREG_S(3) + 100; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, BREG_S(3) + 100), BREG_S(3) + 100); mMoveMode = 1000; break; case 20: @@ -1651,7 +1914,7 @@ void daB_DR_c::executeTailHit() { flapMove(1); } else { revolutionMove(); - cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 8, 0x400); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, 8, 0x400), cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 8, 0x400)); } } } @@ -1721,7 +1984,7 @@ void daB_DR_c::executeTailHit() { } if (mMoveMode >= 10 && mMoveMode <= 11) { - cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), 40, 0x200); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, ¤t.angle.y, fopAcM_searchPlayerAngleY(this), 40, 0x200), cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), 40, 0x200)); shape_angle.y = current.angle.y; } @@ -1760,12 +2023,28 @@ void daB_DR_c::executeWeekHit() { mCount[0] = cM_rndF(2.0f); mCount[1] = 0; - mTimer[2] = l_HIO.back_hit_chance_time; + // Before Link is actually riding Argorok, this is a latch/transition + // window. Keep that part synchronized with movement/action speed so a + // slow Argorok cannot burn through the health-based attack window + // while Link is still completing the latch animation. + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.back_hit_chance_time), l_HIO.back_hit_chance_time); field_0x7d0 = 0; field_0x7d6 = 1; mMoveMode++; /* fallthrough */ case 1: +#if TARGET_PC // enemy attribute integration + // Start the health-based sword-hit opportunity only once Link is + // actually latched onto Argorok's back. Do this before ticking the + // timer so a latch that completes on the old timer's final frame still + // receives the full mounted attack window. + if (mCount[1] == 0 && player->checkDragonHangRide()) { + setBck(ANM_DR_RIDE, 0, 3.0f, 1.0f); + dComIfGp_getVibration().StartShock(VIBMODE_S_POWER2, 0x4F, cXyz(0.0f, 1.0f, 0.0f)); + mCount[1] = 1; + mTimer[2] = dragon_mount_window_for_hits(this, l_HIO.back_hit_chance_time); + } +#endif if (cLib_calcTimer(&mTimer[2]) == 0) { setBck(ANM_DR_SHAKEB, 0, 10.0f, 1.0f); Z2GetAudioMgr()->changeBgmStatus(1); @@ -1773,11 +2052,13 @@ void daB_DR_c::executeWeekHit() { field_0x744 = 0.0f; break; } else { +#if !TARGET_PC // enemy attribute integration if (mCount[1] == 0 && player->checkDragonHangRide()) { setBck(ANM_DR_RIDE, 0, 3.0f, 1.0f); dComIfGp_getVibration().StartShock(VIBMODE_S_POWER2, 0x4F, cXyz(0.0f, 1.0f, 0.0f)); mCount[1] = 1; } +#endif if ((mAnm == ANM_DR_RIDE || mAnm == ANM_DR_DAMAGE_L || mAnm == ANM_DR_DAMAGE_R || mAnm == ANM_DR_CATCH) && mpModelMorf->isStop()) { setBck(ANM_DR_HOVERING_B, 2, 3.0f, 1.0f); @@ -1788,7 +2069,7 @@ void daB_DR_c::executeWeekHit() { } offCutDownHitFlg(); - field_0x7d0 = 4; + field_0x7d0 = DUSK_IF_ELSE(actor_attr::enemy_sync_u8_timer(this, 4), 4); mSound.startCreatureExtraSound(Z2SE_EN_DR_CORE_HIT, 0, -1); dComIfGp_getVibration().StartShock(VIBMODE_S_POWER5, 0x1F, cXyz(0.0f, 1.0f, 0.0f)); @@ -1806,7 +2087,7 @@ void daB_DR_c::executeWeekHit() { } field_0x7e8++; - mTimer[2] = l_HIO.back_hit_chance_time; + mTimer[2] = DUSK_IF_ELSE(dragon_mount_window_for_hits(this, l_HIO.back_hit_chance_time), l_HIO.back_hit_chance_time); mDoMtx_stack_c::copy(mpModelMorf->getModel()->getAnmMtx(15)); mDoMtx_stack_c::multVecZero(&sp50); @@ -1837,12 +2118,12 @@ void daB_DR_c::executeWeekHit() { sp44.x = 0.0f; parentActorID = fopAcM_createChild(fpcNm_B_DRE_e, fopAcM_GetID(this), 0, &sp44, fopAcM_GetRoomNo(this), &shape_angle, NULL, -1, NULL); - mTimer[0] = BREG_S(3) + 20; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, BREG_S(3) + 20), BREG_S(3) + 20); mMoveMode = 1000; return; } - if (field_0x7e8 >= 4) { + if (field_0x7e8 >= DUSK_IF_ELSE(dragon_hits_per_mount(this), 4)) { mSound.startCollisionSE(Z2SE_HIT_SWORD, 0x20); if (bitSw2 != 0xFF) { @@ -1852,7 +2133,7 @@ void daB_DR_c::executeWeekHit() { fopAcM_onSwitch(this, bitSw3); } - if (health <= 8) { + if (health <= DUSK_IF_ELSE(actor_attr::enemy_health_value(this, 8), 8)) { onDownFlg(); } @@ -1901,7 +2182,7 @@ void daB_DR_c::executeWeekHit() { if (mAnm == ANM_DR_FALL_DOWN && mpModelMorf->isStop()) { setBck(ANM_DR_DOWN_WAIT, 2, 10.0f, 1.0f); field_0x7d7 = 1; - mTimer[0] = 35; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 35), 35); } if (mAnm == ANM_DR_DOWN_WAIT && cLib_calcTimer(&mTimer[0]) == 0) { @@ -1974,8 +2255,15 @@ void daB_DR_c::mBreathSet() { cXyz mae; cXyz ato; - cLib_addCalc2(&field_0x72c, 50.0f, 0.7f, 4.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &field_0x72c, 50.0f, 0.7f, 4.0f), cLib_addCalc2(&field_0x72c, 50.0f, 0.7f, 4.0f)); data_805C7974 = field_0x72c; +#if TARGET_PC // enemy attribute integration + + // The fire particles have their own movement, separate from the damaging + // child actor. Give them the same half-strength action speed scaling. + const f32 fireSpeedScale = 1.0f + (actor_attr::enemy_action_step(this, 100.0f) - 100.0f) / 500.0f; + JGeometry::TVec3 fireDynamicsScale(fireSpeedScale, fireSpeedScale, fireSpeedScale); +#endif csXyz sp1C(shape_angle); for (int i = 0; i < 4; i++) { @@ -1985,6 +2273,7 @@ void daB_DR_c::mBreathSet() { JPABaseEmitter* emitter_p = dComIfGp_particle_getEmitter(field_0x27dc[i]); if (emitter_p != NULL) { emitter_p->setGlobalSRTMatrix(mpModelMorf->getModel()->getAnmMtx(0x13)); + IF_DUSK(emitter_p->setGlobalDynamicsScale(fireDynamicsScale);) cMtx_YrotS(*calc_mtx, fopAcM_searchPlayerAngleY(this)); mae.x = 0.0f; @@ -2007,7 +2296,7 @@ void daB_DR_c::mBreathSet() { } sp1C.x = NREG_S(8) - 1000; - mTimer[1] = 3; + mTimer[1] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 3), 3); fopAcM_createChild(fpcNm_B_DR_e, fopAcM_GetID(this), 10, &field_0x760, fopAcM_GetRoomNo(this), &sp1C, NULL, -1, NULL); } } @@ -2045,12 +2334,18 @@ bool daB_DR_c::mBreathHighSet(bool param_0) { } if (param_0 == 0) { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, fopAcM_searchPlayerAngleY(this), field_0x750, 0x800); + actor_attr::enemy_add_action_angle(this, &field_0x750, 4, 4, 40); + actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, 8, 0x300); +#else cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), field_0x750, 0x800); cLib_addCalcAngleS2(&field_0x750, 4, 4, 40); cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 8, 0x300); +#endif } - cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, l_HIO.breath_move_speed_max, l_HIO.breath_move_speed_min); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, field_0x7a8, 0.8f, l_HIO.breath_move_speed_max, l_HIO.breath_move_speed_min), cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, l_HIO.breath_move_speed_max, l_HIO.breath_move_speed_min)); ato = field_0x7a8 - current.pos; if (ato.abs() < 100.0f && fabsf(field_0x7a8.y - current.pos.y) < 40.0f && (param_0 || abs((s16)(shape_angle.y - fopAcM_searchPlayerAngleY(this))) < 0x200)) { @@ -2065,20 +2360,20 @@ bool daB_DR_c::mFeintBreath() { switch (field_0x718) { case 0: - mTimer[2] = l_HIO.breath_feint1_time; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.breath_feint1_time), l_HIO.breath_feint1_time); field_0x718++; /* fallthrough */ case 1: if (cLib_calcTimer(&mTimer[2]) == 0) { setBck(ANM_DR_FEINT_A, 0, 3.0f, 1.0f); - mTimer[2] = l_HIO.breath_feint2_time; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.breath_feint2_time), l_HIO.breath_feint2_time); field_0x718++; } else { mBreathSet(); - cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), l_HIO.body_search2_speed, l_HIO.body_search_speed_max); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, ¤t.angle.y, fopAcM_searchPlayerAngleY(this), l_HIO.body_search2_speed, l_HIO.body_search_speed_max), cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), l_HIO.body_search2_speed, l_HIO.body_search_speed_max)); field_0x756 = current.angle.y - shape_angle.y; - cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 4, 0x400); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, 4, 0x400), cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 4, 0x400)); field_0x754 = current.angle.y; } break; @@ -2088,7 +2383,15 @@ bool daB_DR_c::mFeintBreath() { } if (cLib_calcTimer(&mTimer[2]) == 0) { +#if TARGET_PC // enemy attribute integration + // At high action speed the turn can finish before FEINT_A does. + // State 3 only advances from FEINT_B, so wait for that handoff. + if (mAnm == ANM_DR_FEINT_B) { +#endif field_0x718++; +#if TARGET_PC // enemy attribute integration + } +#endif } else { if (field_0x756 > 0) { ANGLE_ADD(temp_r30, l_HIO.feint_angle * 0xB6); @@ -2097,15 +2400,23 @@ bool daB_DR_c::mFeintBreath() { } if (abs((s16)(current.angle.y - temp_r30)) > 0x100) { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, temp_r30, 1, l_HIO.body_search_feint_speed_max); + actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, 2, 0x800); +#else cLib_addCalcAngleS2(¤t.angle.y, temp_r30, 1, l_HIO.body_search_feint_speed_max); cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 2, 0x800); +#endif } else { mTimer[2] = 0; } } break; case 3: - if (mAnm == ANM_DR_FEINT_B && (int)mpModelMorf->getFrame() >= 23) { + if (mAnm == ANM_DR_FEINT_B && + // FEINT_B loops; at 400% speed it can wrap past frame 23 + // between calls to action(). Detect the crossing as well. + DUSK_IF_ELSE((mpModelMorf->getFrame() >= 23.0f || mpModelMorf->checkFrame(23.0f)), (int)mpModelMorf->getFrame() >= 23)) { setBck(ANM_DR_FEINT_C, 0, 3.0f, 1.0f); field_0x718++; } @@ -2113,7 +2424,7 @@ bool daB_DR_c::mFeintBreath() { case 4: if (mpModelMorf->isStop()) { setBck(ANM_DR_FIREB, 2, 3.0f, 1.0f); - mTimer[2] = l_HIO.breath_feint3_time; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.breath_feint3_time), l_HIO.breath_feint3_time); field_0x718++; } break; @@ -2160,8 +2471,13 @@ void daB_DR_c::executeBreathAttack() { return; } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc0(this, &speedF, 0.5f, 2.0f); + actor_attr::enemy_add_action_calc0(this, &speed.y, 0.5f, 2.0f); +#else cLib_addCalc0(&speedF, 0.5f, 2.0f); cLib_addCalc0(&speed.y, 0.5f, 2.0f); +#endif switch (mMoveMode) { case 0: @@ -2169,7 +2485,7 @@ void daB_DR_c::executeBreathAttack() { gravity = 0.0f; field_0x750 = 200; if (dComIfGp_checkPlayerStatus1(0, 0x2000000)) { - mTimer[3] = l_HIO.unk_0x4c; + mTimer[3] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.unk_0x4c), l_HIO.unk_0x4c); } mMoveMode++; /* fallthrough */ @@ -2184,17 +2500,29 @@ void daB_DR_c::executeBreathAttack() { mMoveMode++; case 2: +#if TARGET_PC // enemy attribute integration + { +#endif sp8 = mBreathHighSet(0); if (mAnm == ANM_DR_HOVERING) { if (field_0x7d1 != 2) { mStatusONOFF(1); } +#if TARGET_PC // enemy attribute integration + const bool crossedBreathFrame = actor_attr::enemy_action_time_speed(this) != 1.0f && (int)mpModelMorf->getFrame() < 33 && mpModelMorf->checkFrame(33.0f); + if (((int)mpModelMorf->getFrame() >= 33 || crossedBreathFrame) && sp8) { + OS_REPORT("[Dusklight randomizer] B_DR phase-two breath start " "(frame %.2f, crossing %u)\n", mpModelMorf->getFrame(), static_cast(crossedBreathFrame)); +#else if ((int)mpModelMorf->getFrame() >= 33 && sp8) { +#endif setBck(ANM_DR_FIREA, 0, 3.0f, 1.0f); mMoveMode++; } } break; +#if TARGET_PC // enemy attribute integration + } +#endif case 3: mBreathHighSet(1); if (!mpModelMorf->isStop()) { @@ -2202,17 +2530,23 @@ void daB_DR_c::executeBreathAttack() { } if (field_0x7d1 != 2 || mTargetHeight > player->current.pos.y || dComIfGp_checkPlayerStatus1(0, 0x2000000)) { - mTimer[2] = l_HIO.breath_continue_time; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.breath_continue_time), l_HIO.breath_continue_time); } else { +#if TARGET_PC // enemy attribute integration + mTimer[2] = actor_attr::enemy_sync_timer(this, l_HIO.breath_continue_no_armor_time); + if (health <= actor_attr::enemy_health_value(this, 8)) { + mTimer[2] = actor_attr::enemy_sync_timer(this, l_HIO.breath_feint1_time); +#else mTimer[2] = l_HIO.breath_continue_no_armor_time; if (health <= 8) { mTimer[2] = l_HIO.breath_feint1_time; +#endif } mActionMode = ACTION_BREATH_ATTACK2; } if (BREG_S(0) != 0) { - mTimer[2] = 10; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 10), 10); } field_0x72c = 0.0f; @@ -2237,7 +2571,7 @@ void daB_DR_c::executeBreathAttack() { var_r28 = -0x2000; } - if (health <= 0x10) { + if (health <= DUSK_IF_ELSE(actor_attr::enemy_health_value(this, 0x10), 0x10)) { spC = l_HIO.neck_search2_speed; spA = l_HIO.body_search2_speed; } else { @@ -2245,13 +2579,13 @@ void daB_DR_c::executeBreathAttack() { spA = l_HIO.body_search_speed; } - if (mMoveMode == 20 && mActionMode == ACTION_BREATH_ATTACK2 && health <= 8 && !mFeintBreath()) { + if (mMoveMode == 20 && mActionMode == ACTION_BREATH_ATTACK2 && health <= DUSK_IF_ELSE(actor_attr::enemy_health_value(this, 8), 8) && !mFeintBreath()) { break; } - cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), spA, l_HIO.body_search_speed_max); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, ¤t.angle.y, fopAcM_searchPlayerAngleY(this), spA, l_HIO.body_search_speed_max), cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), spA, l_HIO.body_search_speed_max)); field_0x756 = current.angle.y - shape_angle.y; - cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 4, 0x400); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, 4, 0x400), cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 4, 0x400)); if (mTargetHeight - 300.0f > player->current.pos.y) { mTimer[2] = 0; @@ -2271,10 +2605,10 @@ void daB_DR_c::executeBreathAttack() { mBreathHighSet(1); if (!mpModelMorf->isStop()) { - cLib_addCalcAngleS2(&mHeadAngle.y, 0, 100, 0x200); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &mHeadAngle.y, 0, 100, 0x200), cLib_addCalcAngleS2(&mHeadAngle.y, 0, 100, 0x200)); } else if (mActionMode == ACTION_BREATH_ATTACK2 && field_0x7d1 == 2 && mTargetHeight < player->current.pos.y) { setBck(ANM_DR_FIRED, 2, 10.0f, 1.0f); - mTimer[3] = l_HIO.after_breath_chance_time; + mTimer[3] = DUSK_IF_ELSE(actor_attr::enemy_stun_timer(this, l_HIO.after_breath_chance_time), l_HIO.after_breath_chance_time); mWeekCc.OnTgSetBit(); mStatusONOFF(1); mMoveMode = 100; @@ -2314,9 +2648,9 @@ void daB_DR_c::executeBreathAttack() { } if (field_0x7d1 != 2) { - mTimer[3] = l_HIO.no_attack_time; + mTimer[3] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.no_attack_time), l_HIO.no_attack_time); } else { - mTimer[3] = l_HIO.no_attack_no_armor_time; + mTimer[3] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.no_attack_no_armor_time), l_HIO.no_attack_no_armor_time); } mMoveMode = 1; break; @@ -2324,7 +2658,7 @@ void daB_DR_c::executeBreathAttack() { mBreathHighSet(1); if (BREG_S(7) != 0) { - mTimer[3] = 1000; + mTimer[3] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 1000), 1000); } attention_info.flags |= fopAc_AttnFlag_BATTLE_e; @@ -2351,8 +2685,13 @@ void daB_DR_c::executeBreathAttack() { break; } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, &mHeadAngle.x, 0, 100, 0x200); + actor_attr::enemy_add_action_angle(this, &mHeadAngle.y, var_r28, spC, l_HIO.neck_search_speed_max); +#else cLib_addCalcAngleS2(&mHeadAngle.x, 0, 100, 0x200); cLib_addCalcAngleS2(&mHeadAngle.y, var_r28, spC, l_HIO.neck_search_speed_max); +#endif if (mMoveMode < 9 && !mPlayerHighCheck()) { if (mAnm == ANM_DR_FIREB) { @@ -2379,8 +2718,13 @@ void daB_DR_c::executeWindAttack() { mHabatakiAnmSet(mActionMode); } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc0(this, &speedF, 0.5f, 5.0f + WREG_F(7)); + actor_attr::enemy_add_action_calc0(this, &speed.y, 0.5f, 5.0f); +#else cLib_addCalc0(&speedF, 0.5f, 5.0f + WREG_F(7)); cLib_addCalc0(&speed.y, 0.5f, 5.0f); +#endif switch (mMoveMode) { case 0: @@ -2400,7 +2744,7 @@ void daB_DR_c::executeWindAttack() { /* fallthrough */ case 1: field_0x7a8.set(home.pos); - cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 40.0f, 20.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, field_0x7a8, 0.8f, 40.0f, 20.0f), cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 40.0f, 20.0f)); sp30 = field_0x7a8 - current.pos; sp30.y = 0.0f; @@ -2431,26 +2775,26 @@ void daB_DR_c::executeWindAttack() { if (mAnm == ANM_DR_FLAP) { if (sp24.abs() > 100.0f + l_HIO.wind_large_attack_lockon_range) { - mpModelMorf->setPlaySpeed(1.0f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation_play_speed(this, mpModelMorf, 1.0f), mpModelMorf->setPlaySpeed(1.0f)); mCount[2] = 0; mTimer[2] = 0; if (mCount[1] == 0 && mTimer[1] == 0) { - mTimer[1] = l_HIO.unk_0x48; + mTimer[1] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.unk_0x48), l_HIO.unk_0x48); mCount[1] = 1; if (WREG_S(1) != 0) { - mTimer[1] = 200; + mTimer[1] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 200), 200); } } } else { field_0x7d3 = 2; - mpModelMorf->setPlaySpeed(1.25f); + DUSK_IF_ELSE(actor_attr::enemy_set_animation_play_speed(this, mpModelMorf, 1.25f), mpModelMorf->setPlaySpeed(1.25f)); if (mCount[2] == 0) { mCount[2] = 1; - mTimer[2] = l_HIO.center_wind_time; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.center_wind_time), l_HIO.center_wind_time); if (WREG_S(1) != 0) { - mTimer[2] = 200; + mTimer[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 200), 200); } } } @@ -2478,15 +2822,21 @@ void daB_DR_c::executeWindAttack() { } } +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, fopAcM_searchPlayerAngleY(this), field_0x750, 0x400); + actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, field_0x750, 0x400); + actor_attr::enemy_add_action_angle(this, &field_0x750, 8, 8, 20); +#else cLib_addCalcAngleS2(¤t.angle.y, fopAcM_searchPlayerAngleY(this), field_0x750, 0x400); cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, 0x400); cLib_addCalcAngleS2(&field_0x750, 8, 8, 20); +#endif if (WREG_S(1) != 0) { mTargetHeight = 1500.0f + mAcch.GetGroundH(); } - cLib_addCalc2(¤t.pos.y, 400.0f + mTargetHeight, 0.8f, 20.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, ¤t.pos.y, 400.0f + mTargetHeight, 0.8f, 20.0f), cLib_addCalc2(¤t.pos.y, 400.0f + mTargetHeight, 0.8f, 20.0f)); } bool daB_DR_c::mGliderMoveSub(f32 param_0) { @@ -2505,7 +2855,7 @@ bool daB_DR_c::mGliderMoveSub(f32 param_0) { } field_0x7a8.y += field_0x740; - cLib_addCalc2(&field_0x740, param_0, 0.8f, 25.0f + WREG_F(0)); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &field_0x740, param_0, 0.8f, 25.0f + WREG_F(0)), cLib_addCalc2(&field_0x740, param_0, 0.8f, 25.0f + WREG_F(0))); sp18 = field_0x7a8 - current.pos; if (sp18.abs() > 1300.0f + NREG_F(6)) { @@ -2698,7 +3048,7 @@ void daB_DR_c::executeGliderAttack() { } } - cLib_addCalc2(&field_0x748, var_f31, 0.1f, 0.025f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &field_0x748, var_f31, 0.1f, 0.025f), cLib_addCalc2(&field_0x748, var_f31, 0.1f, 0.025f)); if (mGliderMoveSub(field_0x748) != 0) { if (field_0x73c < 3000.0f + BREG_F(14)) { @@ -2747,7 +3097,7 @@ void daB_DR_c::executeGliderAttack() { mGlider_AniSet(0); } - cLib_addCalc2(&field_0x748, var_f31, 0.1f, 0.025f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &field_0x748, var_f31, 0.1f, 0.025f), cLib_addCalc2(&field_0x748, var_f31, 0.1f, 0.025f)); if (mGliderMoveSub(field_0x748) != 0) { mStatusONOFF(0); @@ -2756,7 +3106,7 @@ void daB_DR_c::executeGliderAttack() { fopAcM_OffStatus(this, 0); mCount[2] = 0; - mTimer[1] = 20; + mTimer[1] = DUSK_IF_ELSE(actor_attr::enemy_sync_terminal_timer(this, 19), 20); mMoveMode++; } break; @@ -2797,18 +3147,37 @@ void daB_DR_c::executeGliderAttack() { } ato = field_0x7a8 - current.pos; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, (s16)ato.atan2sX_Z(), field_0x750, JREG_S(2) + 0x500); + actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, field_0x750, JREG_S(3) + 0x500); + // field_0x750 is itself the turning divisor used by the glider steering. + // At low action speeds, the scaled divisor passed to cLib_addCalcAngleS2 + // can become larger than the remaining difference to the target. With + // integer angle math that makes the update truncate to zero (at 0.25x it + // was getting stuck at 87 instead of continuing toward the vanilla target + // of 8), leaving Argorok unable to turn tightly enough to reach the + // waypoint and causing an endless circle. + // + // Use the min-step overload so this control value always continues + // converging. action_s16_step keeps a positive minimum at least 1, while + // the divisor/max-step still follow the randomized action speed. + actor_attr::enemy_add_action_angle(this, &field_0x750, JREG_S(4) + 8, JREG_S(5) + 20, JREG_S(6) + 40, 1); + actor_attr::enemy_add_action_angle(this, ¤t.angle.x, ato.atan2sY_XZ(), 20, NREG_S(3) + 0x400); + actor_attr::enemy_add_action_angle(this, &shape_angle.x, current.angle.x, NREG_S(2) + 20, NREG_S(3) + 0x400); +#else cLib_addCalcAngleS2(¤t.angle.y, (s16)ato.atan2sX_Z(), field_0x750, JREG_S(2) + 0x500); cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, JREG_S(3) + 0x500); cLib_addCalcAngleS2(&field_0x750, JREG_S(4) + 8, JREG_S(5) + 20, JREG_S(6) + 40); cLib_addCalcAngleS2(¤t.angle.x, ato.atan2sY_XZ(), 20, NREG_S(3) + 0x400); cLib_addCalcAngleS2(&shape_angle.x, current.angle.x, NREG_S(2) + 20, NREG_S(3) + 0x400); +#endif if (mMoveMode >= 2) { f32 var_f30 = 90.0f + NREG_F(9); if (mAnm == ANM_DR_FLY) { var_f30 = 120.0f + NREG_F(9); } - cLib_addCalc2(&speedF, var_f30, 0.5f, 2.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &speedF, var_f30, 0.5f, 2.0f), cLib_addCalc2(&speedF, var_f30, 0.5f, 2.0f)); } if (mMoveMode < 10 || mMoveMode > 22) { @@ -2818,10 +3187,15 @@ void daB_DR_c::executeGliderAttack() { mae.y = 0.0f; mae.z = speedF; MtxPosition(&mae, &ato); - cLib_addCalc2(&speed.y, ato.y, 0.8f, 10.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &speed.y, ato.y, 0.8f, 10.0f), cLib_addCalc2(&speed.y, ato.y, 0.8f, 10.0f)); } else { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc0(this, &speed.y, 0.8f, 10.0f); + actor_attr::enemy_add_action_calc2(this, ¤t.pos.y, field_0x7a8.y, 0.8f, 25.0f); +#else cLib_addCalc0(&speed.y, 0.8f, 10.0f); cLib_addCalc2(¤t.pos.y, field_0x7a8.y, 0.8f, 25.0f); +#endif } } @@ -2867,19 +3241,29 @@ void daB_DR_c::executePillarSearch() { mMoveMode++; case 2: +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc2(this, ¤t.pos.y, field_0x7b4.y, 0.8f + NREG_F(10), 5.0f + NREG_F(11)); + actor_attr::enemy_add_action_calc2(this, &speedF, 20.0f + NREG_F(14), 0.8f + NREG_F(15), 4.0f + NREG_F(16)); +#else cLib_addCalc2(¤t.pos.y, field_0x7b4.y, 0.8f + NREG_F(10), 5.0f + NREG_F(11)); cLib_addCalc2(&speedF, 20.0f + NREG_F(14), 0.8f + NREG_F(15), 4.0f + NREG_F(16)); +#endif if (!(fabsf(current.pos.y - field_0x7b4.y) > 20.0f) && !(speedF > 22.0f + NREG_F(14))) { mHabatakiAnmSet(0); field_0x7a8.set(home.pos); +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc2(this, ¤t.pos.x, field_0x7b4.x, 0.8f + NREG_F(10), speedF); + actor_attr::enemy_add_action_calc2(this, ¤t.pos.z, field_0x7b4.z, 0.8f + NREG_F(10), speedF); +#else cLib_addCalc2(¤t.pos.x, field_0x7b4.x, 0.8f + NREG_F(10), speedF); cLib_addCalc2(¤t.pos.z, field_0x7b4.z, 0.8f + NREG_F(10), speedF); +#endif sp30 = field_0x7b4 - current.pos; if (mAnm == ANM_DR_HOVERING) { - if ((int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f) && sp30.abs() < 20.0f) { + if (DUSK_IF_ELSE(mpModelMorf->checkFrame(mpModelMorf->getEndFrame() - 1.0f), (int)mpModelMorf->getFrame() >= (int)(mpModelMorf->getEndFrame() - 1.0f)) && sp30.abs() < 20.0f) { speedF = 0.0f; field_0x7a8.set(mPillarTop_dt[mCount[1]]); field_0x7a8.y = 6000.0f; @@ -2892,11 +3276,19 @@ void daB_DR_c::executePillarSearch() { if (mActionMode != ACTION_PILLAR_WAIT) { sp30 = field_0x7a8 - current.pos; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, (s16)sp30.atan2sX_Z(), field_0x750, JREG_S(2) + 0x400); + actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, field_0x750, JREG_S(3) + 0x400); + actor_attr::enemy_add_action_angle(this, &field_0x750, JREG_S(4) + 10, JREG_S(5) + 10, JREG_S(6) + 20); + actor_attr::enemy_add_action_angle(this, ¤t.angle.x, sp30.atan2sY_XZ(), 20, NREG_S(3) + 0x400); + actor_attr::enemy_add_action_angle(this, &shape_angle.x, current.angle.x, NREG_S(2) + 20, NREG_S(3) + 0x400); +#else cLib_addCalcAngleS2(¤t.angle.y, (s16)sp30.atan2sX_Z(), field_0x750, JREG_S(2) + 0x400); cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, field_0x750, JREG_S(3) + 0x400); cLib_addCalcAngleS2(&field_0x750, JREG_S(4) + 10, JREG_S(5) + 10, JREG_S(6) + 20); cLib_addCalcAngleS2(¤t.angle.x, sp30.atan2sY_XZ(), 20, NREG_S(3) + 0x400); cLib_addCalcAngleS2(&shape_angle.x, current.angle.x, NREG_S(2) + 20, NREG_S(3) + 0x400); +#endif if (mPlayerHighCheck() && -G_CM3D_F_INF != mAcch.GetGroundH()) { setActionMode(ACTION_BREATH_ATTACK, 0); @@ -2923,8 +3315,13 @@ void daB_DR_c::executePillarWait() { field_0x7d6 = 0; mAllClr(); +#if TARGET_PC // enemy attribute integration + mTimer[2] = actor_attr::enemy_sync_timer(this, 50); + mTimer[0] = actor_attr::enemy_sync_timer(this, l_HIO.unk_0x62); +#else mTimer[2] = 50; mTimer[0] = l_HIO.unk_0x62; +#endif mPillarCc.OnCoSetBit(); speed.y = 0.0f; @@ -2965,7 +3362,7 @@ void daB_DR_c::executePillarWait() { } if (mPlayerHighCheck()) { - mTimer[0] = l_HIO.unk_0x62; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, l_HIO.unk_0x62), l_HIO.unk_0x62); } if ((fopAcM_searchPlayerDistanceXZ(this) < 350.0f && player->current.pos.y > 4500.0f) || mTargetHeight < player->current.pos.y || cLib_calcTimer(&mTimer[0]) == 0 || (player->current.pos.y >= 5000.0f && fopAcM_searchPlayerDistanceXZ(this) > 3100.0f) || mPillarCc.ChkTgHit()) { @@ -2982,7 +3379,7 @@ void daB_DR_c::executePillarWait() { case 3: if ((int)mpModelMorf->getFrame() >= 14) { field_0x7a8.y = 6150.0f; - cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 40.0f, 10.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, field_0x7a8, 0.8f, 40.0f, 10.0f), cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 40.0f, 10.0f)); } if (!mpModelMorf->isStop()) { @@ -2995,7 +3392,7 @@ void daB_DR_c::executePillarWait() { setBck(ANM_DR_HOVERING, 2, 3.0f, 1.0f); mMoveMode++; case 4: - cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 20.0f, 5.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, field_0x7a8, 0.8f, 20.0f, 5.0f), cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 20.0f, 5.0f)); if (!(mAcch.GetGroundH() > 1000.0f)) { if (home.pos.y + l_HIO.breath_attack_threshold <= player->current.pos.y) { @@ -3013,15 +3410,25 @@ void daB_DR_c::executePillarWait() { sp14.y += -200.0f; mPillarCc.SetC(sp14); +#if TARGET_PC // enemy attribute integration + mPillarCc.SetH(actor_attr::enemy_size_value(this, 1000.0f)); + mPillarCc.SetR(actor_attr::enemy_size_value(this, 400.0f)); +#else mPillarCc.SetH(1000.0f); mPillarCc.SetR(400.0f); +#endif mPillarCc.SetAtVec(at_vec); dComIfG_Ccsp()->Set(&mPillarCc); - cLib_addCalc0(&speedF, 0.8f, 5.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc0(this, &speedF, 0.8f, 5.0f), cLib_addCalc0(&speedF, 0.8f, 5.0f)); sp14 = home.pos - current.pos; +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.y, (s16)sp14.atan2sX_Z(), 10, 0x400); + actor_attr::enemy_add_action_angle(this, &shape_angle.y, current.angle.y, 10, 0x400); +#else cLib_addCalcAngleS2(¤t.angle.y, (s16)sp14.atan2sX_Z(), 10, 0x400); cLib_addCalcAngleS2(&shape_angle.y, current.angle.y, 10, 0x400); +#endif } bool daB_DR_c::startDemoCheck() { @@ -3125,6 +3532,7 @@ void daB_DR_c::executeDamageDemo() { shape_angle.y = current.angle.y = home.angle.y; current.pos.x = home.pos.x; current.pos.z = home.pos.z; + IF_DUSK(field_0x7e0 = 0;) daPy_getPlayerActorClass()->setPlayerPosAndAngle(&player->current.pos, home.angle.y, 0); setBck(ANM_DR_IMPACT, 0, 3.0f, 1.0f); mMoveMode++; @@ -3133,12 +3541,12 @@ void daB_DR_c::executeDamageDemo() { case 11: if (mAnm == ANM_DR_BOOT_C) { if ((int)mpModelMorf->getFrame() >= 34) { - cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f), cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f)); } if (mpModelMorf->isStop()) { setBck(ANM_DR_DOWN_WAIT, 2, 3.0f, 1.0f); - mTimer[0] = 35; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 35), 35); } } @@ -3156,10 +3564,10 @@ void daB_DR_c::executeDamageDemo() { case 12: if (mAnm == ANM_DR_BOOT_C) { if ((int)mpModelMorf->getFrame() >= 34) { - cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f), cLib_addCalc2(&mDemoCamCenter.y, player->current.pos.y, 0.7f, 150.0f)); } - mTimer[0] = 50; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 50), 50); if (!mpModelMorf->isStop()) { break; @@ -3168,14 +3576,14 @@ void daB_DR_c::executeDamageDemo() { if (mAnm != ANM_DR_DOWN_WAIT) { setBck(ANM_DR_DOWN_WAIT, 2, 10.0f, 1.0f); - mTimer[0] = 35; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 35), 35); } if (cLib_calcTimer(&mTimer[0]) != 0) { break; } - mTimer[0] = 70; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 70), 70); mMoveMode++; case 13: if (cLib_calcTimer(&mTimer[0]) == 0) { @@ -3184,7 +3592,7 @@ void daB_DR_c::executeDamageDemo() { dComIfGp_event_reset(); field_0x7d7 = 1; - mTimer[0] = 35; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 35), 35); setActionMode(ACTION_TAIL_HIT, 20); } } @@ -3213,7 +3621,12 @@ void daB_DR_c::executeDamageDemo() { } if (mAnm == ANM_DR_BOOT_C) { +#if TARGET_PC // enemy attribute integration + const bool reached_armor_break_frame = mpModelMorf->checkFrame(45.0f) || (field_0x7d1 == 0 && mpModelMorf->getFrame() >= 45.0f); + if (reached_armor_break_frame) { +#else if (mpModelMorf->checkFrame(45.0f)) { +#endif if (field_0x7d1 == 0) { mSound.startCreatureSound(Z2SE_EN_DR_CRASH, 0, -1); field_0x7d1 = 1; @@ -3312,10 +3725,14 @@ void daB_DR_c::executeBullet() { switch (mMoveMode) { case 0: field_0x730 = 100.0f; - speedF = 100.0f; + // Keep vanilla projectile speed unchanged and apply half of the + // movement speed difference at other settings. + speedF = DUSK_IF_ELSE(100.0f + (actor_attr::enemy_action_step(this, 100.0f) - 100.0f) * 0.2f, 100.0f); mAllClr(); - mTimer[0] = data_805C7974; + // Keep the shot's travel distance when the projectile speed boost is + // smaller than the boss's action speed boost. + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, data_805C7974 * actor_attr::enemy_action_step(this, 100.0f) / speedF), data_805C7974); mMoveMode++; return; case 1: @@ -3339,13 +3756,39 @@ void daB_DR_c::executeBullet() { mae.z = speedF; MtxPosition(&mae, &speed); - field_0x730 += 10.0f; + field_0x730 += DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 10.0f), 10.0f); if (field_0x730 > 100.0f) { field_0x730 = 100.0f; } + IF_DUSK(const f32 breathHitRadius = actor_attr::enemy_size_value(this, field_0x730) * (actor_attr::enemy_size_multiplier(this) > 1.0f ? 0.9f : 1.0f);) mBreathAtCc.SetC(current.pos); +#if TARGET_PC // enemy attribute integration + if (actor_attr::enemy_action_time_speed(this) > 1.0f) { + // This is a sphere, so a fast projectile can pass through Link + // between collision updates. Sweep its upcoming step against + // Link's movement, then place the normal attack sphere at Link's + // current body position only when the two paths crossed. + daPy_py_c* player = daPy_getPlayerActorClass(); + const cXyz playerCenter = player->current.pos + (player->eyePos - player->current.pos) * 0.5f; + const cXyz playerMotion = player->current.pos - player->old.pos; + const cXyz relativeStart = current.pos - (playerCenter - playerMotion); + const cXyz relativeStep = speed - playerMotion; + const f32 stepLenSq = relativeStep.x * relativeStep.x + relativeStep.y * relativeStep.y + relativeStep.z * relativeStep.z; + if (stepLenSq > 0.0001f) { + const f32 t = -(relativeStart.x * relativeStep.x + relativeStart.y * relativeStep.y + relativeStart.z * relativeStep.z) / stepLenSq; + if (t >= 0.0f && t <= 1.0f) { + const cXyz separation = relativeStart + relativeStep * t; + if (separation.x * separation.x + separation.y * separation.y + separation.z * separation.z <= breathHitRadius * breathHitRadius) { + mBreathAtCc.SetC(playerCenter); + } + } + } + } + mBreathAtCc.SetR(breathHitRadius); +#else mBreathAtCc.SetR(field_0x730); +#endif dComIfG_Ccsp()->Set(&mBreathAtCc); } } @@ -3363,12 +3806,16 @@ void daB_DR_c::executeParts() { speed.y += cM_rndFX(10.0f + ZREG_F(11)); field_0x752 = cM_rndF(2048.0f + ZREG_F(9)); - mTimer[0] = 100; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 100), 100); gravity = -3.0f + ZREG_F(12); mMoveMode++; break; case 1: +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_angle_add(this, shape_angle.x , field_0x752); +#else shape_angle.x += field_0x752; +#endif if (!mAcch.ChkGroundHit()) { if (cLib_calcTimer(&mTimer[0]) == 0) { fopAcM_delete(this); @@ -3379,14 +3826,20 @@ void daB_DR_c::executeParts() { field_0x752 *= 2; mMoveMode++; case 2: +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_angle_add(this, shape_angle.x , field_0x752); + actor_attr::enemy_add_action_calc0(this, &speedF, 0.7f, 1.0f + ZREG_F(15)); + actor_attr::enemy_add_action_angle(this, &field_0x752, 0, 8, speedF); +#else shape_angle.x += field_0x752; cLib_addCalc0(&speedF, 0.7f, 1.0f + ZREG_F(15)); cLib_addCalcAngleS2(&field_0x752, 0, 8, speedF); +#endif if (speedF > 0.2f) { break; } speedF = 0.0f; - mTimer[0] = 10; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 10), 10); mMoveMode++; case 3: if (cLib_calcTimer(&mTimer[0]) == 0) { @@ -3396,7 +3849,7 @@ void daB_DR_c::executeParts() { break; case 10: speedF = 40.0f + ZREG_F(13) + cM_rndF(10.0f + ZREG_F(14)); - mTimer[0] = 50; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 50), 50); ANGLE_ADD(current.angle.x, cM_rndFX(16384.0f)); mae = camera->view.lookat.center - current.pos; @@ -3508,8 +3961,13 @@ void daB_DR_c::action() { if (mActionMode != ACTION_GLIDER_ATTACK || mMoveMode == 1000) { if (current.angle.x != 0 || shape_angle.x != 0) { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_angle(this, ¤t.angle.x, 0, 8, 0x400); + actor_attr::enemy_add_action_angle(this, &shape_angle.x, current.angle.x, 8, 0x400); +#else cLib_addCalcAngleS2(¤t.angle.x, 0, 8, 0x400); cLib_addCalcAngleS2(&shape_angle.x, current.angle.x, 8, 0x400); +#endif } field_0x74e = 0; } else { @@ -3522,7 +3980,7 @@ void daB_DR_c::action() { } } - cLib_addCalcAngleS2(&shape_angle.z, field_0x74e, NREG_S(6) + 40, NREG_S(7) + 0x400); + DUSK_IF_ELSE(actor_attr::enemy_add_action_angle(this, &shape_angle.z, field_0x74e, NREG_S(6) + 40, NREG_S(7) + 0x400), cLib_addCalcAngleS2(&shape_angle.z, field_0x74e, NREG_S(6) + 40, NREG_S(7) + 0x400)); if (BREG_S(1) != 0) { mWeekCc.OnTgSetBit(); @@ -3530,7 +3988,7 @@ void daB_DR_c::action() { speed.zero(); } - fopAcM_posMoveF(this, mColliderStts.GetCCMoveP()); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, mColliderStts.GetCCMoveP()), fopAcM_posMoveF(this, mColliderStts.GetCCMoveP())); if (mActionMode != ACTION_BREATH_ATTACK && mActionMode != ACTION_BREATH_ATTACK2) { mHeadAngleSet(); @@ -3553,7 +4011,7 @@ void daB_DR_c::action_fire() { void daB_DR_c::action_parts() { executeParts(); - fopAcM_posMoveF(this, mColliderStts.GetCCMoveP()); + DUSK_IF_ELSE(actor_attr::enemy_action_pos_move_f(this, mColliderStts.GetCCMoveP()), fopAcM_posMoveF(this, mColliderStts.GetCCMoveP())); mAcch.CrrPos(dComIfG_Bgsp()); mSound.framework(0, dComIfGp_getReverb(fopAcM_GetRoomNo(this))); } @@ -3562,7 +4020,11 @@ void daB_DR_c::mtx_set() { mDoMtx_stack_c::transS(current.pos); mDoMtx_stack_c::transM(mBoot_c_trance.x, mBoot_c_trance.y, mBoot_c_trance.z); mDoMtx_stack_c::ZXYrotM(shape_angle); +#if TARGET_PC // enemy attribute integration + mDoMtx_stack_c::scaleM((l_HIO.model_size) * actor_attr::enemy_size_multiplier(this), (l_HIO.model_size) * actor_attr::enemy_size_multiplier(this), (l_HIO.model_size) * actor_attr::enemy_size_multiplier(this)); +#else mDoMtx_stack_c::scaleM(l_HIO.model_size, l_HIO.model_size, l_HIO.model_size); +#endif mpModelMorf->getModel()->setBaseTRMtx(mDoMtx_stack_c::get()); mpModelMorf->modelCalc(); @@ -3581,9 +4043,9 @@ void daB_DR_c::mtx_set() { field_0x7a8.y = var_f31; if ((int)mpModelMorf->getFrame() < 35.0f + JREG_F(5)) { - cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 10.0f + JREG_F(6), 5.0f + JREG_F(7)); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, field_0x7a8, 0.8f, 10.0f + JREG_F(6), 5.0f + JREG_F(7)), cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 10.0f + JREG_F(6), 5.0f + JREG_F(7))); } else { - cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 20.0f + JREG_F(8), 5.0f + JREG_F(9)); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, ¤t.pos, field_0x7a8, 0.8f, 20.0f + JREG_F(8), 5.0f + JREG_F(9)), cLib_addCalcPos(¤t.pos, field_0x7a8, 0.8f, 20.0f + JREG_F(8), 5.0f + JREG_F(9))); } if (current.pos.y < field_0x7a8.y) { @@ -3623,7 +4085,11 @@ void daB_DR_c::mtx_set() { void daB_DR_c::parts_mtx_set() { mDoMtx_stack_c::transS(current.pos); mDoMtx_stack_c::ZXYrotM(shape_angle); +#if TARGET_PC // enemy attribute integration + mDoMtx_stack_c::scaleM(actor_attr::enemy_size_multiplier(this, scale)); +#else mDoMtx_stack_c::scaleM(scale.x, scale.y, scale.z); +#endif mpPartModel->setBaseTRMtx(mDoMtx_stack_c::get()); } @@ -3645,13 +4111,13 @@ void daB_DR_c::cc_set() { } attention_info.position = eyePos; - attention_info.position.y += 100.0f; + attention_info.position.y += DUSK_IF_ELSE(actor_attr::enemy_size_value(this, 100.0f), 100.0f); mDoMtx_stack_c::copy(model_p->getAnmMtx(0x31)); mae.set(0.0f, 0.0f, 50.0f); mDoMtx_stack_c::multVec(&mae, &ato); mTailCc.SetC(ato); - mTailCc.SetR(250.0f); + mTailCc.SetR(DUSK_IF_ELSE(actor_attr::enemy_size_value(this, 250.0f), 250.0f)); dComIfG_Ccsp()->Set(&mTailCc); mDoMtx_stack_c::copy(model_p->getAnmMtx(0x13)); @@ -3715,9 +4181,9 @@ void daB_DR_c::cc_set() { } if (var_r27 == 0) { - cLib_addCalc(&field_0x738, 0.6f, 0.05f, 0.003f, 0.00001f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc(this, &field_0x738, 0.6f, 0.05f, 0.003f, 0.00001f), cLib_addCalc(&field_0x738, 0.6f, 0.05f, 0.003f, 0.00001f)); } else { - cLib_addCalc(&field_0x738, 0.9f, 0.5f, 0.2f, 0.001f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc(this, &field_0x738, 0.9f, 0.5f, 0.2f, 0.001f), cLib_addCalc(&field_0x738, 0.9f, 0.5f, 0.2f, 0.001f)); dKyw_evt_wind_set(0, shape_angle.y); } @@ -3746,8 +4212,13 @@ void daB_DR_c::cc_set() { mae.set(-200.0f + WREG_F(2), WREG_F(3), WREG_F(4)); mDoMtx_stack_c::multVec(&mae, &ato); mWeekCc.SetC(ato); +#if TARGET_PC // enemy attribute integration + mWeekCc.SetH(actor_attr::enemy_size_value(this, 300.0f + WREG_F(5))); + mWeekCc.SetR(actor_attr::enemy_size_value(this, 150.0f + WREG_F(6))); +#else mWeekCc.SetH(300.0f + WREG_F(5)); mWeekCc.SetR(150.0f + WREG_F(6)); +#endif dComIfG_Ccsp()->Set(&mWeekCc); } @@ -3776,8 +4247,13 @@ void daB_DR_c::down_cc_set() { mDoMtx_stack_c::multVec(&mae, &ato); mDownCc[i].SetC(ato); +#if TARGET_PC // enemy attribute integration + mDownCc[i].SetH(actor_attr::enemy_size_value(this, DOWN_CYL_DT[i].height)); + mDownCc[i].SetR(actor_attr::enemy_size_value(this, DOWN_CYL_DT[i].radius)); +#else mDownCc[i].SetH(DOWN_CYL_DT[i].height); mDownCc[i].SetR(DOWN_CYL_DT[i].radius); +#endif dComIfG_Ccsp()->Set(&mDownCc[i]); } @@ -3805,7 +4281,7 @@ void daB_DR_c::down_cc_set() { mDoMtx_stack_c::multVec(&mae, &ato); mDownSphCc[i].SetC(ato); - mDownSphCc[i].SetR(DOWN_SPH_DT[i].radius); + mDownSphCc[i].SetR(DUSK_IF_ELSE(actor_attr::enemy_size_value(this, DOWN_SPH_DT[i].radius), DOWN_SPH_DT[i].radius)); dComIfG_Ccsp()->Set(&mDownSphCc[i]); } } @@ -3912,7 +4388,11 @@ int daB_DR_c::execute() { mpModelMorf->play(0, dComIfGp_getReverb(fopAcM_GetRoomNo(this))); mDoMtx_stack_c::transS(current.pos); mDoMtx_stack_c::ZXYrotM(shape_angle); +#if TARGET_PC // enemy attribute integration + mDoMtx_stack_c::scaleM((l_HIO.model_size) * actor_attr::enemy_size_multiplier(this), (l_HIO.model_size) * actor_attr::enemy_size_multiplier(this), (l_HIO.model_size) * actor_attr::enemy_size_multiplier(this)); +#else mDoMtx_stack_c::scaleM(l_HIO.model_size, l_HIO.model_size, l_HIO.model_size); +#endif mpModelMorf->getModel()->setBaseTRMtx(mDoMtx_stack_c::get()); mpModelMorf->modelCalc(); @@ -4000,7 +4480,7 @@ int daB_DR_c::CreateHeap() { J3DModelData* modelData = (J3DModelData*)dComIfG_getObjectRes("B_DR", 0x49); JUT_ASSERT(6324, modelData != NULL); - mpModelMorf = JKR_NEW mDoExt_McaMorfSO(modelData, NULL, NULL, (J3DAnmTransform*)dComIfG_getObjectRes("B_DR", 0x2F), 2, 1.0f, 0, -1, &mSound, 0, 0x11000084); + mpModelMorf = JKR_NEW mDoExt_McaMorfSO(modelData, NULL, NULL, (J3DAnmTransform*)dComIfG_getObjectRes("B_DR", 0x2F), 2, DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 1.0f), 1.0f), 0, -1, &mSound, 0, 0x11000084); if (mpModelMorf == NULL || mpModelMorf->getModel() == NULL) { return 0; } @@ -4011,7 +4491,7 @@ int daB_DR_c::CreateHeap() { } J3DAnmTevRegKey* pbrk = (J3DAnmTevRegKey*)dComIfG_getObjectRes("B_DR", 0x51); - if (!mpCoreBrk->init(mpModelMorf->getModel()->getModelData(), pbrk, 1, 2, 1.0f, 0, -1)) { + if (!mpCoreBrk->init(mpModelMorf->getModel()->getModelData(), pbrk, 1, 2, DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 1.0f), 1.0f), 0, -1)) { return 0; } @@ -4069,7 +4549,9 @@ int daB_DR_c::create() { if (arg0 == 0xA) { mColliderStts.Init(0xFF, 0, this); mBreathAtCc.Set(cc_dr_breath_at_src); + IF_DUSK(mBreathAtCc.SetAtAtp(actor_attr::enemy_attack_power_byte(this, mBreathAtCc.GetAtAtp()));) mBreathAtCc.SetStts(&mColliderStts); + IF_DUSK(mBreathAtCc.SetAtAtp(actor_attr::enemy_attack_power_byte(this, 4.0f));) setActionMode(0xE, 0); } else { static u32 mParts_EstimateSizeDt[] = {0x1980, 0x1990, 0xD50, 0xD40, 0xD40, 0xD40}; @@ -4078,7 +4560,11 @@ int daB_DR_c::create() { } mAcch.Set(fopAcM_GetPosition_p(this), fopAcM_GetOldPosition_p(this), this, 1, &mAcchCir, fopAcM_GetSpeed_p(this), NULL, NULL); +#if TARGET_PC // enemy attribute integration + mAcchCir.SetWall(actor_attr::enemy_size_value(this, 20.0f), actor_attr::enemy_size_value(this, 40.0f)); +#else mAcchCir.SetWall(20.0f, 40.0f); +#endif mAcch.CrrPos(dComIfG_Bgsp()); gravity = -3.0f; @@ -4140,8 +4626,13 @@ int daB_DR_c::create() { dComIfGs_offSaveDunSwitch(22); onWolfNoLock(); +#if TARGET_PC // enemy attribute integration + health = actor_attr::enemy_health_value(this, 24); + field_0x560 = health; +#else health = 24; field_0x560 = 24; +#endif if (!hio_set) { hio_set = true; @@ -4161,42 +4652,79 @@ int daB_DR_c::create() { fopAcM_SetMax(this, 20000.0f, 20000.0f, 20000.0f); mAcch.Set(fopAcM_GetPosition_p(this), fopAcM_GetOldPosition_p(this), this, 1, &mAcchCir, fopAcM_GetSpeed_p(this), NULL, NULL); +#if TARGET_PC // enemy attribute integration + mAcchCir.SetWall(actor_attr::enemy_size_value(this, 0.0f), actor_attr::enemy_size_value(this, 300.0f)); +#else mAcchCir.SetWall(0.0f, 300.0f); +#endif mAcch.CrrPos(dComIfG_Bgsp()); mColliderStts.Init(0xFF, 0, this); mPillarCc.Set(cc_pillar_src); +#if TARGET_PC // enemy attribute integration + mPillarCc.SetR(actor_attr::enemy_size_value(this, mPillarCc.GetR())); + mPillarCc.SetH(actor_attr::enemy_size_value(this, mPillarCc.GetH())); + mPillarCc.SetAtAtp(actor_attr::enemy_attack_power_byte(this, mPillarCc.GetAtAtp())); +#endif mPillarCc.SetStts(&mColliderStts); mWeekCc.Set(cc_dr_week_src); +#if TARGET_PC // enemy attribute integration + + mWeekCc.SetR(actor_attr::enemy_size_value(this, mWeekCc.GetR())); + + mWeekCc.SetH(actor_attr::enemy_size_value(this, mWeekCc.GetH())); + + mWeekCc.SetAtAtp(actor_attr::enemy_attack_power_byte(this, mWeekCc.GetAtAtp())); +#endif mWeekCc.SetStts(&mColliderStts); mWeekCc.OffTgSetBit(); mWindAtCc.Set(cc_dr_wind_at_src); + IF_DUSK(mWindAtCc.SetAtAtp(actor_attr::enemy_attack_power_byte(this, mWindAtCc.GetAtAtp()));) mWindAtCc.SetStts(&mColliderStts); mWindAtCc2.Set(cc_dr_wind_at_src); + IF_DUSK(mWindAtCc2.SetAtAtp(actor_attr::enemy_attack_power_byte(this, mWindAtCc2.GetAtAtp()));) mWindAtCc2.SetStts(&mColliderStts); mWindAtCc3.Set(cc_dr_wind_at_src); + IF_DUSK(mWindAtCc3.SetAtAtp(actor_attr::enemy_attack_power_byte(this, mWindAtCc3.GetAtAtp()));) mWindAtCc3.SetStts(&mColliderStts); mTailCc.Set(cc_dr_tail_src); +#if TARGET_PC // enemy attribute integration + + mTailCc.SetR(actor_attr::enemy_size_value(this, mTailCc.GetR())); + + mTailCc.SetAtAtp(actor_attr::enemy_attack_power_byte(this, mTailCc.GetAtAtp())); +#endif mTailCc.SetStts(&mColliderStts); for (int i = 0; i < 5; i++) { mDownCc[i].Set(cc_down_src); +#if TARGET_PC // enemy attribute integration + mDownCc[i].SetR(actor_attr::enemy_size_value(this, mDownCc[i].GetR())); + mDownCc[i].SetH(actor_attr::enemy_size_value(this, mDownCc[i].GetH())); + mDownCc[i].SetAtAtp(actor_attr::enemy_attack_power_byte(this, mDownCc[i].GetAtAtp())); +#endif mDownCc[i].SetStts(&mColliderStts); } for (int i = 0; i < 9; i++) { mDownSphCc[i].Set(cc_downSph_src); +#if TARGET_PC // enemy attribute integration + mDownSphCc[i].SetR(actor_attr::enemy_size_value(this, mDownSphCc[i].GetR())); + mDownSphCc[i].SetAtAtp(actor_attr::enemy_attack_power_byte(this, mDownSphCc[i].GetAtAtp())); +#endif mDownSphCc[i].SetStts(&mColliderStts); } for (int i = 0; i < 2; i++) { mFootAtCc[i].Set(cc_dr_foot_at_src); + IF_DUSK(mFootAtCc[i].SetAtAtp(actor_attr::enemy_attack_power_byte(this, mFootAtCc[i].GetAtAtp()));) mFootAtCc[i].SetStts(&mColliderStts); + IF_DUSK(mFootAtCc[i].SetAtAtp(actor_attr::enemy_attack_power_byte(this, 4.0f));) } field_0x784.set(current.pos); @@ -4245,7 +4773,7 @@ int daB_DR_c::create() { if (cDmr_SkipInfo == 0 && !dComIfGs_isZoneSwitch(0, fopAcM_GetRoomNo(this))) { arg0 = 0xFF; field_0x7d5 = 0xB; - mTimer[0] = 2; + mTimer[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 2), 2); if (BREG_S(5) == 0) { parentActorID = fopAcM_createChild(fpcNm_L7ODR_e, fopAcM_GetID(this), 0, ¤t.pos, fopAcM_GetRoomNo(this), &shape_angle, NULL, -1, NULL); diff --git a/src/d/actor/d_a_b_dre.cpp b/src/d/actor/d_a_b_dre.cpp index b26e815ddd..721e89eda1 100644 --- a/src/d/actor/d_a_b_dre.cpp +++ b/src/d/actor/d_a_b_dre.cpp @@ -6,6 +6,9 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep #include "d/actor/d_a_b_dre.h" +#if TARGET_PC // additional actor attribute integration +#include "dusk/mods/svc/actor_attribute_helpers.hpp" +#endif #include "SSystem/SComponent/c_math.h" #include "Z2AudioLib/Z2Instances.h" #include "d/actor/d_a_obj_ystone.h" @@ -15,6 +18,21 @@ #include "d/d_s_play.h" #include "f_op/f_op_camera_mng.h" #include "m_Do/m_Do_graphic.h" +#if TARGET_PC // additional actor attribute integration + +namespace actor_attr = dusk::mods::svc::actor_attr; + +namespace dusk::mods::svc::actor_attr { +template <> struct EnemyAttributeOwner { + static fopAc_ac_c* get(daB_DRE_c* i_this) { + + fopAc_ac_c* parent = fopAcM_SearchByID(i_this->parentActorID); + return parent != NULL && fopAcM_GetName(parent) == fpcNm_B_DR_e ? parent : i_this; + +} +}; +} +#endif int daB_DRE_c::CreateHeap() { J3DModelData* modelData = (J3DModelData*)dComIfG_getObjectRes("B_DR", 0x49); @@ -23,11 +41,11 @@ int daB_DRE_c::CreateHeap() { if (mAction == 0) { mpModelMorf = JKR_NEW mDoExt_McaMorfSO(modelData, NULL, NULL, (J3DAnmTransform*)dComIfG_getObjectRes("B_DR", 0x1D), 0, - 1.0f, 0, -1, &mSound, 0, 0x11000084); + DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 1.0f), 1.0f), 0, -1, &mSound, 0, 0x11000084); } else { mpModelMorf = JKR_NEW mDoExt_McaMorfSO(modelData, NULL, NULL, (J3DAnmTransform*)dComIfG_getObjectRes("B_DR", 0x1F), 2, - 1.0f, 0, -1, &mSound, 0, 0x11000084); + DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 1.0f), 1.0f), 0, -1, &mSound, 0, 0x11000084); if (mpModelMorf == NULL || mpModelMorf->getModel() == NULL) { return 0; } @@ -38,7 +56,7 @@ int daB_DRE_c::CreateHeap() { } J3DAnmTevRegKey* pbrk = (J3DAnmTevRegKey*)dComIfG_getObjectRes("B_DR", 0x51); - if (!mpCoreBrk->init(mpModelMorf->getModel()->getModelData(), pbrk, TRUE, 2, 1.0f, 0, -1)) { + if (!mpCoreBrk->init(mpModelMorf->getModel()->getModelData(), pbrk, TRUE, 2, DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 1.0f), 1.0f), 0, -1)) { return 0; } } @@ -59,8 +77,15 @@ static int daB_DRE_Create(fopAc_ac_c* i_this) { void daB_DRE_c::SetAnm(mDoExt_McaMorfSO* i_modelMorf, int i_anm, int i_attr, f32 i_morf, f32 i_speed) { +#if TARGET_PC // enemy attribute integration + + const actor_attr::ActionAnimationParams params = actor_attr::enemy_animation_params(this, i_morf, i_speed); + i_modelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_DR", i_anm), i_attr, params.morph, + params.playbackSpeed, 0.0f, -1.0f); +#else i_modelMorf->setAnm((J3DAnmTransform*)dComIfG_getObjectRes("B_DR", i_anm), i_attr, i_morf, i_speed, 0.0f, -1.0f); +#endif mAnm = i_anm; } @@ -113,7 +138,7 @@ void daB_DRE_c::SetStopCam(cXyz i_center, f32 param_1, f32 i_eyeY, s16 i_offsetA bool daB_DRE_c::SetCMoveCam(cXyz i_target, f32 i_step) { mCamCenterTarget = i_target; - if (cLib_chasePos(&mCamCenter, mCamCenterTarget, i_step)) { + if (DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mCamCenter, mCamCenterTarget, i_step), cLib_chasePos(&mCamCenter, mCamCenterTarget, i_step))) { return 1; } @@ -122,7 +147,7 @@ bool daB_DRE_c::SetCMoveCam(cXyz i_target, f32 i_step) { void daB_DRE_c::SetCMoveCam(cXyz i_target, f32 i_scale, f32 i_step) { mCamCenterTarget = i_target; - cLib_addCalcPos2(&mCamCenter, mCamCenterTarget, i_scale, i_step); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos2(this, &mCamCenter, mCamCenterTarget, i_scale, i_step), cLib_addCalcPos2(&mCamCenter, mCamCenterTarget, i_scale, i_step)); } void daB_DRE_c::SetEyeMoveCam(cXyz param_0, f32 param_1, f32 param_2, s16 i_offsetAngle, @@ -133,7 +158,7 @@ void daB_DRE_c::SetEyeMoveCam(cXyz param_0, f32 param_1, f32 param_2, s16 i_offs cXyz target(0.0f, 0.0f, 0.0f); cLib_offsetPos(&target, ¶m_0, i_offsetAngle, &offset); mCamEyeTarget = target; - cLib_chasePos(&mCamEye, mCamEyeTarget, i_step); + DUSK_IF_ELSE(actor_attr::enemy_chase_action_pos(this, &mCamEye, mCamEyeTarget, i_step), cLib_chasePos(&mCamEye, mCamEyeTarget, i_step)); camera->mCamera.Set(mCamCenter, mCamEye, mCamFovy, 0); } @@ -145,7 +170,7 @@ void daB_DRE_c::SetEyeMoveCam(cXyz param_0, f32 param_1, f32 param_2, s16 i_offs cXyz target(0.0f, param_2, param_1); cLib_offsetPos(&target, ¶m_0, i_offsetAngle, &target); mCamEyeTarget = target; - cLib_addCalcPos(&mCamEye, mCamEyeTarget, i_scale, i_step, 0.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc_pos(this, &mCamEye, mCamEyeTarget, i_scale, i_step, 0.0f), cLib_addCalcPos(&mCamEye, mCamEyeTarget, i_scale, i_step, 0.0f)); camera->mCamera.Set(mCamCenter, mCamEye, mCamFovy, 0); } @@ -153,8 +178,13 @@ void daB_DRE_c::SetEyeMoveCam(cXyz param_0, f32 param_1, f32 param_2, s16 i_offs void daB_DRE_c::SetMoveCam(f32 i_scale, f32 i_step) { camera_process_class* camera = dComIfGp_getCamera(dComIfGp_getPlayerCameraID(0)); +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc_pos(this, &mCamCenter, mCamCenterTarget, i_scale, i_step, 0.0f); + actor_attr::enemy_add_action_calc_pos(this, &mCamEye, mCamEyeTarget, i_scale, i_step, 0.0f); +#else cLib_addCalcPos(&mCamCenter, mCamCenterTarget, i_scale, i_step, 0.0f); cLib_addCalcPos(&mCamEye, mCamEyeTarget, i_scale, i_step, 0.0f); +#endif camera->mCamera.Set(mCamCenter, mCamEye, mCamFovy, 0); } @@ -205,7 +235,7 @@ void daB_DRE_c::CamAction2() { player->setPlayerPosAndAngle(&pos, shape_angle.y + 0x5000, 0); if (mTimers[0] == 0) { mCameraMode++; - mTimers[0] = 170; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 170), 170); current.pos.y += 1000.0f; spAC = s_BodyPos; @@ -214,7 +244,7 @@ void daB_DRE_c::CamAction2() { } break; case 1: - if (mTimers[0] < 140) { + if (DUSK_IF_ELSE(mTimers[0] <= actor_attr::enemy_sync_countdown_milestone(this, 170, 139), mTimers[0] < 140)) { SetCMoveCam(s_BodyPos, 9.0f); SetEyeMoveCam(s_BodyPos, 300.0f, 200.0f, field_0x5fc, 0.1f, 30.0f); } @@ -227,7 +257,7 @@ void daB_DRE_c::CamAction2() { pos.y = 0.0f; SetStopCam(current.pos, 1000.0f, -40.0f, field_0x5fc); - mTimers[0] = 100; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 100), 100); dComIfGp_event_setTalkPartner(this); player->changeDemoMode(0x17, 3, 0, 0); pos.set(0.0f, 0.0f, 800.0f + TREG_F(8)); @@ -236,7 +266,11 @@ void daB_DRE_c::CamAction2() { } break; case 2: +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_angle_add(this, field_0x5fc, -5.0f); +#else field_0x5fc -= 5; +#endif if (mTimers[0] == 0) { SetAnm(mpModelMorf, 0x38, 0, 5.0f, 1.0f); dScnKy_env_light_c* kankyo = dKy_getEnvlight(); @@ -280,7 +314,11 @@ void daB_DRE_c::CamAction2() { 100.0f + TREG_F(9)); } else { SetEyeMoveCam(pos, 1000.0f, 80.0f, field_0x5fc, 5.0f + TREG_F(10)); +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_angle_add(this, field_0x5fc , -(2.0f + TREG_F(11))); +#else field_0x5fc -= 2.0f + TREG_F(11); +#endif } break; case 4: @@ -315,18 +353,18 @@ void daB_DRE_c::DrAction2() { if (mpModelMorf->checkFrame(110.0f)) { argument = 1; mBlurRate = 150.0f; - mTimers[0] = 100; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 100), 100); mHideParts = true; } else if (mpModelMorf->checkFrame(270.0f)) { mBlurRate = 150.0f; - mTimers[0] = 100; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 100), 100); mHideParts = true; } if (mpModelMorf->getFrame() < 270.0f) { - cLib_addCalc2(&mBlurRate, 0.0f, 0.2f, 1.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &mBlurRate, 0.0f, 0.2f, 1.0f), cLib_addCalc2(&mBlurRate, 0.0f, 0.2f, 1.0f)); } else { - cLib_addCalc2(&mBlurRate, 0.0f, 0.2f, 1.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &mBlurRate, 0.0f, 0.2f, 1.0f), cLib_addCalc2(&mBlurRate, 0.0f, 0.2f, 1.0f)); } if (mpModelMorf->isStop()) { @@ -350,7 +388,7 @@ void daB_DRE_c::DrAction2() { mCamCenter.y += 100.0f; fopAcM_onSwitch(this, 16); - mTimers[0] = 50; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 50), 50); return; } break; @@ -392,12 +430,20 @@ void daB_DRE_c::CamAction() { } break; case 1: +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_angle_add(this, field_0x5fc, -16.0f); +#else field_0x5fc -= 16; +#endif SetCMoveCam(pos, 0.1f, 100.0f); SetEyeMoveCam(pos, field_0x608, 500.0f, field_0x5fc, 50.0f); break; case 2: +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_angle_add(this, field_0x5fc, -32.0f); +#else field_0x5fc -= 32; +#endif SetCMoveCam(s_HeadPos, 0.2f + aREG_F(11), 200.0f + aREG_F(12)); SetEyeMoveCam(pos, 1000.0f + yREG_F(3), -500.0f + yREG_F(4), field_0x5fc, 10.0f); @@ -435,7 +481,7 @@ void daB_DRE_c::CamAction() { SetReleaseCam(); mCameraMode++; mDrMode++; - mTimers[0] = 50; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 50), 50); } SetMoveCam(0.05f + hREG_F(0), 5.0f + hREG_F(1)); @@ -490,22 +536,22 @@ void daB_DRE_c::DrAction() { SetAnm(mpModelMorf, 0x19, 2, 10.0f, 1.0f); mDrMode++; mCameraMode++; - mTimers[3] = 140; + mTimers[3] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 140), 140); player->changeOriginalDemo(); player->changeDemoMode(4, 2, 0, 0); - mTimers[2] = 30; + mTimers[2] = DUSK_IF_ELSE(actor_attr::enemy_sync_terminal_timer(this, 29), 30); } BreathSet(); break; case 3: if (mTimers[0] == 0) { mTargetSpeedY = 30.0f; - mTimers[0] = 50.0f + cM_rndF(50.0f); + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 50.0f + cM_rndF(50.0f)), 50.0f + cM_rndF(50.0f)); } if (mTimers[3] == 0 && mpModelMorf->checkFrame(1.0f)) { - mTimers[3] = 60; + mTimers[3] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 60), 60); mCameraMode++; field_0x5fc = shape_angle.y; sp90 = current.pos; @@ -519,16 +565,26 @@ void daB_DRE_c::DrAction() { } if (mTimers[1] == 1) { +#if TARGET_PC // enemy attribute integration + const f32 duration = 30.0f + cM_rndF(30.0f); + mTimers[2] = actor_attr::enemy_sync_terminal_timer(this, duration - 1.0f); +#else mTimers[2] = 30.0f + cM_rndF(30.0f); +#endif } if (mTimers[2] == 1) { +#if TARGET_PC // enemy attribute integration + const f32 duration = 30.0f + cM_rndF(30.0f); + mTimers[1] = actor_attr::enemy_sync_terminal_timer(this, duration - 1.0f); +#else mTimers[1] = 30.0f + cM_rndF(30.0f); +#endif } break; case 4: if (mpModelMorf->getFrame() > 40.0f) { - mTargetSpeedY -= 1.2f; + mTargetSpeedY -= DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 1.2f), 1.2f); } if (mpModelMorf->isStop()) { @@ -566,7 +622,7 @@ void daB_DRE_c::DrAction() { sp1C.set(0, shape_angle.y + 0x1000, 0); fopAcM_createItemForBoss(&sp90, dItemNo_UTAWA_HEART_e, fopAcM_GetRoomNo(this), &sp1C, NULL, 6.0f + TREG_F(9), 20.0f + TREG_F(10), -1); - mTimers[0] = 30; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 30), 30); } break; case 6: @@ -581,7 +637,7 @@ void daB_DRE_c::DrAction() { sp90.set(150.0f + aREG_F(0), 100.0f, 800.0f + aREG_F(1)); SetStopCam(sp90, 300.0f + aREG_F(3), 0.0f, shape_angle.y - 0x1000); mCameraMode++; - mTimers[0] = 150; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 150), 150); mDrMode++; } break; @@ -589,7 +645,7 @@ void daB_DRE_c::DrAction() { if (mTimers[0] == 0) { mDrMode++; mCameraMode++; - mTimers[0] = 80; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 80), 80); } break; case 8: @@ -605,14 +661,14 @@ void daB_DRE_c::DrAction() { mDrMode++; mCameraMode++; - mTimers[0] = 155; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 155), 155); field_0x638 = 0; } - cLib_addCalc2(&mCamCenterTarget.y, 350.0f, 0.1f + hREG_F(5), 10.0f + hREG_F(6)); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &mCamCenterTarget.y, 350.0f, 0.1f + hREG_F(5), 10.0f + hREG_F(6)), cLib_addCalc2(&mCamCenterTarget.y, 350.0f, 0.1f + hREG_F(5), 10.0f + hREG_F(6))); break; case 9: - if (mTimers[0] == 145) { + if (mTimers[0] == DUSK_IF_ELSE(actor_attr::enemy_sync_countdown_milestone(this, 155, 145), 145)) { Z2GetAudioMgr()->seStart(Z2SE_OBJ_KAGERI_MIRROR_GET, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } @@ -628,11 +684,15 @@ void daB_DRE_c::DrAction() { if (ystone != NULL && mTimers[0] == 0) { player->changeDemoMode(0x2F, 3, 0, 0); cXyz pos(ystone->current.pos); - pos.y -= 1.0f; + pos.y -= DUSK_IF_ELSE(actor_attr::enemy_action_step(this, 1.0f), 1.0f); ((obj_ystone_class*)ystone)->setCurrentPos(pos); if (mCamCenterTarget.y >= (pos.y - 70.0f) + XREG_F(6)) { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc2(this, &mCamCenterTarget.y, (pos.y - 70.0f) + XREG_F(6), 0.1f + hREG_F(7), +#else cLib_addCalc2(&mCamCenterTarget.y, (pos.y - 70.0f) + XREG_F(6), 0.1f + hREG_F(7), +#endif 2.0f + hREG_F(8)); } } @@ -745,9 +805,13 @@ int daB_DRE_c::Execute() { SoundChk(); SpeedSet(); +#if TARGET_PC // enemy attribute integration + current.pos += actor_attr::enemy_move_step(this, speed); +#else current.pos.x += speed.x; current.pos.y += speed.y; current.pos.z += speed.z; +#endif if (mAction == 1) { mpCoreBrk->play(); @@ -760,8 +824,13 @@ int daB_DRE_c::Execute() { } void daB_DRE_c::SpeedSet() { +#if TARGET_PC // enemy attribute integration + actor_attr::enemy_add_action_calc2(this, &speed.y, mTargetSpeedY, 0.2f, 10.0f); + actor_attr::enemy_add_action_calc2(this, &speedF, mTargetSpeedF, 0.2f, 10.0f); +#else cLib_addCalc2(&speed.y, mTargetSpeedY, 0.2f, 10.0f); cLib_addCalc2(&speedF, mTargetSpeedF, 0.2f, 10.0f); +#endif } static u8 data_805CB0D4; @@ -769,7 +838,7 @@ static u8 data_805CB0D4; void daB_DRE_c::BreathSet() { cXyz sp20; cXyz sp14; - cLib_addCalc2(&field_0x618, 50.0f, 0.7f, 4.0f); + DUSK_IF_ELSE(actor_attr::enemy_add_action_calc2(this, &field_0x618, 50.0f, 0.7f, 4.0f), cLib_addCalc2(&field_0x618, 50.0f, 0.7f, 4.0f)); data_805CB0D4 = field_0x618; csXyz angle(shape_angle); @@ -800,7 +869,11 @@ int daB_DRE_c::Delete() { void daB_DRE_c::setBaseMtx() { mDoMtx_stack_c::transS(current.pos); mDoMtx_stack_c::ZXYrotM(shape_angle); +#if TARGET_PC // enemy attribute integration + mDoMtx_stack_c::scaleM((0.8f) * actor_attr::enemy_size_multiplier(this), (0.8f) * actor_attr::enemy_size_multiplier(this), (0.8f) * actor_attr::enemy_size_multiplier(this)); +#else mDoMtx_stack_c::scaleM(0.8f, 0.8f, 0.8f); +#endif mpModelMorf->getModel()->setBaseTRMtx(mDoMtx_stack_c::get()); if (mAction == 0) { @@ -880,7 +953,11 @@ int daB_DRE_c::create() { daPy_py_c* player = daPy_getPlayerActorClass(); fopAcM_SetMtx(this, mpModelMorf->getModel()->getBaseTRMtx()); +#if TARGET_PC // enemy attribute integration + mAcchCir.SetWall(actor_attr::enemy_size_value(this, 50.0f), actor_attr::enemy_size_value(this, 80.0f)); +#else mAcchCir.SetWall(50.0f, 80.0f); +#endif mAcch.Set(fopAcM_GetPosition_p(this), fopAcM_GetOldPosition_p(this), this, 1, &mAcchCir, fopAcM_GetSpeed_p(this), NULL, NULL); @@ -920,7 +997,7 @@ int daB_DRE_c::create() { current.pos.x += 1800.0f; current.pos.y -= 1000.0f; - mTimers[0] = 80; + mTimers[0] = DUSK_IF_ELSE(actor_attr::enemy_sync_timer(this, 80), 80); mSound.startCreatureSound(Z2SE_EN_DR_CORE_GLOW, 0, -1); } diff --git a/src/dusk/mods/svc/actor_attribute.cpp b/src/dusk/mods/svc/actor_attribute.cpp new file mode 100644 index 0000000000..1ede138785 --- /dev/null +++ b/src/dusk/mods/svc/actor_attribute.cpp @@ -0,0 +1,173 @@ +#include "actor_attribute.hpp" + +#include "registry.hpp" + +#include "dusk/mods/loader/loader.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::mods::svc { +namespace { + +struct Resolver { + ActorAttributeResolverHandle handle = 0; + ActorAttributeResolveFn fn = nullptr; + void* userData = nullptr; +}; + +struct PendingResolver { + LoadedMod* owner = nullptr; + ActorAttributeResolveFn fn = nullptr; + void* userData = nullptr; +}; + +std::unordered_map> s_resolvers; +ActorAttributeResolverHandle s_nextHandle = 1; + +bool valid_attribute(const ActorAttribute attribute) { + return attribute >= ACTOR_ATTRIBUTE_MOVEMENT_SPEED && + attribute <= ACTOR_ATTRIBUTE_STUN_DURATION; +} + +ModResult register_resolver(ModContext* context, ActorAttributeResolveFn fn, void* userData, + ActorAttributeResolverHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + + auto* owner = mod_from_context(context); + if (owner == nullptr || fn == nullptr) { + return MOD_INVALID_ARGUMENT; + } + if (s_nextHandle == 0 || s_nextHandle == std::numeric_limits::max()) { + return MOD_UNAVAILABLE; + } + + const ActorAttributeResolverHandle handle = s_nextHandle++; + s_resolvers[owner].push_back(Resolver{ + .handle = handle, + .fn = fn, + .userData = userData, + }); + + if (outHandle != nullptr) { + *outHandle = handle; + } + return MOD_OK; +} + +ModResult unregister_resolver( + ModContext* context, const ActorAttributeResolverHandle handle) { + auto* owner = mod_from_context(context); + if (owner == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + + const auto it = s_resolvers.find(owner); + if (it == s_resolvers.end()) { + return MOD_UNSUPPORTED; + } + + auto& resolvers = it->second; + const auto oldSize = resolvers.size(); + std::erase_if(resolvers, [handle](const Resolver& resolver) { + return resolver.handle == handle; + }); + + const bool removed = resolvers.size() != oldSize; + if (resolvers.empty()) { + s_resolvers.erase(it); + } + return removed ? MOD_OK : MOD_UNSUPPORTED; +} + +constexpr ActorAttributeService s_actorAttributeService{ + .header = SERVICE_HEADER(ActorAttributeService, ACTOR_ATTRIBUTE_SERVICE_MAJOR, + ACTOR_ATTRIBUTE_SERVICE_MINOR), + .register_resolver = register_resolver, + .unregister_resolver = unregister_resolver, + .resolve = [](const void* actor, ActorAttribute attribute, float vanillaValue) { + return resolve_actor_attribute(static_cast(actor), + attribute, vanillaValue); + }, +}; + +} // namespace + +float resolve_actor_attribute( + const fopAc_ac_c* actor, const ActorAttribute attribute, const float vanillaValue) { + if (actor == nullptr || !valid_attribute(attribute) || s_resolvers.empty()) { + return vanillaValue; + } + + /* A callback may unregister itself, so snapshot the active chain first. */ + std::vector pending; + for (auto& mod : ModLoader::instance().mods()) { + if (!mod.active) { + continue; + } + + const auto it = s_resolvers.find(&mod); + if (it == s_resolvers.end()) { + continue; + } + + for (const Resolver& resolver : it->second) { + pending.push_back(PendingResolver{ + .owner = &mod, + .fn = resolver.fn, + .userData = resolver.userData, + }); + } + } + + ActorAttributeInfo info{ + .actor = actor, + .attribute = attribute, + .vanilla_value = vanillaValue, + .current_value = vanillaValue, + }; + + for (const PendingResolver& resolver : pending) { + if (!resolver.owner->active) { + continue; + } + + float candidate = info.current_value; + try { + if (resolver.fn(resolver.owner->context.get(), &info, &candidate, + resolver.userData) && + std::isfinite(candidate)) + { + info.current_value = candidate; + } + } catch (const std::exception& exception) { + fail_mod(*resolver.owner, MOD_ERROR, + std::string{"Exception in actor attribute resolver: "} + exception.what()); + } catch (...) { + fail_mod(*resolver.owner, MOD_ERROR, "Unknown exception in actor attribute resolver"); + } + } + + return info.current_value; +} + +constinit const ServiceModule g_actorAttributeModule{ + .id = ACTOR_ATTRIBUTE_SERVICE_ID, + .majorVersion = ACTOR_ATTRIBUTE_SERVICE_MAJOR, + .minorVersion = ACTOR_ATTRIBUTE_SERVICE_MINOR, + .service = &s_actorAttributeService, + .modDetached = [](LoadedMod& mod) { s_resolvers.erase(&mod); }, + .shutdown = []() { + s_resolvers.clear(); + s_nextHandle = 1; + }, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/actor_attribute.hpp b/src/dusk/mods/svc/actor_attribute.hpp new file mode 100644 index 0000000000..3bfdb120dd --- /dev/null +++ b/src/dusk/mods/svc/actor_attribute.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "mods/svc/actor_attribute.h" + +class fopAc_ac_c; + +namespace dusk::mods::svc { + +/* Host-side entry point used at semantic value-consumption sites. */ +float resolve_actor_attribute( + const fopAc_ac_c* actor, ActorAttribute attribute, float vanillaValue); + + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/actor_attribute_helpers.hpp b/src/dusk/mods/svc/actor_attribute_helpers.hpp new file mode 100644 index 0000000000..bd86677dc0 --- /dev/null +++ b/src/dusk/mods/svc/actor_attribute_helpers.hpp @@ -0,0 +1,1377 @@ +#pragma once + +#include "actor_attribute.hpp" + +#include "dolphin/types.h" +#include "SSystem/SComponent/c_lib.h" +#include "angle_utils.h" + +#include +#include +#include +#include + +/* + * Reusable math and conversion helpers for semantic actor-attribute integration. + * + * These helpers deliberately do not decide which actions, timers, movement + * paths, attachment constraints, health writes, or child actors should be + * scaled. Those semantic decisions remain in the owning actor source. + */ +namespace dusk::mods::svc::actor_attr { + +inline f32 safe_action_speed(const f32 actionSpeed) { + return std::isfinite(actionSpeed) && actionSpeed > 0.0f + ? actionSpeed + : 1.0f; +} + +/* Scale a signed movement, acceleration, or interpolation step in action time. */ +inline f32 action_step(const f32 vanillaStep, const f32 requestedActionSpeed) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return vanillaStep; + } + + return vanillaStep * actionSpeed; +} + +/* Resolve a vanilla multiplier of 1.0 and clamp the composed result. */ +inline f32 resolve_multiplier( + const fopAc_ac_c* actor, + const ActorAttribute attribute, + const f32 minimum = 0.1f, + const f32 maximum = 100.0f +) { + const f32 lower = std::min(minimum, maximum); + const f32 upper = std::max(minimum, maximum); + const f32 resolved = resolve_actor_attribute(actor, attribute, 1.0f); + return std::clamp(std::isfinite(resolved) ? resolved : 1.0f, lower, upper); +} + +/* Resolve a fresh value whose meaningful range begins at zero. */ +inline f32 resolve_nonnegative( + const fopAc_ac_c* actor, + const ActorAttribute attribute, + const f32 vanillaValue +) { + const f32 resolved = resolve_actor_attribute(actor, attribute, vanillaValue); + return std::max(std::isfinite(resolved) ? resolved : vanillaValue, 0.0f); +} + +/* + * Convert a vanilla duration to real frames. At exactly 1x this preserves the + * original float-to-s16 truncation. Other speeds round up so a state never + * ends before its scaled action time has elapsed. + */ +inline s16 sync_timer(const f32 vanillaFrames, const f32 requestedActionSpeed) { + if (!(vanillaFrames > 0.0f)) { + return 0; + } + + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return static_cast(std::min( + vanillaFrames, static_cast(std::numeric_limits::max()))); + } + + const double frames = std::ceil(static_cast(vanillaFrames) / actionSpeed); + return static_cast(std::clamp( + frames, 1.0, static_cast(std::numeric_limits::max()))); +} + +/* Scale a duration directly. Unlike sync_timer(), a larger multiplier keeps + * the state active longer instead of making an action clock run faster. */ +inline s16 duration_timer( + const f32 vanillaFrames, + const f32 requestedDurationMultiplier +) { + if (!(vanillaFrames > 0.0f)) { + return 0; + } + + const f32 multiplier = safe_action_speed(requestedDurationMultiplier); + if (multiplier == 1.0f) { + return static_cast(std::min( + vanillaFrames, static_cast(std::numeric_limits::max()))); + } + + const double frames = std::ceil( + static_cast(vanillaFrames) * multiplier); + return static_cast(std::clamp( + frames, 1.0, static_cast(std::numeric_limits::max()))); +} + +/* Synchronize a timer stored in a byte without allowing overflow to wrap. */ +inline u8 sync_u8_timer( + const f32 vanillaFrames, + const f32 requestedActionSpeed +) { + return static_cast(std::clamp( + sync_timer(vanillaFrames, requestedActionSpeed), 0, 255)); +} + +/* Synchronize a timer stored in a signed byte without allowing overflow. */ +inline s8 sync_s8_timer( + const f32 vanillaFrames, + const f32 requestedActionSpeed +) { + return static_cast(std::clamp( + sync_timer(vanillaFrames, requestedActionSpeed), 0, 127)); +} + +inline u8 duration_u8_timer( + const f32 vanillaFrames, + const f32 requestedDurationMultiplier +) { + return static_cast(std::clamp( + duration_timer(vanillaFrames, requestedDurationMultiplier), 0, 255)); +} + +inline s8 duration_s8_timer( + const f32 vanillaFrames, + const f32 requestedDurationMultiplier +) { + return static_cast(std::clamp( + duration_timer(vanillaFrames, requestedDurationMultiplier), 0, 127)); +} + +/* Round a meaningful positive value into an s16 field. Do not use for zero + * death writes or special sentinel values. */ +inline s16 positive_s16(const f32 value) { + const f32 clamped = std::clamp( + std::isnan(value) ? 1.0f : value, 1.0f, + static_cast(std::numeric_limits::max())); + return static_cast(std::lround(clamped)); +} + +inline s16 resolve_positive_s16( + const fopAc_ac_c* actor, + const ActorAttribute attribute, + const f32 vanillaValue +) { + return positive_s16(resolve_actor_attribute(actor, attribute, vanillaValue)); +} + +/* Round and clamp a value consumed by a byte-sized engine field. */ +inline int byte_value(const f32 value) { + return static_cast(std::lround(std::clamp( + std::isnan(value) ? 0.0f : value, 0.0f, 255.0f))); +} + +inline int resolve_byte( + const fopAc_ac_c* actor, + const ActorAttribute attribute, + const f32 vanillaValue +) { + return byte_value(resolve_actor_attribute(actor, attribute, vanillaValue)); +} + +/* + * Advance a vanilla exponential interpolation by S frames at a time. The 1x + * fast path returns the exact original argument. + */ +inline f32 action_fraction( + const f32 vanillaFraction, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return vanillaFraction; + } + + const f32 fraction = std::clamp(vanillaFraction, 0.0f, 1.0f); + return 1.0f - std::pow(1.0f - fraction, actionSpeed); +} + +struct ActionCalcParams { + f32 fraction; + f32 maximumStep; +}; + +/* Parameters for a cLib_addCalc2/cLib_addCalc0-style action interpolation. */ +inline ActionCalcParams action_calc_params( + const f32 vanillaFraction, + const f32 vanillaMaximumStep, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return ActionCalcParams{vanillaFraction, vanillaMaximumStep}; + } + + return ActionCalcParams{ + action_fraction(vanillaFraction, actionSpeed), + action_step(vanillaMaximumStep, actionSpeed), + }; +} + +struct ActionAnimationParams { + f32 morph; + f32 playbackSpeed; +}; + +/* Keep animation playback and positive morph durations on the action clock. */ +inline ActionAnimationParams action_animation_params( + const f32 vanillaMorph, + const f32 vanillaPlaybackSpeed, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return ActionAnimationParams{vanillaMorph, vanillaPlaybackSpeed}; + } + + return ActionAnimationParams{ + vanillaMorph > 0.0f ? vanillaMorph / actionSpeed : vanillaMorph, + vanillaPlaybackSpeed * actionSpeed, + }; +} + +/* Scale a multiplicative per-frame damping/retention factor in action time. */ +inline f32 action_damping( + const f32 vanillaFactor, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return vanillaFactor; + } + + return std::pow(std::clamp(vanillaFactor, 0.0f, 1.0f), actionSpeed); +} + +struct ActionAngleParams { + s16 divisor; + s16 maximumStep; +}; + +/* + * cLib_addCalcAngleS2-style turns have two rate controls. Scaling both keeps + * the turn consistent both far from and close to its target. + */ +inline ActionAngleParams action_angle_params( + const s16 vanillaDivisor, + const s16 vanillaMaximumStep, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return ActionAngleParams{vanillaDivisor, vanillaMaximumStep}; + } + + const f32 s16Maximum = static_cast(std::numeric_limits::max()); + const long divisor = std::lround(std::clamp( + static_cast(vanillaDivisor) / actionSpeed, 1.0f, s16Maximum)); + const long maximumStep = vanillaMaximumStep == 0 ? 0 : std::lround(std::clamp( + static_cast(vanillaMaximumStep) * actionSpeed, 1.0f, s16Maximum)); + + return ActionAngleParams{ + static_cast(divisor), + static_cast(maximumStep), + }; +} + +/* Scale and safely convert a positive s16 action step. */ +inline s16 action_s16_step( + const s16 vanillaStep, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f || vanillaStep == 0) { + return vanillaStep; + } + + const long scaledStep = std::lround( + static_cast(vanillaStep) * actionSpeed + ); + + return static_cast(std::clamp( + scaledStep, + 1L, + static_cast(std::numeric_limits::max()) + )); +} + +/* Scale a signed s16 step while preserving its direction. */ +inline s16 signed_action_s16_step( + const s16 vanillaStep, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f || vanillaStep == 0) { + return vanillaStep; + } + if (vanillaStep < 0) { + // Widen before negating: -INT16_MIN is not representable in s16. + const long magnitude = std::lround(-static_cast(vanillaStep) * actionSpeed); + return static_cast(-std::clamp(magnitude, 1L, 32768L)); + } + + return action_s16_step(vanillaStep, actionSpeed); +} + +/* Scale a byte-sized chase step without allowing a positive step to vanish. */ +inline u8 action_u8_step( + const u8 vanillaStep, + const f32 requestedActionSpeed +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f || vanillaStep == 0) { + return vanillaStep; + } + + const long scaledStep = std::lround( + static_cast(vanillaStep) * actionSpeed); + return static_cast(std::clamp(scaledStep, 1L, 255L)); +} + +/* Scale an always-running counter used as a procedural angle/phase source. */ +inline s16 action_phase_angle( + const s32 counter, + const s32 vanillaRate, + const f32 requestedActionSpeed, + const s32 phaseOffset = 0 +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return static_cast(static_cast(counter) * vanillaRate + phaseOffset); + } + + const double angle = static_cast(counter) * actionSpeed * vanillaRate + + phaseOffset; + return static_cast(std::lround(std::fmod(angle, 65536.0))); +} + +inline s16 action_phase_angle( + const s32 counter, + const f32 vanillaRate, + const f32 requestedActionSpeed, + const f32 phaseOffset = 0.0f +) { + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + // Preserve vanilla float arithmetic and truncation without converting + // an out-of-range float directly to the signed 16-bit angle type. + const f32 angle = counter * vanillaRate + phaseOffset; + return static_cast(static_cast(std::fmod(angle, 65536.0f))); + } + + const double angle = static_cast(counter) * actionSpeed * vanillaRate + + phaseOffset; + return static_cast(std::lround(std::fmod(angle, 65536.0))); +} + +/* + * Direct-inheritance enemies use this default accessor. Enemies containing + * their actor as a member specialize this trait in their actor source. + */ +template +struct EnemyActorAccessor { + static fopAc_ac_c* get(Enemy* i_this) { + return static_cast(i_this); + } +}; + +/* + * A boss part may inherit attributes from its owner, but it is still a separate + * physical actor. Keep ownership out of EnemyActorAccessor so movement never + * updates the parent in place of the child. + */ +template +struct EnemyAttributeOwner { + static fopAc_ac_c* get(Enemy* i_this) { + return EnemyActorAccessor::get(i_this); + } +}; + +/* + * Action time is enabled by default. Actors with exact-time attachment, + * cutscene, or controller states specialize this trait in their actor source. + */ +template +struct EnemyActionTimePolicy { + static bool uses(Enemy*) { + return true; + } +}; + +template +inline fopAc_ac_c* enemy_attribute_actor(Enemy* i_this) { + return EnemyAttributeOwner::get(i_this); +} + +template +inline f32 enemy_action_speed_multiplier(Enemy* i_this) { + return resolve_multiplier( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_MOVEMENT_SPEED); +} + +template +inline f32 enemy_size_multiplier(Enemy* i_this) { + return resolve_multiplier( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_SIZE); +} + +/* A fresh geometric length/radius, not an already-scaled world position. */ +template +inline f32 enemy_size_value(Enemy* i_this, const f32 vanillaLength) { + return vanillaLength * enemy_size_multiplier(i_this); +} + +template +inline f32 enemy_notice_range_multiplier(Enemy* i_this) { + return resolve_multiplier( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_NOTICE_RANGE); +} + +/* + * Scale only a distance that controls whether an enemy notices, acquires, or + * forgets the player. Attack reach, grab bounds, scripted triggers, and + * movement/formation distances remain size- or world-space calculations. + */ +template +inline f32 enemy_notice_distance(Enemy* i_this, const f32 vanillaDistance) { + return vanillaDistance * enemy_notice_range_multiplier(i_this); +} + +/* + * Resolve the horizontal distance multiplier applied when this source actor + * damages Link. Projectile/part ownership is resolved by the registered mod + * resolver because Link's collision record exposes only a raw actor pointer. + */ +inline f32 enemy_player_knockback_multiplier(const fopAc_ac_c* sourceActor) { + return resolve_multiplier( + sourceActor, + ACTOR_ATTRIBUTE_PLAYER_KNOCKBACK); +} + +inline f32 enemy_player_knockback_value( + const fopAc_ac_c* sourceActor, + const f32 vanillaValue +) { + const f32 multiplier = enemy_player_knockback_multiplier(sourceActor); + return multiplier == 1.0f ? vanillaValue : vanillaValue * multiplier; +} + +template +inline f32 enemy_stun_duration_multiplier(Enemy* i_this) { + return resolve_multiplier( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_STUN_DURATION); +} + +template +inline f32 enemy_stun_duration_value( + Enemy* i_this, + const f32 vanillaDuration +) { + return resolve_nonnegative( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_STUN_DURATION, + vanillaDuration); +} + +/* + * A stun whose exit is gated by animation progress needs the inverse of an + * action-speed scale: a 2x duration plays at half speed, while positive morph + * time is doubled. The 1x path remains byte-for-byte equivalent in value. + */ +template +inline ActionAnimationParams enemy_stun_animation_params( + Enemy* i_this, + const f32 vanillaMorph, + const f32 vanillaPlaybackSpeed +) { + const f32 durationMultiplier = enemy_stun_duration_multiplier(i_this); + if (durationMultiplier == 1.0f) { + return ActionAnimationParams{vanillaMorph, vanillaPlaybackSpeed}; + } + + return ActionAnimationParams{ + vanillaMorph > 0.0f + ? vanillaMorph * durationMultiplier + : vanillaMorph, + vanillaPlaybackSpeed / durationMultiplier, + }; +} + +template +inline cXyz enemy_size_multiplier(Enemy* i_this, const f32 vanillaScale) { + const f32 scale = vanillaScale * enemy_size_multiplier(i_this); + return cXyz(scale, scale, scale); +} + +template +inline cXyz enemy_size_multiplier(Enemy* i_this, cXyz vanillaScale) { + vanillaScale *= enemy_size_multiplier(i_this); + return vanillaScale; +} + +template +inline bool enemy_uses_action_time_scale(Enemy* i_this) { + return EnemyActionTimePolicy::uses(i_this); +} + +template +inline f32 enemy_action_time_speed(Enemy* i_this) { + return enemy_uses_action_time_scale(i_this) + ? enemy_action_speed_multiplier(i_this) + : 1.0f; +} + +inline int action_event_count( + const s32 frameCounter, + const s32 vanillaPeriod, + const f32 requestedActionSpeed, + const s32 vanillaPhaseOffset = 0 +) { + if (vanillaPeriod <= 0) { + return 0; + } + + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return (static_cast(frameCounter) + vanillaPhaseOffset) % + vanillaPeriod == 0 ? 1 : 0; + } + + const double currentActionFrame = + static_cast(frameCounter) * actionSpeed + vanillaPhaseOffset; + const double previousActionFrame = currentActionFrame - actionSpeed; + const double currentBoundary = std::floor(currentActionFrame / vanillaPeriod); + const double previousBoundary = std::floor(previousActionFrame / vanillaPeriod); + + // Windows long is only 32 bits. Convert the small per-frame difference, + // not the absolute action-frame boundary of a long-running counter. + return static_cast(std::clamp(currentBoundary - previousBoundary, + 0.0, static_cast(std::numeric_limits::max()))); +} + +template +inline int enemy_action_event_count(Enemy* i_this, const s32 frameCounter, + const s32 vanillaPeriod, + const s32 vanillaPhaseOffset = 0) { + return action_event_count( + frameCounter, + vanillaPeriod, + enemy_action_time_speed(i_this), + vanillaPhaseOffset); +} + +/* Periodic checks for signed 16-bit counters that intentionally wrap. */ +template +inline bool enemy_action_period_check( + Enemy* i_this, + const s16 frameCounter, + const f32 vanillaPeriod +) { + return action_event_count( + static_cast(frameCounter), + static_cast(vanillaPeriod), + enemy_action_time_speed(i_this)) > 0; +} + +template +inline bool enemy_action_period_is_odd( + Enemy* i_this, + const s16 frameCounter, + const f32 vanillaPeriod +) { + if (!(vanillaPeriod > 0.0f)) { + return false; + } + + const double actionFrame = + static_cast(static_cast(frameCounter)) * + enemy_action_time_speed(i_this); + return std::fmod(std::floor(actionFrame / vanillaPeriod), 2.0) != 0.0; +} + +/* Count periodic boundaries crossed by a timer that decreases once per frame. */ +inline int action_countdown_event_count( + const s32 remainingRealFrames, + const s32 vanillaPeriod, + const f32 requestedActionSpeed +) { + if (remainingRealFrames < 0 || vanillaPeriod <= 0) { + return 0; + } + + const f32 actionSpeed = safe_action_speed(requestedActionSpeed); + if (actionSpeed == 1.0f) { + return remainingRealFrames % vanillaPeriod == 0 ? 1 : 0; + } + + const double currentRemaining = + static_cast(remainingRealFrames) * actionSpeed; + const double previousRemaining = + (static_cast(remainingRealFrames) + 1.0) * actionSpeed; + const double currentBoundary = std::ceil(currentRemaining / vanillaPeriod); + const double previousBoundary = std::ceil(previousRemaining / vanillaPeriod); + + return static_cast(std::clamp(previousBoundary - currentBoundary, + 0.0, static_cast(std::numeric_limits::max()))); +} + +template +inline int enemy_action_countdown_event_count( + Enemy* i_this, + const s32 remainingRealFrames, + const s32 vanillaPeriod +) { + return action_countdown_event_count( + remainingRealFrames, + vanillaPeriod, + enemy_action_time_speed(i_this)); +} + +/* Identify the current alternating block of a decreasing countdown. */ +template +inline bool enemy_action_countdown_period_is_odd( + Enemy* i_this, + const s32 remainingRealFrames, + const s32 vanillaPeriod +) { + if (remainingRealFrames < 0 || vanillaPeriod <= 0) { + return false; + } + + const double remainingActionFrames = + static_cast(remainingRealFrames) * + enemy_action_time_speed(i_this); + return std::fmod(std::floor(remainingActionFrames / vanillaPeriod), 2.0) != 0.0; +} + +template +inline f32 enemy_action_step( + Enemy* i_this, + const f32 vanillaStep +) { + return action_step( + vanillaStep, + enemy_action_time_speed(i_this)); +} + +template +inline void enemy_angle_add( + Enemy* i_this, + s16& value, + const f32 vanillaStep +) { + const f32 step = enemy_action_step(i_this, vanillaStep); + const s32 wrappedStep = static_cast(std::fmod(step, 65536.0f)); + value = static_cast(static_cast(value) + wrappedStep); +} + +template +inline s16 enemy_action_s16_step( + Enemy* i_this, + const s16 vanillaStep +) { + return action_s16_step( + vanillaStep, + enemy_action_time_speed(i_this)); +} + +template +inline s16 enemy_signed_action_s16_step( + Enemy* i_this, + const s16 vanillaStep +) { + return signed_action_s16_step( + vanillaStep, + enemy_action_time_speed(i_this)); +} + +template +inline f32 enemy_move_step(Enemy* i_this, const f32 vanillaStep) { + return action_step( + vanillaStep, + enemy_action_time_speed(i_this)); +} + +template +inline cXyz enemy_move_step(Enemy* i_this, cXyz vanillaStep) { + const f32 actionSpeed = enemy_action_time_speed(i_this); + if (actionSpeed == 1.0f) { + return vanillaStep; + } + + vanillaStep *= actionSpeed; + return vanillaStep; +} + +template +inline int enemy_chase_action_pos( + Enemy* i_this, + cXyz* value, + const cXyz& target, + const f32 vanillaStep +) { + return cLib_chasePos( + value, + target, + enemy_action_step(i_this, vanillaStep)); +} + +template +inline int enemy_chase_action_pos_xz( + Enemy* i_this, + cXyz* value, + const cXyz& target, + const f32 vanillaStep +) { + return cLib_chasePosXZ( + value, + target, + enemy_action_step(i_this, vanillaStep)); +} + +/* Integrate already-resolved physical values without the engine's generic + * forward-speed resolver. Collision separation remains in world units. */ +inline void pos_move_resolved_f( + fopAc_ac_c* actor, + const f32 physicalSpeedF, + const f32 physicalGravity, + const f32 physicalMaxFallSpeed, + const cXyz* collisionMove +) { + const f32 physicalX = physicalSpeedF * cM_ssin(actor->current.angle.y); + f32 physicalY = actor->speed.y + physicalGravity; + const f32 physicalZ = physicalSpeedF * cM_scos(actor->current.angle.y); + if (physicalY < physicalMaxFallSpeed) { + physicalY = physicalMaxFallSpeed; + } + actor->speed.x = physicalX; + actor->speed.y = physicalY; + actor->speed.z = physicalZ; + fopAcM_posMove(actor, collisionMove); +} + +/* + * Compatibility path for earlier actors: only forward speed is resolved here. + * Their gravity and vertical velocity already use physical frame units, and + * their callers may already resolve gravity at assignment. Do not resolve it + * again or change their stored velocity convention. Forward speed still must + * be resolved only once, not here and again in fopAcM_calcSpeed. + */ +template +inline void enemy_pos_move_f(Enemy* i_this, const cXyz* collisionMove) { + fopAc_ac_c* actor = enemy_attribute_actor(i_this); + pos_move_resolved_f(actor, enemy_move_step(i_this, actor->speedF), + actor->gravity, actor->maxFallSpeed, collisionMove); +} + +/* + * For explicitly migrated actors, stored velocities stay in vanilla units. + * Convert them only while integrating + * this frame, then restore the units for the actor's next action. + * Gravity is resolved separately (the mod controls whether it follows speed), + * and collision separation is deliberately not time-scaled. + * + * The host's fopAcM_calcSpeed already resolves forward speed for generic actors. + * Reproduce its velocity calculation here with our already-resolved speed and + * call only fopAcM_posMove, avoiding a second resolution (and honoring child + * ownership and attachment policies even when the action speed is exactly 1). + */ +template +inline void enemy_action_pos_move_f( + Enemy* i_this, + const cXyz* collisionMove +) { + fopAc_ac_c* actor = EnemyActorAccessor::get(i_this); + const f32 actionSpeed = enemy_action_time_speed(i_this); + const f32 vanillaGravity = actor->gravity; + const f32 scaledGravity = enemy_uses_action_time_scale(i_this) + ? std::copysign(resolve_nonnegative( + enemy_attribute_actor(i_this), ACTOR_ATTRIBUTE_GRAVITY, + std::fabs(vanillaGravity)), vanillaGravity) + : vanillaGravity; + + actor->speed.y = action_step(actor->speed.y, actionSpeed); + pos_move_resolved_f(actor, action_step(actor->speedF, actionSpeed), scaledGravity, + action_step(actor->maxFallSpeed, actionSpeed), collisionMove); + if (actionSpeed != 1.0f) { + actor->speed /= actionSpeed; + } +} + +template +inline f32 enemy_gravity_step(Enemy* i_this, const f32 vanillaStep) { + if (!enemy_uses_action_time_scale(i_this)) { + return vanillaStep; + } + + return resolve_nonnegative( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_GRAVITY, + vanillaStep); +} + +template +inline void enemy_add_action_calc2( + Enemy* i_this, + f32* value, + const f32 target, + const f32 vanillaFraction, + const f32 vanillaMaxStep +) { + const ActionCalcParams params = action_calc_params( + vanillaFraction, + vanillaMaxStep, + enemy_action_time_speed(i_this)); + + cLib_addCalc2( + value, + target, + params.fraction, + params.maximumStep); +} + +template +inline void enemy_add_action_calc0( + Enemy* i_this, + f32* value, + const f32 vanillaFraction, + const f32 vanillaMaxStep +) { + const ActionCalcParams params = action_calc_params( + vanillaFraction, + vanillaMaxStep, + enemy_action_time_speed(i_this)); + + cLib_addCalc0( + value, + params.fraction, + params.maximumStep); +} + +template +inline f32 enemy_add_action_calc( + Enemy* i_this, + f32* value, + const f32 target, + const f32 vanillaFraction, + const f32 vanillaMaxStep, + const f32 vanillaMinStep +) { + const f32 actionSpeed = enemy_action_time_speed(i_this); + const ActionCalcParams params = action_calc_params( + vanillaFraction, + vanillaMaxStep, + actionSpeed); + + return cLib_addCalc( + value, + target, + params.fraction, + params.maximumStep, + action_step(vanillaMinStep, actionSpeed)); +} + +template +inline f32 enemy_add_action_calc_pos( + Enemy* i_this, + cXyz* value, + const cXyz& target, + const f32 vanillaFraction, + const f32 vanillaMaxStep, + const f32 vanillaMinStep +) { + const f32 actionSpeed = enemy_action_time_speed(i_this); + const ActionCalcParams params = action_calc_params( + vanillaFraction, + vanillaMaxStep, + actionSpeed); + + return cLib_addCalcPos( + value, + target, + params.fraction, + params.maximumStep, + action_step(vanillaMinStep, actionSpeed)); +} + +template +inline f32 enemy_add_action_calc_pos_xz( + Enemy* i_this, + cXyz* value, + const cXyz& target, + const f32 vanillaFraction, + const f32 vanillaMaxStep, + const f32 vanillaMinStep +) { + const f32 actionSpeed = enemy_action_time_speed(i_this); + const ActionCalcParams params = action_calc_params( + vanillaFraction, + vanillaMaxStep, + actionSpeed); + + return cLib_addCalcPosXZ( + value, + target, + params.fraction, + params.maximumStep, + action_step(vanillaMinStep, actionSpeed)); +} + +template +inline void enemy_add_action_calc_pos2( + Enemy* i_this, + cXyz* value, + const cXyz& target, + const f32 vanillaFraction, + const f32 vanillaMaxStep +) { + const ActionCalcParams params = action_calc_params( + vanillaFraction, + vanillaMaxStep, + enemy_action_time_speed(i_this)); + + cLib_addCalcPos2( + value, + target, + params.fraction, + params.maximumStep); +} + +template +inline void enemy_add_action_calc_pos_xz2( + Enemy* i_this, + cXyz* value, + const cXyz& target, + const f32 vanillaFraction, + const f32 vanillaMaxStep +) { + const ActionCalcParams params = action_calc_params( + vanillaFraction, + vanillaMaxStep, + enemy_action_time_speed(i_this)); + + cLib_addCalcPosXZ2( + value, + target, + params.fraction, + params.maximumStep); +} + +template +inline f32 enemy_action_damping( + Enemy* i_this, + const f32 vanillaFactor +) { + return action_damping( + vanillaFactor, + enemy_action_time_speed(i_this)); +} + +template +inline f32 enemy_action_growth(Enemy* i_this, const f32 vanillaFactor) { + const f32 speed = enemy_action_time_speed(i_this); + return speed == 1.0f ? vanillaFactor : std::pow(std::max(vanillaFactor, 0.0f), speed); +} + +/* For visual timers whose remaining count is also an amplitude/phase value. */ +template +inline void enemy_tick_visual_timer(Enemy* i_this, Timer& value, const s32 frameCounter) { + const int steps = enemy_action_event_count(i_this, frameCounter, 1); + value = static_cast(std::max(0, static_cast(value) - steps)); +} + +template +inline f32 enemy_move_acceleration( + Enemy* i_this, + const f32 vanillaStep +) { + const f32 actionSpeed = enemy_action_time_speed(i_this); + return vanillaStep * actionSpeed * actionSpeed; +} + +template +inline void enemy_add_action_angle( + Enemy* i_this, + s16* value, + const s16 target, + const s16 vanillaDivisor, + const s16 vanillaMaxStep +) { + const ActionAngleParams params = action_angle_params( + vanillaDivisor, + vanillaMaxStep, + enemy_action_time_speed(i_this)); + + cLib_addCalcAngleS2( + value, + target, + params.divisor, + params.maximumStep); +} + +template +inline int enemy_chase_action_angle( + Enemy* i_this, + s16* value, + const s16 target, + const s16 vanillaStep +) { + const s16 step = action_s16_step( + vanillaStep, + enemy_action_time_speed(i_this)); + + return cLib_chaseAngleS( + value, + target, + step); +} + +template +inline s16 enemy_add_action_angle( + Enemy* i_this, + s16* value, + const s16 target, + const s16 vanillaDivisor, + const s16 vanillaMaxStep, + const s16 vanillaMinStep +) { + const f32 actionSpeed = enemy_action_time_speed(i_this); + + const ActionAngleParams params = action_angle_params( + vanillaDivisor, + vanillaMaxStep, + actionSpeed); + + const s16 minimumStep = action_s16_step( + vanillaMinStep, + actionSpeed); + + return cLib_addCalcAngleS( + value, + target, + params.divisor, + params.maximumStep, + minimumStep); +} + +template +inline s16 enemy_action_phase_angle( + Enemy* i_this, + const s32 vanillaCounter, + const s32 vanillaRate, + const s32 phaseOffset = 0 +) { + return action_phase_angle( + vanillaCounter, + vanillaRate, + enemy_action_time_speed(i_this), + phaseOffset); +} + +template +inline s16 enemy_action_phase_angle( + Enemy* i_this, + const s32 vanillaCounter, + const f32 vanillaRate, + const f32 phaseOffset = 0.0f +) { + return action_phase_angle( + vanillaCounter, + vanillaRate, + enemy_action_time_speed(i_this), + phaseOffset); +} + +template +inline int enemy_chase_action_float( + Enemy* i_this, + f32* value, + const f32 target, + const f32 vanillaStep +) { + const f32 step = action_step( + vanillaStep, + enemy_action_time_speed(i_this)); + + return cLib_chaseF( + value, + target, + step); +} + +template +inline int enemy_chase_action_s16( + Enemy* i_this, + s16* value, + const s16 target, + const s16 vanillaStep +) { + const s16 step = action_s16_step( + vanillaStep, + enemy_action_time_speed(i_this)); + + return cLib_chaseS(value, target, step); +} + +template +inline int enemy_chase_action_u8( + Enemy* i_this, + u8* value, + const u8 target, + const u8 vanillaStep +) { + const u8 step = action_u8_step( + vanillaStep, + enemy_action_time_speed(i_this)); + + return cLib_chaseUC(value, target, step); +} + +template +inline s16 enemy_sync_timer( + Enemy* i_this, + const f32 vanillaFrames +) { + return sync_timer( + vanillaFrames, + enemy_action_time_speed(i_this)); +} + +template +inline s16 enemy_stun_timer( + Enemy* i_this, + const f32 vanillaFrames +) { + return duration_timer( + vanillaFrames, + enemy_stun_duration_multiplier(i_this)); +} + +template +inline Timer enemy_stun_terminal_timer( + Enemy* i_this, + const f32 vanillaActiveFrames +) { + const int maximum = std::numeric_limits::max(); + return static_cast(std::min( + enemy_stun_timer(i_this, vanillaActiveFrames), maximum - 1) + 1); +} + +template +inline s16 enemy_stun_countdown_milestone( + Enemy* i_this, + const f32 vanillaStartFrames, + const f32 vanillaRemainingFrames +) { + const f32 durationMultiplier = enemy_stun_duration_multiplier(i_this); + if (durationMultiplier == 1.0f) { + return duration_timer(vanillaRemainingFrames, 1.0f); + } + + const f32 elapsedFrames = std::max( + vanillaStartFrames - vanillaRemainingFrames, 0.0f); + const s16 remaining = static_cast( + duration_timer(vanillaStartFrames, durationMultiplier) - + duration_timer(elapsedFrames, durationMultiplier)); + return vanillaRemainingFrames > 0.0f ? std::max(remaining, 1) : 0; +} + +template +inline u8 enemy_stun_u8_timer( + Enemy* i_this, + const f32 vanillaFrames +) { + return duration_u8_timer( + vanillaFrames, + enemy_stun_duration_multiplier(i_this)); +} + +template +inline s8 enemy_stun_s8_timer( + Enemy* i_this, + const f32 vanillaFrames +) { + return duration_s8_timer( + vanillaFrames, + enemy_stun_duration_multiplier(i_this)); +} + +/* Reserve a terminal/sentinel frame before converting to the storage type. + * Adding one after a saturated byte/s16 conversion would wrap the timer. */ +template +inline Timer enemy_sync_terminal_timer(Enemy* i_this, const f32 vanillaActiveFrames) { + const int maximum = std::numeric_limits::max(); + return static_cast(std::min( + enemy_sync_timer(i_this, vanillaActiveFrames), maximum - 1) + 1); +} + +/* + * Convert a remaining-value milestone on a countdown that began at + * vanillaStartFrames. Synchronizing vanillaRemainingFrames by itself is + * subtly wrong because both rounded durations must share the same origin. + */ +template +inline s16 enemy_sync_countdown_milestone( + Enemy* i_this, + const f32 vanillaStartFrames, + const f32 vanillaRemainingFrames +) { + if (enemy_action_time_speed(i_this) == 1.0f) { + return sync_timer(vanillaRemainingFrames, 1.0f); + } + const f32 elapsedFrames = std::max( + vanillaStartFrames - vanillaRemainingFrames, 0.0f); + const s16 remaining = static_cast( + enemy_sync_timer(i_this, vanillaStartFrames) - + enemy_sync_timer(i_this, elapsedFrames)); + // A positive "last frame" event must precede the zero/expired branch. + return vanillaRemainingFrames > 0.0f ? std::max(remaining, 1) : 0; +} + +/* Recover the countdown's approximate remaining position on its action clock. */ +template +inline f32 enemy_countdown_action_frames_remaining( + Enemy* i_this, + const s32 remainingRealFrames, + const f32 vanillaStartFrames +) { + const s32 elapsedRealFrames = std::max( + enemy_sync_timer(i_this, vanillaStartFrames) - remainingRealFrames, 0); + return std::max( + vanillaStartFrames - + elapsedRealFrames * enemy_action_time_speed(i_this), + 0.0f); +} + +template +inline u8 enemy_sync_u8_timer( + Enemy* i_this, + const f32 vanillaFrames +) { + return sync_u8_timer( + vanillaFrames, + enemy_action_time_speed(i_this)); +} + +template +inline s8 enemy_sync_s8_timer( + Enemy* i_this, + const f32 vanillaFrames +) { + return sync_s8_timer( + vanillaFrames, + enemy_action_time_speed(i_this)); +} + +template +inline ActionAnimationParams enemy_animation_params( + Enemy* i_this, + const f32 vanillaMorph, + const f32 vanillaPlaybackSpeed +) { + return action_animation_params( + vanillaMorph, + vanillaPlaybackSpeed, + enemy_action_time_speed(i_this)); +} + +template +inline void enemy_set_animation( + Enemy* i_this, + Morph* morph, + Animation animation, + const int mode, + const f32 vanillaMorph, + const f32 vanillaPlaybackSpeed, + Extra... extra +) { + const ActionAnimationParams params = enemy_animation_params( + i_this, + vanillaMorph, + vanillaPlaybackSpeed); + + morph->setAnm( + animation, + mode, + params.morph, + params.playbackSpeed, + extra...); +} + +template +inline void enemy_set_animation_play_speed( + Enemy* i_this, + Morph* morph, + const f32 vanillaPlaybackSpeed +) { + morph->setPlaySpeed( + action_step(vanillaPlaybackSpeed, enemy_action_time_speed(i_this))); +} + +template +inline s16 enemy_health_value( + Enemy* i_this, + const f32 vanillaHealth +) { + return resolve_positive_s16( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_HEALTH, + vanillaHealth); +} + +/* Bosses with their own damage budgets keep the budget in its native units. */ +template +inline f32 enemy_health_amount(Enemy* i_this, const f32 vanillaHealth) { + return resolve_nonnegative(enemy_attribute_actor(i_this), ACTOR_ATTRIBUTE_HEALTH, + vanillaHealth); +} + +template +inline f32 enemy_attack_damage( + Enemy* i_this, + const f32 vanillaDamage +) { + return resolve_nonnegative( + enemy_attribute_actor(i_this), + ACTOR_ATTRIBUTE_ATTACK_DAMAGE, + vanillaDamage); +} + +template +inline int enemy_attack_power_byte( + Enemy* i_this, + const f32 vanillaDamage +) { + return byte_value( + enemy_attack_damage(i_this, vanillaDamage)); +} + +template +inline f32 enemy_signed_gravity_step( + Enemy* i_this, + const f32 vanillaAcceleration +) { + if (vanillaAcceleration < 0.0f) { + return -enemy_gravity_step(i_this, -vanillaAcceleration); + } + + return enemy_gravity_step(i_this, vanillaAcceleration); +} + +/* + * Manual integrators keep velocity in vanilla units, then use enemy_move_step + * at the displacement consumer. Convert resolved physical acceleration back + * to those units; applying resolved gravity directly would scale it twice. + */ +template +inline f32 enemy_velocity_gravity_step( + Enemy* i_this, + const f32 vanillaAcceleration +) { + return enemy_signed_gravity_step(i_this, vanillaAcceleration) / + enemy_action_time_speed(i_this); +} + +} // namespace dusk::mods::svc::actor_attr diff --git a/src/dusk/mods/svc/actor_attribute_swept_acch.hpp b/src/dusk/mods/svc/actor_attribute_swept_acch.hpp new file mode 100644 index 0000000000..88994b55b5 --- /dev/null +++ b/src/dusk/mods/svc/actor_attribute_swept_acch.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "dusk/mods/svc/actor_attribute_helpers.hpp" + +namespace dusk::mods::svc::actor_attr { + +// Acch can miss the floor when one scaled movement step crosses all the way +// through it. Check the feet's path, then let the usual CrrPos call finish +// ground, wall, and speed correction. +template +inline void enemy_swept_ground_correct(Enemy* i_this, dBgS_Acch& acch, const cXyz& movementStart) { + if (enemy_action_time_speed(i_this) <= 1.0f) { + return; + } + + fopAc_ac_c* actor = EnemyActorAccessor::get(i_this); + if (movementStart.y <= actor->current.pos.y || actor->speed.y >= 0.0f) { + return; + } + + cXyz sweepStart = movementStart; + sweepStart.y += 1.0f; + cBgS_LinChk groundSweep; + groundSweep.Set2(&sweepStart, &actor->current.pos, fopAcM_GetID(actor)); + groundSweep.SetExtChk(acch); + if (!dComIfG_Bgsp().LineCross(&groundSweep) || !dBgS_CheckBGroundPoly(groundSweep)) { + return; + } + + const int groundCode = dComIfG_Bgsp().GetGroundCode(groundSweep); + if (groundCode == 4 || groundCode == 10 || groundCode == 5) { + return; + } + + actor->current.pos = groundSweep.GetCross(); + actor->current.pos.y -= 1.0f; +} + +} // namespace dusk::mods::svc::actor_attr diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 69903fcf4f..948c73bc06 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -289,6 +289,7 @@ void ModLoader::init_services() { &svc::g_messageModule, &svc::g_gamemodeModule, &svc::g_actorModule, + &svc::g_actorAttributeModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 9126f8f55b..0734f99f7a 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -97,5 +97,6 @@ extern const ServiceModule g_flowModule; extern const ServiceModule g_messageModule; extern const ServiceModule g_gamemodeModule; extern const ServiceModule g_actorModule; +extern const ServiceModule g_actorAttributeModule; } // namespace dusk::mods::svc