A reimplementation of Mario Kart Wii's physics engine in C++
Loading...
Searching...
No Matches
KartHalfPipe.cc
1#include "KartHalfPipe.hh"
2
3#include "game/kart/KartCollide.hh"
4#include "game/kart/KartDynamics.hh"
5#include "game/kart/KartMove.hh"
6#include "game/kart/KartParam.hh"
7#include "game/kart/KartPhysics.hh"
8#include "game/kart/KartState.hh"
9
10#include <egg/math/Math.hh>
11
12namespace Kinoko::Kart {
13
15KartHalfPipe::KartHalfPipe() : m_prevPos(EGG::Vector3f::zero) {
16 // Not initialized here in the base game because normally the game's heaps are zero-initialized
17 m_nextTimer = 0;
18 m_trick = System::Trick::None;
19}
20
22KartHalfPipe::~KartHalfPipe() = default;
23
25void KartHalfPipe::reset() {
26 m_stunt = StuntType::None;
27 m_touchingZipper = false;
28 m_timer = 0;
29}
30
32void KartHalfPipe::calc() {
33 constexpr s16 LANDING_BOOST_DELAY = 3;
34
35 auto &status = KartObjectProxy::status();
36
37 if (state()->airtime() > 15 && status.onBit(eStatus::OverZipper)) {
38 m_timer = LANDING_BOOST_DELAY;
39 }
40
41 bool isLanding = status.onBit(eStatus::HalfPipeRamp) && m_timer <= 0;
42
43 calcTrick();
44
45 if (status.offBit(eStatus::InAction) &&
46 collide()->surfaceFlags().offBit(KartCollide::eSurfaceFlags::StopHalfPipeState)) {
47 if (m_touchingZipper && status.onBit(eStatus::AirStart)) {
48 dynamics()->setExtVel(EGG::Vector3f::zero);
49 status.setBit(eStatus::OverZipper);
50
51 EGG::Vector3f upXZ = move()->up();
52 upXZ.y = 0.0f;
53 upXZ.normalise();
54 EGG::Vector3f up = move()->dir().perpInPlane(upXZ, true);
55
56 EGG::Vector3f local_64 = up.cross(bodyUp().perpInPlane(up, true));
57 m_nextSign = local_64.dot(EGG::Vector3f::ey) > 0.0f ? 1.0f : -1.0f;
58
59 EGG::Vector3f velNorm = velocity();
60 velNorm.normalise();
61 EGG::Vector3f rot = dynamics()->mainRot().rotateVectorInv(velNorm);
62
63 m_rot.makeVectorRotation(rot, EGG::Vector3f::ez);
64 m_prevPos = prevPos();
65
66 calcLanding(false);
67
68 f32 scaledDir = std::min(65.0f, move()->dir().y * move()->speed());
69 m_attemptedTrickTimer = std::max<s32>(0, scaledDir * 2.0f / 1.3f - 1.0f);
70 } else if (status.onBit(eStatus::OverZipper)) {
71 dynamics()->setGravity(-1.3f);
72
73 EGG::Vector3f side = mainRot().rotateVector(EGG::Vector3f::ez);
74 EGG::Vector3f velNorm = velocity();
75 velNorm.normalise();
76
77 EGG::Quatf sideRot;
78 sideRot.makeVectorRotation(side, velNorm);
79 sideRot = sideRot.multSwap(mainRot()).multSwap(m_rot);
80
81 f32 t = move()->calcSlerpRate(DEG2RAD360, mainRot(), sideRot);
82 EGG::Quatf slerp = mainRot().slerpTo(sideRot, t);
83 dynamics()->setFullRot(slerp);
84 dynamics()->setMainRot(slerp);
85
86 --m_attemptedTrickTimer;
87
88 calcRot();
89 calcLanding(false);
90 } else if (status.onBit(eStatus::HalfPipeRamp)) {
91 calcLanding(true);
92 } else {
93 status.resetBit(eStatus::HalfpipeMidair);
94 }
95 }
96
97 m_timer = std::max(0, m_timer - 1);
98 m_touchingZipper = isLanding;
99}
100
102void KartHalfPipe::calcTrick() {
103 constexpr s16 TRICK_COOLDOWN = 10;
104
105 auto &trick = inputs()->currentState().trick;
106
107 if (trick != System::Trick::None) {
108 m_nextTimer = TRICK_COOLDOWN;
109 m_trick = trick;
110 }
111
112 const auto &status = KartObjectProxy::status();
113
114 if (status.onBit(eStatus::OverZipper)) {
115 if (status.offBit(eStatus::ZipperTrick) && m_nextTimer > 0 && state()->airtime() > 3 &&
116 state()->airtime() < 10) {
117 activateTrick(m_attemptedTrickTimer, m_trick);
118 }
119 }
120
121 m_nextTimer = std::max(0, m_nextTimer - 1);
122}
123
125void KartHalfPipe::calcRot() {
126 if (m_stunt == StuntType::None) {
127 return;
128 }
129
130 m_stuntManager.calcAngle();
131
132 f32 angle = m_rotSign * (DEG2RAD * m_stuntManager.angle);
133
134 switch (m_stunt) {
135 case StuntType::Side360:
136 case StuntType::Side720:
137 m_stuntRot.setRPY(0.0f, angle, 0.0f);
138 break;
139 case StuntType::Backside: {
140 auto rpy = EGG::Quatf::FromRPY(0.0f, DEG2RAD * (0.25f * -m_rotSign * m_stuntManager.angle),
141 0.0f);
142 EGG::Vector3f rot = rpy.rotateVector(EGG::Vector3f::ez);
143 m_stuntRot.setAxisRotation(angle, rot);
144 } break;
145 case StuntType::Frontside: {
146 EGG::Quatf rpy = EGG::Quatf::FromRPY(0.0f, 0.0f,
147 DEG2RAD * (0.2f * -m_rotSign * m_stuntManager.angle));
148 EGG::Vector3f rot = rpy.rotateVector(EGG::Vector3f::ey);
149 m_stuntRot.setAxisRotation(angle, rot);
150 } break;
151 case StuntType::Frontflip:
152 m_stuntRot.setRPY(m_rotSign * angle, 0.0f, 0.0f);
153 break;
154 case StuntType::Backflip:
155 m_stuntRot.setRPY(-m_rotSign * angle, 0.0f, 0.0f);
156 break;
157 default:
158 break;
159 }
160
161 physics()->composeStuntRot(m_stuntRot);
162}
163
165void KartHalfPipe::calcLanding(bool notAirborne) {
166 constexpr f32 LANDING_RADIUS = 150.0f;
167 constexpr f32 PREVIOUS_RADIUS = 200.0f;
168 constexpr f32 MIDAIR_RADIUS = 50.0f;
169 constexpr f32 WALL_RADIUS = 100.0f;
170
171 constexpr f32 COS_PI_OVER_4 = 0.707f;
172
173 Field::CollisionInfo colInfo;
174 Field::CollisionInfo colInfo2;
175 Field::KCLTypeMask maskOut;
176 EGG::Vector3f pos;
177 EGG::Vector3f upLocal;
178
179 auto &status = KartObjectProxy::status();
180 Field::KCLTypeMask mask = KCL_TYPE_ANY_INVISIBLE_WALL;
181 bool overZipper = status.onBit(eStatus::OverZipper);
182 if (!overZipper) {
183 if (notAirborne && velocity().y < 0.0f) {
184 mask = KCL_NONE;
185 } else {
187 }
188 }
189
190 status.resetBit(eStatus::HalfpipeMidair);
191
192 EGG::Vector3f prevPos = m_prevPos + EGG::Vector3f::ey * PREVIOUS_RADIUS;
193
194 bool hasDriverFloorCollision = move()->calcZipperCollision(LANDING_RADIUS, bsp().initialYPos,
195 pos, upLocal, prevPos, &colInfo, &maskOut, KCL_TYPE_DRIVER_FLOOR);
196
197 prevPos = hasDriverFloorCollision ? EGG::Vector3f::inf : prevPos;
198
199 if (overZipper) {
200 if (!move()->calcZipperCollision(MIDAIR_RADIUS, bsp().initialYPos, pos, upLocal, prevPos,
201 &colInfo2, &maskOut, mask)) {
202 mask |= KCL_TYPE_DRIVER_WALL;
203 }
204 }
205
206 if (move()->calcZipperCollision(WALL_RADIUS, bsp().initialYPos, pos, upLocal, prevPos,
207 &colInfo2, &maskOut, mask)) {
208 if ((maskOut & ~KCL_TYPE_BIT(COL_TYPE_HALFPIPE_INVISIBLE_WALL)) == 0) {
209 status.setBit(eStatus::HalfpipeMidair);
210 }
211
212 EGG::Vector3f up = move()->up();
213 move()->setUp(up + (colInfo2.wallNrm - up) * 0.2f);
214 move()->setSmoothedUp(move()->up());
215
216 f32 yScale = bsp().initialYPos * scale().y;
217 EGG::Vector3f newPos =
218 pos + colInfo2.tangentOff + -WALL_RADIUS * colInfo2.wallNrm + yScale * upLocal;
219 newPos.y += move()->hopPosY();
220
221 dynamics()->setPos(newPos);
222 move()->setDir(move()->dir().perpInPlane(move()->up(), true));
223 move()->setVel1Dir(move()->dir());
224
225 if (overZipper) {
226 status.setBit(eStatus::ZipperStick);
227 }
228
229 m_prevPos = newPos;
230 } else {
231 if (overZipper) {
232 status.resetBit(eStatus::ZipperStick);
233 }
234 }
235
236 if (!hasDriverFloorCollision || status.onBit(eStatus::HalfpipeMidair) ||
237 state()->airtime() <= 5) {
238 return;
239 }
240
241 if (colInfo.floorNrm.dot(EGG::Vector3f::ey) <= COS_PI_OVER_4) {
242 return;
243 }
244
245 if (status.onBit(eStatus::OverZipper)) {
246 status.resetBit(eStatus::ZipperStick);
247 }
248}
249
251void KartHalfPipe::activateTrick(s32 duration, System::Trick trick) {
252 if (duration < 51 || trick == System::Trick::None) {
253 m_stunt = StuntType::None;
254 } else {
255 m_rotSign = m_nextSign;
256 bool timerThreshold = duration > 70;
257
258 switch (trick) {
259 case System::Trick::Up:
260 m_stunt = timerThreshold ? StuntType::Backside : StuntType::Backflip;
261 break;
262 case System::Trick::Down:
263 m_stunt = timerThreshold ? StuntType::Frontside : StuntType::Frontflip;
264 break;
265 case System::Trick::Left:
266 case System::Trick::Right:
267 m_stunt = timerThreshold ? StuntType::Side720 : StuntType::Side360;
268 m_rotSign = trick == System::Trick::Left ? 1.0f : -1.0f;
269 break;
270 default:
271 break;
272 }
273
274 m_stuntManager.setProperties(static_cast<size_t>(m_stunt));
275
276 status().setBit(eStatus::ZipperTrick);
277 }
278
279 m_stuntRot = EGG::Quatf::ident;
280}
281
283void KartHalfPipe::end(bool boost) {
284 auto &status = KartObjectProxy::status();
285 bool overZipper = status.onBit(eStatus::OverZipper);
286
287 if (overZipper && state()->airtime() > 5 && boost) {
288 move()->activateZipperBoost();
289 }
290
291 if (status.onBit(eStatus::ZipperTrick)) {
292 physics()->composeDecayingStuntRot(m_stuntRot);
293 }
294
295 if (overZipper) {
296 move()->setDir(mainRot().rotateVector(EGG::Vector3f::ez));
297 move()->setVel1Dir(move()->dir());
298 }
299
300 status.resetBit(eStatus::OverZipper, eStatus::ZipperTrick, eStatus::ZipperStick,
301 eStatus::HalfpipeMidair);
302
303 m_stunt = StuntType::None;
304}
305
306void KartHalfPipe::StuntManager::calcAngle() {
307 if (finalAngle * properties.finalAngleScalar < angle) {
308 angleDelta = std::max(properties.angleDeltaMin, angleDelta * angleDeltaFactor);
309 angleDeltaFactor = std::max(properties.angleDeltaFactorMin,
310 angleDeltaFactor - properties.angleDeltaFactorDecr);
311 }
312
313 angle = std::min(finalAngle, angle + angleDelta);
314}
315
316void KartHalfPipe::StuntManager::setProperties(size_t idx) {
317 static constexpr std::array<StuntProperties, 6> STUNT_PROPERTIES = {{
318 {6.0f, 2.5f, 0.955f, 0.01f, 0.7f, 360.0f},
319 {7.0f, 3.0f, 0.955f, 0.01f, 0.7f, 360.0f},
320 {7.0f, 3.0f, 0.95f, 0.01f, 0.7f, 360.0f},
321 {12.0f, 2.5f, 0.955f, 0.01f, 0.0f, 360.0f},
322 {4.0f, 4.0f, 0.98f, 0.01f, 0.0f, 360.0f},
323 {9.0f, 3.0f, 0.92f, 0.01f, 0.8f, 720.0f},
324 }};
325
326 ASSERT(idx < STUNT_PROPERTIES.size());
327
328 properties = STUNT_PROPERTIES[idx];
329 finalAngle = properties.finalAngle;
330 angleDelta = properties.angleDelta;
331 angleDeltaFactorDecr = properties.angleDeltaFactorDecr;
332 angle = 0.0f;
333 angleDeltaFactor = 1.0f;
334}
335
336} // namespace Kinoko::Kart
#define KCL_TYPE_DRIVER_WALL
0xC010B000
@ COL_TYPE_HALFPIPE_INVISIBLE_WALL
Invisible wall after a half-pipe jump, like in BC.
#define KCL_TYPE_DRIVER_FLOOR
0x20E80DFF - Any KCL that the player can drive on.
#define KCL_TYPE_BIT(x)
#define KCL_TYPE_ANY_INVISIBLE_WALL
0x90002000
Pertains to kart-related functionality.