Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,188 @@
}
],
"externalEvents": [],
"tests": [
{
"name": "Clicking sends the player walking there",
"type": "gameplay",
"description": "Clicking a spot gives the player a path and it walks toward it; it stands still while nothing is clicked.",
"source": [
"// The core of the game: clicking somewhere sends the player walking there.",
"await harness.goToScene('Game Scene');",
"harness.watch('Player');",
"",
"const getPlayer = () => harness.getObjects('Player')[0];",
"const pathfinding = () => getPlayer().behaviors.Pathfinding.state;",
"",
"/** Click on a point of the ground. */",
"const clickAt = async (x, y) => {",
" harness.setMousePosition(x, y, '');",
" await harness.stepFrames(2);",
" harness.setMouseButtonPressed(true);",
" await harness.stepFrames(3);",
" harness.setMouseButtonPressed(false);",
" await harness.stepFrames(3);",
"};",
"",
"await harness.stepFrames(4);",
"const start = getPlayer();",
"",
"// Nothing happens on its own: the player only walks when told to.",
"await harness.stepFrames(12);",
"const idle = getPlayer();",
"harness.assert(",
" Math.hypot(idle.centerX - start.centerX, idle.centerY - start.centerY) < 5,",
" 'The player stands still while nothing is clicked.'",
");",
"",
"// Click on a spot with no NPC near it: clicking an NPC talks to it instead of",
"// simply walking (the game raycasts the cursor onto the NPCs).",
"const isClearOfNPCs = (x, y) =>",
" harness",
" .getObjects('NPC')",
" .every((npc) => Math.hypot(npc.centerX - x, npc.centerY - y) > 200);",
"let destinationX = null;",
"let destinationY = null;",
"for (const [dx, dy] of [[200, 150], [-200, 150], [200, -150], [0, 250]]) {",
" if (destinationX === null && isClearOfNPCs(start.centerX + dx, start.centerY + dy)) {",
" destinationX = start.centerX + dx;",
" destinationY = start.centerY + dy;",
" }",
"}",
"harness.assert(",
" destinationX !== null,",
" 'There is a spot to walk to with no NPC standing on it.'",
");",
"await clickAt(destinationX, destinationY);",
"",
"console.log(",
" 'destinationAsked=' + Math.round(destinationX) + ',' + Math.round(destinationY) +",
" ' pathFound=' + pathfinding().PathFound +",
" ' nodes=' + pathfinding().NodeCount",
");",
"harness.assert(",
" pathfinding().PathFound === true,",
" 'Clicking gives the player a path to walk (it found ' +",
" pathfinding().NodeCount + ' node(s)).'",
");",
"",
"const walked = await harness.stepUntil(",
" () => {",
" const player = getPlayer();",
" return (",
" Math.hypot(player.centerX - start.centerX, player.centerY - start.centerY) >",
" 100",
" );",
" },",
" { maxFrames: 90 }",
");",
"const after = getPlayer();",
"const travelled = Math.hypot(",
" after.centerX - start.centerX,",
" after.centerY - start.centerY",
");",
"const closerToDestination =",
" Math.hypot(destinationX - after.centerX, destinationY - after.centerY) <",
" Math.hypot(destinationX - start.centerX, destinationY - start.centerY);",
"console.log(",
" 'walked=' + Math.round(travelled) +",
" ' to=' + Math.round(after.centerX) + ',' + Math.round(after.centerY)",
");",
"harness.assert(",
" walked,",
" 'The player walks after the click (it moved ' + Math.round(travelled) + ' units).'",
");",
"harness.assert(",
" closerToDestination,",
" 'The player walks toward where it was sent, not away from it.'",
");"
]
},
{
"name": "Talking to an NPC and saying yes",
"type": "gameplay",
"description": "Clicking an NPC walks the player to it and opens the dialog, and saying yes makes that NPC leave.",
"source": [
"// Talking to an NPC: clicking one sends the player to it, being next to it",
"// opens the dialog, and saying yes makes it leave.",
"await harness.goToScene('Game Scene');",
"harness.watch('NPC');",
"",
"const getPlayer = () => harness.getObjects('Player')[0];",
"const dialogLayer = harness.getRuntimeLayer('Dialog layer');",
"const isDialogOpen = () => dialogLayer.isVisible();",
"const getDialog = () => harness.getObjects('TwoChoicesDialogBox')[0];",
"",
"/** Click on a point of a layer (the ground by default). */",
"const clickAt = async (x, y, layerName = '') => {",
" harness.setMousePosition(x, y, layerName);",
" await harness.stepFrames(2);",
" harness.setMouseButtonPressed(true);",
" await harness.stepFrames(3);",
" harness.setMouseButtonPressed(false);",
" await harness.stepFrames(3);",
"};",
"",
"await harness.stepFrames(4);",
"harness.assert(!isDialogOpen(), 'No dialog is open before talking to anyone.');",
"const npcsBefore = harness.getObjects('NPC').length;",
"harness.assert(npcsBefore > 0, 'There are NPCs to talk to.');",
"",
"// Click on the nearest NPC: the game raycasts the cursor onto it, picks it",
"// and walks the player over.",
"const npc = harness.getNearby('NPC', 'Player', 9000)[0];",
"const start = getPlayer();",
"console.log(",
" 'npc=' + npc.id + ' at ' + Math.round(npc.centerX) + ',' + Math.round(npc.centerY) +",
" ' distance=' + Math.round(npc.distance) + ' npcsBefore=' + npcsBefore",
");",
"harness.assert(",
" npc.distance > 150,",
" 'The NPC is out of reach to start with (it is ' +",
" Math.round(npc.distance) + ' units away), so the dialog has to be walked to.'",
");",
"await clickAt(npc.centerX, npc.centerY);",
"harness.assert(!isDialogOpen(), 'Clicking the NPC does not open the dialog on its own.');",
"",
"const opened = await harness.stepUntil(isDialogOpen, { maxFrames: 120 });",
"const walkedDistance = Math.hypot(",
" getPlayer().centerX - start.centerX,",
" getPlayer().centerY - start.centerY",
");",
"console.log(",
" 'dialogOpened=' + opened + ' walked=' + Math.round(walkedDistance) +",
" ' playerAt=' + Math.round(getPlayer().centerX) + ',' + Math.round(getPlayer().centerY)",
");",
"harness.assert(",
" walkedDistance > 100,",
" 'The player walked to the NPC it was told to talk to (it moved ' +",
" Math.round(walkedDistance) + ' units).'",
");",
"harness.assert(opened, 'Reaching the NPC opens the dialog.');",
"harness.assert(",
" harness.getObjects('NPC').length === npcsBefore,",
" 'Opening the dialog does not make the NPC leave on its own.'",
");",
"",
"// Saying yes is what makes the NPC leave.",
"const yes = getDialog().children.YesButton[0];",
"await clickAt(yes.centerX, yes.centerY, yes.layer);",
"await harness.stepFrames(5);",
"",
"const npcsAfter = harness.getObjects('NPC').length;",
"console.log('npcsAfter=' + npcsAfter + ' dialogStillOpen=' + isDialogOpen());",
"harness.assert(",
" npcsAfter === npcsBefore - 1,",
" 'Saying yes makes the NPC leave (' + npcsAfter + ' NPCs left of ' + npcsBefore + ').'",
");",
"harness.assert(",
" !harness.getObjects('NPC').some((one) => one.id === npc.id),",
" 'The NPC that was talked to is the one that left.'",
");",
"harness.assert(!isDialogOpen(), 'The dialog closes once it is answered.');"
]
}
],
"eventsFunctionsExtensions": [
{
"author": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6312,6 +6312,209 @@
}
],
"externalEvents": [],
"tests": [
{
"name": "Walking and strafing with WASD",
"type": "gameplay",
"description": "\"w\" walks the player toward what it faces and \"d\" strafes it sideways, without turning it.",
"source": [
"// First person movement: \"w\" walks toward what the player faces, \"d\"",
"// strafes to its right (Shooter3DKeyboardMapper, camera relative).",
"await harness.goToScene('Game Scene');",
"harness.watch('Player');",
"",
"const getPlayer = () => harness.getObjects('Player')[0];",
"const characterState = () => getPlayer().behaviors.PhysicsCharacter3D.state;",
"",
"await harness.stepFrames(4);",
"harness.assert(characterState().IsOnFloor === true, 'The player stands on the ground.');",
"",
"const start = getPlayer();",
"const facingRadians = (start.angle * Math.PI) / 180;",
"",
"// Nothing pressed: the player stays where it is.",
"await harness.stepFrames(5);",
"const idle = getPlayer();",
"const drift = Math.hypot(idle.centerX - start.centerX, idle.centerY - start.centerY);",
"harness.assert(",
" drift < 3,",
" 'The player stands still while no key is pressed (drifted ' + drift.toFixed(2) + ').'",
");",
"",
"// Walk forward.",
"harness.setKeyPressed('w', true);",
"await harness.stepFrames(15);",
"harness.setKeyPressed('w', false);",
"await harness.stepFrames(2);",
"",
"const afterWalk = getPlayer();",
"const walkX = afterWalk.centerX - idle.centerX;",
"const walkY = afterWalk.centerY - idle.centerY;",
"const walked = Math.hypot(walkX, walkY);",
"const forward = walkX * Math.cos(facingRadians) + walkY * Math.sin(facingRadians);",
"console.log(",
" 'walked=' + Math.round(walked) + ' forward=' + Math.round(forward) +",
" ' facing=' + Math.round(start.angle)",
");",
"harness.assert(",
" walked > 15,",
" 'Holding \"w\" walks the player (moved ' + Math.round(walked) + ' units).'",
");",
"harness.assert(",
" forward > 0.9 * walked,",
" 'The player walks toward what it is facing, not sideways or backwards.'",
");",
"",
"// Strafe right.",
"const beforeStrafe = getPlayer();",
"harness.setKeyPressed('d', true);",
"await harness.stepFrames(14);",
"harness.setKeyPressed('d', false);",
"await harness.stepFrames(2);",
"const afterStrafe = getPlayer();",
"const strafeX = afterStrafe.centerX - beforeStrafe.centerX;",
"const strafeY = afterStrafe.centerY - beforeStrafe.centerY;",
"const strafed = Math.hypot(strafeX, strafeY);",
"// The player's right hand side, in scene coordinates.",
"const sideways = -strafeX * Math.sin(facingRadians) + strafeY * Math.cos(facingRadians);",
"console.log('strafed=' + Math.round(strafed) + ' sideways=' + Math.round(sideways));",
"harness.assert(",
" strafed > 15,",
" 'Holding \"d\" moves the player (moved ' + Math.round(strafed) + ' units).'",
");",
"harness.assert(",
" Math.abs(sideways) > 0.9 * strafed,",
" 'Holding \"d\" strafes sideways instead of walking forward (' +",
" Math.round(sideways) + ' of ' + Math.round(strafed) + ' units sideways).'",
");",
"harness.assert(",
" Math.abs(afterStrafe.angle - start.angle) < 1,",
" 'Strafing does not turn the player around.'",
");"
]
},
{
"name": "Harvesting a seed patch fills the inventory",
"type": "gameplay",
"description": "A seed patch on the ground is harvested by looking down at it and acting; the seed goes into the inventory.",
"source": [
"// The core loop: look at something harvestable, act on it, and what comes",
"// out ends up in the inventory. Farming happens on the ground, so this is",
"// also the test that the player can act on what it is looking down at.",
"await harness.goToScene('Game Scene');",
"harness.watch('Seed_Carrot');",
"",
"const getPlayer = () => harness.getObjects('Player')[0];",
"",
"/** The inventory the game keeps in a scene variable, slot by slot. */",
"const inventorySlots = () => {",
" const inventory = harness.getSceneVariable('Inventory');",
" const slots = (inventory && inventory.children) || [];",
" return slots.map((slot) => {",
" const entries = slot.children || [];",
" const read = (name) => {",
" const entry = entries.find((one) => one.name === name);",
" return entry ? entry.value : null;",
" };",
" return { name: read('Name'), quantity: Number(read('Quantity')) };",
" });",
"};",
"const filledSlots = () => inventorySlots().filter((slot) => slot.name !== 'Empty');",
"",
"await harness.stepFrames(6);",
"const player = getPlayer();",
"console.log(",
" 'player=' + Math.round(player.centerX) + ',' + Math.round(player.centerY) +",
" ' angle=' + Math.round(player.angle)",
");",
"harness.assert(filledSlots().length === 0, 'The inventory starts empty.');",
"",
"// Arrange: bring a seed patch in front of the player. The game harvests what",
"// the camera is looking at, and the patches are static, so the patch is moved",
"// to the player rather than the player walked over to it. Every other patch",
"// is taken out of the level, so what ends up in the inventory can only have",
"// come from this one. Harvesting it is still up to the game.",
"const patch = harness.getObjects('Harvest_Seed_Carrot')[0];",
"harness.assert(!!patch, 'There is a seed patch to harvest.');",
"for (const other of [",
" ...harness.getObjects('Harvest_Seed_Carrot'),",
" ...harness.getObjects('Harvest_Seed_Beet'),",
"]) {",
" if (other.id !== patch.id) harness.removeObject(other.id);",
"}",
"const facing = (player.angle * Math.PI) / 180;",
"const AHEAD = 120;",
"harness.setObjectPosition(",
" patch.id,",
" patch.x + (player.centerX + Math.cos(facing) * AHEAD - patch.centerX),",
" patch.y + (player.centerY + Math.sin(facing) * AHEAD - patch.centerY),",
" patch.z",
");",
"await harness.stepFrames(4);",
"const moved = harness.getObjects('Harvest_Seed_Carrot').find((one) => one.id === patch.id);",
"console.log(",
" 'patchInFront=' + Math.round(moved.centerX) + ',' + Math.round(moved.centerY) +",
" ',' + Math.round(moved.centerZ) + ' distance=' + Math.round(AHEAD)",
");",
"",
"// The action is ignored while the cursor sits over the controls toggle in the",
"// top left corner, which is where it starts: put it in the middle of the",
"// screen. The first click is also what makes the game take the pointer lock,",
"// without which the mouse movements below are ignored.",
"harness.setMousePositionScreen(",
" harness.getGameResolutionWidth() / 2,",
" harness.getGameResolutionHeight() / 2",
");",
"harness.setMouseButtonPressed(true);",
"await harness.stepFrames(2);",
"harness.setMouseButtonPressed(false);",
"await harness.stepFrames(2);",
"harness.assert(",
" filledSlots().length === 0,",
" 'Acting while looking straight ahead, over the patch, harvests nothing.'",
");",
"",
"// Look down at it: the patch lies flat on the ground, so a level view goes",
"// straight over it. The helper aims on the camera of the player's layer,",
"// which is what the game casts its ray from.",
"const aim = await harness.lookTowardWithMouseDelta(",
" 'Player',",
" { name: 'Harvest_Seed_Carrot', id: patch.id },",
" { toleranceDegrees: 1 }",
");",
"console.log('aim=' + JSON.stringify(aim));",
"harness.assert(",
" !!aim && aim.aimed,",
" 'The view can be aimed down onto the patch.'",
");",
"",
"// Act on it.",
"harness.setMouseButtonPressed(true);",
"await harness.stepFrames(3);",
"harness.setMouseButtonPressed(false);",
"const harvested = await harness.stepUntil(",
" () => filledSlots().some((slot) => slot.name === 'Seed_Carrot'),",
" { maxFrames: 30 }",
");",
"harness.releaseAllInputs();",
"",
"console.log(",
" 'lookedDownTo=' + getPlayer().rotationY.toFixed(1) +",
" ' degrees, inventory=' + JSON.stringify(filledSlots())",
");",
"harness.assert(",
" harvested,",
" 'Looking down at the seed patch and acting puts a carrot seed in the inventory (it holds ' +",
" JSON.stringify(filledSlots()) + ').'",
");",
"harness.assert(",
" getPlayer().rotationY > 2,",
" 'The view really had to be lowered onto the patch (it ended at ' +",
" getPlayer().rotationY.toFixed(1) + ' degrees).'",
");"
]
}
],
"eventsFunctionsExtensions": [
{
"author": "",
Expand Down
Loading
Loading