Skip to content

Commit 12e64bf

Browse files
author
DavidQ
committed
Rotate manifest-driven Asteroids bullets to match fire direction - PR_26133_126-asteroids-manifest-bullet-rotation
1 parent de5d33c commit 12e64bf

8 files changed

Lines changed: 120 additions & 80 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# PR_26133_126 Asteroids Manifest Bullet Rotation Report
2+
3+
## Summary
4+
5+
- Asteroids bullets now retain an instance `angle` when fired or restored from state.
6+
- Player bullets capture the ship fire angle at spawn; UFO bullets capture the aimed fire angle.
7+
- Bullet rendering still uses `object.asteroids.bullet` from the Object Vector Studio V2 manifest and now applies the bullet instance rotation through the shared Object Vector runtime.
8+
- The manifest bullet object geometry, style, origin, and active state data are not mutated by runtime rendering.
9+
10+
## Implementation Notes
11+
12+
- `Bullet` stores a sanitized `angle` value beside position, velocity, and life.
13+
- `AsteroidsWorld.fire()` passes `this.ship.angle` into new bullets.
14+
- `Ufo.fireAt()` passes its computed `aimAngle` into new UFO bullets.
15+
- `AsteroidsWorld` save/load includes bullet `angle`, with legacy state fallback inferred from bullet velocity when angle is missing.
16+
- `AsteroidsGameScene` renders both player bullets and UFO bullets with `objectId: object.asteroids.bullet`, `requireManifestBinding: true`, and `rotation: bullet.angle`.
17+
18+
## Validation
19+
20+
- PASS `node -e "import('./tests/games/AsteroidsPresentation.test.mjs').then((m)=>m.run()).then(()=>console.log('PASS AsteroidsPresentation'))"`
21+
- PASS `node -e "import('./tests/games/AsteroidsVectorTransforms.test.mjs').then((m)=>m.run()).then(()=>console.log('PASS AsteroidsVectorTransforms'))"`
22+
- PASS `node -e "import('./tests/games/AsteroidsValidation.test.mjs').then((m)=>m.run()).then(()=>console.log('PASS AsteroidsValidation'))"`
23+
- PASS `node -e "import('./tests/games/AsteroidsPlatformDemo.test.mjs').then((m)=>m.run()).then(()=>console.log('PASS AsteroidsPlatformDemo'))"`
24+
- PASS `node -e "import('./tests/games/AsteroidsAssetReferenceAdoption.test.mjs').then((m)=>m.run()).then(()=>console.log('PASS AsteroidsAssetReferenceAdoption'))"`
25+
- PASS `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "loads Object Vector Studio V2 runtime assets into Asteroids gameplay rendering"`
26+
- PASS `git diff --check` with line-ending warnings only.
27+
28+
## Manual Notes
29+
30+
- Playwright impacted: Yes.
31+
- Playwright validates the Asteroids Object Vector runtime asset path and gameplay bullet manifest rendering path.
32+
- Expected pass behavior: bullets fired at different ship angles render the manifest bullet object with matching instance rotations.
33+
- Expected fail behavior: missing `object.asteroids.bullet` or mismatched manifest binding fails through the Object Vector runtime instead of a hardcoded bullet draw fallback.
34+
- Full regression and full samples smoke tests were skipped per PR instructions.

games/Asteroids/entities/Bullet.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ function normalizePoints(points) {
1717
}
1818

1919
export default class Bullet {
20-
constructor(x, y, vx, vy, life = 1.1, { collisionPoints = [] } = {}) {
20+
constructor(x, y, vx, vy, life = 1.1, { angle = 0, collisionPoints = [] } = {}) {
2121
this.x = x;
2222
this.y = y;
2323
this.vx = vx;
2424
this.vy = vy;
2525
this.life = life;
26+
this.angle = Number.isFinite(angle) ? angle : 0;
2627
this.collisionPoints = normalizePoints(collisionPoints);
2728
if (this.collisionPoints.length < 3) {
2829
throw new Error('Asteroids Bullet requires manifest-loaded bullet object geometry.');

games/Asteroids/entities/Ufo.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export default class Ufo {
9999
Math.sin(aimAngle) * shotSpeed,
100100
fullScreenLife,
101101
{
102+
angle: aimAngle,
102103
collisionPoints: this.bulletCollisionPoints,
103104
}
104105
);

games/Asteroids/game.manifest.json

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@
219219
"shapes": [
220220
{
221221
"tool": "polygon",
222-
"order": 0,
222+
"order": 1,
223223
"visible": true,
224224
"locked": false,
225225
"geometry": {
@@ -256,36 +256,6 @@
256256
"x": 0,
257257
"y": 0
258258
}
259-
},
260-
{
261-
"tool": "line",
262-
"order": 1,
263-
"visible": true,
264-
"locked": false,
265-
"geometry": {
266-
"point1": {
267-
"x": -16,
268-
"y": 2
269-
},
270-
"point2": {
271-
"x": -3,
272-
"y": 2
273-
}
274-
},
275-
"style": {
276-
"fill": "#00000000",
277-
"stroke": "#F87171",
278-
"strokeWidth": 2,
279-
"fillOpacity": 1,
280-
"strokeOpacity": 1
281-
},
282-
"transform": {
283-
"rotation": 0,
284-
"scaleX": 1,
285-
"scaleY": 1,
286-
"x": 0,
287-
"y": 0
288-
}
289259
}
290260
],
291261
"states": [
@@ -334,11 +304,11 @@
334304
"locked": false,
335305
"geometry": {
336306
"point1": {
337-
"x": -6,
338-
"y": -3
307+
"x": -6.6,
308+
"y": -3.3
339309
},
340310
"point2": {
341-
"x": -10,
311+
"x": -11,
342312
"y": 0
343313
}
344314
},
@@ -364,12 +334,12 @@
364334
"locked": false,
365335
"geometry": {
366336
"point1": {
367-
"x": -10,
337+
"x": -11,
368338
"y": 0
369339
},
370340
"point2": {
371-
"x": -6,
372-
"y": 3
341+
"x": -6.6,
342+
"y": 3.3
373343
}
374344
},
375345
"style": {
@@ -395,27 +365,27 @@
395365
"geometry": {
396366
"points": [
397367
{
398-
"x": 14,
368+
"x": 15.4,
399369
"y": 0
400370
},
401371
{
402-
"x": -10,
403-
"y": -8
372+
"x": -11,
373+
"y": -8.8
404374
},
405375
{
406-
"x": -6,
407-
"y": -3
376+
"x": -6.6,
377+
"y": -3.3
408378
},
409379
{
410-
"x": -6,
411-
"y": 3
380+
"x": -6.6,
381+
"y": 3.3
412382
},
413383
{
414-
"x": -10,
415-
"y": 8
384+
"x": -11,
385+
"y": 8.8
416386
},
417387
{
418-
"x": 14,
388+
"x": 15.4,
419389
"y": 0
420390
}
421391
]
@@ -453,11 +423,11 @@
453423
"locked": false,
454424
"geometry": {
455425
"point1": {
456-
"x": -6,
457-
"y": -3
426+
"x": -6.6,
427+
"y": -3.3
458428
},
459429
"point2": {
460-
"x": -8,
430+
"x": -8.8,
461431
"y": 0
462432
}
463433
},
@@ -483,11 +453,11 @@
483453
"locked": false,
484454
"geometry": {
485455
"point1": {
486-
"x": -6,
487-
"y": 3
456+
"x": -6.6,
457+
"y": 3.3
488458
},
489459
"point2": {
490-
"x": -8,
460+
"x": -8.8,
491461
"y": 0
492462
}
493463
},
@@ -909,7 +879,7 @@
909879
},
910880
"style": {
911881
"fill": "#00000000",
912-
"stroke": "#F87171",
882+
"stroke": "#DBEAFE",
913883
"strokeWidth": 2,
914884
"fillOpacity": 1,
915885
"strokeOpacity": 1
@@ -1239,7 +1209,7 @@
12391209
},
12401210
"style": {
12411211
"fill": "#00000000",
1242-
"stroke": "#FFBE64",
1212+
"stroke": "#DBEAFE",
12431213
"strokeWidth": 2,
12441214
"fillOpacity": 1,
12451215
"strokeOpacity": 1
@@ -1339,7 +1309,7 @@
13391309
},
13401310
"style": {
13411311
"fill": "#00000000",
1342-
"stroke": "#78B7FF",
1312+
"stroke": "#DBEAFE",
13431313
"strokeWidth": 2,
13441314
"fillOpacity": 1,
13451315
"strokeOpacity": 1

games/Asteroids/game/AsteroidsGameScene.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,9 @@ export default class AsteroidsGameScene extends Scene {
763763
this.world.bullets.forEach((bullet) => {
764764
this.drawObjectVectorAsset(renderer, "bullet", {
765765
...this.objectVectorTagOptions("bullet"),
766+
objectId: this.objectVectorRuntimeObjectValidation.objectsByKey.bullet?.id || "",
767+
requireManifestBinding: true,
768+
rotation: bullet.angle,
766769
stateId: "active",
767770
x: bullet.x,
768771
y: bullet.y,
@@ -772,6 +775,9 @@ export default class AsteroidsGameScene extends Scene {
772775
this.world.ufoBullets.forEach((bullet) => {
773776
this.drawObjectVectorAsset(renderer, "bullet", {
774777
...this.objectVectorTagOptions("bullet"),
778+
objectId: this.objectVectorRuntimeObjectValidation.objectsByKey.bullet?.id || "",
779+
requireManifestBinding: true,
780+
rotation: bullet.angle,
775781
stateId: "active",
776782
x: bullet.x,
777783
y: bullet.y,

games/Asteroids/game/AsteroidsWorld.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,16 @@ export default class AsteroidsWorld {
305305

306306
createBulletFromState(state) {
307307
const source = state && typeof state === 'object' ? state : {};
308+
const vx = sanitizeFiniteNumber(source.vx, 0);
309+
const vy = sanitizeFiniteNumber(source.vy, 0);
308310
return new Bullet(
309311
sanitizeFiniteNumber(source.x, this.bounds.width * 0.5),
310312
sanitizeFiniteNumber(source.y, this.bounds.height * 0.5),
311-
sanitizeFiniteNumber(source.vx, 0),
312-
sanitizeFiniteNumber(source.vy, 0),
313+
vx,
314+
vy,
313315
Math.max(0, sanitizeFiniteNumber(source.life, 1.1)),
314316
{
317+
angle: sanitizeFiniteNumber(source.angle, (vx || vy) ? Math.atan2(vy, vx) : 0),
315318
collisionPoints: this.bulletCollisionPoints,
316319
}
317320
);
@@ -371,13 +374,15 @@ export default class AsteroidsWorld {
371374
radius: asteroid.radius,
372375
})),
373376
bullets: this.bullets.map((bullet) => ({
377+
angle: bullet.angle,
374378
x: bullet.x,
375379
y: bullet.y,
376380
vx: bullet.vx,
377381
vy: bullet.vy,
378382
life: bullet.life,
379383
})),
380384
ufoBullets: this.ufoBullets.map((bullet) => ({
385+
angle: bullet.angle,
381386
x: bullet.x,
382387
y: bullet.y,
383388
vx: bullet.vx,
@@ -570,6 +575,7 @@ export default class AsteroidsWorld {
570575
this.ship.vy + Math.sin(this.ship.angle) * shotSpeed,
571576
fullScreenLife,
572577
{
578+
angle: this.ship.angle,
573579
collisionPoints: this.bulletCollisionPoints,
574580
}
575581
));

tests/games/AsteroidsPresentation.test.mjs

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ function objectHasTags(object, tags = []) {
5555
return tags.every((tag) => objectTags.has(String(tag).toLowerCase()));
5656
}
5757

58+
function roundedAngle(value) {
59+
return Number(value.toFixed(6));
60+
}
61+
5862
function createObjectVectorRuntime(calls) {
5963
return {
6064
getDiagnostics() {
@@ -70,6 +74,7 @@ function createObjectVectorRuntime(calls) {
7074
objectId: object?.id || options.objectId,
7175
requireManifestBinding: options.requireManifestBinding === true,
7276
renderKey: options.runtimeRole,
77+
rotation: options.rotation,
7378
stroke: shape?.style?.stroke || '',
7479
stateId: options.stateId,
7580
tags: options.tags,
@@ -253,27 +258,31 @@ function testAsteroidsAttractAsteroidsUseManifestObjectsAndStyles() {
253258

254259
function testAsteroidsGameplayBulletsUseManifestObjectGeometry() {
255260
const renderCalls = [];
261+
const payload = loadAsteroidsObjectVectorPayload();
262+
const bulletObject = payload.objects.find((object) => object.id === 'object.asteroids.bullet');
263+
const bulletShapesBeforeRender = JSON.stringify(bulletObject.shapes);
256264
const scene = new AsteroidsGameScene(createAsteroidsTestSceneOptions({
257265
objectVectorAssets: createObjectVectorAssetSet(),
258266
objectVectorRuntime: createObjectVectorRuntime(renderCalls),
259267
}));
260268
scene.session.mode = 'playing';
261269
scene.attractController.active = false;
262270
scene.world.asteroids = [];
263-
scene.world.bullets = [scene.world.createBulletFromState({
264-
x: 100,
265-
y: 110,
266-
vx: 0,
267-
vy: 0,
268-
life: 1,
269-
})];
270-
scene.world.ufoBullets = [scene.world.createBulletFromState({
271-
x: 120,
272-
y: 130,
273-
vx: 0,
274-
vy: 0,
275-
life: 1,
276-
})];
271+
scene.world.bullets = [];
272+
scene.world.ufoBullets = [];
273+
scene.world.ship.x = 220;
274+
scene.world.ship.y = 240;
275+
scene.world.ship.vx = 0;
276+
scene.world.ship.vy = 0;
277+
const fireAngles = [-Math.PI / 2, 0, Math.PI / 3];
278+
fireAngles.forEach((angle) => {
279+
scene.world.ship.angle = angle;
280+
assert.equal(scene.world.fire(), true);
281+
});
282+
assert.deepEqual(
283+
scene.world.bullets.map((bullet) => roundedAngle(bullet.angle)),
284+
fireAngles.map(roundedAngle),
285+
);
277286

278287
const polygonCalls = [];
279288
const renderer = {
@@ -288,9 +297,17 @@ function testAsteroidsGameplayBulletsUseManifestObjectGeometry() {
288297
};
289298

290299
scene.render(renderer);
291-
assert.equal(scene.objectVectorRenderCounts.bullet, 2);
292-
assert.equal(renderCalls.filter((call) => call.objectId === 'object.asteroids.bullet').length, 2);
293-
assert.equal(polygonCalls.length >= 2, true);
300+
const bulletCalls = renderCalls.filter((call) => call.objectId === 'object.asteroids.bullet');
301+
assert.equal(scene.objectVectorRenderCounts.bullet, fireAngles.length);
302+
assert.equal(bulletCalls.length, fireAngles.length);
303+
assert.deepEqual(
304+
bulletCalls.map((call) => roundedAngle(call.rotation)),
305+
fireAngles.map(roundedAngle),
306+
);
307+
assert.equal(bulletCalls.every((call) => call.requireManifestBinding), true);
308+
assert.equal(bulletCalls.every((call) => call.stroke === bulletObject.shapes[0].style.stroke), true);
309+
assert.equal(JSON.stringify(bulletObject.shapes), bulletShapesBeforeRender);
310+
assert.equal(polygonCalls.length >= fireAngles.length, true);
294311
}
295312

296313
function testAsteroidsGameplayRenderDoesNotCoverBackgroundLayer() {

0 commit comments

Comments
 (0)