diff --git a/src/games/snake/package.json b/src/games/snake/package.json index d07408d0..3ab84661 100644 --- a/src/games/snake/package.json +++ b/src/games/snake/package.json @@ -22,8 +22,13 @@ "webpack-dev-server": "5.0.2" }, "dependencies": { + "@eduplay/sdk-core": "file:/home/lloydho/eduplay/sdk/core", + "@eduplay/sdk-web": "file:/home/lloydho/eduplay/sdk/web", "phaser": "3.80.1" }, + "resolutions": { + "@eduplay/sdk-core": "file:/home/lloydho/eduplay/sdk/core" + }, "repository": { "type": "git", "url": "git+https://github.com/digitsensitive/phaser3-typescript.git" diff --git a/src/games/snake/src/eduplay.ts b/src/games/snake/src/eduplay.ts new file mode 100644 index 00000000..dace1779 --- /dev/null +++ b/src/games/snake/src/eduplay.ts @@ -0,0 +1,62 @@ +// === EduPlay integration === +// Generated by /eduplay. See https://app.eduplay.com/docs/getting-started +// +// This is the mock-mode evaluation wiring. To ship for real: +// 1. Register a game at https://app.eduplay.com to get a real placement ID. +// 2. Remove `mock: true` and replace `placementId` with the real value. +// 3. Wire `studentToken` from your parent-portal-issued session URL param. + +import { EduPlayWebClient, type EduPlayWebAd } from '@eduplay/sdk-web'; + +const client = EduPlayWebClient.initialize({ + apiUrl: 'https://api.eduplay.com', + placementId: 'pl_mock00000000000000000000', + studentToken: new URLSearchParams(location.search).get('token') ?? '', + // Mock mode tells the hosted /break iframe to serve bundled mock questions + // instead of hitting the live API. No real placement / subscription needed + // for the integration to render end-to-end during evaluation. + mock: true, +}); + +client.start(); + +let pendingAd: EduPlayWebAd | null = null; + +function preloadNextAd(): void { + client.loadAd({ + onAdLoaded: (ad) => { + pendingAd = ad; + }, + onAdFailedToLoad: (err) => { + // eslint-disable-next-line no-console + console.warn('[EduPlay] load failed', err); + }, + }); +} + +preloadNextAd(); + +// Call this at natural pause points (level complete, lives lost, game over). +// `onReward` fires only on a correct answer; `onResume` always fires after +// the ad dismisses, so put your "return to game" logic there. +// Returns `true` if an ad was actually shown; `false` if none was preloaded +// (the caller should resume the game immediately in that case so it doesn't +// hang waiting for an ad that will never appear). +export function showAdBreak(opts?: { + onReward?: () => void; + onResume?: () => void; +}): boolean { + if (!pendingAd || !pendingAd.isLoaded()) return false; + pendingAd.show({ + onUserEarnedReward: (reward) => { + if (reward.amount === 1) opts?.onReward?.(); + }, + onAdDismissed: () => { + pendingAd = null; + opts?.onResume?.(); + preloadNextAd(); + }, + }); + return true; +} +// === end EduPlay integration === diff --git a/src/games/snake/src/game.ts b/src/games/snake/src/game.ts index 5456d56c..43893083 100644 --- a/src/games/snake/src/game.ts +++ b/src/games/snake/src/game.ts @@ -1,4 +1,5 @@ import 'phaser'; +import './eduplay'; // initializes EduPlay client + preloads the first question import { GameConfig } from './config'; export class Game extends Phaser.Game { diff --git a/src/games/snake/src/scenes/game-scene.ts b/src/games/snake/src/scenes/game-scene.ts index dc77995e..d7a7a9c0 100644 --- a/src/games/snake/src/scenes/game-scene.ts +++ b/src/games/snake/src/scenes/game-scene.ts @@ -1,6 +1,7 @@ import { Apple } from '../objects/apple'; import { Snake } from '../objects/snake'; import { CONST } from '../const/const'; +import { showAdBreak } from '../eduplay'; export class GameScene extends Phaser.Scene { // field and game setting @@ -21,6 +22,10 @@ export class GameScene extends Phaser.Scene { // texts private scoreText: Phaser.GameObjects.BitmapText; + // EduPlay break gating — prevents re-firing showAdBreak() every frame while + // the player sits dead before the scene transitions back to the menu. + private adBreakTriggered: boolean; + constructor() { super({ key: 'GameScene' @@ -35,6 +40,7 @@ export class GameScene extends Phaser.Scene { this.horizontalFields = this.boardWidth / CONST.FIELD_SIZE; this.verticalFields = this.boardHeight / CONST.FIELD_SIZE; this.tick = 0; + this.adBreakTriggered = false; } create(): void { @@ -96,8 +102,19 @@ export class GameScene extends Phaser.Scene { this.tick = time; } this.player.handleInput(); - } else { - this.scene.start('MainMenuScene'); + } else if (!this.adBreakTriggered) { + // EduPlay break-point: show an educational question at the natural + // game-over pause. onResume returns to the main menu (same behaviour + // as the original bare scene.start). If no ad is preloaded yet (rare + // — preloadNextAd is called at boot) fall back to the original flow + // so the player isn't stranded on the death screen. + this.adBreakTriggered = true; + const shown = showAdBreak({ + onResume: () => this.scene.start('MainMenuScene'), + }); + if (!shown) { + this.scene.start('MainMenuScene'); + } } } diff --git a/src/games/snake/yarn.lock b/src/games/snake/yarn.lock index 5b013c0c..9c9325fe 100644 --- a/src/games/snake/yarn.lock +++ b/src/games/snake/yarn.lock @@ -14,6 +14,14 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@eduplay/sdk-core@^0.1.0", "@eduplay/sdk-core@file:../../../../../eduplay/sdk/core", "@eduplay/sdk-core@file:/home/lloydho/eduplay/sdk/core": + version "0.1.0" + +"@eduplay/sdk-web@file:../../../../../eduplay/sdk/web": + version "0.1.0" + dependencies: + "@eduplay/sdk-core" "^0.1.0" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -2401,7 +2409,16 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2433,7 +2450,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==