summaryrefslogtreecommitdiff
path: root/2026/games/cammymoop/js
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
committerLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
commit32a62cfcb925225359a5c90761b795f2f737ebd2 (patch)
tree95c2261964803b19f2d39cb35d8d514ed6e5a6f4 /2026/games/cammymoop/js
parentde99c5f7636a952025ea13de02bafd25bcb6bdec (diff)
Re-populate games
Diffstat (limited to '2026/games/cammymoop/js')
-rw-r--r--2026/games/cammymoop/js/dice_game.js622
-rw-r--r--2026/games/cammymoop/js/goblin.js521
-rw-r--r--2026/games/cammymoop/js/hints.js83
-rw-r--r--2026/games/cammymoop/js/micro.js158
-rw-r--r--2026/games/cammymoop/js/stopwatch.js59
5 files changed, 1443 insertions, 0 deletions
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);
+ }
+};