@@ -18,7 +18,7 @@ import {
1818 type Race ,
1919} from './kart-engine' ;
2020import { heightAt , parseSurface , SURFACE , surfaceAt } from './kart-surface' ;
21- import { pointAt as trackPoint , wrapAngle } from './kart-track' ;
21+ import { nearest , pointAt as trackPoint , wrapAngle } from './kart-track' ;
2222
2323const bin = readFileSync (
2424 path . resolve ( import . meta. dirname , '../../../public/decorative/kart/luigi-circuit-surface.bin' ) ,
@@ -153,23 +153,117 @@ describe('kart engine', () => {
153153 expect ( race . player . speed ) . toBeLessThan ( 0 ) ;
154154 } ) ;
155155
156- it ( 'drifting charges blue then orange sparks and releases a mini-turbo' , ( ) => {
156+ it . each ( [
157+ [ 200 , 'right' , 1 , 1 ] ,
158+ [ 200 , 'right' , 2 , 1.7 ] ,
159+ [ 400 , 'left' , 1 , 1 ] ,
160+ ] as const ) (
161+ 'drifts from %s to the %s and naturally earns stage %s' ,
162+ ( distance , direction , stage , seconds ) => {
163+ const race = solo ( distance ) ;
164+ race . karts = [ race . player ] ;
165+ race . player . speed = 60 ;
166+ for ( let i = 0 ; i < Math . round ( seconds / STEP ) ; i ++ ) {
167+ stepRace ( race , { ...EMPTY_CONTROLS , throttle : true , drift : true , [ direction ] : true } , STEP ) ;
168+ expect ( race . events . some ( ( e ) => e . type === 'wall' ) ) . toBe ( false ) ;
169+ race . events . length = 0 ;
170+ }
171+ expect ( race . player . driftStage ) . toBe ( stage ) ;
172+ const speed = race . player . speed ;
173+ stepRace ( race , { ...EMPTY_CONTROLS , throttle : true } , STEP ) ;
174+ expect ( race . events ) . toContainEqual ( {
175+ type : 'mini-turbo' ,
176+ kart : race . player . index ,
177+ value : stage ,
178+ } ) ;
179+ expect ( race . player . driftDir ) . toBe ( 0 ) ;
180+ expect ( race . player . driftCharge ) . toBe ( 0 ) ;
181+ expect ( race . player . boost ) . toBeCloseTo ( stage === 2 ? 1.25 : 0.7 ) ;
182+ run ( race , { throttle : true } , 0.1 ) ;
183+ expect ( race . player . speed ) . toBeGreaterThan ( speed + 2 ) ;
184+ } ,
185+ ) ;
186+
187+ it ( 'starts a held drift after accelerating out of a low-speed hop' , ( ) => {
188+ const race = solo ( 190 ) ;
189+ race . karts = [ race . player ] ;
190+ race . player . speed = 11 ;
191+ run ( race , { drift : true } , 0.5 ) ;
192+ expect ( race . player . grounded ) . toBe ( true ) ;
193+ expect ( race . player . driftDir ) . toBe ( 0 ) ;
194+ run ( race , { throttle : true , drift : true , right : true } , 0.7 ) ;
195+ expect ( race . player . driftDir ) . toBe ( - 1 ) ;
196+ expect ( race . player . driftCharge ) . toBeGreaterThan ( 0 ) ;
197+ } ) ;
198+
199+ it ( 'does not reward an uncharged drift or a spin-out with a mini-turbo' , ( ) => {
200+ for ( const spin of [ false , true ] ) {
201+ const race = solo ( 200 ) ;
202+ race . karts = [ race . player ] ;
203+ race . player . speed = 60 ;
204+ run ( race , { throttle : true , drift : true , right : true } , spin ? 1 : 0.3 ) ;
205+ expect ( race . player . driftStage ) . toBe ( spin ? 1 : 0 ) ;
206+ if ( spin ) race . player . spin = 1 ;
207+ stepRace ( race , { ...EMPTY_CONTROLS , throttle : true } , STEP ) ;
208+ expect ( race . player . boost ) . toBe ( 0 ) ;
209+ expect ( race . events . some ( ( e ) => e . type === 'mini-turbo' ) ) . toBe ( false ) ;
210+ }
211+ } ) ;
212+
213+ it ( 'does not activate a boost strip while flying above it' , ( ) => {
157214 const race = solo ( ) ;
158- run ( race , { throttle : true } , 1.5 ) ;
159- run ( race , { throttle : true , drift : true , left : true } , 0.3 ) ;
160- expect ( race . player . driftDir ) . toBe ( 1 ) ;
161- run ( race , { throttle : true , drift : true , left : true } , 0.5 ) ;
162- expect ( race . player . driftStage ) . toBe ( 0 ) ;
163- expect ( race . player . driftCharge ) . toBeGreaterThan ( 0.5 ) ;
164- // Skip ahead in the charge rather than circling into the wall on a straight.
165- race . player . driftCharge = 1.1 ;
166- run ( race , { throttle : true , drift : true , left : true } , 0.1 ) ;
167- expect ( race . player . driftStage ) . toBe ( 1 ) ;
168- race . player . driftCharge = 2.05 ;
169- run ( race , { throttle : true , drift : true , left : true } , 0.15 ) ;
170- expect ( race . player . driftStage ) . toBe ( 2 ) ;
171- run ( race , { throttle : true } , STEP ) ;
172- expect ( race . player . boost ) . toBeGreaterThan ( 0 ) ;
215+ race . karts = [ race . player ] ;
216+ Object . assign ( race . player , {
217+ x : - 125.13 ,
218+ z : 511.65 ,
219+ y : 30 ,
220+ speed : 60 ,
221+ grounded : false ,
222+ } ) ;
223+ stepRace ( race , { ...EMPTY_CONTROLS , throttle : true } , STEP ) ;
224+ expect ( race . player . boost ) . toBe ( 0 ) ;
225+ expect ( race . player . boostPanelContact ) . toBe ( false ) ;
226+ } ) ;
227+
228+ // Centers of all nine ef_dushBoard strips in the shipped course mesh.
229+ it . each ( [
230+ [ - 157.58 , 505.58 ] ,
231+ [ - 185.15 , 488.04 ] ,
232+ [ - 203.74 , 462.55 ] ,
233+ [ - 125.13 , 511.65 ] ,
234+ [ - 92.65 , 505.64 ] ,
235+ [ - 65.04 , 488.15 ] ,
236+ [ - 39.95 , 439.66 ] ,
237+ [ - 46.41 , 462.68 ] ,
238+ [ - 210.19 , 439.41 ] ,
239+ ] ) ( 'drives through boost strip at (%s, %s) without a wall impact' , ( x , z ) => {
240+ const race = solo ( ) ;
241+ race . karts = [ race . player ] ;
242+ expect ( surfaceAt ( surface , x , z ) ) . toBe ( SURFACE . boost ) ;
243+ const n = nearest ( x , z ) ;
244+ Object . assign ( race . player , {
245+ x : x - n . tx * 9 ,
246+ z : z - n . tz * 9 ,
247+ heading : Math . atan2 ( n . tx , n . tz ) ,
248+ speed : 60 ,
249+ } ) ;
250+ race . player . y = heightAt ( surface , race . player . x , race . player . z ) ;
251+ let boosts = 0 ;
252+ for ( let i = 0 ; i < 15 ; i ++ ) {
253+ stepRace ( race , { ...EMPTY_CONTROLS , throttle : true } , STEP ) ;
254+ expect ( race . events . some ( ( e ) => e . type === 'wall' ) ) . toBe ( false ) ;
255+ boosts += race . events . filter ( ( e ) => e . type === 'boost' ) . length ;
256+ race . events . length = 0 ;
257+ }
258+ expect ( boosts ) . toBe ( 1 ) ;
259+ expect ( race . player . speed ) . toBeGreaterThan ( 70 ) ;
260+ expect ( ( race . player . x - x ) * n . tx + ( race . player . z - z ) * n . tz ) . toBeGreaterThan ( 5 ) ;
261+ // Moving to ordinary road must let the boost expire, not refresh it forever.
262+ const p = pointAt ( 10 ) ;
263+ Object . assign ( race . player , { x : p . x , z : p . z , y : p . y , heading : p . heading } ) ;
264+ run ( race , { throttle : true } , 1.2 ) ;
265+ expect ( race . player . boost ) . toBe ( 0 ) ;
266+ expect ( race . player . boostPanelContact ) . toBe ( false ) ;
173267 } ) ;
174268
175269 it ( 'rocket start rewards timing and burns out when early' , ( ) => {
0 commit comments