kgames

KGames — free keyboard games for kids
git clone https://www.keyboard.games/code/kgames.git
Log | Files | Refs | README | LICENSE

commit 1ad5439617774e2f2137de8fcd5c432417c73195
parent b49f541d1a70eb5554ac1a4905ec050c68782106
Author: Kyle Barlow <kb@kylebarlow.com>
Date:   Sat, 25 Apr 2026 19:43:35 -0700

Animate animal and always show accented name on correct answer

On correct answer, always display the animal name with the first letter
in the round accent color before advancing, reinforcing word recognition.
The same accented display is used for the first-wrong hint. Animals now
gently sway and pulse while waiting for input to make the game feel alive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Diffstat:
Mgames/animal-letter/game.js | 150++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 118 insertions(+), 32 deletions(-)

diff --git a/games/animal-letter/game.js b/games/animal-letter/game.js @@ -29,22 +29,24 @@ const BASS = [ class AnimalLetterScene extends Phaser.Scene { constructor() { super({ key: 'AnimalLetterScene' }); - this.audio = null; - this.currentAnimal = null; - this.roundColor = null; - this.wrongCount = 0; - this.accepting = false; - this.lettersFound = 0; - this.winShowing = false; - this.progressGfx = null; - this.winElements = []; - this.guessTexts = []; - this.animalImg = null; - this.instrText = null; - this.nameText = null; - this.hintText = null; - this._imgScale = 1; - this._deck = []; // shuffled queue; drawn without replacement per game + this.audio = null; + this.currentAnimal = null; + this.roundColor = null; + this.wrongCount = 0; + this.accepting = false; + this.lettersFound = 0; + this.winShowing = false; + this.progressGfx = null; + this.winElements = []; + this.guessTexts = []; + this.animalImg = null; + this.instrText = null; + this.nameFirstLetterText = null; + this.nameRestText = null; + this.hintText = null; + this._imgScale = 1; + this._idleTweens = []; + this._deck = []; // shuffled queue; drawn without replacement per game } preload() { @@ -70,11 +72,19 @@ class AnimalLetterScene extends Phaser.Scene { this.animalImg = this.add.image(W / 2, H * 0.43, '__DEFAULT').setVisible(false); - this.nameText = this.add.text(W / 2, 0, '', { + // First letter of name — shown in accent color + this.nameFirstLetterText = this.add.text(0, 0, '', { fontFamily: 'Fredoka, sans-serif', fontStyle: 'bold', color: '#ffffff', - }).setOrigin(0.5, 0.5).setVisible(false); + }).setOrigin(0, 0.5).setVisible(false); + + // Remainder of name — shown in white + this.nameRestText = this.add.text(0, 0, '', { + fontFamily: 'Fredoka, sans-serif', + fontStyle: 'bold', + color: '#ffffff', + }).setOrigin(0, 0.5).setVisible(false); // Large colored first-letter hint shown at hint level 3 this.hintText = this.add.text(W / 2, 0, '', { @@ -94,20 +104,23 @@ class AnimalLetterScene extends Phaser.Scene { applyLayout(W, H) { const instrSize = Math.max(16, Math.floor(H * 0.055)); - const nameSize = Math.max(24, Math.floor(H * 0.075)); const hintSize = Math.max(40, Math.floor(H * 0.12)); const imgSize = Math.max(80, Math.floor(H * 0.38)); this.instrText.setStyle({ fontSize: instrSize + 'px' }); this.instrText.setPosition(W / 2, H * 0.14); + const wasAccepting = this.accepting; + this._stopIdleAnimation(); + this._imgScale = imgSize / 256; if (this.animalImg.visible) { this.animalImg.setPosition(W / 2, H * 0.43).setScale(this._imgScale); } - this.nameText.setStyle({ fontSize: nameSize + 'px' }); - this.nameText.setPosition(W / 2, H * 0.70); + if (wasAccepting) this._startIdleAnimation(); + + this._reflowNameDisplay(W, H); this.hintText.setStyle({ fontSize: hintSize + 'px' }); this.hintText.setPosition(W / 2, H * 0.80); @@ -120,7 +133,73 @@ class AnimalLetterScene extends Phaser.Scene { this.applyLayout(gameSize.width, gameSize.height); } + // Place both name text objects centered together at H*0.70 + _reflowNameDisplay(W, H) { + const nameSize = Math.max(24, Math.floor(H * 0.075)); + this.nameFirstLetterText.setStyle({ fontSize: nameSize + 'px' }); + this.nameRestText.setStyle({ fontSize: nameSize + 'px' }); + + if (!this.nameFirstLetterText.visible) return; + + const firstW = this.nameFirstLetterText.width; + const restW = this.nameRestText.width; + const totalW = firstW + restW; + const startX = W / 2 - totalW / 2; + this.nameFirstLetterText.setPosition(startX, H * 0.70); + this.nameRestText.setPosition(startX + firstW, H * 0.70); + } + + // Show animal name with first letter in accent color + _showNameDisplay(animal, color) { + const { width: W, height: H } = this.scale; + const nameSize = Math.max(24, Math.floor(H * 0.075)); + + this.nameFirstLetterText + .setText(animal.name[0]) + .setStyle({ fontSize: nameSize + 'px', fontFamily: 'Fredoka, sans-serif', fontStyle: 'bold', color }); + this.nameRestText + .setText(animal.name.slice(1)) + .setStyle({ fontSize: nameSize + 'px', fontFamily: 'Fredoka, sans-serif', fontStyle: 'bold', color: '#ffffff' }); + + this.nameFirstLetterText.setVisible(true); + this.nameRestText.setVisible(true); + + this._reflowNameDisplay(W, H); + } + + _startIdleAnimation() { + const { width: W } = this.scale; + const swayAmt = Math.max(6, Math.floor(W * 0.012)); + + this._idleTweens = [ + this.tweens.add({ + targets: this.animalImg, + x: this.animalImg.x + swayAmt, + ease: 'Sine.InOut', + duration: 1600, + yoyo: true, + repeat: -1, + }), + this.tweens.add({ + targets: this.animalImg, + scaleX: this._imgScale * 1.06, + scaleY: this._imgScale * 1.06, + ease: 'Sine.InOut', + duration: 1100, + yoyo: true, + repeat: -1, + }), + ]; + } + + _stopIdleAnimation() { + this._idleTweens.forEach(t => { try { t.destroy(); } catch (_) {} }); + this._idleTweens = []; + } + newRound() { + this._stopIdleAnimation(); + // Refill deck (shuffled, no-repeat) when exhausted if (this._deck.length === 0) { this._deck = AnimalLetterLib.shuffleAnimals(Math.random); @@ -131,7 +210,8 @@ class AnimalLetterScene extends Phaser.Scene { const available = COLORS.filter(c => c !== this.roundColor); this.roundColor = available[Math.floor(Math.random() * available.length)]; - this.nameText.setVisible(false); + this.nameFirstLetterText.setVisible(false); + this.nameRestText.setVisible(false); this.hintText.setVisible(false); this.instrText.setText('Press the first letter').setColor('#888888'); @@ -151,7 +231,10 @@ class AnimalLetterScene extends Phaser.Scene { scaleY: this._imgScale, ease: 'Back.Out', duration: 250, - onComplete: () => { this.accepting = true; }, + onComplete: () => { + this.accepting = true; + this._startIdleAnimation(); + }, }); } else { this.animalImg.setVisible(false); @@ -178,12 +261,18 @@ class AnimalLetterScene extends Phaser.Scene { onCorrect() { this.accepting = false; + this._stopIdleAnimation(); + this.audio.playSuccess(); this.audio.playSuccessJingle(); const { width: W, height: H } = this.scale; KGames.burstConfetti(this, W / 2, H * 0.43); + // Always show the animal name with first letter accented so kids read the word + this._showNameDisplay(this.currentAnimal, this.roundColor); + this.audio.speak(this.currentAnimal.name); + this.lettersFound++; KGames.drawProgressBar(this.progressGfx, W, H, this.lettersFound, GOAL); @@ -191,7 +280,7 @@ class AnimalLetterScene extends Phaser.Scene { this.guessTexts = []; if (this.lettersFound >= GOAL) { - this.time.delayedCall(700, () => { + this.time.delayedCall(1500, () => { this.winShowing = true; this.accepting = false; this.winElements = KGames.showWinScreen(this, { @@ -202,7 +291,7 @@ class AnimalLetterScene extends Phaser.Scene { }); }); } else { - this.time.delayedCall(950, () => { + this.time.delayedCall(1500, () => { this.newRound(); this.applyLayout(this.scale.width, this.scale.height); }); @@ -220,16 +309,11 @@ class AnimalLetterScene extends Phaser.Scene { updateHints() { const animal = this.currentAnimal; const { width: W, height: H } = this.scale; - const nameSize = Math.max(24, Math.floor(H * 0.075)); const hintSize = Math.max(40, Math.floor(H * 0.12)); if (this.wrongCount === 1) { - // Show name — speak it so the child can sound out the first letter - this.nameText - .setText(animal.name) - .setStyle({ fontSize: nameSize + 'px', color: '#ffffff' }) - .setPosition(W / 2, H * 0.70) - .setVisible(true); + // Show name with first letter accented — speak it so child can sound out the first letter + this._showNameDisplay(animal, this.roundColor); this.audio.speak(animal.name); } else if (this.wrongCount >= 2) { // Show big colored first letter + update instruction — speak just the letter @@ -292,6 +376,8 @@ class AnimalLetterScene extends Phaser.Scene { } resetGame() { + this._stopIdleAnimation(); + this.winElements.forEach(el => { if (el.active) el.destroy(); }); this.winElements = []; this.winShowing = false;