diff --git a/backend/src/api.ts b/backend/src/api.ts index 75dd366c86bcafdf5e96ec482ae177d92f79fa0b..39605ba8eddd7788ccb4c75eaf5698e9a4b8ff64 100644 --- a/backend/src/api.ts +++ b/backend/src/api.ts @@ -105,17 +105,11 @@ const apiPlugin: FastifyPluginCallback = (fastify, options, next) => { }, }, handler: async (request, reply) => { - const game = await connection<Types.Game>("games") + const games = await connection<Types.Game>("games") .where("id", request.params.id) - .first(); - if (game === undefined) { - reply.code(404).send(); - } else { - const games = await connection<Types.Game>("games") - .update(request.body) - .returning("*"); - reply.code(200).send(games[0]); - } + .update(request.body) + .returning("*"); + reply.code(200).send(games[0]); }, }); @@ -136,7 +130,7 @@ const apiPlugin: FastifyPluginCallback = (fastify, options, next) => { handler: async (request, reply) => { const clues = await connection .table("clues") - .insert(request.body) + .insert({ game_id: request.params.id, ...request.body }) .returning<Types.Clue[]>("*"); reply.code(200).send(clues[0]); @@ -158,17 +152,11 @@ const apiPlugin: FastifyPluginCallback = (fastify, options, next) => { }, }, handler: async (request, reply) => { - const clue = await connection<Types.Clue>("clues") + const clues = await connection<Types.Clue>("clues") .where("id", request.params.id) - .first(); - if (clue === undefined) { - reply.code(404).send(); - } else { - const clues = await connection<Types.Clue>("clues") - .update(request.body) - .returning("*"); - reply.code(200).send(clues[0]); - } + .update(request.body) + .returning("*"); + reply.code(200).send(clues[0]); }, }); diff --git a/backend/src/schemas.ts b/backend/src/schemas.ts index aee0dcb1f14844859864a23795e5ca0683461dc7..64068e7e1db852cc37c1a28cb8e4f21c9ac5e731 100644 --- a/backend/src/schemas.ts +++ b/backend/src/schemas.ts @@ -36,6 +36,6 @@ export const Shot = Type.Object({ }); export const GameUpdate = Type.Omit(Game, ["id"]); -export const ClueCreate = Type.Pick(Clue, ["game_id", "word_id"]); +export const ClueCreate = Type.Pick(Clue, ["word_id"]); export const ClueUpdate = Type.Pick(Clue, ["began_at", "ended_at"]); -export const ShotCreate = Type.Omit(Shot, ["id"]); +export const ShotCreate = Type.Omit(Shot, ["id", "game_id"]); diff --git a/frontend/src/js/clue.ts b/frontend/src/js/clue.ts index cdde3cfe69b87ffec8305285a50213ac5dc2e23d..1e29919ed190b1e5f85584a9a18346818df5c01f 100644 --- a/frontend/src/js/clue.ts +++ b/frontend/src/js/clue.ts @@ -1,23 +1,18 @@ import "phaser"; import FightScene from "./fight_scene"; -interface WordObject { - id: string; - image: string; - ocr_confidence: number; - ocr_transcript: string; -} +import * as Types from "../../../backend/src/types"; class Clue extends Phaser.GameObjects.Sprite { - word: WordObject; + word: Types.Word; scene: FightScene; - constructor(scene: FightScene, word: WordObject) { + constructor(scene: FightScene, word: Types.Word) { // TODO: set positions super(scene, 400, 300, word.id); + scene.add.existing(this); this.setAlpha(0); - scene.add.existing(this); this.scene = scene; this.word = word; @@ -26,6 +21,7 @@ class Clue extends Phaser.GameObjects.Sprite { } loadTexture() { + // this.scene.textures.remove() this.scene.textures.addBase64(this.word.id, this.word.image); this.scene.textures.once( "addtexture", @@ -45,23 +41,35 @@ class Clue extends Phaser.GameObjects.Sprite { 5 + this.width / 2; this.setPosition(x, 50); + this.fadeIn(); + } + + delete() { + this.fadeOut(() => { + this.scene.textures.remove(this.texture); // TODO + this.destroy.bind(this); + }); + } + + fadeIn(onComplete?: Phaser.Types.Tweens.TweenOnCompleteCallback) { this.scene.tweens.add({ targets: this, alpha: 1, ease: "Linear", delay: 0, duration: 100, + onComplete: onComplete, }); } - delete() { + fadeOut(onComplete?: Phaser.Types.Tweens.TweenOnCompleteCallback) { this.scene.tweens.add({ targets: this, alpha: 0, ease: "Linear", delay: 0, - duration: 2000, - onComplete: this.destroy.bind(this), + duration: 100, + onComplete: onComplete, }); } } diff --git a/frontend/src/js/critter.ts b/frontend/src/js/critter.ts index 5807151e92074621eb96398c3617f58ed6d363a6..b0a64b168e0083a6dd2446a808c92f1a108a2178 100644 --- a/frontend/src/js/critter.ts +++ b/frontend/src/js/critter.ts @@ -1,5 +1,4 @@ import "phaser"; -import Clue from "./clue"; import FightScene from "./fight_scene"; const SPECIES = ["bear", "wolf", "deer", "boar"]; @@ -15,19 +14,17 @@ enum CritterState { class Critter extends Phaser.Physics.Arcade.Sprite { state: CritterState; - clue: Clue; scene: FightScene; species: string; body: Phaser.Physics.Arcade.Body; - constructor(scene: FightScene, clue: Clue) { + constructor(scene: FightScene) { const species = SPECIES[Math.floor(Math.random() * SPECIES.length)]; super(scene, -100, scene.cameras.main.height - 100, species); scene.add.existing(this); this.scene = scene; this.species = species; - this.clue = clue; let scale = 2; if (this.species === "deer") { @@ -42,12 +39,7 @@ class Critter extends Phaser.Physics.Arcade.Sprite { this.setScale(scale); - this.move(); - - this.scene.physics.add.overlap(this.scene.player, this, () => { - this.escape(); - this.clue.delete(); - }); + this.walk(); } preUpdate(time, delta) { @@ -68,7 +60,7 @@ class Critter extends Phaser.Physics.Arcade.Sprite { return this.x + this.width * 0.5 < 0; } - move() { + walk() { this.state = CritterState.Moving; this.flipX = true; this.play({ key: this.species + "_walk", repeat: -1 }); @@ -78,14 +70,14 @@ class Critter extends Phaser.Physics.Arcade.Sprite { flee() { this.state = CritterState.Fleeing; this.flipX = false; - this.play(this.species + "_run"); + this.play({ key: this.species + "_run", repeat: -1 }); this.body.setVelocity(FLEE_VELOCITY, 0); } escape() { this.state = CritterState.Escaping; this.flipX = true; - this.play(this.species + "_run"); + this.play({ key: this.species + "_run", repeat: -1 }); this.body.setVelocity(ESCAPE_VELOCITY, 0); } } diff --git a/frontend/src/js/fight_scene.ts b/frontend/src/js/fight_scene.ts index dd057835ce7f961f610a52d0b732da9de85fa982..87a24b8c53771a1fb207f1cafad91b9d38a9f363 100644 --- a/frontend/src/js/fight_scene.ts +++ b/frontend/src/js/fight_scene.ts @@ -165,11 +165,12 @@ export default class FightScene extends Phaser.Scene { initAndBindGuessPreview(this); this.beGame = (await backend.createGame()).data; - this.beGame.began_at = new Date().toISOString(); - await backend.updateGame(this.beGame.id, { - began_at: this.beGame.began_at, - ended_at: null, - }); + this.beGame = ( + await backend.updateGame(this.beGame.id, { + began_at: new Date().toISOString(), + ended_at: null, + }) + ).data; gameStart(this); } @@ -204,20 +205,6 @@ export default class FightScene extends Phaser.Scene { }); } - shootSpear(foe: Foe | null, hit: boolean) { - const scene = this; - if (foe === null || !hit) { - this.showMissMessage(); - new Spear(this, this.player, undefined); - } else { - this.showHitMessage(); - // TODO: ew. - foe.clue.delete(); - // scene.foes.splice(scene.foes.indexOf(foe), 1); // FIXME - new Spear(this, this.player, foe.critter); - } - } - initCluesGroup() { const bounds = new Phaser.Geom.Rectangle( 0, @@ -252,6 +239,23 @@ export default class FightScene extends Phaser.Scene { // console.log(similarity, match.beWord.ocr_transcript); return result; } + + submitTranscription(transcription: string) { + const { score, match } = this.findMatchingFoe(transcription); + // TODO: visual near misses based on score + if (match === null) { + // NOOP + } else if (score < 0.9) { + match.handleFailure(); + this.showMissMessage(); + new Spear(this, this.player, undefined); + } else { + this.foes.splice(this.foes.indexOf(match), 1); + match.handleSuccess(); + this.showHitMessage(); + new Spear(this, this.player, match.critter); + } + } } // TODO: remove any @@ -325,30 +329,25 @@ function initAndBindGuessPreview(scene: FightScene) { event.keyCode === Phaser.Input.Keyboard.KeyCodes.ENTER && textEntry.text.length > 0 ) { - submitTranscription(textEntry.text, scene); + scene.submitTranscription(textEntry.text); textEntry.text = textEntry.text.substr(0, 0); } }, ); } -function submitTranscription(transcription: string, scene: FightScene) { - const { score, match } = scene.findMatchingFoe(transcription); - scene.shootSpear(match, score >= 0.9); -} - function gameStart(scene: any) { spawn(scene); } async function spawn(scene: any) { - await dispatchEnemy(scene); + await spawnFoe(scene); scene.time.now; const delay = (8 * 1000 * (60 * 1000 - scene.time.now)) / 60 / 1000 + 2 * 1000; setTimeout(() => spawn(scene), Math.max(delay, 2000)); } -async function dispatchEnemy(scene: any) { +async function spawnFoe(scene: any) { await new Foe(scene).initialize(); } diff --git a/frontend/src/js/foe.ts b/frontend/src/js/foe.ts index 08abc869cd8ebeb1e51e36db5948e1ed58989a3d..c588a637e1ca0a4497e2150511a79b55f3dadc51 100644 --- a/frontend/src/js/foe.ts +++ b/frontend/src/js/foe.ts @@ -20,9 +20,48 @@ class Foe { async initialize() { this.beWord = (await backend.getWord()).data; + // this.beClue = ( + // await backend.createClue(this.scene.beGame.id, { + // word_id: this.beWord.id, + // }) + // ).data; + // this.beClue = ( + // await backend.updateClue(this.beClue.id, { + // began_at: new Date().toISOString(), + // ended_at: null, + // }) + // ).data; + this.clue = new Clue(this.scene, this.beWord); - this.critter = new Critter(this.scene, this.clue); + this.critter = new Critter(this.scene); this.scene.foes.push(this); + + const overlap = this.scene.physics.add.overlap( + this.scene.player, + this.critter, + () => { + this.scene.physics.world.removeCollider(overlap); + this.critter.escape(); + }, + ); + } + + async handleSuccess() { + // TODO: update clue + // TODO: post shot + // TODO: destroy foe + this.clue.delete(); + } + + async handleFailure() { + // TODO: post shot + // await backend.createShot(this.scene.beGame.id, { + // clue_id: this.beClue.id, + // began_at: "", + // ended_at: new Date().toISOString(), + // typed: "", + // final: "", + // }); } }