diff --git a/docs/README.md b/docs/README.md index 8b7b0823..48d916c3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,7 +18,7 @@ - [build:firefox](#buildfirefox) - [build:firefox:zip](#buildfirefoxzip) - [Architecture](#architecture) - - [Secure spawn messages](#secure-spawn-messages) +- [Balloon spawn chances](#balloon-spawn-chances) - [Balloons](#balloons) - [Abstract balloon class](#abstract-balloon-class) - [Default balloon](#default-balloon) @@ -151,27 +151,13 @@ The zip file will be created in the `build/` directory. ## Architecture -### Secure spawn messages - -To prevent spawning balloons from untrusted sources like devtools, the extension keeps secrets in the content script an background. +## Balloon spawn chances ```mermaid -sequenceDiagram - participant Content - participant Background - - Content->>Background: Get secret - Background->>Content: Secret - Note over Background,Content: Secret is stored - loop Random interval - Background->>Content: Spawn balloon - alt Secret is correct - Note over Content: Balloon is spawned - Content->>Background: Get secret - Background->>Content: Secret - Note over Background,Content: Secret is stored - end - end +pie showdata +title Balloon spawn chances + "Default" : 0.90 + "Confetti" : 0.10 ``` ## Balloons diff --git a/docs/images/Screenshot-1-1280x800.png b/docs/images/Screenshot-1-1280x800.png new file mode 100644 index 00000000..2242903a Binary files /dev/null and b/docs/images/Screenshot-1-1280x800.png differ diff --git a/docs/images/Screenshot-1.png b/docs/images/Screenshot-1.png deleted file mode 100644 index 7ecbee28..00000000 Binary files a/docs/images/Screenshot-1.png and /dev/null differ diff --git a/docs/images/Screenshot-2-1280x800.png b/docs/images/Screenshot-2-1280x800.png new file mode 100644 index 00000000..1af3cd84 Binary files /dev/null and b/docs/images/Screenshot-2-1280x800.png differ diff --git a/docs/images/Screenshot-2-640x400.png b/docs/images/Screenshot-2-640x400.png deleted file mode 100644 index 1480cb92..00000000 Binary files a/docs/images/Screenshot-2-640x400.png and /dev/null differ diff --git a/docs/images/Screenshot-2.png b/docs/images/Screenshot-2.png deleted file mode 100644 index 9b7d9cad..00000000 Binary files a/docs/images/Screenshot-2.png and /dev/null differ diff --git a/docs/images/Screenshot-3-1280x800.png b/docs/images/Screenshot-3-1280x800.png new file mode 100644 index 00000000..ffaab574 Binary files /dev/null and b/docs/images/Screenshot-3-1280x800.png differ diff --git a/docs/images/Screenshot-3-640x400.png b/docs/images/Screenshot-3-640x400.png deleted file mode 100644 index 0f387185..00000000 Binary files a/docs/images/Screenshot-3-640x400.png and /dev/null differ diff --git a/docs/images/Screenshot-3.png b/docs/images/Screenshot-3.png deleted file mode 100644 index 66f3f6e3..00000000 Binary files a/docs/images/Screenshot-3.png and /dev/null differ diff --git a/docs/images/Screenshot-4-640x400.png b/docs/images/Screenshot-4-640x400.png deleted file mode 100644 index 588b28fb..00000000 Binary files a/docs/images/Screenshot-4-640x400.png and /dev/null differ diff --git a/docs/images/Screenshot-4.png b/docs/images/Screenshot-4.png deleted file mode 100644 index 13a9634a..00000000 Binary files a/docs/images/Screenshot-4.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 5ba3a19c..9e1c287d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pop-a-loon", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pop-a-loon", - "version": "1.8.1", + "version": "1.9.0", "license": "Apache-2.0", "dependencies": { "@hookform/resolvers": "^3.3.4", diff --git a/package.json b/package.json index 7f566ded..7eb81f42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pop-a-loon", - "version": "1.8.1", + "version": "1.9.0", "description": "The new rising trend (literally) that changes the browser game completely.", "private": true, "scripts": { diff --git a/resources/balloons/confetti/confetti.css b/resources/balloons/confetti/confetti.css new file mode 100644 index 00000000..d9d91305 --- /dev/null +++ b/resources/balloons/confetti/confetti.css @@ -0,0 +1,17 @@ +@keyframes bang { + from { + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +i.particle { + animation: bang 750ms ease-out forwards; + position: absolute; + display: block; + left: 50%; + top: 0; + width: 3px; + height: 8px; + opacity: 0; +} diff --git a/resources/balloons/confetti/mask.png b/resources/balloons/confetti/mask.png new file mode 100644 index 00000000..9705d96f Binary files /dev/null and b/resources/balloons/confetti/mask.png differ diff --git a/resources/balloons/default/icon.png b/resources/balloons/default/icon.png index 32db35f2..4d6e70dc 100644 Binary files a/resources/balloons/default/icon.png and b/resources/balloons/default/icon.png differ diff --git a/resources/icons/balloon.svg b/resources/icons/balloon.svg new file mode 100644 index 00000000..55ab36b5 --- /dev/null +++ b/resources/icons/balloon.svg @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/icon-128.png b/resources/icons/icon-128.png index 32db35f2..4d6e70dc 100644 Binary files a/resources/icons/icon-128.png and b/resources/icons/icon-128.png differ diff --git a/resources/icons/icon-16.png b/resources/icons/icon-16.png index e843b640..d8561290 100644 Binary files a/resources/icons/icon-16.png and b/resources/icons/icon-16.png differ diff --git a/resources/icons/icon-24.png b/resources/icons/icon-24.png index e428a9ab..38e3ac2b 100644 Binary files a/resources/icons/icon-24.png and b/resources/icons/icon-24.png differ diff --git a/resources/icons/icon-32.png b/resources/icons/icon-32.png index 740179f7..2ad827ae 100644 Binary files a/resources/icons/icon-32.png and b/resources/icons/icon-32.png differ diff --git a/resources/icons/icon-48.png b/resources/icons/icon-48.png index 3758b5f6..4246985f 100644 Binary files a/resources/icons/icon-48.png and b/resources/icons/icon-48.png differ diff --git a/src/background/background.ts b/src/background/background.ts index e7dcfeb5..fb9417ee 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -5,7 +5,7 @@ import storage from '@/storage'; import remote from '@/remote'; import { calculateBalloonSpawnDelay, - generateRandomNumber, + random, getBrowser, isRunningInBackground, sendMessage, @@ -102,7 +102,7 @@ const updateBadgeColors = () => { // Get all active tabs const tabs = await browser.tabs.query({ active: true }); // Select a random tab - const num = Math.round(generateRandomNumber(0, tabs.length - 1)); + const num = Math.round(random(0, tabs.length - 1)); const tab = tabs[num]; if (!tab.id) return skipSpawnMessage('No tab id'); diff --git a/src/balloon.ts b/src/balloon.ts index ff2e7403..b1a57867 100644 --- a/src/balloon.ts +++ b/src/balloon.ts @@ -1,16 +1,20 @@ import browser from 'webextension-polyfill'; import storage from '@/storage'; -import { generateRandomNumber, sendMessage } from '@utils'; +import { random, sendMessage } from '@utils'; export const balloonContainer = document.createElement('div'); balloonContainer.id = 'balloon-container'; -const resourceLocation = browser.runtime.getURL('resources/balloons/'); +export const balloonResourceLocation = browser.runtime.getURL( + 'resources/balloons/' +); +export const defaultBalloonFolderName = 'default'; +export const defaultBalloonResourceLocation = + balloonResourceLocation + `${defaultBalloonFolderName}/`; const buildBalloonElement = ( element: HTMLDivElement, props: { - balloonImage: HTMLImageElement; size: number; positionX: number; riseDuration: number; @@ -19,9 +23,6 @@ const buildBalloonElement = ( ) => { element.classList.add('balloon'); - // Add an image to the balloon - element.appendChild(props.balloonImage); - // Set the balloon's width and height element.style.width = props.size + 'px'; element.style.height = element.style.width; @@ -37,44 +38,62 @@ const buildBalloonElement = ( export default abstract class Balloon { public abstract readonly name: string; - public abstract getRandomDuration(): number; - private readonly element: HTMLDivElement; private readonly _popSound: HTMLAudioElement = new Audio(); protected readonly balloonImage: HTMLImageElement = document.createElement('img'); + public readonly element: HTMLDivElement; + public readonly riseDurationThreshold: [number, number] = [10000, 15000]; + public get popSound(): HTMLAudioElement { - if (!this._popSound.src) this._popSound.src = this.popSoundUrl; + if (!this._popSound.src) { + this._popSound.src = this.popSoundUrl; + } return this._popSound; } + public get balloonImageUrl(): string { - return resourceLocation + this.name + '/icon.png'; + return balloonResourceLocation + this.name + '/icon.png'; } + public get popSoundUrl(): string { - return resourceLocation + this.name + '/pop.mp3'; + return balloonResourceLocation + this.name + '/pop.mp3'; } constructor() { // Create the balloon element this.element = document.createElement('div'); + + // Add the balloon image to the balloon element + this.element.appendChild(this.balloonImage); + // Add an event listener to the balloon - this.element.addEventListener('click', this.pop.bind(this)); + this.element.addEventListener('click', this._pop.bind(this)); + + this.balloonImage.addEventListener('error', (e) => { + this.balloonImage.src = defaultBalloonResourceLocation + 'icon.png'; + }); } - public isRising() { + public isRising(): boolean { return this.element.style.animationName === 'rise'; } - public rise() { + public getRandomDuration( + duration: [number, number] = this.riseDurationThreshold + ): number { + return random(duration[0], duration[1]); + } + + public rise(): void { // Load the balloon image this.balloonImage.src = this.balloonImageUrl; // Build the balloon element buildBalloonElement(this.element, { - size: generateRandomNumber(50, 75), - balloonImage: this.balloonImage, - positionX: generateRandomNumber(5, 95), + size: random(50, 75), + positionX: random(5, 95), riseDuration: this.getRandomDuration(), onAnimationend: this.remove.bind(this), }); @@ -82,21 +101,28 @@ export default abstract class Balloon { balloonContainer.appendChild(this.element); } - public remove() { + public remove(): void { this.element.remove(); this.element.style.animationName = 'none'; } - public async pop() { + private async _pop(event: MouseEvent): Promise { // Remove the balloon this.remove(); + // Send message with the new count + sendMessage({ action: 'incrementCount' }); + // Set volume this.popSound.volume = (await storage.get('config')).popVolume / 100; // Play the pop sound - this.popSound.play(); + this.popSound.play().catch((e) => { + this.popSound.src = defaultBalloonResourceLocation + 'pop.mp3'; + this.popSound.play(); + }); - // Send message with the new count - sendMessage({ action: 'incrementCount' }); + this.pop(event); } + + public pop(event?: MouseEvent): void | Promise {} } diff --git a/src/balloons/confetti.ts b/src/balloons/confetti.ts new file mode 100644 index 00000000..3d7eab63 --- /dev/null +++ b/src/balloons/confetti.ts @@ -0,0 +1,83 @@ +import Balloon, { balloonResourceLocation } from '@/balloon'; +import { random } from '@/utils'; +import '@/../resources/balloons/confetti/confetti.css'; + +export default class Confetti extends Balloon { + public readonly name = 'confetti'; + public static readonly spawn_chance = 0.1; + + private readonly mask = document.createElement('img'); + + constructor() { + super(); + + this.element.appendChild(this.mask); + this.mask.src = balloonResourceLocation + this.name + '/mask.png'; + this.mask.style.position = 'absolute'; + this.mask.style.top = '-10px'; + this.mask.style.left = '0'; + // Give it a random rotation + this.mask.style.transform = `rotate(${Math.random() * 360}deg)`; + } + + public pop(event?: MouseEvent) { + // Get the click position + const x = event?.clientX || window.innerWidth / 2; + const y = event?.clientY || window.innerHeight / 2; + // Add an element to that position + const confetti = document.createElement('div'); + confetti.style.position = 'absolute'; + confetti.style.top = `${y}px`; + confetti.style.left = `${x}px`; + confetti.style.zIndex = '1000'; + confetti.style.pointerEvents = 'none'; + document.body.appendChild(confetti); + // Throw confetti + throwConfetti(confetti, 100); + // Remove the confetti after 2 seconds + setTimeout(() => { + confetti.remove(); + }, 2000); + } +} + +function throwConfetti(element: HTMLElement, amount: number = 100) { + const df = document.createDocumentFragment(); + const offset = 10; + const particles: HTMLElement[] = []; + for (let i = 0; i < amount; i++) { + const c = document.createElement('i'); + c.className = 'particle'; + const angle = random(360); + const radius = random(250); + let x = radius * Math.cos(angle); + let y = radius * Math.sin(angle); + c.style.cssText = ` + transform: translate3d(${x}px, ${y}px, 0) + rotate(${angle}deg); + background: hsla(${random(360)},100%,50%,1); + `; + df.appendChild(c); + particles.push(c); + } + element.appendChild(df); + + function checkBounds() { + particles.forEach((particle, index) => { + const rect = particle.getBoundingClientRect(); + if ( + rect.bottom < +offset || + rect.top > window.innerHeight - offset || + rect.left > window.innerWidth - offset || + rect.right < +offset + ) { + particle.remove(); + particles.splice(index, 1); + } + }); + if (particles.length > 0) { + requestAnimationFrame(checkBounds); + } + } + requestAnimationFrame(checkBounds); +} diff --git a/src/balloons/default.ts b/src/balloons/default.ts index 79e78c0a..c6f4afef 100644 --- a/src/balloons/default.ts +++ b/src/balloons/default.ts @@ -1,11 +1,6 @@ -import Balloon from '@/balloon'; -import { generateRandomNumber } from '@/utils'; +import Balloon, { defaultBalloonFolderName } from '@/balloon'; export default class Default extends Balloon { - public readonly name = 'default'; - public static readonly spawn_chance = 0.95; - - getRandomDuration() { - return generateRandomNumber(10000, 15000); - } + public readonly name = defaultBalloonFolderName; + public static readonly spawn_chance = 0.9; } diff --git a/src/balloons/index.ts b/src/balloons/index.ts index bd4eb513..05d885f6 100644 --- a/src/balloons/index.ts +++ b/src/balloons/index.ts @@ -1 +1,2 @@ export { default as Default } from './default'; +export { default as Confetti } from './confetti'; diff --git a/src/content/content.ts b/src/content/content.ts index edbdd305..cf83e988 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -29,8 +29,10 @@ import './style.css'; ); // Create a new balloon and make it rise - const Balloon = - weightedRandom(balloonClasses, spawnChances) || balloons.Default; + const Balloon = weightedRandom(balloonClasses, spawnChances, { + default: balloons.Default, + }); + const balloon = new Balloon(); balloon.rise(); } diff --git a/src/utils.ts b/src/utils.ts index d189f74b..fd422f79 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,8 +16,11 @@ export function minutesToMilliseconds(minutes: number) { return secondsToMilliseconds(minutes * 60); } -export function generateRandomNumber(min: number, max: number) { - return Math.random() * (max - min) + min; +export function random(max: number): number; +export function random(min: number, max: number): number; +export function random(minOrMax: number, max?: number): number { + if (max === undefined) return Math.random() * (minOrMax - 0) + 0; + return Math.random() * (max - minOrMax) + minOrMax; } export function sleep(ms: number) { @@ -58,7 +61,15 @@ export function isRunningInBackground() { return !isRunningInPopup; } -export function weightedRandom(results: T[], weights: number[]): T | null { +type WeightedRandomOptions = { + default?: T; +}; + +export function weightedRandom( + results: T[], + weights: number[], + options: WeightedRandomOptions = {} +): T | D { // Calculate the total weight const totalWeight = weights.reduce((sum, weight) => sum + weight, 0); @@ -74,14 +85,14 @@ export function weightedRandom(results: T[], weights: number[]): T | null { } } - // Return null if no result is found (shouldn't happen if the weights are correct) - return null; + // Return the default value if no result was selected or null if no default was provided + return (options.default ?? null) as D; } export async function calculateBalloonSpawnDelay() { const config = await storage.get('config'); // Generate a random delay between the min and max spawn interval - const randomDelay = generateRandomNumber( + const randomDelay = random( config.spawnInterval.min, config.spawnInterval.max ); diff --git a/tests/balloon.test.ts b/tests/balloon.test.ts index 3d246424..f1dafc0b 100644 --- a/tests/balloon.test.ts +++ b/tests/balloon.test.ts @@ -1,11 +1,11 @@ import Balloon from '@/balloon'; -import { generateRandomNumber } from '@/utils'; +import { random } from '@utils'; // Create a concrete subclass of Balloon for testing class TestBalloon extends Balloon { public readonly name = 'test'; getRandomDuration() { - return generateRandomNumber(10000, 15000); + return random(10000, 15000); } }