mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
feat(model): the scene camera shakes; PlaySpellVisual at a table of targets
Camera shakes. A 1.12.1 kit names a SpellEffectCameraShakes row at +0x38; the kit player plays it once per play (0x0060EE2E) through PlaySpellEffectCameraShakes (0x006ECB40), which adds each of the row's three CameraShakes ids with CameraAddShake (0x00511D40): the row's type, axis, amplitude times 1/36, frequency, duration, pre-roll and decay, kept with the position and the clock (0x00511C60). A dynamic object's $SHK event adds the row its payload names at the event's point (0x005D5920 to 0x005D592B). Each frame the camera's walk (0x00511760) ages them (ms * 0.001 plus the pre-roll, gone at the duration), cuts each by its distance from the eye (0x005116E0: whole within 9 yards, nothing past 80, times 0.7^((d - 9) / 9) between, the double 0.7 at 0x00808AD0), keeps the strongest per axis, and evaluates it (0x00511610: A sin(2 pi t f), times exp(-t k) for type 1; axis 0 along the heading, 1 across, 2 up). The driver adds the sum to the eye (0x0050EDCC); the view never turns. model/SceneShake.cpp does this per scene on the scene clock, so a paused scene holds its shakes. Paint adds the offset to the eye it draws the sky, world, models, decals and beams from; the projection stays unshaken. The kits a scene plays (casts, SetSpellVisualKit, ApplySpellVisualKit, aura visuals) and a spell area's $SHK shake it. The heading is the scene camera's, the world's being the followed unit's facing. A kit's shake plays at the actor: the client plays it from the first effect node's position, which a bone-attached node has not filled yet (read at 0x00620E0E, filled at 0x0062101D), so a kit like Thunder Clap's 356 shakes at the world origin there and the camera drops it. The footstep (0x005FBF70) and swing (0x00625C30) shakes stay off; stock scenes would shake. A table of targets. A spell go carries a hit list: the cast kit plays once, the launcher makes one missile per hit (0x006E8A50), and without missiles the instant impact walks the list (0x006E8BF0). PlaySpellVisual now takes a table of actors as that one cast: the first is the cast's target, each other gets a sibling cast with the same ID and its own missile, or its impact at once, and OnSpellVisualImpact runs for each target. The siblings join the cast list after the step that made them, as a step holds a reference into it. Each missile still starts its own flight loop through PlaySoundLoop3D (0x004589A0), as in the world.
This commit is contained in:
+19
-2
@@ -20486,8 +20486,16 @@ end)
|
||||
|
||||
Plays the visual of a spell that this actor casts at `target`, and returns
|
||||
a cast ID. `target` is another actor of the same scene. Without a target, the
|
||||
spell is cast on this actor. The visual plays in these steps, as it does in
|
||||
the world:
|
||||
spell is cast on this actor.
|
||||
|
||||
`target` can also be a table of actors, for a spell that hits several
|
||||
targets at once, such as a volley. That is one cast: the preparation, the
|
||||
cast animation and their sounds play one time, and each target gets its own
|
||||
missile (or, for a spell without a missile, its own impact). The script
|
||||
`OnSpellVisualImpact` runs one time for each target, with the same cast ID.
|
||||
Actors of other scenes in the table are skipped.
|
||||
|
||||
The visual plays in these steps, as it does in the world:
|
||||
|
||||
1. The preparation: the effects and the animation of the preparation
|
||||
repeat, and its sound plays. This step lasts `castTimeMs` milliseconds. Without `castTimeMs`,
|
||||
@@ -20524,6 +20532,15 @@ caster, the sounds of the impact play at the target, and the flight sound
|
||||
moves with the missile. While the scene or the caster is paused, the flight
|
||||
sound stops. It starts again when the pause ends.
|
||||
|
||||
Some spells shake the camera of the scene, as they shake the camera in the
|
||||
world. For example, Thunder Clap and War Stomp shake it when they are cast,
|
||||
and Knockdown shakes it at the impact. The camera moves, but it does not
|
||||
turn. A shake is strongest within 9 yards of the camera, weaker farther
|
||||
away, and gone past 80 yards. While the scene is paused, its shakes stop.
|
||||
The auras of `AddAuraVisual`, the kits of `SetSpellVisualKit` and
|
||||
`ApplySpellVisualKit`, and the ground areas of `SetModelBySpellArea` shake
|
||||
the camera in the same way.
|
||||
|
||||
A spell with a chain effect, for example Chain Lightning, also shows a bolt
|
||||
of lightning at the cast. The bolt goes from the hand of the caster to the
|
||||
target, and it follows the two actors when they move. It lasts as long as
|
||||
|
||||
@@ -6421,6 +6421,44 @@ enum Offsets {
|
||||
// release gate tests `kit+0x08 > 0` (benilla spell/go, 0x006E8293).
|
||||
OFF_SPELLVISUALKIT_ANIMATION = 0x08,
|
||||
OFF_SPELLVISUALKIT_SOUND = 0x34,
|
||||
// The kit's camera shake group (+0x38, a SpellEffectCameraShakes id):
|
||||
// the kit player plays it once per play when it is above 0 (0x0060EE2E,
|
||||
// benilla spell/shakes). Thunder Clap's cast kit 356 and the Goblin
|
||||
// Sapper Charge's 1424 have one (parsed from the DBC).
|
||||
OFF_SPELLVISUALKIT_SHAKE = 0x38,
|
||||
// ---- Camera shakes (benilla uicamera/effects, spell/shakes).
|
||||
// SpellEffectCameraShakes.dbc: three CameraShakes ids from +0x04;
|
||||
// PlaySpellEffectCameraShakes (0x006ECB40) adds each nonzero one.
|
||||
VAR_SPELLEFFECTCAMERASHAKES_RECORDS = 0x00C0D814,
|
||||
VAR_SPELLEFFECTCAMERASHAKES_COUNT = 0x00C0D818,
|
||||
OFF_SPELLEFFECTCAMERASHAKES_SHAKES = 0x04,
|
||||
SPELL_EFFECT_CAMERA_SHAKE_COUNT = 3,
|
||||
// CameraShakes.dbc, as CameraAddShake (0x00511D40) passes the row to
|
||||
// 0x00511C60: type (1 decays), axis (0 along the facing, 1 across it, 2
|
||||
// up), amplitude (times 1/36 once), frequency, duration, pre-roll (the
|
||||
// seconds a shake starts into its curve) and the decay rate. Verified
|
||||
// against the disassembly and the DBC (row 1: 1, 2, 2.0, 3.0, 0.4,
|
||||
// 0.06, 1.0).
|
||||
VAR_CAMERASHAKES_RECORDS = 0x00C0DFD0,
|
||||
VAR_CAMERASHAKES_COUNT = 0x00C0DFD4,
|
||||
OFF_CAMERASHAKES_TYPE = 0x04,
|
||||
OFF_CAMERASHAKES_AXIS = 0x08,
|
||||
OFF_CAMERASHAKES_AMPLITUDE = 0x0C,
|
||||
OFF_CAMERASHAKES_FREQUENCY = 0x10,
|
||||
OFF_CAMERASHAKES_DURATION = 0x14,
|
||||
OFF_CAMERASHAKES_PREROLL = 0x18,
|
||||
OFF_CAMERASHAKES_DECAY = 0x1C,
|
||||
CAMERA_SHAKE_TYPE_DECAYING = 1,
|
||||
// The falloff (0x005116E0): a shake past 80 yards of the eye (squared,
|
||||
// the static at 0x00BE1060, 6400) adds nothing; within 9 (0x00808A0C;
|
||||
// squared at 0x00BE103C, 81) it is whole; between, its amplitude is
|
||||
// times 0.7^((d - 9) / 9) with the double 0.7 at 0x00808AD0
|
||||
// (0.699999988079071). The statics are set at load from 0x00808A0C and
|
||||
// 0x00808A10 (0x0050B2C0, 0x0050B2F0).
|
||||
VAR_CAMERA_SHAKE_NEAR = 0x00808A0C,
|
||||
VAR_CAMERA_SHAKE_NEAR_SQ = 0x00BE103C,
|
||||
VAR_CAMERA_SHAKE_FAR_SQ = 0x00BE1060,
|
||||
VAR_CAMERA_SHAKE_FALLOFF_BASE = 0x00808AD0,
|
||||
// SpellVisual.dbc (16 columns), verified by parsing the DBC (Fireball's
|
||||
// row 67: 30, 38, 286, 0, 0, 1, 365, 0, 1, 3011, ...) against the
|
||||
// readers: the precast kit (+0x04, 0x006EC220), cast kit (+0x08), impact
|
||||
|
||||
@@ -708,6 +708,13 @@ void __fastcall Paint(void *frame) {
|
||||
eye[0] = e.x;
|
||||
eye[1] = e.y;
|
||||
eye[2] = e.z;
|
||||
// The shakes move the eye, not the view (the world's driver adds
|
||||
// them to the eye, 0x0050EDCC); everything draws from the moved eye.
|
||||
const Fields &f = F(frame);
|
||||
float shake[3];
|
||||
Shakes::Offset(frame, eye, std::atan2(f.forward.y, f.forward.x), shake);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
eye[i] += shake[i];
|
||||
CameraTransforms(frame, camera, rect);
|
||||
} else {
|
||||
const float point[2] = {rect[1], rect[0]};
|
||||
@@ -962,6 +969,7 @@ using AddCallback_t = void(__fastcall *)(void *batch, void *edx, Callback_t fn,
|
||||
void Destroy(void *frame) {
|
||||
Fields &f = F(frame);
|
||||
Chains::Destroy(frame);
|
||||
Shakes::Destroy(frame);
|
||||
for (Actor *actor : *f.actors)
|
||||
delete actor;
|
||||
delete f.actors;
|
||||
|
||||
+26
-1
@@ -72,6 +72,9 @@ struct SpellCast {
|
||||
uint32_t ammoDisplay = 0;
|
||||
uint32_t ammoType = 0;
|
||||
Actor *target = nullptr; // the caster for a cast on itself, null at a point
|
||||
// The rest of the go's hit list (a cast at several targets): at the cast
|
||||
// each gets a missile of its own, or its impact at once.
|
||||
std::vector<Actor *> otherTargets;
|
||||
bool atPoint = false; // cast at the ground point `destination`
|
||||
Phase phase = Phase::Precast;
|
||||
float precastLeftSec = 0.0f;
|
||||
@@ -406,6 +409,9 @@ class Actor : public Animation::Object {
|
||||
// user data of a hung bow's string; each update, before the draw.
|
||||
alignas(4) uint8_t stringUnit[Offsets::OFF_CGUNIT_STATE_FLAGS + 4] = {};
|
||||
std::vector<SpellCast> spellCasts;
|
||||
// The missiles a cast made for its other targets this step, joining
|
||||
// spellCasts after it (a step holds a reference into that list).
|
||||
std::vector<SpellCast> castsMade;
|
||||
// The casts that reached their target, in order, until the update runs
|
||||
// OnSpellVisualImpact for them: outside the casts' step, which a script
|
||||
// that casts or stops a spell would change under it.
|
||||
@@ -423,7 +429,10 @@ namespace Casts {
|
||||
// or the caster's model is missing. `castTimeMs` < 0 takes the spell's cast
|
||||
// time.
|
||||
// channelMs: a channeled spell's channel time, -1 for its duration.
|
||||
uint32_t Play(Actor &caster, uint32_t spellID, Actor *target, int castTimeMs, int channelMs);
|
||||
// `others`: more targets of the same cast (one cast kit, a missile or an
|
||||
// impact each).
|
||||
uint32_t Play(Actor &caster, uint32_t spellID, Actor *target, int castTimeMs, int channelMs,
|
||||
const std::vector<Actor *> &others = {});
|
||||
// The same cast at a ground point of the scene instead of an actor.
|
||||
uint32_t PlayAtPoint(Actor &caster, uint32_t spellID, const float (&point)[3], int castTimeMs, int channelMs);
|
||||
// Interrupts the casts still preparing; casts already made go on.
|
||||
@@ -482,6 +491,22 @@ void Forget(Actor &actor);
|
||||
void Destroy(void *frame);
|
||||
} // namespace Chains
|
||||
|
||||
// The scene camera's shakes (model/SceneShake.cpp).
|
||||
namespace Shakes {
|
||||
// CameraAddShake (0x00511D40): the CameraShakes row `shakeID` at a scene point.
|
||||
void Add(void *frame, int32_t shakeID, const float (&position)[3]);
|
||||
// PlaySpellEffectCameraShakes (0x006ECB40): the three shakes of a
|
||||
// SpellEffectCameraShakes row.
|
||||
void AddGroup(void *frame, int32_t groupID, const float (&position)[3]);
|
||||
// A kit's shake group (+0x38), once per play of the kit, at the actor.
|
||||
void PlayKit(const Actor &actor, const uint8_t *kit);
|
||||
// The eye's offset this frame from the unshaken eye and the camera's heading
|
||||
// (the walk 0x00511760); the shakes past their time go.
|
||||
void Offset(void *frame, const float (&eye)[3], float heading, float (&out)[3]);
|
||||
// The scene goes.
|
||||
void Destroy(void *frame);
|
||||
} // namespace Shakes
|
||||
|
||||
// The auras an actor shows (model/SceneAura.cpp).
|
||||
namespace AuraVisuals {
|
||||
// The spell's aura joins the actor's, its state kit shown once the model has
|
||||
|
||||
@@ -262,6 +262,7 @@ void ApplyKit(Actor &actor) {
|
||||
if (kit == nullptr)
|
||||
return;
|
||||
Model::VisualKit::Apply(actor.SceneFrame(), actor.model, kit, actor.spellVisualKitOneShot, &actor.kitEffects);
|
||||
Shakes::PlayKit(actor, kit);
|
||||
// The world effect, planted where the actor stands (PlantEffect
|
||||
// 0x0061FCF0). A kit given without a spell has no root aura, so it stays
|
||||
// there, as the world's does without flag 0x4000.
|
||||
@@ -1143,9 +1144,12 @@ void Actor::Update(float elapsedSec, float sceneSec) {
|
||||
PutOnWaiting(*this);
|
||||
ApplyKit(*this);
|
||||
AuraVisuals::Show(*this);
|
||||
for (const auto &[kitID, oneShot] : pendingKits)
|
||||
if (const uint8_t *kit = Model::VisualKit::Record(kitID))
|
||||
for (const auto &[kitID, oneShot] : pendingKits) {
|
||||
if (const uint8_t *kit = Model::VisualKit::Record(kitID)) {
|
||||
Model::VisualKit::Apply(SceneFrame(), model, kit, oneShot);
|
||||
Shakes::PlayKit(*this, kit);
|
||||
}
|
||||
}
|
||||
pendingKits.clear();
|
||||
// The pick an unloaded model could not take (PlayAnimation keeps
|
||||
// it pending at +0xD64).
|
||||
@@ -1773,7 +1777,32 @@ int __fastcall Script_PlaySpellVisual(void *L) {
|
||||
if (caster == nullptr || !NumberArgs(L, 2, 1, caster->DisplayName(), kUsage))
|
||||
return 0;
|
||||
Actor *target = nullptr;
|
||||
if (Game::Lua::Type(L, 3) > Game::Lua::TYPE_NIL) {
|
||||
// A table of actors is one cast at all of them: the first is the target,
|
||||
// the rest the go's other hits.
|
||||
std::vector<Actor *> others;
|
||||
if (Game::Lua::Type(L, 3) == Game::Lua::TYPE_TABLE) {
|
||||
for (int i = 1;; ++i) {
|
||||
Game::Lua::RawGetI(L, 3, i);
|
||||
if (Game::Lua::Type(L, -1) <= Game::Lua::TYPE_NIL) {
|
||||
Game::Lua::SetTop(L, -2);
|
||||
break;
|
||||
}
|
||||
auto *actor = static_cast<Actor *>(Animation::Resolve(L, Game::Lua::GetTop(L), Actor::s_class));
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (actor == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: %s:%s", caster->DisplayName(), kUsage);
|
||||
return 0;
|
||||
}
|
||||
if (actor->SceneFrame() != caster->SceneFrame())
|
||||
continue;
|
||||
if (target == nullptr)
|
||||
target = actor;
|
||||
else
|
||||
others.push_back(actor);
|
||||
}
|
||||
if (target == nullptr)
|
||||
return 0;
|
||||
} else if (Game::Lua::Type(L, 3) > Game::Lua::TYPE_NIL) {
|
||||
target = static_cast<Actor *>(Animation::Resolve(L, 3, Actor::s_class));
|
||||
if (target == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: %s:%s", caster->DisplayName(), kUsage);
|
||||
@@ -1790,7 +1819,8 @@ int __fastcall Script_PlaySpellVisual(void *L) {
|
||||
Game::Lua::IsNumber(L, 5) ? static_cast<int>(Game::Lua::ToNumber(L, 5)) : -1;
|
||||
const uint32_t castID =
|
||||
target == nullptr || target->SceneFrame() == caster->SceneFrame()
|
||||
? Casts::Play(*caster, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)), target, castTimeMs, channelMs)
|
||||
? Casts::Play(*caster, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)), target, castTimeMs, channelMs,
|
||||
others)
|
||||
: 0;
|
||||
if (castID == 0)
|
||||
return 0;
|
||||
@@ -1859,10 +1889,12 @@ int __fastcall Script_ApplySpellVisualKit(void *L) {
|
||||
const uint8_t *kit = Model::VisualKit::Record(kitID);
|
||||
if (kit == nullptr || actor->model == nullptr)
|
||||
return 0;
|
||||
if (actor->loaded)
|
||||
if (actor->loaded) {
|
||||
Model::VisualKit::Apply(actor->SceneFrame(), actor->model, kit, oneShot);
|
||||
else
|
||||
Shakes::PlayKit(*actor, kit);
|
||||
} else {
|
||||
actor->pendingKits.emplace_back(kitID, oneShot);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3154,7 +3186,8 @@ const Game::Doc::Function kAttachToActor{
|
||||
const Game::Doc::Field kPlaySpellVisualArgs[] = {
|
||||
Req("spellID", "number"),
|
||||
Opt("target", "ModelSceneFrameActor", nullptr,
|
||||
"Another actor of the scene. Without one, the spell is cast on this actor."),
|
||||
"Another actor of the scene, or a table of actors: one cast at all of them, a missile or an impact "
|
||||
"each. Without one, the spell is cast on this actor."),
|
||||
Opt("castTimeMs", "number", nullptr,
|
||||
"How long the spell is prepared. Without it, the cast time of the spell."),
|
||||
Opt("channelTimeMs", "number", nullptr,
|
||||
|
||||
@@ -94,6 +94,7 @@ void ShowOne(Actor &actor, AuraVisual &aura) {
|
||||
aura.shown = true;
|
||||
void *frame = actor.SceneFrame();
|
||||
Model::VisualKit::Apply(frame, actor.model, aura.kit, false, &aura.effects);
|
||||
Shakes::PlayKit(actor, aura.kit);
|
||||
// The kit's sound, once per play: a looping one stays on the unit until
|
||||
// the aura's effects stop.
|
||||
const auto sound = Game::Read<int32_t>(aura.kit, Offsets::OFF_SPELLVISUALKIT_SOUND);
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
//
|
||||
// Not played: the combat tags $CSS, $CAH and $AH0..$AH3 present a pending
|
||||
// swing (0x006247D0) and play nothing without one, and an actor never swings
|
||||
// at anything; footprints, splashes and camera shakes are world effects.
|
||||
// at anything; footprints and splashes are world effects, and so are the
|
||||
// footstep shake (CreatureModelData +0x2C, 0x005FBF70) and the swing shake
|
||||
// (+0x30, 0x00625C30), which stock scenes, unlike the world, do not show.
|
||||
|
||||
#include "model/Scene.h"
|
||||
|
||||
@@ -90,7 +92,9 @@ constexpr uint32_t kWGG = Tag('$', 'W', 'G', 'G');
|
||||
constexpr uint32_t kCSD = Tag('$', 'C', 'S', 'D');
|
||||
constexpr uint32_t kTRD = Tag('$', 'T', 'R', 'D');
|
||||
constexpr uint32_t kSND = Tag('$', 'S', 'N', 'D');
|
||||
constexpr uint32_t kSHK = Tag('$', 'S', 'H', 'K');
|
||||
static_assert(kSND == 0x444E5324, "$SND as the dynamic object compares it (0x005D58E1)");
|
||||
static_assert(kSHK == 0x4B485324, "$SHK as the dynamic object compares it (0x005D58E9)");
|
||||
static_assert(kCSR == 0x52534324 && kWNG == 0x474E5724, "tags compare as the engine's dwords");
|
||||
|
||||
constexpr int kStandChance = 40; // 0x008626D4[5]: pass at <= 40 of 0..100
|
||||
@@ -252,11 +256,16 @@ void __stdcall OnModelEvent(uint32_t tag, uint32_t data, const float *point, uin
|
||||
uint32_t userLow, uint32_t /*userHigh*/) {
|
||||
auto *actor = reinterpret_cast<Actor *>(static_cast<uintptr_t>(userLow));
|
||||
// A spell's ground area takes a dynamic object's events (0x005D58C0):
|
||||
// $SND plays its payload as a SoundEntries id at the area, always; its
|
||||
// other tag, $SHK, shakes the world's camera, which a scene does not have.
|
||||
// $SND plays its payload as a SoundEntries id at the area, always; $SHK
|
||||
// adds the three shakes of the SpellEffectCameraShakes row its payload
|
||||
// names at the event's point (0x005D5920 to 0x005D592B).
|
||||
if (actor->areaEffect != nullptr) {
|
||||
if (tag == kSND)
|
||||
if (tag == kSND) {
|
||||
Play(*actor, static_cast<int32_t>(data));
|
||||
} else if (tag == kSHK && point != nullptr) {
|
||||
const float at[3] = {point[0], point[1], point[2]};
|
||||
Shakes::AddGroup(actor->SceneFrame(), static_cast<int32_t>(data), at);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (tag == kCSL || tag == kCSR || tag == kCST || tag == kBWR) {
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// A scene camera's shakes: the world camera's (benilla uicamera/effects,
|
||||
// spell/shakes) on a scene's camera, step for step:
|
||||
//
|
||||
// 1. Adding one (CameraAddShake 0x00511D40): a CameraShakes row by id, its
|
||||
// amplitude times 1/36 once (yards), kept with the position and the
|
||||
// spawn time (0x00511C60). An id outside the table adds nothing.
|
||||
// 2. A group (PlaySpellEffectCameraShakes 0x006ECB40): the three ids of a
|
||||
// SpellEffectCameraShakes row, each nonzero one added. A kit plays its
|
||||
// group (+0x38) once per play (the kit player, 0x0060EDF0); a spell's
|
||||
// ground area plays the row its model's $SHK event names, at the
|
||||
// event's point (the dynamic object's arm, 0x005D58C0).
|
||||
// 3. Each frame (the walk 0x00511760): a shake's time is the milliseconds
|
||||
// since its spawn times 0.001 plus its pre-roll; at or past its duration
|
||||
// it goes. A live one's amplitude is cut by its distance from the eye
|
||||
// (0x005116E0: whole within 9 yards, nothing past 80, times
|
||||
// 0.7^((d - 9) / 9) between). Per axis the strongest wins, the older on
|
||||
// a tie; it adds `a = A sin(2 pi t f)`, times exp(-t k) for type 1
|
||||
// (0x00511610): axis 0 along the heading, axis 1 across it, axis 2 up.
|
||||
// 4. The sum moves the eye in world space; it never turns the view (the
|
||||
// driver 0x0050E8D0 adds it to the eye at 0x0050EDCC).
|
||||
//
|
||||
// The scene's clock times them, so a paused scene holds its shakes. The
|
||||
// heading is the scene camera's: the world's is the followed unit's facing,
|
||||
// which its camera looks along, and a scene follows no unit.
|
||||
//
|
||||
// The position of a kit's shake: the client plays it in the first effect
|
||||
// node's arm pass from that node's position, which a bone-attached node has
|
||||
// not filled yet (+0x48 read at 0x00620E0E, filled at 0x0062101D), so the
|
||||
// group of a kit whose first effect hangs on a bone plays at the world
|
||||
// origin, and the camera drops it past 80 yards (benilla spell/shakes "Kit
|
||||
// shake position fault"). Thunder Clap's cast kit (356) hangs its first
|
||||
// effect on a hand. Here a kit shakes at the actor, where its node is.
|
||||
|
||||
#include "model/Scene.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Model::Scene::Shakes {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Shake {
|
||||
int32_t type = 0;
|
||||
int32_t axis = 0;
|
||||
float amplitude = 0.0f; // yards
|
||||
float frequency = 0.0f;
|
||||
float duration = 0.0f;
|
||||
float preroll = 0.0f;
|
||||
float decay = 0.0f;
|
||||
float position[3] = {};
|
||||
uint32_t spawnMs = 0; // the scene clock
|
||||
};
|
||||
|
||||
std::unordered_map<void *, std::vector<Shake>> g_shakes; // by scene frame, oldest first
|
||||
|
||||
const Game::Live<float> kAmplitudeScale{Offsets::VAR_ONE_THIRTY_SIXTH};
|
||||
const Game::Live<float> kTwoPi{Offsets::VAR_HEADING_TWO_PI};
|
||||
const Game::Live<float> kHalfPi{Offsets::VAR_HALF_PI};
|
||||
const Game::Live<float> kNear{Offsets::VAR_CAMERA_SHAKE_NEAR};
|
||||
const Game::Live<float> kNearSq{Offsets::VAR_CAMERA_SHAKE_NEAR_SQ};
|
||||
const Game::Live<float> kFarSq{Offsets::VAR_CAMERA_SHAKE_FAR_SQ};
|
||||
const Game::Live<double> kFalloffBase{Offsets::VAR_CAMERA_SHAKE_FALLOFF_BASE};
|
||||
|
||||
// The falloff (0x005116E0): false past the far range; else the amplitude,
|
||||
// cut beyond the near range. The squared distance is a float, as stored.
|
||||
bool Falloff(const float (&eye)[3], const Shake &shake, float &litude) {
|
||||
const float dx = eye[0] - shake.position[0];
|
||||
const float dy = eye[1] - shake.position[1];
|
||||
const float dz = eye[2] - shake.position[2];
|
||||
const float distanceSq = dx * dx + dy * dy + dz * dz;
|
||||
if (distanceSq > kFarSq)
|
||||
return false;
|
||||
amplitude = shake.amplitude;
|
||||
if (distanceSq > kNearSq) {
|
||||
const float distance = std::sqrt(distanceSq);
|
||||
amplitude = static_cast<float>(std::pow(static_cast<double>(kFalloffBase),
|
||||
static_cast<double>((distance - kNear) / kNear)) *
|
||||
amplitude);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// One shake's offset (0x00511610), each step rounded to a float as stored.
|
||||
void Evaluate(const Shake &shake, float amplitude, float t, float heading, float (&out)[3]) {
|
||||
float a = static_cast<float>(amplitude * std::sin(static_cast<float>(kTwoPi * t * shake.frequency)));
|
||||
if (shake.type == Offsets::CAMERA_SHAKE_TYPE_DECAYING)
|
||||
a = static_cast<float>(a * std::exp(-(t * shake.decay)));
|
||||
switch (shake.axis) {
|
||||
case 0:
|
||||
out[0] += static_cast<float>(a * std::cos(heading));
|
||||
out[1] += static_cast<float>(a * std::sin(heading));
|
||||
break;
|
||||
case 1: {
|
||||
const float across = heading + kHalfPi;
|
||||
out[0] += static_cast<float>(a * std::cos(across));
|
||||
out[1] += static_cast<float>(a * std::sin(across));
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
out[2] += a;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Add(void *frame, int32_t shakeID, const float (&position)[3]) {
|
||||
const uint8_t *row = shakeID > 0 ? DBC::Record(Offsets::VAR_CAMERASHAKES_RECORDS, Offsets::VAR_CAMERASHAKES_COUNT,
|
||||
static_cast<uint32_t>(shakeID))
|
||||
: nullptr;
|
||||
if (row == nullptr)
|
||||
return;
|
||||
Shake shake;
|
||||
shake.type = Game::Read<int32_t>(row, Offsets::OFF_CAMERASHAKES_TYPE);
|
||||
shake.axis = Game::Read<int32_t>(row, Offsets::OFF_CAMERASHAKES_AXIS);
|
||||
shake.amplitude = Game::Read<float>(row, Offsets::OFF_CAMERASHAKES_AMPLITUDE) * kAmplitudeScale;
|
||||
shake.frequency = Game::Read<float>(row, Offsets::OFF_CAMERASHAKES_FREQUENCY);
|
||||
shake.duration = Game::Read<float>(row, Offsets::OFF_CAMERASHAKES_DURATION);
|
||||
shake.preroll = Game::Read<float>(row, Offsets::OFF_CAMERASHAKES_PREROLL);
|
||||
shake.decay = Game::Read<float>(row, Offsets::OFF_CAMERASHAKES_DECAY);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
shake.position[i] = position[i];
|
||||
shake.spawnMs = ClockMs(frame);
|
||||
g_shakes[frame].push_back(shake);
|
||||
}
|
||||
|
||||
void AddGroup(void *frame, int32_t groupID, const float (&position)[3]) {
|
||||
const uint8_t *group = groupID > 0 ? DBC::Record(Offsets::VAR_SPELLEFFECTCAMERASHAKES_RECORDS,
|
||||
Offsets::VAR_SPELLEFFECTCAMERASHAKES_COUNT,
|
||||
static_cast<uint32_t>(groupID))
|
||||
: nullptr;
|
||||
if (group == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < Offsets::SPELL_EFFECT_CAMERA_SHAKE_COUNT; ++i)
|
||||
Add(frame, Game::Read<int32_t>(group, Offsets::OFF_SPELLEFFECTCAMERASHAKES_SHAKES + i * 4), position);
|
||||
}
|
||||
|
||||
void PlayKit(const Actor &actor, const uint8_t *kit) {
|
||||
if (kit != nullptr)
|
||||
AddGroup(actor.SceneFrame(), Game::Read<int32_t>(kit, Offsets::OFF_SPELLVISUALKIT_SHAKE), actor.position);
|
||||
}
|
||||
|
||||
void Offset(void *frame, const float (&eye)[3], float heading, float (&out)[3]) {
|
||||
out[0] = out[1] = out[2] = 0.0f;
|
||||
const auto it = g_shakes.find(frame);
|
||||
if (it == g_shakes.end())
|
||||
return;
|
||||
std::vector<Shake> &shakes = it->second;
|
||||
const uint32_t now = ClockMs(frame);
|
||||
// A shake's time: the scene milliseconds since its spawn (an unsigned
|
||||
// difference, as the walk forms it) times 0.001, plus its pre-roll.
|
||||
auto timeOf = [now](const Shake &shake) {
|
||||
return static_cast<float>(now - shake.spawnMs) * 0.001f + shake.preroll;
|
||||
};
|
||||
shakes.erase(std::remove_if(shakes.begin(), shakes.end(),
|
||||
[&](const Shake &shake) { return !(timeOf(shake) < shake.duration); }),
|
||||
shakes.end());
|
||||
if (shakes.empty()) {
|
||||
g_shakes.erase(it);
|
||||
return;
|
||||
}
|
||||
constexpr int kAxes = 3;
|
||||
int best[kAxes] = {-1, -1, -1};
|
||||
float bestAmplitude[kAxes] = {};
|
||||
for (int i = 0; i < static_cast<int>(shakes.size()); ++i) {
|
||||
const Shake &shake = shakes[static_cast<size_t>(i)];
|
||||
float amplitude = 0.0f;
|
||||
if (shake.axis < 0 || shake.axis >= kAxes || !Falloff(eye, shake, amplitude))
|
||||
continue;
|
||||
if (best[shake.axis] < 0 || amplitude > bestAmplitude[shake.axis]) { // the older on a tie
|
||||
best[shake.axis] = i;
|
||||
bestAmplitude[shake.axis] = amplitude;
|
||||
}
|
||||
}
|
||||
for (int axis = 0; axis < kAxes; ++axis) {
|
||||
if (best[axis] >= 0) {
|
||||
const Shake &shake = shakes[static_cast<size_t>(best[axis])];
|
||||
Evaluate(shake, bestAmplitude[axis], timeOf(shake), heading, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Destroy(void *frame) { g_shakes.erase(frame); }
|
||||
|
||||
} // namespace Model::Scene::Shakes
|
||||
@@ -25,7 +25,9 @@
|
||||
// speed above 0, a target that is not the caster) makes the missile
|
||||
// model (+0x1C's SpellVisualEffectName file, else Spells\ErrorCube.mdx,
|
||||
// as the launcher 0x0060A3D0), held until its release. Without one the
|
||||
// impact plays at once (0x006E8BF0).
|
||||
// impact plays at once (0x006E8BF0). A cast at several targets is one
|
||||
// go with a hit list: the kits play once, the launcher makes a missile
|
||||
// for each hit (0x006E8A50), else each gets its impact at once.
|
||||
// 3. Release (CGUnit_C::ReleaseMissiles, 0x0060C940): at the caster
|
||||
// model's $CSL / $CSR / $CST event, from the event's point (the unit's
|
||||
// event callback, 0x005FFBD0). Without the event it is
|
||||
@@ -196,6 +198,7 @@ void PlayKit(Actor &actor, const uint8_t *kit, std::vector<void *> *effects, Spe
|
||||
return;
|
||||
const bool lasting = loop != nullptr;
|
||||
Model::VisualKit::Apply(actor.SceneFrame(), actor.model, kit, !lasting, effects);
|
||||
Shakes::PlayKit(actor, kit);
|
||||
const int anim = KitAnimation(kit);
|
||||
if (anim >= 0) {
|
||||
actor.PlayKitAnimation(anim, lasting);
|
||||
@@ -711,6 +714,32 @@ void StartCast(Actor &caster, SpellCast &cast) {
|
||||
}
|
||||
if (cast.atPoint && Game::Read<uint32_t>(cast.visual, Offsets::OFF_SPELLVISUAL_HAS_MISSILE) == 0)
|
||||
PlantEffect(caster, cast);
|
||||
// The go's other hits: the launcher makes one missile per hit
|
||||
// (0x006E8A50); without missiles the instant impact walks the hit list
|
||||
// (0x006E8BF0). The cast kit played once, above.
|
||||
for (Actor *other : cast.otherTargets) {
|
||||
SpellCast hit;
|
||||
hit.id = cast.id;
|
||||
hit.spellID = cast.spellID;
|
||||
std::memcpy(hit.visual, cast.visual, sizeof hit.visual);
|
||||
hit.ammoDisplay = cast.ammoDisplay;
|
||||
hit.ammoType = cast.ammoType;
|
||||
hit.target = other;
|
||||
hit.phase = SpellCast::Phase::Cast;
|
||||
hit.speed = cast.speed;
|
||||
hit.sticks = cast.sticks;
|
||||
if (cast.missile == nullptr) {
|
||||
Impact(caster, hit, false);
|
||||
continue;
|
||||
}
|
||||
hit.missile = MakeMissile(caster, hit);
|
||||
if (hit.missile == nullptr)
|
||||
continue;
|
||||
LightAsEntity(hit.missile, hit.light, caster.SceneFrame());
|
||||
Aim(hit);
|
||||
caster.castsMade.push_back(std::move(hit));
|
||||
}
|
||||
cast.otherTargets.clear();
|
||||
if (cast.missile != nullptr) {
|
||||
LightAsEntity(cast.missile, cast.light, caster.SceneFrame());
|
||||
Aim(cast);
|
||||
@@ -878,8 +907,17 @@ int RangedAnimationAfter(const Actor &caster, int ended) {
|
||||
|
||||
namespace {
|
||||
|
||||
// The cast at the target, else at `point` when given, else on the caster.
|
||||
uint32_t Begin(Actor &caster, uint32_t spellID, Actor *target, const float *point, int castTimeMs, int channelMs) {
|
||||
// The casts made for a cast's other targets join the list.
|
||||
void AddCastsMade(Actor &caster) {
|
||||
for (SpellCast &made : caster.castsMade)
|
||||
caster.spellCasts.push_back(std::move(made));
|
||||
caster.castsMade.clear();
|
||||
}
|
||||
|
||||
// The cast at the target (and `others`), else at `point` when given, else on
|
||||
// the caster.
|
||||
uint32_t Begin(Actor &caster, uint32_t spellID, Actor *target, const float *point, int castTimeMs, int channelMs,
|
||||
const std::vector<Actor *> &others) {
|
||||
using GetCastTime_t = int(__fastcall *)(int spellID, int pet, int allowNegative);
|
||||
static uint32_t s_lastCastID = 0;
|
||||
const uint8_t *spell =
|
||||
@@ -914,6 +952,7 @@ uint32_t Begin(Actor &caster, uint32_t spellID, Actor *target, const float *poin
|
||||
cast.destination[i] = point[i];
|
||||
} else {
|
||||
cast.target = target != nullptr ? target : &caster;
|
||||
cast.otherTargets = others;
|
||||
}
|
||||
// A channeled spell channels after its cast, for the time given, else its
|
||||
// duration (forever for an endless one).
|
||||
@@ -938,17 +977,19 @@ uint32_t Begin(Actor &caster, uint32_t spellID, Actor *target, const float *poin
|
||||
const uint32_t id = added.id;
|
||||
if (added.missile == nullptr && added.phase != SpellCast::Phase::Channel)
|
||||
caster.spellCasts.pop_back();
|
||||
AddCastsMade(caster);
|
||||
return id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t Play(Actor &caster, uint32_t spellID, Actor *target, int castTimeMs, int channelMs) {
|
||||
return Begin(caster, spellID, target, nullptr, castTimeMs, channelMs);
|
||||
uint32_t Play(Actor &caster, uint32_t spellID, Actor *target, int castTimeMs, int channelMs,
|
||||
const std::vector<Actor *> &others) {
|
||||
return Begin(caster, spellID, target, nullptr, castTimeMs, channelMs, others);
|
||||
}
|
||||
|
||||
uint32_t PlayAtPoint(Actor &caster, uint32_t spellID, const float (&point)[3], int castTimeMs, int channelMs) {
|
||||
return Begin(caster, spellID, nullptr, point, castTimeMs, channelMs);
|
||||
return Begin(caster, spellID, nullptr, point, castTimeMs, channelMs, {});
|
||||
}
|
||||
|
||||
void Update(Actor &caster, float elapsedSec) {
|
||||
@@ -964,6 +1005,7 @@ void Update(Actor &caster, float elapsedSec) {
|
||||
DropCast(caster, caster.spellCasts[i]);
|
||||
caster.spellCasts.erase(caster.spellCasts.begin() + static_cast<ptrdiff_t>(i));
|
||||
}
|
||||
AddCastsMade(caster);
|
||||
caster.releaseEvent = false;
|
||||
caster.castAnimationEnded = false;
|
||||
// The world has no pause; a paused scene (or caster) holds its missiles
|
||||
@@ -1079,6 +1121,7 @@ void Drop(Actor &caster) {
|
||||
for (SpellCast &cast : caster.spellCasts)
|
||||
DropCast(caster, cast);
|
||||
caster.spellCasts.clear();
|
||||
caster.castsMade.clear(); // empty outside a step
|
||||
for (ImpactModel &impact : caster.impactModels)
|
||||
FreeImpactModel(impact);
|
||||
caster.impactModels.clear();
|
||||
|
||||
Reference in New Issue
Block a user