From 761a3172bf4c15aed3836e4d1cd6f815b651b0e0 Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Sat, 29 Aug 2026 04:06:51 -0400 Subject: Fix --- 2026/games/cammymoop/js/dice_game.js | 622 +++++++++++++++++++++++++++++++++++ 2026/games/cammymoop/js/goblin.js | 521 +++++++++++++++++++++++++++++ 2026/games/cammymoop/js/hints.js | 83 +++++ 2026/games/cammymoop/js/micro.js | 158 +++++++++ 2026/games/cammymoop/js/stopwatch.js | 59 ++++ 5 files changed, 1443 insertions(+) create mode 100644 2026/games/cammymoop/js/dice_game.js create mode 100644 2026/games/cammymoop/js/goblin.js create mode 100644 2026/games/cammymoop/js/hints.js create mode 100644 2026/games/cammymoop/js/micro.js create mode 100644 2026/games/cammymoop/js/stopwatch.js (limited to '2026/games/cammymoop/js') diff --git a/2026/games/cammymoop/js/dice_game.js b/2026/games/cammymoop/js/dice_game.js new file mode 100644 index 0000000..2810872 --- /dev/null +++ b/2026/games/cammymoop/js/dice_game.js @@ -0,0 +1,622 @@ + +function randOffset(size) { + return Math.round((Math.random() - 0.5) * 2 * size); +} + +function smoothRandOffset(size) { + return (Math.random() - 0.5) * 2 * size; +} + +M.Ease = Phaser.Math.Easing; + +const ROLL_TOTAL = 290; +const ROLL_ONCE = 60; + +const battles = [ + // difficulty tier 0 + [ + [3, "Goblin", "Goblin"], + [2, "BombGnome", "Goblin"], + [2, "BombGnome", "BombGnome"], + ], + // difficulty tier 1 + [ + [3, "Goblin", "Goblin", "BombGnome"], + [2, "Goblin", "Goblin"], + [1, "Goblin"], + [4, "ShieldGoblin"], + ], + // difficulty tier 2 + [ + [4, "ShieldGoblin", "ShieldGoblin"], + [7, "Goblin", "Goblin", "Goblin", "Goblin"], + [3, "ShieldGoblin", "BombGnome"], + [3, "Goblin", "BombGnome", "Goblin", "Goblin"], + [1, "BombGnome", "BombGnome", "BombGnome", "Goblin"], + ], + // difficulty tier 3 + [ + [2, "ShieldGoblin", "BombGnome"], + [3, "ShieldGoblin", "BombGnome", "BombGnome", "ShieldGoblin"], + [7, "Goblin", "Goblin", "Goblin", "ShieldGoblin", "Goblin"], + [7, "ShieldGoblin", "ShieldGoblin", "ShieldGoblin"], + ], + // difficulty tier 4 + [ + [3, "ShieldGoblin", "Goblin", "BombGnome"], + [6, "Goblin", "Goblin", "Goblin", "ShieldGoblin", "Goblin"], + [7, "ShieldGoblin", "ShieldGoblin", "ShieldGoblin"], + ], + // difficulty tier 5 + [ + [10, "ShieldGoblin", "ShieldGoblin", "ShieldGoblin", "ShieldGoblin", "ShieldGoblin"], + [8, "ShieldGoblin", "ShieldGoblin", "ShieldGoblin", "ShieldGoblin", "BombGnome"], + ], + +]; +const maxTier = battles.length - 1; + +const battleBags = [[],[],[],[],[], []]; + +const timerTimes = [12, 14, 16, 14, 10, 14]; + +function arrShuffle(inArr) { + return inArr.map((v) => ({v: v, r: Math.random()})).sort((a, b) => a.r - b.r).map(({v}) => v); +} + +function getBattle(tier) { + tier = Math.min(battles.length - 1, tier); + if (battleBags[tier].length < 1) { + battleBags[tier] = arrShuffle([...battles[tier]]); + } + return [...battleBags[tier].pop()]; +} + +const enemyTypes = { + "Goblin": (s) => new M.Goblin(s), + "ShieldGoblin": (s) => new M.Goblin(s, true), + "BombGnome": (s) => new M.BombGnome(s), +}; + +M.DiceGame = class extends Phaser.Scene +{ + create(data) + { + this.finished = false; + this.victory = false; + this.usedGrace = false; + + this.difficultyVal = data.difficulty; + this.tier = Math.min(maxTier, Math.max(0, Math.floor(this.difficultyVal * 0.2))); + + this.battle = getBattle(this.tier); + this.timeLimit = timerTimes[Math.min(timerTimes.length - 1, this.tier)] + // make it a bit easier for now + this.timeLimit = Math.ceil(this.timeLimit * 1.4); + + console.log("STARTED!!!!!!!!!!!!!!!!"); + M.gameStarted(); + + this.diceIsActive = false; + + this.justClickedLMB = false; + this.LMBWasClicked = false; + + this.bg = this.add.image(-30, -22, 'background').setOrigin(0, 0); + + this.stopwatch = new M.Stopwatch(this, this.timeLimit, new M.Vec2(26, 26)); + this.stopwatch.depth = 5; + + this.circle = this.add.image(-6000, 0, "red_circle"); + this.targetArrow = this.add.image(-6000, 0, "target_arrow"); + this.targetArrow.setOrigin(1, 0.5); + this.targetArrow.depth = 2; + + this.allEnemies = []; + + const enemySpacing = 50; + const enemyYOffset = -12; + const startOffset = ((this.battle.length - 2) / 2) * enemySpacing; + for (let i = 1; i < this.battle.length; i++) { + const newEnemy = enemyTypes[this.battle[i]](this); + const xOffset = ((i - 1) * enemySpacing) - startOffset; + newEnemy.setHomePos(this.screenCenterPos().add(new M.Vec2( + xOffset, enemyYOffset + ((i % 2) * 6) + ))); + newEnemy.addToDisplayUpdate(); + this.allEnemies.push(newEnemy); + } + + this.reroll = this.add.image(220, 132, "reroll"); + this.reroll.depth = 1; + + this.numDice = this.battle[0]; + this.allDice = []; + this.rollDice(this.numDice); + } + + screenCenterPos() { + const h_center = this.game.scale.gameSize.width / 2; + const v_center = this.game.scale.gameSize.height / 2; + return new M.Vec2(h_center, v_center); + } + + update(t, dt) { + let pickableDice = 0; + for (let dice of this.allDice) { + if (dice.throwing) { + this.updateThrownDice(dice, dt); + } else if (dice.rolling) { + dice.roll_timer += dt; + dice.roll_anim_timer += dt; + + if (dice.roll_anim_timer > ROLL_ONCE) { + dice.roll_anim_timer = 0; + let rand_side = Math.floor(Math.random() * 5); + let cur_side = parseInt(dice.frame.name); + if (rand_side >= cur_side) { + rand_side += 1; + } + dice.setFrame(rand_side); + } + + const progress = dice.roll_timer / dice.total_roll_time; + + const elevation = Math.sin(progress * Math.PI) * 38; + const rollVec = dice.roll_target_pos.clone().subtract(dice.roll_from_pos); + dice.setPosVec(dice.roll_from_pos); + dice.addPosVec(rollVec.scale(progress)); + dice.addPosVec(M.Vec2.UP.clone().scale(elevation)); + + if (dice.roll_timer >= dice.total_roll_time) { + this.sound.play('land_sfx'); + dice.total_roll_time = ROLL_TOTAL; + dice.rolling = false; + pickableDice += 1; + dice.setPosVec(dice.roll_target_pos); + this.setupRolledDice(dice); + } + } else { + pickableDice += 1; + } + } + + const activePointer = this.input.activePointer; + activePointer.updateWorldPoint(this.cameras.default); + const pVec = new M.Vec2(activePointer.worldX, activePointer.worldY); + + const clicked = activePointer.primaryDown + this.justClickedLMB = !this.LMBWasClicked && clicked + this.LMBWasClicked = clicked + + let closestEnemy = null; + let closestDice = null; + let minDist = 0; + for (let dice of this.allDice) { + if (dice.rolling) { + continue; + } + let pointerDist = pVec.distance(dice.getPosVec()); + if (pointerDist > 49 || (!dice.emphasis && pointerDist > 45)) { + continue; + } else if (!closestDice || pointerDist < minDist) { + minDist = pointerDist; + closestDice = dice; + } + } + for (let enemy of this.allEnemies) { + if (!enemy.alive) { + continue; + } + let pointerDist = pVec.distance(enemy.getPosVec()); + if (pointerDist > 49) { + continue; + } else if (!closestEnemy || pointerDist < minDist) { + minDist = pointerDist; + closestEnemy = enemy; + } + } + + if (this.isOverReroll(pVec)) { + closestDice = null; + closestEnemy = null; + if (this.justClickedLMB && pickableDice > 0) { + this.rerollUnused(); + } + } + + // If close enough to click dice or enemy and any dice is already selected, only consider enemy + if (closestDice && closestEnemy && this.diceIsActive) { + closestDice = null; + } + + let clickedDice = false; + this.changeEmphasisDice(closestDice); + if (this.justClickedLMB) { + if (closestDice && (!closestEnemy || !this.diceIsActive)) { + clickedDice = true; + this.changeActiveDice(closestDice); + } + } + for (let enemy of this.allEnemies) { + if (closestEnemy && enemy === closestEnemy) { + enemy.showHint(); + } else { + enemy.hideHint(); + } + } + + if (closestEnemy && this.diceIsActive && !clickedDice) { + const activeDice = this.getActiveDice(); + let diceToTargetVec = closestEnemy.getPosVec().subtract(activeDice.getPosVec()); + + this.targetArrow.setPosVec(closestEnemy.getPosVec()); + this.targetArrow.rotation = diceToTargetVec.angle(); + if (this.justClickedLMB) { + const activeDiceVal = this.getDiceVal(activeDice); + this.throwDice(activeDice, closestEnemy); + //closestEnemy.attack(activeDiceVal); + //this.removeDice(activeDice); + } + } else { + this.targetArrow.x = -6000 + } + + if (!this.finished) { + let aliveEnemies = this.aliveEnemiesCount(); + if (aliveEnemies < 1) { + this.nowFinished(true); + } else if (this.unusedDiceNum() < 1) { + this.nowFinished(false); + } + } + } + + aliveEnemiesCount() { + let aliveEnemies = 0; + for (let e of this.allEnemies) { + if (e.alive) { + aliveEnemies += 1; + } + } + return aliveEnemies; + } + + aliveEnemyNames() { + let names = []; + for (let e of this.allEnemies) { + if (e.alive) { + names.push(e.enemyName); + } + } + return names; + } + + nowFinished(won, fromTimer = false) { + if (this.finished) { + return; + } + this.victory = won; + if (!won) { + battleBags[this.tier].push([...this.battle]); + battleBags[this.teir] = arrShuffle(battleBags[this.tier]); + } + this.finished = true; + const timeLeft = this.stopwatch.getTimeLeft(); + + const waitForText = Math.min(timeLeft, 200); + if (waitForText <= 0) { + this.showEndText(won); + } else { + setTimeout( () => this.showEndText(won), waitForText); + } + + if (fromTimer) { + setTimeout(() => this.timeUp(), 300); + } else { + const continueDelay = Math.max(Math.min(2200, timeLeft), 300); + setTimeout(() => this.timeUp(), continueDelay); + } + } + + showEndText(won) { + const c = this.screenCenterPos(); + if (won) { + this.add.image(c.x, c.y, "yes"); + } else { + this.add.image(c.x, c.y, "ohno"); + } + } + + getDiceVal(dice) { + return parseInt(dice.frame.name) + 1; + } + + setupRolledDice(dice) { + dice.tint = 0xdddddd; + dice.emphasis = false; + dice.activeDice = false; + if (this.stopwatch.getTimeLeft() < 3500 && !this.usedGrace) { + this.grace(dice); + } + } + + grace(dice) { + const unusedCount = this.unusedDiceNum(); + const enemyNames = this.aliveEnemyNames(); + let rolledNumbers = []; + for (let otherDice of this.allDice) { + if (otherDice === dice || otherDice.throwing) { + continue; + } + rolledNumbers.push(this.getDiceVal(otherDice)); + } + if (enemyNames.length < 1) { + return; + } + if (enemyNames.includes("BombGnome")) { + if (enemyNames.length > unusedCount) { + this.usedGrace = true; + this.setDiceTo(dice, 6); + } else if (unusedCount === 1) { + this.usedGrace = true; + this.setDiceTo(dice, 2); + } + } else { + if (enemyNames.includes("ShieldGoblin")) { + this.usedGrace = true; + this.setDiceTo(dice, 3); + } else if (unusedCount === 1) { + this.usedGrace = true; + this.setDiceTo(dice, Math.floor(Math.random() * 4) + 3); + } else if (unusedCount - rolledNumbers.length === 1) { + let greaterThanTwo = false; + for (let rolledNum of rolledNumbers) { + if (rolledNum > 2) { + greaterThanTwo = true; + } + } + if (!greaterThanTwo) { + this.usedGrace = true; + this.setDiceTo(dice, 3); + } + } + } + + if (this.usedGrace) { + console.log("halleluyah"); + } + } + + setDiceTo(dice, toNum) { + dice.setFrame(toNum - 1); + } + + throwDice(dice, atEnemy) { + if (!atEnemy || !atEnemy.alive) { + return; + } + if (dice.activeDice) { + this.clearDiceActive(dice); + } + atEnemy.preattack(this.getDiceVal(dice)); + dice.throwing = true; + dice.throwingAt = atEnemy; + dice.throwFrom = dice.getPosVec(); + dice.throwTo = atEnemy.getPosVec(); + dice.throwTimer = 0; + dice.throwTotalTime = 80; + } + + updateThrownDice(dice, dt) { + dice.throwTimer += dt; + if (dice.throwTimer > dice.throwTotalTime) { + this.finishThrownDice(dice); + } else { + const progress = dice.throwTimer / dice.throwTotalTime; + dice.scale = Math.max(0.1, 1 - progress); + const delta = dice.throwTo.clone().subtract(dice.throwFrom); + dice.setPosVec(dice.throwFrom); + dice.addPosVec(delta.scale(M.Ease.Cubic.Out(progress))); + } + } + + finishThrownDice(dice) { + dice.throwingAt.attack(this.getDiceVal(dice)); + this.removeDice(dice); + } + + removeDice(dice) { + const diceIndex = this.allDice.indexOf(dice); + if (diceIndex < 0) { + return; + } + this.clearDiceActive(dice); + this.allDice.splice(diceIndex, 1); + dice.destroy(); + this.numDice = this.allDice.length; + } + + setDiceEmphasis(dice) { + if (dice.emphasis) { + return; + } + dice.emphasis = true; + dice.clearTint(); + dice.y -= 4; + } + clearDiceEmphasis(dice) { + if (!dice.emphasis) { + return; + } + dice.emphasis = false; + dice.y += 4; + if (!dice.activeDice) { + dice.tint = 0xdddddd; + } + } + + isOverReroll(pointerPos) { + const relPointer = pointerPos.clone().subtract(this.reroll.getPosVec()); + if (Math.abs(relPointer.x) * 2 > this.reroll.width) { + return false; + } else if (Math.abs(relPointer.y) * 2 > this.reroll.height) { + return false; + } + return true; + } + + setDiceActive(dice) { + dice.activeDice = true; + dice.clearTint(); + this.circle.setPosVec(dice.getPosVec()); + if (dice.emphasis) { + this.circle.y += 4; + } + } + clearDiceActive(dice) { + dice.activeDice = false; + this.circle.x = -6000; + this.diceIsActive = false; + } + + changeEmphasisDice(newEmphasis) { + for (let dice of this.allDice) { + if (dice === newEmphasis) { + this.setDiceEmphasis(dice); + } else if (dice.emphasis) { + this.clearDiceEmphasis(dice); + } + } + } + + changeActiveDice(newActiveDice) { + for (let dice of this.allDice) { + if (dice !== newActiveDice) { + this.clearDiceActive(dice); + } + } + this.diceIsActive = true; + this.setDiceActive(newActiveDice); + } + + enemyExploded(explodingEnemy) { + this.sound.play('boom_sfx'); + const enemyIndex = this.allEnemies.indexOf(explodingEnemy); + if (enemyIndex === -1) { + console.log("exploding enemy not found in allEnemies"); + return; + } + if (enemyIndex > 0) { + const leftEnemy = this.allEnemies[enemyIndex - 1]; + if (!leftEnemy.gone) { + leftEnemy.attack(8); + } + } + if (enemyIndex < this.allEnemies.length - 1) { + const rightEnemy = this.allEnemies[enemyIndex + 1]; + if (!rightEnemy.gone) { + rightEnemy.attack(8); + } + } + } + + preExplode(explodingEnemy) { + const enemyIndex = this.allEnemies.indexOf(explodingEnemy); + if (enemyIndex === -1) { + console.log("pre-exploding enemy not found in allEnemies"); + return; + } + if (enemyIndex > 0) { + const leftEnemy = this.allEnemies[enemyIndex - 1]; + if (leftEnemy.alive) { + leftEnemy.preattack(8); + } + } + if (enemyIndex < this.allEnemies.length - 1) { + const rightEnemy = this.allEnemies[enemyIndex + 1]; + if (rightEnemy.alive) { + rightEnemy.preattack(8); + } + } + } + + getActiveDice() { + if (!this.diceIsActive || this.allDice.length === 0) { + return null; + } + for (let dice of this.allDice) { + if (dice.activeDice) { + return dice; + } + } + return null; + } + + clearUnusedDice() { + const oldDice = this.allDice; + this.allDice = []; + for (let dice of oldDice) { + if (dice.throwing) { + this.allDice.push(dice); + continue; + } + if (dice.activeDice) { + this.clearDiceActive(dice); + } + dice.destroy(); + } + } + + unusedDiceNum() { + let num = 0; + for (let dice of this.allDice) { + if (!dice.throwing) { + num += 1; + } + } + return num; + } + + rerollUnused() { + let unusedNum = this.unusedDiceNum(); + this.clearUnusedDice(); + this.rollDice(unusedNum); + } + + rollDice(num) { + this.sound.play('fwoof_sfx'); + let diceSpacing = 52; + if (num > 1) { + diceSpacing = Math.min(diceSpacing, Math.round(154 / (num - 1))); + } + const center = this.screenCenterPos(); + for (let i = 0; i < num; i++) { + const xPos = Math.round(center.x - ((num - 1) * diceSpacing * 0.5) + (i * diceSpacing) - 10); + const dice = this.add.sprite(xPos, center.y + 52, 'dices'); + this.allDice.push(dice); + + dice.addPosVec(new M.Vec2(randOffset(3), randOffset(11))); + dice.roll_target_pos = dice.getPosVec(); + dice.addPosVec(new M.Vec2(randOffset(16), 30)); + dice.roll_from_pos = dice.getPosVec(); + + dice.rolling = true; + dice.roll_anim_timer = 0; + dice.roll_timer = 0; + dice.total_roll_time = ROLL_TOTAL + Math.random() * 400; + } + } + + timeUp() { + let difficulty = this.difficultyVal + if (battleBags[this.tier].length < 1) { + difficulty += 1; + M.gameDifficulty = difficulty + } + M.gameEnd(this.victory); + this.scene.switch("wait"); + this.scene.stop(); + //this.scene.restart({difficulty: difficulty}); + } +} diff --git a/2026/games/cammymoop/js/goblin.js b/2026/games/cammymoop/js/goblin.js new file mode 100644 index 0000000..2514cdf --- /dev/null +++ b/2026/games/cammymoop/js/goblin.js @@ -0,0 +1,521 @@ + +M.Enemy = class extends Phaser.GameObjects.Container +{ + constructor(scene) { + super(scene); + this.sprites = {} + this.spriteNames = []; + this.homePos = M.Vec2.ZERO; + this.hint = null; + this.alive = true; + this.gone = false; + this.deathAnimating = false; + } + + setHomePos(newHomePos) { + this.setPosVec(newHomePos); + this.homePos = new M.Vec2(newHomePos.x, newHomePos.y); + } + + addToDisplayUpdate() { + this.addToDisplayList(); + this.addToUpdateList(); + } + + setupHint(height, hintData) { + this.addHintData(hintData); + this.hint.y = -height; + } + + addHintData(hintData) { + if (this.hint) { + this.remove(this.hint); + this.hint.destroy(); + } + this.hint = new M.Hint(this.scene); + this.add(this.hint); + this.hint.setup(hintData); + } + + hideHint() { + if (this.hint) { + this.hint.visible = false; + } + } + + showHint() { + if (this.hint) { + this.hint.visible = true; + } + } + + removeSprite(spriteName) { + if (!this.sprites[spriteName]) { + return; + } + if (this.exists(this.sprites[spriteName])) { + this.sprites[spriteName].destroy(); + this.remove(this.sprites[spriteName]); + } + const nameIndex = this.spriteNames.indexOf(spriteName); + if (nameIndex >= 0) { + this.spriteNames.splice(nameIndex, 1); + } + delete this.sprites[spriteName]; + } + + addSprite(spriteName, pos, textureKey, frameIdx) { + if (!pos) { + pos = {x: 0, y: 0}; + } + if (this.sprites[spriteName]) { + removeSprite(spriteName); + } + if (!this.spriteNames.includes(spriteName)) { + this.spriteNames.push(spriteName); + } + const spr = new Phaser.GameObjects.Sprite(this.scene, pos.x, pos.y, textureKey, frameIdx); + this.sprites[spriteName] = spr; + this.add(spr); + } + + tintSprites(newTint) { + for (let spName of this.spriteNames) { + this.sprites[spName].setTint(newTint); + } + } + + resetSpritesTint() { + for (let spName of this.spriteNames) { + this.sprites[spName].clearTint(); + } + } + + preUpdate (t, dt) { + super.preUpdate(t, dt); + } + + attack(diceValue) { + console.log("base enemy attacked: " + diceValue); + } + +} + +M.Goblin = class extends M.Enemy +{ + constructor(scene, withShield = false) { + super(scene); + this.baseAnimInterval = 160; + this.anim_interval = this.baseAnimInterval; + this.anim_timer = 0; + + this.current_animation = 1; + this.anim_state = { side: -1 } + + this.head_shift = 1; + + this.dyingTint = Phaser.Display.Color.IntegerToColor(0x003300); + + this.addSprite("body", M.Vec2.ZERO, "gob_small", 0); + this.addSprite("shield", M.Vec2.ZERO, "gob_small", 2); + this.addSprite("head", M.Vec2.ZERO, "gob_small", 4); + + this.shieldBreaking = false; + this.shielded = withShield; + if (!this.shielded) { + this.enemyName = "Goblin"; + this.sprites.shield.visible = false; + } else { + this.enemyName = "ShieldGoblin"; + } + + this.refreshHint(); + this.hideHint(); + } + + refreshHint() { + if (this.shielded) { + this.setupHint(20, [["less", "4", "", "arrow", "", "guard_break"]]); + } else { + this.setupHint(20, [["greater", "2", "", "arrow", "", "skull"]]); + } + } + + preUpdate (t, dt) { + + this.anim_timer += dt; + this.animProcess(dt); + if (this.anim_timer > this.anim_interval) { + this.anim_timer = 0; + this.animUpdate(); + } + } + + changeAnim(animNumber) { + this.current_animation = animNumber; + this.resetAnim(); + } + + resetAnim() { + this.sprites.head.y = 0; + this.anim_interval = this.baseAnimInterval; + switch (this.current_animation) { + case 1: + this.sprites.head.setFrame(4); + this.sprites.body.setFrame(0); + this.sendToBack(this.sprites.body); + break; + case 2: + this.sprites.head.setFrame(5); + this.sprites.head.scaleX = 1; + this.sprites.head.x = 0; + this.sprites.body.setFrame(1); + this.bringToTop(this.sprites.body); + this.anim_state.death_timer = 0; + this.anim_state.death_total_time = 400; + break; + case 3: // laugh + this.sprites.head.setFrame(4); + this.sprites.head.x = 0; + this.anim_interval = 60; + this.anim_timer = 30; + this.anim_state.anim_timer = 0; + this.anim_state.anim_total_time = 600; + break; + case 4: // shield break + this.sprites.head.setFrame(5); + this.bringToTop(this.sprites.body); + this.anim_state.anim_timer = 0; + this.anim_state.anim_total_time = 280; + break; + } + } + + animProcess (dt) { + switch (this.current_animation) { + case 2: + this.anim_state.death_timer += dt; + const progress = this.anim_state.death_timer / this.anim_state.death_total_time; + const tintProgress = Math.min(1, progress * 2.2); + + let tintColor = this.dyingTint; + if (tintProgress < 1) { + const colorWhite = Phaser.Display.Color.IntegerToColor(0xffffff); + tintColor = Phaser.Display.Color.Interpolate.ColorWithColor( + colorWhite, this.dyingTint, 1, tintProgress + ); + } + this.tintSprites(tintColor.color); + + const scaleProgress = Math.max(0, (M.Ease.Cubic.In(progress) * 1.5) - 0.5); + for (let spName of this.spriteNames) { + this.sprites[spName].scaleY = 1 - scaleProgress; + } + + if (progress >= 1) { + this.finishDie(); + } + break; + case 3: + this.anim_state.anim_timer += dt; + if (this.anim_state.anim_timer > this.anim_state.anim_total_time) { + this.changeAnim(1); + } + break; + case 4: + this.anim_state.anim_timer += dt; + if (this.anim_state.anim_timer > this.anim_state.anim_total_time) { + this.changeAnim(1); + } + break; + } + } + + animUpdate () { + const head = this.sprites.head; + switch (this.current_animation) { + case 1: + this.anim_state.side *= -1; + head.x = this.head_shift * this.anim_state.side; + if (Math.random() < 0.05) { + head.scaleX *= -1; + if (Math.random() < 0.7) { + const pitchScale = Math.random() * 0.3 + 0.9 + this.scene.sound.play('gob2_sfx', {pitch: pitchScale}); + } + } + break; + case 3: + if (head.y === 0) { + head.y = 1; + } else { + head.y = 0; + } + break; + } + } + + attack(diceValue) { + if (this.shielded) { + if (diceValue <= 3 || diceValue > 6) { + this.scene.sound.play('crack_sfx'); + this.breakShield(); + + if (Math.random() < 0.2) { + this.scene.sound.play('gob_grunt_sfx'); + } + } else { + const laughNum = Math.floor(Math.random() * 2 + 1); + this.scene.sound.play('laugh' + laughNum + '_sfx'); + this.changeAnim(3); // laugh + } + } else { + if (diceValue <= 2) { + const laughNum = Math.floor(Math.random() * 2 + 1); + this.scene.sound.play('laugh' + laughNum + '_sfx'); + this.changeAnim(3); // laugh + } else { + this.scene.sound.play('punch_sfx'); + if (Math.random() < 0.75) { + this.scene.sound.play('gob_grunt_sfx'); + } else { + this.scene.sound.play('gob1_sfx'); + } + this.startDie(); + } + } + } + + preattack(diceValue) { + if (this.shielded && !this.shieldBreaking) { + if (diceValue <= 3 || diceValue > 6) { + this.shieldBreaking = true; + } + } else { + if (diceValue > 2) { + this.alive = false; + } + } + } + + breakShield() { + this.enemyName = "Goblin"; + this.shielded = false; + this.sprites.shield.visible = false; + this.refreshHint(); + this.changeAnim(4); + } + + startDie() { + this.alive = false; + this.gone = true; + this.deathAnimating = true; + this.changeAnim(2); + } + + finishDie() { + this.deathAnimating = false; + this.removeFromDisplayList(); + this.removeFromUpdateList(); + } +} + +M.BombGnome = class extends M.Enemy +{ + constructor(scene) { + super(scene); + this.enemyName = "BombGnome"; + + this.anim_interval = 320; + this.anim_timer = 0; + this.total_time = 0; + + this.current_animation = 1; + this.anim_state = {}; + + this.arm_speed = 200 + Math.random() * 20; + this.arm_angle_delta = (Math.PI / 6); + this.snap = Math.PI / 64; + + this.addSprite("body", M.Vec2.ZERO, "bomb_gnome", 0); + this.addSprite("arm", M.Vec2.ZERO, "bomb_gnome", 2); + this.addSprite("bomb", M.Vec2.ZERO, "bomb_gnome", 4); + this.addSprite("head", M.Vec2.ZERO, "bomb_gnome", 6); + + this.dyingTint = Phaser.Display.Color.IntegerToColor(0x330000); + + this.setupHint(26, [ + ["greater", "1", "", "arrow", "", "skull", ""], + ["equal", "6", "", "arrow", "", "skull", "boom"], + ]); + this.hideHint(); + } + + preUpdate (t, dt) { + this.total_time += dt; + this.animProcess(dt); + + this.anim_timer += dt; + if (this.anim_timer > this.anim_interval) { + this.anim_timer = 0; + this.animInterval(); + } + } + + animProcess (dt) { + let progress + let tintProgress + switch (this.current_animation) { + case 1: + const arm = this.sprites.arm + const smoothAngle = this.arm_angle_delta * ( + Math.cos(this.total_time / this.arm_speed) / 2 - 0.5 + ); + arm.rotation = Math.round(smoothAngle / this.snap) * this.snap; + break; + case 2: + this.anim_state.death_timer += dt; + progress = this.anim_state.death_timer / this.anim_state.death_total_time; + + tintProgress = Math.min(1, progress * 2.2); + this.setDeathTint(tintProgress); + + this.setDeathScale(progress, 0.5); + + if (progress >= 1) { + this.finishDie(); + } + break; + case 3: + this.anim_state.death_timer += dt; + progress = this.anim_state.death_timer / this.anim_state.death_total_time; + + tintProgress = Math.min(1, progress * 4.2); + this.setDeathTint(tintProgress); + + this.setDeathScale(progress, 1.2); + + const boomThreshold = 0.45; + if (this.spriteNames.includes("boom")) { + if (progress < boomThreshold) { + const boomScaleProg = M.Ease.Back.Out(Math.min(1, progress * 3.0)); + this.sprites.boom.scale = 0.75 + boomScaleProg * 0.25 + } else { + this.removeSprite("boom"); + } + } + + if (progress >= 1) { + this.finishDie(); + } + break; + } + } + + setDeathTint(tintProgress) { + let tintColor = this.dyingTint; + if (tintProgress < 1) { + const colorWhite = Phaser.Display.Color.IntegerToColor(0xffffff); + tintColor = Phaser.Display.Color.Interpolate.ColorWithColor( + colorWhite, this.dyingTint, 1, tintProgress + ); + } + this.tintSprites(tintColor.color); + if (this.spriteNames.includes("boom")) { + this.sprites.boom.clearTint(); + } + } + + setDeathScale(deathProgress, shrinkOffset) { + const clampedAdjusted = Math.max(0, Math.min(1, (deathProgress * (1 + shrinkOffset)) - 0.5)) + const scaleProgress = M.Ease.Cubic.In(clampedAdjusted); + for (let spName of this.spriteNames) { + this.sprites[spName].scaleY = 1 - scaleProgress; + } + } + + animInterval () { + switch (this.current_animation) { + case 1: + const head = this.sprites.head; + head.y = head.y > 0 ? 0 : 1; + break; + } + } + + changeAnim(animNumber) { + this.current_animation = animNumber; + this.resetAnim(); + } + + resetAnim() { + this.sprites.arm.rotation = 0; + switch (this.current_animation) { + case 1: + this.sprites.body.setFrame(0); + this.sprites.arm.setFrame(2); + this.sprites.head.setFrame(6); + break; + case 2: + this.sprites.body.setFrame(1); + this.sprites.arm.setFrame(3); + this.sprites.head.setFrame(7); + + this.anim_state.death_timer = 0; + this.anim_state.death_total_time = 600; + break; + case 3: + this.addSprite("boom", M.Vec2.ZERO, "explosion"); + this.sprites.boom.scale = 0.75; + + this.sprites.body.setFrame(1); + this.sprites.arm.setFrame(3); + this.sprites.bomb.visible = false; + this.sprites.head.setFrame(7); + + this.anim_state.death_timer = 0; + this.anim_state.death_total_time = 900; + break; + } + } + + attack(diceValue) { + if (diceValue <= 1) { + this.scene.sound.play('hoo_sfx'); + } else if (diceValue >= 6) { + this.startDie(true); + } else { + this.scene.sound.play('punch_sfx'); + this.startDie(false); + } + } + + preattack(diceValue) { + if (diceValue > 1) { + this.alive = false; + if (diceValue >= 6) { + this.scene.preExplode(this); + } + } + } + + startDie(exploding) { + this.alive = false; + this.gone = true; + this.deathAnimating = true; + this.changeAnim(exploding ? 3 : 2); + if (exploding) { + setTimeout(() => this.scene.enemyExploded(this), 150); + //this.scene.enemyExploded(this); + } else { + const gruntNum = Math.floor(Math.random() * 3 + 1); + this.scene.sound.play('gnome_grunt' + gruntNum + '_sfx'); + } + } + + finishDie() { + this.deathAnimating = false; + this.removeFromDisplayList(); + this.removeFromUpdateList(); + } +} diff --git a/2026/games/cammymoop/js/hints.js b/2026/games/cammymoop/js/hints.js new file mode 100644 index 0000000..250c080 --- /dev/null +++ b/2026/games/cammymoop/js/hints.js @@ -0,0 +1,83 @@ + +const nums = ['1', '2', '3', '4', '5', '6']; +const shortNames = []; + +M.Hint = class extends Phaser.GameObjects.Container +{ + constructor(scene) { + super(scene); + this.rows = []; + this.allImgs = []; + this.maxWidth = 0; + } + + setupExample() { + this.addToDisplayList(); + this.addRow(["equal", "5", "", "arrow", "", "skull", "dice"]); + this.addRow(["greater", "2", "", "arrow", "", "skull"]); + this.build(); + } + + setup(hintData) { + this.addToDisplayList(); + this.rows = []; + for (let row of hintData) { + this.addRow(row); + } + this.build(); + } + + addRow(contents) { + this.rows.push(contents); + } + + clearRows() { + this.rows = []; + } + + clearImgs() { + for (let img of this.allImgs) { + img.destroy(); + } + this.allImgs = []; + } + + build() { + this.clearImgs(); + + for (let i = 0; i < this.rows.length; i++) { + // Place rows in reverse order from bottom to top, moving coords upward so the end result is in order + const row = this.rows[this.rows.length - 1 - i]; + if (row.length < 1) { + continue; + } + const row_width = row.length * 8; + for (let j = 0; j < row.length; j++) { + if (row[j] === '') { + continue; + } + const charImg = this.getImageForKey(row[j]); + this.allImgs.push(charImg); + this.add(charImg); + charImg.x = (8 * j) - (row_width/2); + charImg.y = -i * 10; + } + } + } + + getImageForKey(charKey) { + if (nums.includes(charKey)) { + return this.newImage("number_icons", parseInt(charKey) - 1); + } else { + return this.newImage(charKey + "_icon"); + } + } + + newImage(texKey, frame = '') { + if (frame !== '') { + return new Phaser.GameObjects.Image(this.scene, 0, 0, texKey, frame); + } else { + return new Phaser.GameObjects.Image(this.scene, 0, 0, texKey); + } + } +}; diff --git a/2026/games/cammymoop/js/micro.js b/2026/games/cammymoop/js/micro.js new file mode 100644 index 0000000..c825081 --- /dev/null +++ b/2026/games/cammymoop/js/micro.js @@ -0,0 +1,158 @@ + +M.Vec2 = Phaser.Math.Vector2; + +// Extend Phaser game objects for vector usage +Phaser.GameObjects.GameObject.prototype.setPosVec = function (newPos) { + this.x = newPos.x; + this.y = newPos.y; +}; + +Phaser.GameObjects.GameObject.prototype.addPosVec = function (newPos) { + this.x += newPos.x; + this.y += newPos.y; +}; + +Phaser.GameObjects.GameObject.prototype.getPosVec = function () { + return new M.Vec2(this.x, this.y); +}; + +M.WaitScene = class extends Phaser.Scene +{ + preload() + { + //this.load.image('image', 'assets/image.png'); + this.load.image('background', 'assets/background.png'); + this.load.image('goblin', 'assets/goblin.png'); + + this.load.image('explosion', 'assets/explosion.png'); + + this.load.image('red_circle', 'assets/red_circle.png'); + this.load.image('target_arrow', 'assets/target_arrow.png'); + this.load.image('reroll', 'assets/reroll.png'); + + this.load.image('yes', 'assets/yes.png'); + this.load.image('ohno', 'assets/ohno.png'); + + this.load.image('stopwatch_frame', 'assets/stopwatch_frame.png'); + this.load.spritesheet('stopwatch_numbers', 'assets/stopwatch_numbers.png', { + "frameWidth": 12, "frameHeight": 14, + }); + + this.load.image('arrow_icon', 'assets/arrow_icon.png'); + this.load.image('boom_icon', 'assets/boom_icon.png'); + this.load.image('dice_icon', 'assets/dice_icon.png'); + this.load.image('equal_icon', 'assets/equal_icon.png'); + this.load.image('greater_icon', 'assets/greater_icon.png'); + this.load.image('guard_break_icon', 'assets/guard_break_icon.png'); + this.load.image('less_icon', 'assets/less_icon.png'); + this.load.image('skull_icon', 'assets/skull_icon.png'); + this.load.spritesheet('number_icons', 'assets/number_icons.png', { + "frameWidth": 8, "frameHeight": 8, + }); + + this.load.spritesheet('gob_small', 'assets/gob_small.png', { + "frameWidth": 48, "frameHeight": 46, + }); + this.load.spritesheet('bomb_gnome', 'assets/bomb_gnome.png', { + "frameWidth": 42, "frameHeight": 50, + }); + + this.load.spritesheet('dices', 'assets/dices.png', { + "frameWidth": 30, "frameHeight": 30, + }); + + this.load.audio('boom_sfx', 'assets/sfx/boom.wav'); + this.load.audio('fwoof_sfx', 'assets/sfx/fwoof.wav'); + this.load.audio('land_sfx', 'assets/sfx/land.wav'); + this.load.audio('punch_sfx', 'assets/sfx/punch.wav'); + this.load.audio('crack_sfx', 'assets/sfx/crack.wav'); + + this.load.audio('laugh1_sfx', 'assets/sfx/laugh1.wav'); + this.load.audio('laugh2_sfx', 'assets/sfx/laugh2.wav'); + this.load.audio('hoo_sfx', 'assets/sfx/hoo.wav'); + + this.load.audio('gnome_grunt1_sfx', 'assets/sfx/gnome_grunt1.wav'); + this.load.audio('gnome_grunt2_sfx', 'assets/sfx/gnome_grunt2.wav'); + this.load.audio('gnome_grunt3_sfx', 'assets/sfx/gnome_grunt3.wav'); + + this.load.audio('gob1_sfx', 'assets/sfx/gob1.wav'); + this.load.audio('gob2_sfx', 'assets/sfx/gob2.wav'); + this.load.audio('gob_grunt_sfx', 'assets/sfx/gob_grunt.wav'); + } + + create() + { + this.difficulty = M.gameDifficulty; + this.textures.get("bomb_gnome").setFilter(Phaser.Textures.FilterMode.Nearest); + this.textures.get("dices").setFilter(Phaser.Textures.FilterMode.Nearest); + + Phaser.Display.Canvas.CanvasInterpolation.setCrisp(this.game.canvas); + window.parent.postMessage({op: "ready"}); + } + + update(t, dt) + { + if (M.standalone && this.input.activePointer.primaryDown) { + this.startDiceGame(); + } + } + + timeToStart(difficulty) { + this.difficulty = difficulty; + this.startDiceGame(); + } + + startDiceGame() + { + const data = { difficulty: this.difficulty }; + this.scene.switch("play", data); + this.scene.stop(); + } +} + +M.gameDifficulty = 0; + +M.waitingScene = new M.WaitScene("wait"); +M.gameScene = new M.DiceGame({key: "play", active: false}); + +M.gameStarted = function () { + window.parent.postMessage({op: "started", verb: "Defeat!"}); +}; + +M.gameEnd = function (won) { + window.parent.postMessage({op: "done", win: won}); +}; + +M.handleMessage = function (msgData) { + if (!msgData) { + return; + } + if (msgData.data.op == "start") { + console.log("detected start!"); + M.waitingScene.timeToStart(msgData.data.difficulty); + } +} +window.addEventListener("message", M.handleMessage); + +const config = { + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_HORIZONTALLY, + }, + type: Phaser.AUTO, + parent: 'game-container', + width: 240, + height: 160, + backgroundColor: '#304858', + scene: [ + M.waitingScene, M.gameScene, + ], + smoothPixelArt: true, + //antialias: false, + //antialiasGL: true, +}; + +M.standalone = window.self === window.top + +const game = new Phaser.Game(config); +M.game = game; diff --git a/2026/games/cammymoop/js/stopwatch.js b/2026/games/cammymoop/js/stopwatch.js new file mode 100644 index 0000000..4578ee9 --- /dev/null +++ b/2026/games/cammymoop/js/stopwatch.js @@ -0,0 +1,59 @@ + +M.Stopwatch = class extends Phaser.GameObjects.Container +{ + constructor(scene, totalSeconds, pos) { + super(scene); + this.setPosVec(pos); + + this.elapsed = 0; + this.totalSeconds = totalSeconds; + + this.frame = new Phaser.GameObjects.Image(scene, 0, -3, 'stopwatch_frame'); + this.add(this.frame); + this.tens = new Phaser.GameObjects.Image(scene, 0, 0, 'stopwatch_numbers', 0); + this.add(this.tens); + this.tens.setOrigin(1, 0.5); + this.ones = new Phaser.GameObjects.Image(scene, 0, 0, 'stopwatch_numbers', 0); + this.add(this.ones); + this.ones.setOrigin(0, 0.5); + + this.addToDisplayList(); + this.addToUpdateList(); + this.showNumber(totalSeconds - 1); + } + + pause() { + this.removeFromUpdateList(); + } + + resume() { + this.addToUpdateList(); + } + + getTimeLeft() { + return this.totalSeconds * 1000 - this.elapsed; + } + + setupHint(height, hintData) { + this.addHintData(hintData); + this.hint.y = -height; + } + + preUpdate (t, dt) { + this.elapsed += dt; + + const dispSeconds = Math.max(0, Math.floor(this.totalSeconds - (this.elapsed / 1000))); + this.showNumber(dispSeconds); + + if (this.elapsed / 1000 > this.totalSeconds) { + this.scene.nowFinished(false, true); + this.pause(); + } + } + + showNumber(dispNumber) { + const num = Math.round(dispNumber) % 100; + this.tens.setFrame(Math.floor(num / 10)); + this.ones.setFrame(num % 10); + } +}; -- cgit v1.3.1