diff --git a/apps/games/bit-jumper/.babelrc b/apps/games/bit-jumper/.babelrc new file mode 100644 index 00000000..f2f38067 --- /dev/null +++ b/apps/games/bit-jumper/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@nx/js/babel"] +} diff --git a/apps/games/bit-jumper/.eslintrc.json b/apps/games/bit-jumper/.eslintrc.json new file mode 100644 index 00000000..3456be9b --- /dev/null +++ b/apps/games/bit-jumper/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/games/bit-jumper/.swcrc b/apps/games/bit-jumper/.swcrc new file mode 100644 index 00000000..a2d5b04f --- /dev/null +++ b/apps/games/bit-jumper/.swcrc @@ -0,0 +1,8 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript" + }, + "target": "es2016" + } +} diff --git a/apps/games/bit-jumper/index.html b/apps/games/bit-jumper/index.html new file mode 100644 index 00000000..64ed1f1c --- /dev/null +++ b/apps/games/bit-jumper/index.html @@ -0,0 +1,18 @@ + + + + + Bit Jumper + + + + + + + +
.
+ + + + \ No newline at end of file diff --git a/apps/games/bit-jumper/project.json b/apps/games/bit-jumper/project.json new file mode 100644 index 00000000..840f4434 --- /dev/null +++ b/apps/games/bit-jumper/project.json @@ -0,0 +1,9 @@ +{ + "name": "games-bit-jumper", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/games/bit-jumper/src", + "tags": [], + "// targets": "to see all targets run: nx show project games-bit-jumper --web", + "targets": {} +} diff --git a/apps/games/bit-jumper/public/assets/achievements/1000.png b/apps/games/bit-jumper/public/assets/achievements/1000.png new file mode 100644 index 00000000..90fcf64d Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/1000.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/10000.png b/apps/games/bit-jumper/public/assets/achievements/10000.png new file mode 100644 index 00000000..e867cb43 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/10000.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/15000.png b/apps/games/bit-jumper/public/assets/achievements/15000.png new file mode 100644 index 00000000..4e6aaced Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/15000.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/20000.png b/apps/games/bit-jumper/public/assets/achievements/20000.png new file mode 100644 index 00000000..d74ccdca Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/20000.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/2500.png b/apps/games/bit-jumper/public/assets/achievements/2500.png new file mode 100644 index 00000000..f8a96f2f Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/2500.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/5000.png b/apps/games/bit-jumper/public/assets/achievements/5000.png new file mode 100644 index 00000000..17dfa1e6 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/5000.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/7500.png b/apps/games/bit-jumper/public/assets/achievements/7500.png new file mode 100644 index 00000000..3afa0f48 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/7500.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/balloon.png b/apps/games/bit-jumper/public/assets/achievements/balloon.png new file mode 100644 index 00000000..3a3da9c6 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/balloon.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/chomper.png b/apps/games/bit-jumper/public/assets/achievements/chomper.png new file mode 100644 index 00000000..4e3221c3 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/chomper.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/floater.png b/apps/games/bit-jumper/public/assets/achievements/floater.png new file mode 100644 index 00000000..a588c407 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/floater.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/helicopter.png b/apps/games/bit-jumper/public/assets/achievements/helicopter.png new file mode 100644 index 00000000..7af11b01 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/helicopter.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/rocket.png b/apps/games/bit-jumper/public/assets/achievements/rocket.png new file mode 100644 index 00000000..4f638607 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/rocket.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/smasher.png b/apps/games/bit-jumper/public/assets/achievements/smasher.png new file mode 100644 index 00000000..ab103edb Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/smasher.png differ diff --git a/apps/games/bit-jumper/public/assets/achievements/spiker.png b/apps/games/bit-jumper/public/assets/achievements/spiker.png new file mode 100644 index 00000000..45d8716b Binary files /dev/null and b/apps/games/bit-jumper/public/assets/achievements/spiker.png differ diff --git a/apps/games/bit-jumper/public/assets/audio/balloon_end.mp3 b/apps/games/bit-jumper/public/assets/audio/balloon_end.mp3 new file mode 100644 index 00000000..bd2bbd04 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/balloon_end.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/balloon_start.mp3 b/apps/games/bit-jumper/public/assets/audio/balloon_start.mp3 new file mode 100644 index 00000000..23315e03 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/balloon_start.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/break.mp3 b/apps/games/bit-jumper/public/assets/audio/break.mp3 new file mode 100644 index 00000000..ac260313 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/break.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/death.mp3 b/apps/games/bit-jumper/public/assets/audio/death.mp3 new file mode 100644 index 00000000..e5c95f4f Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/death.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/helicopter_end.mp3 b/apps/games/bit-jumper/public/assets/audio/helicopter_end.mp3 new file mode 100644 index 00000000..1320b715 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/helicopter_end.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/helicopter_start.mp3 b/apps/games/bit-jumper/public/assets/audio/helicopter_start.mp3 new file mode 100644 index 00000000..90aefc8f Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/helicopter_start.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/jump.mp3 b/apps/games/bit-jumper/public/assets/audio/jump.mp3 new file mode 100644 index 00000000..544e60b5 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/jump.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/menu.mp3 b/apps/games/bit-jumper/public/assets/audio/menu.mp3 new file mode 100644 index 00000000..31a613ef Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/menu.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/rocket_end.mp3 b/apps/games/bit-jumper/public/assets/audio/rocket_end.mp3 new file mode 100644 index 00000000..24411cbd Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/rocket_end.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/rocket_start.mp3 b/apps/games/bit-jumper/public/assets/audio/rocket_start.mp3 new file mode 100644 index 00000000..0126a67c Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/rocket_start.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/smash.mp3 b/apps/games/bit-jumper/public/assets/audio/smash.mp3 new file mode 100644 index 00000000..7602ab7f Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/smash.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/audio/spring.mp3 b/apps/games/bit-jumper/public/assets/audio/spring.mp3 new file mode 100644 index 00000000..f20719d4 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/audio/spring.mp3 differ diff --git a/apps/games/bit-jumper/public/assets/fonts/frog-block.ttf b/apps/games/bit-jumper/public/assets/fonts/frog-block.ttf new file mode 100644 index 00000000..0e487a24 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/fonts/frog-block.ttf differ diff --git a/apps/games/bit-jumper/public/assets/fonts/license.txt b/apps/games/bit-jumper/public/assets/fonts/license.txt new file mode 100644 index 00000000..354f1e04 --- /dev/null +++ b/apps/games/bit-jumper/public/assets/fonts/license.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/apps/games/bit-jumper/public/assets/icons/back.png b/apps/games/bit-jumper/public/assets/icons/back.png new file mode 100644 index 00000000..2d1f0458 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/icons/back.png differ diff --git a/apps/games/bit-jumper/public/assets/icons/heart.png b/apps/games/bit-jumper/public/assets/icons/heart.png new file mode 100644 index 00000000..c56a338d Binary files /dev/null and b/apps/games/bit-jumper/public/assets/icons/heart.png differ diff --git a/apps/games/bit-jumper/public/assets/icons/sound_off.png b/apps/games/bit-jumper/public/assets/icons/sound_off.png new file mode 100644 index 00000000..ed55f95a Binary files /dev/null and b/apps/games/bit-jumper/public/assets/icons/sound_off.png differ diff --git a/apps/games/bit-jumper/public/assets/icons/sound_on.png b/apps/games/bit-jumper/public/assets/icons/sound_on.png new file mode 100644 index 00000000..886cab7e Binary files /dev/null and b/apps/games/bit-jumper/public/assets/icons/sound_on.png differ diff --git a/apps/games/bit-jumper/public/assets/media/banner.png b/apps/games/bit-jumper/public/assets/media/banner.png new file mode 100644 index 00000000..c8e3009d Binary files /dev/null and b/apps/games/bit-jumper/public/assets/media/banner.png differ diff --git a/apps/games/bit-jumper/public/assets/media/thumbnail.png b/apps/games/bit-jumper/public/assets/media/thumbnail.png new file mode 100644 index 00000000..ee04edf4 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/media/thumbnail.png differ diff --git a/apps/games/bit-jumper/public/assets/particles/dot.png b/apps/games/bit-jumper/public/assets/particles/dot.png new file mode 100644 index 00000000..668fe4fc Binary files /dev/null and b/apps/games/bit-jumper/public/assets/particles/dot.png differ diff --git a/apps/games/bit-jumper/public/assets/particles/ring.png b/apps/games/bit-jumper/public/assets/particles/ring.png new file mode 100644 index 00000000..ed4590ab Binary files /dev/null and b/apps/games/bit-jumper/public/assets/particles/ring.png differ diff --git a/apps/games/bit-jumper/public/assets/sprites/infinite_jumper.png b/apps/games/bit-jumper/public/assets/sprites/infinite_jumper.png new file mode 100644 index 00000000..191f5247 Binary files /dev/null and b/apps/games/bit-jumper/public/assets/sprites/infinite_jumper.png differ diff --git a/apps/games/bit-jumper/public/favicon.ico b/apps/games/bit-jumper/public/favicon.ico new file mode 100644 index 00000000..317ebcb2 Binary files /dev/null and b/apps/games/bit-jumper/public/favicon.ico differ diff --git a/apps/games/bit-jumper/public/sources.txt b/apps/games/bit-jumper/public/sources.txt new file mode 100644 index 00000000..3faf2c37 --- /dev/null +++ b/apps/games/bit-jumper/public/sources.txt @@ -0,0 +1,3 @@ +https://i-am-44.itch.io/free-1-bit-infinite-jumper-assets +https://polyducks.itch.io/frogblock +https://coloralpha.itch.io/50-menu-interface-sfx \ No newline at end of file diff --git a/apps/games/bit-jumper/src/lib/data.ts b/apps/games/bit-jumper/src/lib/data.ts new file mode 100644 index 00000000..63ac1b4f --- /dev/null +++ b/apps/games/bit-jumper/src/lib/data.ts @@ -0,0 +1,15 @@ +export const DEPTHS = { + UI: 100, + PLAYER: 10, + ENEMY: 7, + PLATFORM: 5, + POWER_UP: 3, +}; +export const OUT_OF_BOUNDS = { x: -100, y: -100 }; + +export enum StorageKey { + HIGH_SCORE = 'high_score', + MUTE = 'mute-sound', +} + +export const VERSION = 'v1.0.2'; diff --git a/apps/games/bit-jumper/src/lib/objects/collisions.ts b/apps/games/bit-jumper/src/lib/objects/collisions.ts new file mode 100644 index 00000000..b4b285ca --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/collisions.ts @@ -0,0 +1,123 @@ +import { Enemies, Enemy } from './enemies'; +import { Particles } from './particles'; +import { BreakingPlatform, Platform, Platforms, Spring } from './platforms'; +import { Player } from './player'; +import { Balloon, Helicopter, PowerUp, PowerUps, Rocket } from './power-ups'; + +export class Collisions { + scene: Phaser.Scene; + powerUps: PowerUps; + constructor( + scene: Phaser.Scene, + player: Player, + platforms: Platforms, + powerUps: PowerUps, + enemies: Enemies + ) { + this.scene = scene; + scene.physics.add.collider( + player, + platforms, + this.onPlatformsCollide, + undefined, + this + ); + + scene.physics.add.overlap( + player, + powerUps, + this.onPowerUpsOverlap, + undefined, + this + ); + + scene.physics.add.overlap( + player, + enemies, + this.onEnemiesOverlap, + undefined, + this + ); + } + + onPlatformsCollide( + a: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile, + b: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile + ) { + if (a instanceof Player) { + if (!a.jumping) { + if (b instanceof Platform) { + if (a.body.touching.down) { + if (b instanceof BreakingPlatform) { + const { x, y } = b.center(); + this.scene.sound.play('break'); + this.scene.events.emit( + Particles.DOTS, + BreakingPlatform.PARTICLE_QUANTITY, + x, + y + ); + b.terminate(); + } else if (b instanceof Spring) { + this.scene.sound.play('spring'); + b.launch(a); + } else { + this.scene.sound.play('jump'); + a.jump(); + } + } + } + } + } + } + + onPowerUpsOverlap( + a: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile, + b: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile + ) { + if (a instanceof Player) { + if (b instanceof PowerUp) { + if (a.powering) return; + + if (b instanceof Helicopter) { + a.helicopter(); + } else if (b instanceof Balloon) { + a.balloon(); + } else if (b instanceof Rocket) { + a.rocket(); + } + + b.terminate(); + } + } + } + + onEnemiesOverlap( + a: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile, + b: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile + ) { + if (a instanceof Player) { + if (b instanceof Enemy) { + if (a.powering) { + this.scene.events.emit( + Particles.IMPACT_DEATH, + Enemy.PARTICLE_QUANTITY, + b.x, + b.y + ); + this.scene.sound.play('smash'); + a.emit(Player.EVENT_SMASH_ENEMY, b.type); + b.terminate(); + } else { + this.scene.events.emit( + Particles.IMPACT_DEATH, + Player.IMPACT_DEATH_PARTICLE_QUANTITY, + a.x, + a.y + ); + a.explode(b.type); + } + } + } + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/controller.ts b/apps/games/bit-jumper/src/lib/objects/controller.ts new file mode 100644 index 00000000..d06772ff --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/controller.ts @@ -0,0 +1,75 @@ +import { Particles } from './particles'; +import { Player } from './player'; + +export class Controller { + static DEATH_OFFSET = 5; + static TOUCH_OFFSET = 8; + scene: Phaser.Scene; + player: Player; + enabled: boolean; + + constructor(scene: Phaser.Scene, player: Player) { + this.scene = scene; + this.player = player; + this.enabled = false; + } + + enable() { + this.enabled = true; + } + + update() { + if (!this.enabled) return; + + const width = this.scene.sys.game.canvas.width; + if (this.scene.input.keyboard?.addKey('LEFT').isDown) { + this.player.moveLeft(); + } else if (this.scene.input.keyboard?.addKey('RIGHT').isDown) { + this.player.moveRight(); + } else { + this.player.body.setVelocityX(0); + } + + // detect if user is pressing the left side of the screen + if ( + this.scene.input.pointer1.isDown && + this.scene.input.pointer1.x < width / 2 - Controller.TOUCH_OFFSET + ) { + this.player.moveLeft(); + } else if ( + this.scene.input.pointer1.isDown && + this.scene.input.pointer1.x > width / 2 + Controller.TOUCH_OFFSET + ) { + this.player.moveRight(); + } + + if (this.player.body.velocity.y > 0) { + this.player.fall(); + } + + // if the character goes to far to the left, reset it to the right + if (this.player.x < 0 - this.player.width) { + this.player.x = this.scene.sys.game.canvas.width; + } else if (this.player.x > this.scene.sys.game.canvas.width) { + this.player.x = 0 - this.player.width; + } + + // if the player goes too low, explode them. + const camera = this.scene.cameras.main; + if ( + this.player.y > + this.scene.sys.game.canvas.height + + camera.scrollY + + Controller.DEATH_OFFSET + ) { + if (this.player.explode('fall')) { + this.scene.events.emit( + Particles.FLOOR_DEATH, + Player.FLOOR_DEATH_PARTICLE_QUANTITY, + this.player.x, + this.player.y + ); + } + } + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/enemies.ts b/apps/games/bit-jumper/src/lib/objects/enemies.ts new file mode 100644 index 00000000..003e2ce5 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/enemies.ts @@ -0,0 +1,348 @@ +import { assertNever } from '@worksheets/util/errors'; +import { once } from '@worksheets/util/functions'; + +import { DEPTHS, OUT_OF_BOUNDS } from '../data'; +import { EnemyType } from '../types'; +import { Observer } from './observer'; + +export class Enemies extends Phaser.GameObjects.Group { + static SPAWN_CHANCE = 7; + static RECLAIM_OFFSET = 100; + static PREFILL_QUANTITY = 20; + observer: Observer; + recycled: Record; + constructor(scene: Phaser.Scene) { + super(scene); + this.recycled = { + spiker: [], + chomper: [], + floater: [], + }; + this.createAnimations(); + this.prefill(); + } + + prefill() { + // create 10 of each enemy type + for (let i = 0; i < Enemies.PREFILL_QUANTITY; i++) { + this.recycled.spiker.push(new Spiker(this)); + this.recycled.chomper.push(new Chomper(this)); + this.recycled.floater.push(new Floater(this)); + } + } + + spawn(x: number, y: number) { + this.spawnType(x, y, this.randomKey()); + } + + spawnType(x: number, y: number, type: EnemyType) { + const enemy = this.getOrCreate(type); + this.add(enemy.place(x, y)); + } + + createAnimations() { + if (!this.scene.anims.get('spike')) { + this.scene.anims.create({ + key: 'spike', + frames: this.scene.anims.generateFrameNumbers('jumper_spritesheet', { + start: 60, + end: 62, + }), + frameRate: 4, + }); + } + } + + update() { + this.children.iterate((enemy: Phaser.GameObjects.GameObject) => { + if (enemy instanceof Enemy) { + const camera = this.scene.cameras.main; + if (enemy.y > camera.scrollY + camera.height + Enemies.RECLAIM_OFFSET) { + enemy.terminate(); + } + } + return true; + }); + } + + getOrCreate(key: EnemyType) { + const category = this.recycled[key]; + if (category.length) { + return category.pop() as Enemy; + } else { + const EnemyConstructor = this.pickConstructor(key); + return new EnemyConstructor(this); + } + } + + randomKey() { + const keys = Object.keys(this.recycled) as EnemyType[]; + return Phaser.Math.RND.pick(keys); + } + + pickConstructor(key: EnemyType) { + switch (key) { + case 'spiker': + return Spiker; + case 'chomper': + return Chomper; + case 'floater': + return Floater; + default: + throw assertNever(key); + } + } +} + +export abstract class Enemy extends Phaser.Physics.Arcade.Sprite { + static PARTICLE_QUANTITY = 25; + declare type: EnemyType; + enemies: Enemies; + body: Phaser.Physics.Arcade.Body; + constructor(enemies: Enemies) { + super( + enemies.scene, + OUT_OF_BOUNDS.x, + OUT_OF_BOUNDS.y, + 'jumper_spritesheet', + 0 + ); + this.enemies = enemies; + enemies.scene.add.existing(this); + enemies.scene.physics.add.existing(this); + this.setBodySize(12, 12); + this.setDepth(DEPTHS.ENEMY); + if (!(this.body instanceof Phaser.Physics.Arcade.Body)) { + throw new Error( + 'Expected body to be an instance of Phaser.Physics.Arcade.Body' + ); + } + this.body.setAllowGravity(false); + this.body.setImmovable(true); + } + + place(x: number, y: number) { + this.body.checkCollision.none = false; + this.setVisible(true); + this.x = x; + this.y = y; + this.animate(); + return this; + } + + terminate() { + this.body.checkCollision.none = true; + this.setVisible(false); + this.enemies.remove(this); + this.enemies.recycled[this.type].push(this); + } + + abstract animate(): void; +} + +export class Spiker extends Enemy { + type: EnemyType = 'spiker'; + bouncing?: Phaser.Tweens.Tween; + blinking?: Phaser.Time.TimerEvent; + wandering?: Phaser.Tweens.Tween; + + constructor(enemies: Enemies) { + super(enemies); + this.setTexture('jumper_spritesheet', 60); + } + + animate() { + this.blink(); + this.wander(); + this.bounce(); + } + + terminate() { + super.terminate(); + + this.wandering?.stop(); + this.wandering = undefined; + this.bouncing?.stop(); + this.bouncing = undefined; + this.blinking?.remove(); + this.blinking = undefined; + } + + private bounce() { + this.bouncing = this.scene.tweens.add({ + targets: this, + y: this.y + Phaser.Math.RND.pick([-10, -5, 5, 10]), + duration: 1000, + yoyo: true, + repeat: -1, + }); + } + + private wander() { + let lastX = 1; + this.wandering = this.scene.tweens.add({ + targets: this, + x: this.x + 50, + duration: 3000, + yoyo: true, + repeat: -1, + onUpdate: () => { + if (lastX !== this.x) { + this.flipX = this.x < lastX; + lastX = this.x; + } + }, + }); + } + + private blink(waitFor = 2000) { + this.blinking = this.scene.time.delayedCall( + waitFor, + () => { + this.anims.play({ + key: 'spike', + repeat: 1, + }); + + this.blink(Phaser.Math.Between(1000, 5000)); + }, + [], + this + ); + } +} + +export class Chomper extends Enemy { + type: EnemyType = 'chomper'; + direction: boolean; + tween?: Phaser.Tweens.Tween; + constructor(enemies: Enemies) { + super(enemies); + this.direction = false; + this.pickSprite(); + } + + pickSprite() { + this.setTexture('jumper_spritesheet', this.direction ? 63 : 64); + } + + animate() { + this.wander(); + } + + terminate() { + super.terminate(); + + this.tween?.stop(); + this.tween = undefined; + } + + wander() { + this.pickSprite(); + this.tween = this.scene.tweens.add({ + targets: this, + x: this.x + (this.direction ? -25 : +25), + duration: 3000, + onComplete: () => { + this.direction = !this.direction; + this.chomp(); + }, + }); + } + + chomp() { + this.setTexture('jumper_spritesheet', 65); + + const openMouth = once(() => this.setTexture('jumper_spritesheet', 66)); + + const originalX = this.x; + this.tween = this.scene.tweens.add({ + targets: this, + x: { from: this.x - 1, to: this.x + 1 }, + duration: 50, + yoyo: true, + repeat: 14, + delay: 1000, + onUpdate: (t) => { + const v = t.progress; + if (v > 0.1) { + openMouth(); + } + }, + onComplete: () => { + this.x = originalX; + this.wander(); + }, + }); + } +} + +export class Floater extends Enemy { + type: EnemyType = 'floater'; + tween?: Phaser.Tweens.Tween; + + constructor(enemies: Enemies) { + super(enemies); + this.setTexture('jumper_spritesheet', 67); + } + + animate() { + this.moveRandomly(); + } + + terminate() { + super.terminate(); + + this.tween?.stop(); + this.tween = undefined; + } + + moveRandomly() { + // pick a random point to float to. + const offset = 20; + const x = Phaser.Math.Between( + offset, + this.scene.sys.game.canvas.width - offset + ); + const y = Phaser.Math.Between(this.y - 50, this.y + 50); + this.tween = this.scene.tweens.add({ + targets: this, + x, + y, + duration: 3000, + onComplete: () => { + this.laugh(); + }, + }); + } + + laugh() { + const openMouth = once(() => { + this.setTexture('jumper_spritesheet', 69); + }); + const closeMouth = once(() => { + this.setTexture('jumper_spritesheet', 68); + }); + + this.setTexture('jumper_spritesheet', 68); + // bounce up and down + this.tween = this.scene.tweens.add({ + targets: this, + y: this.y + 3, + duration: 150, + yoyo: true, + repeat: 6, + delay: 50, + onUpdate: (t) => { + if (t.progress > 0.9) { + closeMouth(); + } else if (t.progress > 0.1) { + openMouth(); + } + }, + onComplete: () => { + this.moveRandomly(); + }, + }); + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/loading-bar.ts b/apps/games/bit-jumper/src/lib/objects/loading-bar.ts new file mode 100644 index 00000000..da9b91a1 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/loading-bar.ts @@ -0,0 +1,52 @@ +export type ProgressBarOptions = { + width: number; + height: number; + background?: number; + progress?: number; +}; + +export class ProgressBar extends Phaser.GameObjects.Container { + background: Phaser.GameObjects.Graphics; + progress: Phaser.GameObjects.Graphics; + options: ProgressBarOptions; + + constructor( + scene: Phaser.Scene, + x: number, + y: number, + options: ProgressBarOptions + ) { + super(scene, x, y); + this.options = options; + + this.createBars(); + this.scene.add.existing(this); + } + + createBars() { + this.background = new Phaser.GameObjects.Graphics(this.scene); + this.background.fillStyle(this.options.background ?? 0xffffff, 1); + this.background.fillRect( + -this.options.width / 2 - 1, + -this.options.height / 2 - 1, + this.options.width + 2, + this.options.height + 2 + ); + + this.add(this.background); + + this.progress = new Phaser.GameObjects.Graphics(this.scene); + this.add(this.progress); + } + + updateProgress(value: number) { + this.progress.clear(); + this.progress.fillStyle(this.options.background ?? 0x000000, 1); + this.progress.fillRect( + -this.options.width / 2, + -this.options.height / 2, + this.options.width * value, + this.options.height + ); + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/observer.ts b/apps/games/bit-jumper/src/lib/objects/observer.ts new file mode 100644 index 00000000..d1935c75 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/observer.ts @@ -0,0 +1,53 @@ +import { Player } from './player'; + +export class Observer { + static POWER_UP_SCREEN_OFFSET = 130; + static DEFAULT_SCREEN_OFFSET = 90; + static DEFAULT_STARTING_APEX = 60; + static DEFAULT_SCREEN_INTERPOLATION = 0.05; + static EVENT_APEX_CHANGE = 'apex-change'; + apex: number; + scene: Phaser.Scene; + main: Phaser.Cameras.Scene2D.Camera; + player: Player; + screenOffset: number; + interpolation: number; + events: Phaser.Events.EventEmitter; + + constructor(scene: Phaser.Scene, player: Player) { + this.events = new Phaser.Events.EventEmitter(); + this.scene = scene; + this.main = scene.cameras.main; + this.player = player; + this.apex = Observer.DEFAULT_STARTING_APEX; + this.screenOffset = Observer.DEFAULT_SCREEN_OFFSET; + this.interpolation = Observer.DEFAULT_SCREEN_INTERPOLATION; + + this.player.on(Player.EVENT_POWER_UP_START, () => { + this.screenOffset = Observer.POWER_UP_SCREEN_OFFSET; + }); + + this.player.on(Player.EVENT_POWER_UP_OVER, () => { + this.screenOffset = Observer.DEFAULT_SCREEN_OFFSET; + }); + } + + cleanup() { + this.player.off(Player.EVENT_POWER_UP_START); + this.player.off(Player.EVENT_POWER_UP_OVER); + } + + update() { + if (this.player.y < this.apex) { + this.apex = this.player.y; + + this.main.scrollY = Phaser.Math.Linear( + this.main.scrollY, + this.apex - this.screenOffset, + this.interpolation + ); + + this.events.emit(Observer.EVENT_APEX_CHANGE, this.apex); + } + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/particles.ts b/apps/games/bit-jumper/src/lib/objects/particles.ts new file mode 100644 index 00000000..80193833 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/particles.ts @@ -0,0 +1,129 @@ +export class Particles { + static FLOOR_DEATH = 'floor_death'; + static IMPACT_DEATH = 'impact_death'; + static DOTS = 'explode_dots'; + static RINGS = 'explode_rings'; + static EXHAUST = 'rocket_exhaust'; + constructor(scene: Phaser.Scene, x: number, y: number) { + const exhaust = new Phaser.GameObjects.Particles.ParticleEmitter( + scene, + x, + y, + 'dot', + { + lifespan: 1000, + speedX: { min: -30, max: 30 }, + scale: { min: 0.1, max: 2.5 }, + alpha: 1, + emitting: false, + } + ); + + const dots = new Phaser.GameObjects.Particles.ParticleEmitter( + scene, + x, + y, + 'dot', + { + lifespan: 1000, + speedX: { min: -50, max: 50 }, + speedY: { min: -10, max: -40 }, + scale: { min: 0.5, max: 2 }, + alpha: { start: 1, end: 0 }, + gravityY: 200, + emitting: false, + } + ); + + const rings = new Phaser.GameObjects.Particles.ParticleEmitter( + scene, + x, + y, + 'ring', + { + lifespan: 150, + scale: { start: 0, end: 1.25 }, + alpha: { start: 1, end: 0 }, + emitting: false, + } + ); + + const impact = new Phaser.GameObjects.Particles.ParticleEmitter( + scene, + x, + y, + 'dot', + { + lifespan: 3000, + speedX: { min: -100, max: 100 }, + speedY: { min: -150, max: 50 }, + scale: { min: 0.5, max: 2 }, + alpha: { start: 1, end: 0 }, + gravityY: 100, + emitting: false, + } + ); + const floor = new Phaser.GameObjects.Particles.ParticleEmitter( + scene, + x, + y, + 'dot', + { + lifespan: 3000, + speedX: { min: -100, max: 100 }, + speedY: { min: -300, max: 0 }, + scale: { min: 0.5, max: 2 }, + alpha: { start: 1, end: 0 }, + gravityY: 200, + emitting: false, + } + ); + + scene.add.existing(dots); + scene.add.existing(rings); + scene.add.existing(exhaust); + scene.add.existing(impact); + scene.add.existing(floor); + + scene.events.on( + Particles.DOTS, + (quantity: number, x: number, y: number) => { + dots.explode(quantity, x, y); + } + ); + + scene.events.on(Particles.RINGS, (x: number, y: number) => { + rings.emitParticleAt(x, y); + }); + + scene.events.on( + Particles.EXHAUST, + (quantity: number, x: number, y: number) => { + exhaust.explode(quantity, x, y); + } + ); + + scene.events.on( + Particles.IMPACT_DEATH, + (quantity: number, x: number, y: number) => { + impact.explode(quantity, x, y); + } + ); + + scene.events.on( + Particles.FLOOR_DEATH, + (quantity: number, x: number, y: number) => { + floor.explode(quantity, x, y); + } + ); + + scene.events.on('shutdown', () => { + scene.events.off(Particles.DOTS); + scene.events.off(Particles.RINGS); + scene.events.off(Particles.EXHAUST); + scene.events.off(Particles.IMPACT_DEATH); + scene.events.off(Particles.FLOOR_DEATH); + scene.events.off('shutdown'); + }); + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/platforms.ts b/apps/games/bit-jumper/src/lib/objects/platforms.ts new file mode 100644 index 00000000..b34e412d --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/platforms.ts @@ -0,0 +1,347 @@ +import { assertNever } from '@worksheets/util/errors'; + +import { DEPTHS, OUT_OF_BOUNDS } from '../data'; +import { PlatformType } from '../types'; +import { Observer } from './observer'; +import { Player } from './player'; + +export class PlatformsPool { + static PREFILL_QUANTITY = { + basic: 100, + spring: 20, + sliding: 20, + breaking: 20, + floor: 1, + }; + scene: Phaser.Scene; + recycled: Record; + constructor(scene: Phaser.Scene) { + this.scene = scene; + this.recycled = { + basic: [], + spring: [], + sliding: [], + breaking: [], + floor: [], + }; + } + + terminate(platform: Platform) { + const type = platform.type as PlatformType; + const exists = this.recycled[type].some((p) => p.id === platform.id); + if (exists) return; + + this.recycled[type].push(platform); + } + + get(type: PlatformType) { + const category = this.recycled[type]; + if (category) { + const platform = category.pop(); + if (platform) { + return platform; + } + } + + return this.create(type); + } + + prefill() { + for (const key in this.recycled) { + const k = key as PlatformType; + for (let i = 0; i < PlatformsPool.PREFILL_QUANTITY[k]; i++) { + this.create(k).terminate(); + } + } + } + + private create(type: PlatformType) { + const PlatformConstructor = this.pickConstructor(type); + return new PlatformConstructor( + this.scene, + this, + OUT_OF_BOUNDS.x, + OUT_OF_BOUNDS.y + ); + } + + private pickConstructor(type: PlatformType) { + switch (type) { + case 'basic': + return Platform; + case 'spring': + return Spring; + case 'sliding': + return SlidingPlatform; + case 'breaking': + return BreakingPlatform; + case 'floor': + return Floor; + default: + throw assertNever(type); + } + } + + group() { + // return the count of each type of platform + return Object.keys(this.recycled).reduce( + (acc, type) => ({ + ...acc, + [type]: this.recycled[type as PlatformType].length, + }), + { basic: 0, spring: 0, sliding: 0, breaking: 0, floor: 0 } + ); + } + + count() { + return Object.keys(this.recycled).reduce( + (acc, type) => acc + this.recycled[type as PlatformType].length, + 0 + ); + } +} + +export class Platforms extends Phaser.GameObjects.Group { + static SPAWN_EVENT = 'spawn'; + static SPAWN_RATE = 2; + static GAP = 12; + static SCREEN_OFFSET = 200; + + observer: Observer; + scene: Phaser.Scene; + height: number; + width: number; + pool: PlatformsPool; + constructor(scene: Phaser.Scene, observer: Observer) { + super(scene); + this.scene = scene; + this.observer = observer; + this.height = 0; + this.pool = new PlatformsPool(scene); + + const { width, height } = scene.sys.game.canvas; + this.width = width; + this.height = height; + + this.initialize(); + + this.spawnPlatforms(height - 100); + } + + private initialize() { + this.pool.prefill(); + const { width, height } = this; + this.add(this.pool.get('floor').place(width / 2, height)); + this.add(this.pool.get('basic').place(width / 2, height / 1.25)); + this.add(this.pool.get('spring').place(width / 2, height / 1.25)); + } + + private spawnPlatforms(height: number) { + for (let i = 0; i < Platforms.SPAWN_RATE; i++) { + const y = + height - + Phaser.Math.Between(Platforms.GAP * 1, Platforms.GAP * 1.5) * i; + const x = Phaser.Math.Between(0, this.width); + this.spawnPlatform(x, y); + this.height = y; + } + } + + private spawnPlatform(x: number, y: number) { + const type = Phaser.Math.Between(0, 100); + const generator = (type: PlatformType) => + this.add(this.pool.get(type).place(x, y)); + if (type < 15) { + generator('sliding'); + } else if (type < 25) { + generator('breaking'); + } else if (type < 30) { + generator('basic'); + generator('spring'); + } else { + generator('basic'); + this.emit(Platforms.SPAWN_EVENT, x, y); + } + } + + update() { + if (this.observer.apex < this.height + Platforms.SCREEN_OFFSET) { + this.spawnPlatforms(this.height - Platforms.GAP); + } + // iterate over all children and check if they are out of bounds + const lowerBound = + this.observer.main.scrollY + this.scene.sys.game.canvas.height; + this.children.iterate((platform) => { + if (platform instanceof Platform) { + if (platform.y - Platforms.SCREEN_OFFSET > lowerBound) { + this.remove(platform); + platform.terminate(); + } + } + return true; + }); + } +} + +export class Platform extends Phaser.Physics.Arcade.Sprite { + type: PlatformType = 'basic'; + pool: PlatformsPool; + body: Phaser.Physics.Arcade.Body; + id: string; + constructor(scene: Phaser.Scene, pool: PlatformsPool, x: number, y: number) { + super(scene, x, y, 'jumper_spritesheet', 27); + this.pool = pool; + this.id = Phaser.Math.RND.uuid(); + scene.add.existing(this); + scene.physics.add.existing(this); + this.setBodySize(8, 2); + this.setDepth(DEPTHS.PLATFORM); + if (!(this.body instanceof Phaser.Physics.Arcade.Body)) { + throw new Error( + 'Expected body to be an instance of Phaser.Physics.Arcade.Body' + ); + } + this.body.setImmovable(true); + this.body.setAllowGravity(false); + + this.body.checkCollision.down = false; + this.body.checkCollision.left = false; + this.body.checkCollision.right = false; + } + + place(x: number, y: number) { + this.x = x; + this.y = y; + return this; + } + + terminate() { + this.pool.terminate(this); + } +} + +export class Floor extends Platform { + type: PlatformType = 'floor'; + constructor(scene: Phaser.Scene, pool: PlatformsPool, x: number, y: number) { + super(scene, pool, x, y); + this.scaleX = 20; + } +} + +export class SlidingPlatform extends Platform { + type: PlatformType = 'sliding'; + tween?: Phaser.Tweens.Tween; + constructor(scene: Phaser.Scene, pool: PlatformsPool, x: number, y: number) { + super(scene, pool, x, y); + this.setTexture('jumper_spritesheet', 29); + } + place(x: number, y: number) { + super.place(x, y); + + this.tween = this.scene.tweens.add({ + targets: this, + x: x + Phaser.Math.RND.pick([-30, 30]), + ease: 'Linear', + duration: 1500, + yoyo: true, + repeat: -1, + }); + + return this; + } + + terminate() { + super.terminate(); + if (this.tween) { + this.tween.stop(); + this.tween = undefined; + } + } +} + +export class BreakingPlatform extends Platform { + static PARTICLE_QUANTITY = 10; + type: PlatformType = 'breaking'; + breaking = false; + constructor(scene: Phaser.Scene, pool: PlatformsPool, x: number, y: number) { + super(scene, pool, x, y); + this.setTexture('jumper_spritesheet', 28); + } + + place(x: number, y: number) { + super.place(x, y); + this.breaking = false; + this.body.checkCollision.none = false; + this.setVisible(true); + this.setActive(true); + return this; + } + + terminate() { + super.terminate(); + if (!this.breaking) { + this.breaking = true; + this.setVisible(false); + this.setActive(false); + this.body.checkCollision.none = true; + } + } + + /** The middle of the square for particle emission */ + center() { + return { x: this.x, y: this.y }; + } +} + +export class Spring extends Platform { + type: PlatformType = 'spring'; + constructor(scene: Phaser.Scene, pool: PlatformsPool, x: number, y: number) { + super(scene, pool, x, y); + this.setBodySize(6, 2); + this.setOffset(5, 3); + this.setTexture('jumper_spritesheet', 19); + this.setDepth(DEPTHS.PLATFORM + 1); + + if (scene.anims.get('spring') === undefined) { + scene.anims.create({ + key: 'spring', + frames: scene.anims.generateFrameNumbers('jumper_spritesheet', { + start: 17, + end: 19, + }), + frameRate: 24, + }); + } + } + + launch(player: Player) { + this.playReverse({ + key: 'spring', + yoyo: true, + }); + + player.jump(1.5); + player.on('jump', () => { + this.body.checkCollision.none = true; + this.body.setAllowGravity(true); + this.setVelocityY(-50); + + this.scene.time.delayedCall(1000, () => { + this.terminate(); + }); + player.off('jump'); + }); + } + + terminate() { + super.terminate(); + this.body.setAllowGravity(false); + this.setVelocity(0); + } + + place(x: number, y: number) { + super.place(x, y); + this.body.checkCollision.none = false; + return this; + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/player.ts b/apps/games/bit-jumper/src/lib/objects/player.ts new file mode 100644 index 00000000..eb7cb74a --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/player.ts @@ -0,0 +1,223 @@ +import { DEPTHS } from '../data'; +import { DeathReason, PowerUpType } from '../types'; +import { Particles } from './particles'; +import { Balloon, Helicopter, Rocket } from './power-ups'; + +export class Player extends Phaser.Physics.Arcade.Sprite { + static EVENT_POWER_UP_START = 'power-up-start'; + static EVENT_POWER_UP_OVER = 'power-up-over'; + static EVENT_DEATH = 'death'; + static EVENT_SMASH_ENEMY = 'smash-enemy'; + static IMPACT_DEATH_PARTICLE_QUANTITY = 50; + static FLOOR_DEATH_PARTICLE_QUANTITY = 150; + static FLASH_DURATION = 1000; + static FLASH_REPETITIONS = 5; + jumpLag = 50; + speed = 75; + power = 175; + balloonSpeed = 100; + balloonDuration = 3000; + helicopterSpeed = 125; + helicopterDuration = 3500; + rocketDuration = 4000; + rocketSpeed = 250; + body: Phaser.Physics.Arcade.Body; + jumping = false; + powering?: PowerUpType = undefined; + dead = false; + + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'jumper_spritesheet', 0); + scene.add.existing(this); + scene.physics.add.existing(this); + this.setBodySize(8, 8); + this.setDepth(DEPTHS.PLAYER); + if (!(this.body instanceof Phaser.Physics.Arcade.Body)) { + throw new Error( + 'Expected body to be an instance of Phaser.Physics.Arcade.Body' + ); + } + + this.createAnimations(); + } + + createAnimations() { + if (!this.anims.get('balloon')) { + this.anims.create({ + key: 'balloon', + frames: this.anims.generateFrameNumbers('jumper_spritesheet', { + start: 10, + end: 13, + }), + frameRate: 12, + }); + } + + if (!this.anims.get('helicopter')) { + this.anims.create({ + key: 'helicopter', + frames: this.anims.generateFrameNumbers('jumper_spritesheet', { + start: 20, + end: 24, + }), + frameRate: 36, + }); + } + + if (!this.anims.get('rocket')) { + this.anims.create({ + key: 'rocket', + frames: this.anims.generateFrameNumbers('jumper_spritesheet', { + start: 30, + end: 34, + }), + frameRate: 24, + }); + } + } + + fall() { + if (this.jumping) return; + this.setTexture('jumper_spritesheet', 1); + } + + jump(multiplier = 1) { + if (this.body.blocked.down) { + this.jumping = true; + this.setTexture('jumper_spritesheet', 2); + setTimeout(() => { + this.body.setVelocityY(-this.power * multiplier); + this.rotation = 0; + this.setTexture('jumper_spritesheet', 0); + this.jumping = false; + this.emit('jump'); + }, this.jumpLag); + } + } + + moveLeft() { + if (this.jumping) this.body.setVelocityX(0); + else this.body.setVelocityX(-this.speed); + } + + moveRight() { + if (this.jumping) this.body.setVelocityX(0); + else this.body.setVelocityX(this.speed); + } + + balloon() { + this.powerUp({ + key: 'balloon', + duration: this.balloonDuration, + speed: this.balloonSpeed, + onComplete: () => { + const balloon = new Balloon(this.scene, this.x, this.y).release( + this.balloonSpeed * 1.15, + this.body.velocity.x + ); + this.scene.time.delayedCall(5000, () => { + balloon.terminate(); + }); + }, + }); + } + + helicopter() { + this.powerUp({ + key: 'helicopter', + duration: this.helicopterDuration, + speed: this.helicopterSpeed, + onComplete: () => { + const helicopter = new Helicopter(this.scene, this.x, this.y).release( + this.helicopterSpeed * 1.25 + ); + this.scene.time.delayedCall(5000, () => { + helicopter.terminate(); + }); + }, + }); + } + + rocket() { + this.scene.cameras.main.flash(1000, 255, 255, 255); + + const event = this.scene.time.addEvent({ + delay: 50, + callback: () => { + this.scene.events.emit(Particles.EXHAUST, 3, this.x, this.y); + }, + repeat: -1, + }); + + this.powerUp({ + key: 'rocket', + duration: this.rocketDuration, + speed: this.rocketSpeed, + onComplete: () => { + event.remove(); + const rocket = new Rocket(this.scene, this.x, this.y).release(200); + this.scene.time.delayedCall(5000, () => { + rocket.terminate(); + }); + }, + }); + } + + explode(reason: DeathReason) { + if (this.dead) return false; + this.body.setVelocity(0); + this.body.setAllowGravity(false); + this.body.checkCollision.none = true; + this.setVisible(false); + this.dead = true; + this.emit(Player.EVENT_DEATH, reason); + this.scene.sound.play('death'); + return true; + } + + private powerUp({ + key, + duration, + speed, + onComplete, + }: { + key: PowerUpType; + duration: number; + speed: number; + onComplete: () => void; + }) { + if (this.powering || this.dead) return; + + this.powering = key; + this.emit(Player.EVENT_POWER_UP_START, key); + this.scene.sound.play(`${key}_start`, { + loop: true, + }); + this.anims.play({ + key, + repeat: -1, + }); + this.body.setVelocityY(-Math.abs(speed)); + this.body.setAllowGravity(false); + this.scene.time.delayedCall(duration, () => { + this.scene.tweens.add({ + targets: this, + alpha: 0, + duration: Player.FLASH_DURATION / Player.FLASH_REPETITIONS / 2, + yoyo: true, + repeat: Player.FLASH_REPETITIONS, + onComplete: () => { + this.scene.sound.stopByKey(`${key}_start`); + this.scene.sound.play(`${key}_end`); + this.alpha = 1; + this.powering = undefined; + this.anims.stop(); + this.body.setAllowGravity(true); + this.setTexture('jumper_spritesheet', 1); + this.emit(Player.EVENT_POWER_UP_OVER); + onComplete(); + }, + }); + }); + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/power-ups.ts b/apps/games/bit-jumper/src/lib/objects/power-ups.ts new file mode 100644 index 00000000..45fc0f49 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/power-ups.ts @@ -0,0 +1,93 @@ +import { DEPTHS } from '../data'; +import { Particles } from './particles'; + +export class PowerUps extends Phaser.GameObjects.Group { + static SPAWN_CHANCE = 1; + constructor(scene: Phaser.Scene) { + super(scene); + } + + spawn(x: number, y: number) { + const type = Phaser.Math.Between(0, 100); + + if (type < 50) { + this.add(new Balloon(this.scene, x, y - Balloon.HEIGHT_OFFSET)); + } else if (type < 90) { + this.add(new Helicopter(this.scene, x, y - Helicopter.HEIGHT_OFFSET)); + } else { + this.add(new Rocket(this.scene, x, y - Rocket.HEIGHT_OFFSET)); + } + } +} + +export class PowerUp extends Phaser.Physics.Arcade.Sprite { + body: Phaser.Physics.Arcade.Body; + constructor(scene: Phaser.Scene, x: number, y: number, frame: number) { + super(scene, x, y, 'jumper_spritesheet', frame); + scene.add.existing(this); + scene.physics.add.existing(this); + this.setDepth(DEPTHS.POWER_UP); + if (!(this.body instanceof Phaser.Physics.Arcade.Body)) { + throw new Error( + 'Expected body to be an instance of Phaser.Physics.Arcade.Body' + ); + } + + this.body.setAllowGravity(false); + } + + release(speed = 10, variance = 10) { + this.setVelocityY(-Math.abs(speed)); + this.setVelocityX(Phaser.Math.Between(-variance, variance)); + return this; + } + + terminate() { + this.destroy(); + } +} + +export class Balloon extends PowerUp { + static HEIGHT_OFFSET = 6; + event: Phaser.Time.TimerEvent; + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 6); + + this.setBodySize(5, 8); + } + + terminate() { + this.scene.events.emit(Particles.RINGS, this.x, this.y); + super.terminate(); + } +} + +export class Helicopter extends PowerUp { + static HEIGHT_OFFSET = 5; + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 7); + this.body.setSize(8, 8); + } + + release(speed: number) { + super.release(speed); + this.body.setAllowGravity(true); + this.body.setAngularVelocity(Phaser.Math.Between(speed, speed * 3)); + return this; + } +} + +export class Rocket extends PowerUp { + static HEIGHT_OFFSET = 5; + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 8); + this.setBodySize(8, 8); + } + + release(speed: number) { + super.release(speed); + this.setTexture('jumper_spritesheet', 35); + this.setVelocityY(-Math.abs(speed)); + return this; + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/ui/button.ts b/apps/games/bit-jumper/src/lib/objects/ui/button.ts new file mode 100644 index 00000000..d26b7ed1 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/ui/button.ts @@ -0,0 +1,26 @@ +import { DEPTHS } from '../../data'; + +export class Button extends Phaser.GameObjects.Rectangle { + constructor(scene: Phaser.Scene, x: number, y: number, text: string) { + super(scene, x, y, text.length * 8 + 4, 11, 0xffffff); + this.setInteractive({ useHandCursor: true }); + this.on('pointerdown', () => { + this.emit('click'); + this.scene.sound.play('menu'); + }).setDepth(DEPTHS.UI); + + scene.add.existing(this); + + scene.add + .text(x, y, text, { + fontSize: '8px', + align: 'center', + color: '#fff', + fontFamily: 'frog-block', + }) + .setStroke('#000', 4) + .setOrigin(0.5, 0.5) + .setResolution(10) + .setDepth(DEPTHS.UI); + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/ui/icons.ts b/apps/games/bit-jumper/src/lib/objects/ui/icons.ts new file mode 100644 index 00000000..b2569c86 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/ui/icons.ts @@ -0,0 +1,61 @@ +import { DEPTHS } from '../../data'; + +export class Icon extends Phaser.GameObjects.Sprite { + constructor(scene: Phaser.Scene, x: number, y: number, texture: string) { + super(scene, x, y, texture); + this.setDepth(DEPTHS.UI); + this.setInteractive({ useHandCursor: true }); + this.setOrigin(0, 0); + + this.on( + 'pointerdown', + () => { + this.emit('click'); + this.scene.sound.play('menu'); + }, + this + ); + + this.scene.add.existing(this); + } +} + +export class BackIcon extends Icon { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'back'); + } +} + +export class HeartIcon extends Icon { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'heart'); + } +} + +export class SoundIcon extends Icon { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'sound_on'); + + this.on( + 'pointerdown', + () => { + this.toggle(); + }, + this + ); + + this.scene.add.existing(this); + } + + mute(mute: boolean) { + this.scene.sound.mute = mute; + this.setTexture(mute ? 'sound_off' : 'sound_on'); + return this; + } + + toggle() { + const newState = !this.scene.sound.mute; + this.mute(newState); + this.emit('toggle', newState); + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/ui/link.ts b/apps/games/bit-jumper/src/lib/objects/ui/link.ts new file mode 100644 index 00000000..e297e3da --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/ui/link.ts @@ -0,0 +1,16 @@ +import { Typography } from './typography'; + +export class Link extends Typography { + constructor( + scene: Phaser.Scene, + x: number, + y: number, + text: string, + href: string + ) { + super(scene, x, y, text); + this.setInteractive({ useHandCursor: true }).on('pointerdown', () => { + window.open(href, '_blank'); + }); + } +} diff --git a/apps/games/bit-jumper/src/lib/objects/ui/typography.ts b/apps/games/bit-jumper/src/lib/objects/ui/typography.ts new file mode 100644 index 00000000..6f5343a2 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/objects/ui/typography.ts @@ -0,0 +1,17 @@ +import { DEPTHS } from '../../data'; + +export class Typography extends Phaser.GameObjects.Text { + constructor(scene: Phaser.Scene, x: number, y: number, text: string) { + super(scene, x, y, text, { + fontSize: '8px', + align: 'center', + color: '#fff', + fontFamily: 'frog-block', + }); + this.setStroke('#000', 4); + this.setOrigin(0.5, 0.5); + this.setResolution(10); + this.setDepth(DEPTHS.UI); + scene.add.existing(this); + } +} diff --git a/apps/games/bit-jumper/src/lib/scenes/boot.ts b/apps/games/bit-jumper/src/lib/scenes/boot.ts new file mode 100644 index 00000000..bf5ac231 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/scenes/boot.ts @@ -0,0 +1,97 @@ +import { CharityGamesPlugin } from '@worksheets/phaser/plugins'; +import Phaser from 'phaser'; + +import { ProgressBar } from '../objects/loading-bar'; +import { Typography } from '../objects/ui/typography'; +import { Menu } from './menu'; + +export class Boot extends Phaser.Scene { + static readonly KEY = 'boot'; + server: CharityGamesPlugin; + serverProgress: ProgressBar; + assetProgress: ProgressBar; + constructor() { + super(Boot.KEY); + } + + preload() { + this.server = CharityGamesPlugin.find(this); + this.server.initialize(); + + this.createBars(); + this.setLoadEvents(); + + const { width, height } = this.cameras.main; + + new Typography(this, width / 2, height * 0.65, 'LOADING'); + + this.load.image('dot', './assets/particles/dot.png'); + this.load.image('ring', './assets/particles/ring.png'); + this.load.image('sound_on', './assets/icons/sound_on.png'); + this.load.image('sound_off', './assets/icons/sound_off.png'); + this.load.image('back', './assets/icons/back.png'); + this.load.image('heart', './assets/icons/heart.png'); + + this.load.audio('balloon_end', './assets/audio/balloon_end.mp3'); + this.load.audio('balloon_start', './assets/audio/balloon_start.mp3'); + this.load.audio('break', './assets/audio/break.mp3'); + this.load.audio('death', './assets/audio/death.mp3'); + this.load.audio('helicopter_end', './assets/audio/helicopter_end.mp3'); + this.load.audio('helicopter_start', './assets/audio/helicopter_start.mp3'); + this.load.audio('jump', './assets/audio/jump.mp3'); + this.load.audio('menu', './assets/audio/menu.mp3'); + this.load.audio('rocket_end', './assets/audio/rocket_end.mp3'); + this.load.audio('rocket_start', './assets/audio/rocket_start.mp3'); + this.load.audio('smash', './assets/audio/smash.mp3'); + this.load.audio('spring', './assets/audio/spring.mp3'); + + this.load.spritesheet( + 'jumper_spritesheet', + './assets/sprites/infinite_jumper.png', + { + frameHeight: 16, + frameWidth: 16, + } + ); + } + + createBars() { + const { width, height } = this.cameras.main; + this.assetProgress = new ProgressBar(this, width / 2, height / 2 - 6, { + height: 8, + width: width * 0.5, + }); + this.serverProgress = new ProgressBar(this, width / 2, height / 2 + 6, { + height: 8, + width: width * 0.5, + }); + } + + setLoadEvents() { + this.load.on( + 'progress', + (value: number) => { + this.assetProgress.updateProgress(value); + }, + this + ); + + this.load.on('complete', () => { + this.startGame(); + }); + + this.server.on('initializing', (value: number) => { + this.serverProgress.updateProgress(value); + }); + + this.server.on('initialized', () => { + this.startGame(); + }); + } + + startGame() { + if (this.server.isInitialized && this.load.isReady()) { + this.scene.start(Menu.KEY); + } + } +} diff --git a/apps/games/bit-jumper/src/lib/scenes/credits.ts b/apps/games/bit-jumper/src/lib/scenes/credits.ts new file mode 100644 index 00000000..c749f6ea --- /dev/null +++ b/apps/games/bit-jumper/src/lib/scenes/credits.ts @@ -0,0 +1,33 @@ +import { VERSION } from '../data'; +import { BackIcon } from '../objects/ui/icons'; +import { Link } from '../objects/ui/link'; +import { Typography } from '../objects/ui/typography'; +import { Menu } from './menu'; + +export class Credits extends Phaser.Scene { + static readonly KEY = 'credits'; + constructor() { + super(Credits.KEY); + } + + create() { + new BackIcon(this, 2, 2).on('click', () => { + this.scene.start(Menu.KEY); + }); + new Typography(this, 45, 20, 'CREDITS'); + // draw a line under the title + this.add.rectangle(45, 25, 60, 1, 0xffffff); + + new Typography(this, 45, 40, 'CODE:'); + new Link(this, 45, 50, 'ModestDuck', 'https://modestduck.itch.io'); + + new Typography(this, 45, 70, 'ART:'); + new Link(this, 45, 80, 'i-am-44', 'https://i-am-44.itch.io/'); + new Link(this, 45, 90, 'Polyducks', 'https://polyducks.itch.io/'); + + new Typography(this, 45, 110, 'MUSIC:').setFontSize(8); + new Link(this, 45, 120, 'ColorAlpha', 'https://coloralpha.itch.io/'); + + new Typography(this, 45, 150, VERSION); + } +} diff --git a/apps/games/bit-jumper/src/lib/scenes/end.ts b/apps/games/bit-jumper/src/lib/scenes/end.ts new file mode 100644 index 00000000..19751c1a --- /dev/null +++ b/apps/games/bit-jumper/src/lib/scenes/end.ts @@ -0,0 +1,81 @@ +import { CharityGamesPlugin } from '@worksheets/phaser/plugins'; + +import { StorageKey } from '../data'; +import { Enemies } from '../objects/enemies'; +import { Button } from '../objects/ui/button'; +import { Typography } from '../objects/ui/typography'; +import { GameOverPayload } from '../types'; +import { Menu } from './menu'; +import { Play } from './play'; + +export class End extends Phaser.Scene { + static readonly KEY = 'end'; + payload: GameOverPayload; + server: CharityGamesPlugin; + + constructor() { + super(End.KEY); + } + + init(payload: GameOverPayload) { + this.payload = payload; + this.server = CharityGamesPlugin.find(this); + } + + create() { + const { width, height } = this.cameras.main; + const highScore = Math.max( + this.payload.score, + this.server.storage.get(StorageKey.HIGH_SCORE, 0) + ); + + this.server.storage.set(StorageKey.HIGH_SCORE, highScore); + + new Typography(this, width * 0.5, 25, 'GAME\nOVER').setFontSize(16); + + new Typography(this, width * 0.5, 65, `SCR: ${this.payload.score}`); + + new Typography(this, width * 0.5, 75, `HI: ${highScore}`); + + new Button(this, width * 0.5, height - 60, 'MENU').on('click', () => { + this.scene.start(Menu.KEY); + }); + new Button(this, width * 0.5, height - 40, 'RESTART').on('click', () => { + this.scene.start(Play.KEY); + }); + + const enemies = new Enemies(this); + enemies.spawn(width * Phaser.Math.FloatBetween(0.2, 0.8), height * 0.9); + + this.server.storage.save(); + this.submitScore(highScore); + this.submitAchievements(highScore); + } + + submitScore(score: number) { + this.server.leaderboard.submit(score); + } + + submitAchievements(score: number) { + const achievements: string[] = []; + const { death, powerUps, smashed } = this.payload; + death === 'spiker' && achievements.push(achievement('spiker')); + death === 'chomper' && achievements.push(achievement('chomper')); + death === 'floater' && achievements.push(achievement('floater')); + smashed >= 5 && achievements.push(achievement('smasher')); + powerUps['balloon'] >= 2 && achievements.push(achievement('balloon')); + powerUps['helicopter'] >= 2 && achievements.push(achievement('helicopter')); + powerUps['rocket'] >= 2 && achievements.push(achievement('rocket')); + score >= 1000 && achievements.push(achievement('1000')); + score >= 2500 && achievements.push(achievement('2500')); + score >= 5000 && achievements.push(achievement('5000')); + score >= 7500 && achievements.push(achievement('7500')); + score >= 10000 && achievements.push(achievement('10000')); + score >= 15000 && achievements.push(achievement('15000')); + score >= 20000 && achievements.push(achievement('20000')); + + this.server.achievements.unlock(achievements); + } +} + +const achievement = (name: string) => `bit-jumper:${name}`; diff --git a/apps/games/bit-jumper/src/lib/scenes/menu.ts b/apps/games/bit-jumper/src/lib/scenes/menu.ts new file mode 100644 index 00000000..cfce4142 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/scenes/menu.ts @@ -0,0 +1,86 @@ +import { CharityGamesPlugin } from '@worksheets/phaser/plugins'; + +import { StorageKey } from '../data'; +import { Enemies } from '../objects/enemies'; +import { PlatformsPool } from '../objects/platforms'; +import { Button } from '../objects/ui/button'; +import { HeartIcon, SoundIcon } from '../objects/ui/icons'; +import { Typography } from '../objects/ui/typography'; +import { EnemyType, PlatformType } from '../types'; +import { Credits } from './credits'; + +export class Menu extends Phaser.Scene { + static readonly KEY = 'menu'; + server: CharityGamesPlugin; + + constructor() { + super(Menu.KEY); + } + + preload() { + this.server = CharityGamesPlugin.find(this); + } + + create() { + const { width, height } = this.cameras.main; + const highScore = this.server.storage.get(StorageKey.HIGH_SCORE, 0); + const muted = this.server.storage.get(StorageKey.MUTE, false); + + new HeartIcon(this, 2, 2).on('click', () => { + this.scene.start(Credits.KEY); + }); + new SoundIcon(this, width - 2, 2) + .setOrigin(1, 0) + .mute(muted) + .on('toggle', (update: boolean) => { + this.server.storage.set(StorageKey.MUTE, update); + this.server.storage.save(); + }); + + new Typography(this, width / 2, 40, 'BIT\nJUMPER') + .setFontSize(16) + .setScale(0.9); + + new Button(this, width / 2, height - 60, 'PLAY').on('click', () => { + this.scene.start('play'); + }); + + new Button(this, width / 2, height - 40, `HI: ${highScore}`); + + this.spawnPlatforms(); + this.spawnEnemies(); + } + + spawnPlatforms() { + const { width, height } = this.cameras.main; + + const pool = new PlatformsPool(this); + const offset = 5; + const options: PlatformType[] = ['basic', 'basic', 'breaking', 'sliding']; + const randomWithinScreen = () => + Phaser.Math.Between(offset, width - offset); + for (let i = 1; i < 8; i++) { + const key = Phaser.Math.RND.pick(options); + this.add.existing( + pool.get(key).place(randomWithinScreen(), height - 21 * i) + ); + } + } + + spawnEnemies() { + const { width, height } = this.cameras.main; + + const pool = new Enemies(this); + const offset = 15; + const options: EnemyType[] = ['spiker', 'chomper', 'floater']; + + const randomWithinScreen = () => + Phaser.Math.Between(width / 2 - offset, width / 2 + offset); + + const key = Phaser.Math.RND.pick(options); + + this.add.existing( + pool.getOrCreate(key).place(randomWithinScreen(), height / 2) + ); + } +} diff --git a/apps/games/bit-jumper/src/lib/scenes/play.ts b/apps/games/bit-jumper/src/lib/scenes/play.ts new file mode 100644 index 00000000..4e408c25 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/scenes/play.ts @@ -0,0 +1,117 @@ +import Phaser from 'phaser'; + +import { Collisions } from '../objects/collisions'; +import { Controller } from '../objects/controller'; +import { Enemies } from '../objects/enemies'; +import { Observer } from '../objects/observer'; +import { Particles } from '../objects/particles'; +import { Platforms } from '../objects/platforms'; +import { Player } from '../objects/player'; +import { PowerUps } from '../objects/power-ups'; +import { DeathReason, GameOverPayload, PowerUpType } from '../types'; +import { End } from './end'; +import { ScorePlane } from './score-plane'; + +export class Play extends Phaser.Scene { + static readonly KEY = 'play'; + static readonly GAME_OVER_DELAY = 3000; + particles: Particles; + player: Player; + controller: Controller; + observer: Observer; + collisions: Collisions; + platforms: Platforms; + powerUps: PowerUps; + enemies: Enemies; + + constructor() { + super(Play.KEY); + } + + create() { + const { width, height } = this.sys.game.canvas; + this.particles = new Particles(this, 0, 0); + this.player = new Player(this, width / 1.25, height / 1.5); + this.observer = new Observer(this, this.player); + this.platforms = new Platforms(this, this.observer); + this.controller = new Controller(this, this.player); + this.powerUps = new PowerUps(this); + this.enemies = new Enemies(this); + this.collisions = new Collisions( + this, + this.player, + this.platforms, + this.powerUps, + this.enemies + ); + + this.scene.launch(ScorePlane.KEY); + this.connect(); + this.cleanup(); + } + + connect() { + const scorePlane = this.scene.get(ScorePlane.KEY) as ScorePlane; + this.time.delayedCall(250, () => { + this.controller.enable(); + }); + + this.platforms.on(Platforms.SPAWN_EVENT, (x: number, y: number) => { + const luck = Phaser.Math.Between(0, 100); + if (luck > PowerUps.SPAWN_CHANCE) return; + + this.powerUps.spawn(x, y); + }); + + this.platforms.on(Platforms.SPAWN_EVENT, (x: number, y: number) => { + const luck = Phaser.Math.Between(0, 100); + if (luck > Enemies.SPAWN_CHANCE) return; + + this.enemies.spawn(x, y); + }); + + this.observer.events.on(Observer.EVENT_APEX_CHANGE, (apex: number) => { + if (apex < 0) { + const score = Math.floor(Math.abs(apex)); + this.scene + .get(ScorePlane.KEY) + .events.emit(ScorePlane.EVENT_UPDATE_SCORE, score); + } + }); + + this.player.on(Player.EVENT_DEATH, (reason: DeathReason) => { + this.time.delayedCall(Play.GAME_OVER_DELAY, () => { + const payload: GameOverPayload = { + death: reason, + score: scorePlane.score, + powerUps: scorePlane.powerUps, + smashed: scorePlane.smashed, + }; + this.scene.stop(ScorePlane.KEY); + this.scene.start(End.KEY, payload); + }); + }); + + this.player.on(Player.EVENT_POWER_UP_START, (key: PowerUpType) => { + scorePlane.events.emit(ScorePlane.EVENT_POWER_UP, key); + }); + + this.player.on(Player.EVENT_SMASH_ENEMY, () => { + scorePlane.events.emit(ScorePlane.EVENT_SMASH_ENEMY); + }); + } + + update() { + this.controller.update(); + this.observer.update(); + this.platforms.update(); + this.enemies.update(); + } + + cleanup() { + this.events.on('shutdown', () => { + this.observer.cleanup(); + this.platforms.off(Platforms.SPAWN_EVENT); + }); + } +} diff --git a/apps/games/bit-jumper/src/lib/scenes/score-plane.ts b/apps/games/bit-jumper/src/lib/scenes/score-plane.ts new file mode 100644 index 00000000..7a7ca2a5 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/scenes/score-plane.ts @@ -0,0 +1,60 @@ +import { Typography } from '../objects/ui/typography'; +import { PowerUpType } from '../types'; + +export class ScorePlane extends Phaser.Scene { + static readonly KEY = 'score-plane'; + static readonly EVENT_UPDATE_SCORE = 'update-score'; + static readonly EVENT_POWER_UP = 'power-up'; + static readonly EVENT_SMASH_ENEMY = 'smash-enemy'; + static readonly SMASH_ENEMY_POINTS = 100; + typography: Typography; + score: number; + smashed: number; + powerUps: Record; + constructor() { + super(ScorePlane.KEY); + this.score = 0; + this.smashed = 0; + this.powerUps = { + balloon: 0, + helicopter: 0, + rocket: 0, + }; + } + + updateScore(score: number) { + this.score = score; + this.typography.setText(`${score}`); + } + + create() { + this.typography = new Typography(this, 4, 4, '0') + .setAlign('left') + .setOrigin(0); + + this.events.on( + ScorePlane.EVENT_UPDATE_SCORE, + (score: number) => { + this.updateScore(score); + }, + this + ); + + this.events.on( + ScorePlane.EVENT_POWER_UP, + (key: PowerUpType) => { + this.powerUps[key]++; + }, + this + ); + + this.events.on( + ScorePlane.EVENT_SMASH_ENEMY, + () => { + this.updateScore(this.score + ScorePlane.SMASH_ENEMY_POINTS); + this.smashed++; + }, + this + ); + } +} diff --git a/apps/games/bit-jumper/src/lib/types.ts b/apps/games/bit-jumper/src/lib/types.ts new file mode 100644 index 00000000..91049ae0 --- /dev/null +++ b/apps/games/bit-jumper/src/lib/types.ts @@ -0,0 +1,19 @@ +export type PlatformType = + | 'basic' + | 'spring' + | 'sliding' + | 'breaking' + | 'floor'; + +export type EnemyType = 'spiker' | 'chomper' | 'floater'; + +export type PowerUpType = 'balloon' | 'helicopter' | 'rocket'; + +export type GameOverPayload = { + score: number; + death: DeathReason; + powerUps: Record; + smashed: number; +}; + +export type DeathReason = EnemyType | 'fall'; diff --git a/apps/games/bit-jumper/src/main.ts b/apps/games/bit-jumper/src/main.ts new file mode 100644 index 00000000..2fb2edaa --- /dev/null +++ b/apps/games/bit-jumper/src/main.ts @@ -0,0 +1,50 @@ +import 'phaser'; + +import { CharityGamesPlugin } from '@worksheets/phaser/plugins'; + +import { Boot } from './lib/scenes/boot'; +import { Credits } from './lib/scenes/credits'; +import { End } from './lib/scenes/end'; +import { Menu } from './lib/scenes/menu'; +import { Play } from './lib/scenes/play'; +import { ScorePlane } from './lib/scenes/score-plane'; + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + backgroundColor: '#000000', + roundPixels: true, + render: { + pixelArt: true, + }, + scale: { + parent: 'game', + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + width: 90, + height: 160, + }, + scene: [Boot, Menu, Play, End, ScorePlane, Credits], + plugins: { + global: [ + { + key: CharityGamesPlugin.KEY, + plugin: CharityGamesPlugin, + start: true, + }, + ], + }, + physics: { + default: 'arcade', + arcade: { + gravity: { y: 250, x: 0 }, + debug: false, + }, + }, + dom: { + createContainer: true, + }, +}; + +window.addEventListener('load', () => { + new Phaser.Game(config); +}); diff --git a/apps/games/bit-jumper/src/styles.css b/apps/games/bit-jumper/src/styles.css new file mode 100644 index 00000000..169a5b28 --- /dev/null +++ b/apps/games/bit-jumper/src/styles.css @@ -0,0 +1,25 @@ +body, +html { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + margin: 0px; + position: relative; + touch-action: none; + /* background: linear-gradient(180deg, #fff -10%, #d0eeff 110%); */ + background-color: #000; +} +canvas { + border: 4px solid #fff; + box-sizing: border-box; + margin: auto; +} + +@font-face { + font-family: 'frog-block'; + src: url('/assets/fonts/frog-block.ttf'); + font-weight: 400; + font-weight: normal; +} diff --git a/apps/games/bit-jumper/tsconfig.app.json b/apps/games/bit-jumper/tsconfig.app.json new file mode 100644 index 00000000..357ad000 --- /dev/null +++ b/apps/games/bit-jumper/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/apps/games/bit-jumper/tsconfig.json b/apps/games/bit-jumper/tsconfig.json new file mode 100644 index 00000000..149b69e8 --- /dev/null +++ b/apps/games/bit-jumper/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "strictPropertyInitialization": false, + "types": ["vite/client"] + }, + "include": ["src"], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/apps/games/bit-jumper/vite.config.ts b/apps/games/bit-jumper/vite.config.ts new file mode 100644 index 00000000..72f042a4 --- /dev/null +++ b/apps/games/bit-jumper/vite.config.ts @@ -0,0 +1,31 @@ +/// +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import dotenv from 'dotenv'; +import { defineConfig } from 'vite'; + +dotenv.config({ + path: '.env', +}); +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/apps/games/bit-jumper', + base: './', // Use relative paths for all assets + define: { + 'process.env': process.env, + }, + server: { + port: 7013, + host: 'localhost', + }, + + plugins: [nxViteTsPaths()], + + build: { + outDir: '../../../dist/apps/games/bit-jumper', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +}); diff --git a/apps/prisma/seed/src/insert-achievements.ts b/apps/prisma/seed/src/insert-achievements.ts index bb5a8df7..8e49527c 100644 --- a/apps/prisma/seed/src/insert-achievements.ts +++ b/apps/prisma/seed/src/insert-achievements.ts @@ -3,12 +3,15 @@ import { prisma } from '@worksheets/prisma'; import { getSeedingChanges, seedingProperties } from '@worksheets/util/seeding'; import { SeedableGameAchievementSchema } from '@worksheets/util/types'; +type GameAchievementSchema = SeedableGameAchievementSchema & { gameId: string }; export const insertAchievements = async () => { const storedAchievements = await prisma.gameAchievement.findMany({ select: seedingProperties, }); - const achievements = games.flatMap((g) => g.achievements); + const achievements: GameAchievementSchema[] = games.flatMap((g) => + g.achievements.map((a) => ({ ...a, gameId: g.id })) + ); const { creating, updating } = getSeedingChanges( achievements, @@ -32,9 +35,7 @@ export const insertAchievements = async () => { } }; -const insertAchievement = async ( - achievement: SeedableGameAchievementSchema -) => { +const insertAchievement = async (achievement: GameAchievementSchema) => { await prisma.$transaction(async (tx) => { await tx.gameAchievement.create({ data: { @@ -63,9 +64,7 @@ const insertAchievement = async ( }); }; -const updateAchievement = async ( - achievement: SeedableGameAchievementSchema -) => { +const updateAchievement = async (achievement: GameAchievementSchema) => { await prisma.$transaction(async (tx) => { await tx.gameAchievement.update({ where: { diff --git a/libs/data/games/src/index.ts b/libs/data/games/src/index.ts index db6594b0..388d023d 100644 --- a/libs/data/games/src/index.ts +++ b/libs/data/games/src/index.ts @@ -11,6 +11,266 @@ import { import { SeedableGameSchema } from '@worksheets/util/types'; const integratedGames: SeedableGameSchema[] = [ + { + version: 2, + id: 'bit-jumper', + name: 'Bit Jumper', + developerId: 'charity-games', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/media/thumbnail.png', + bannerUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/media/banner.png', + viewport: viewports['PORTRAIT-ONLY'], + categories: ['1p', 'arcade', 'endless', 'mobile', 'desktop'], + file: { + type: 'HTML', + url: 'https://cdn.charity.games/_games/bit-jumper/index.html', + }, + markets: {}, + createdAt: new Date('2024-08-24T00:00:00.000Z'), + updatedAt: new Date('2024-08-24T00:00:00.000Z'), + description: + '

Bit Jumper is a fast-paced endless runner where you must jump from platform to platform to reach the highest score possible. How high can you jump?

Move with left/right arrow keys or touching the left/right sides of the screen.

', + multiplier: 0.002, + leaderboard: true, + cloudStorage: true, + loot: [], + achievements: [ + { + id: 'bit-jumper:spiker', + version: 2, + name: 'Die to a Spiker', + description: 'Slam into a Spiker enemy and die in Bit Jumper.', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/spiker.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:chomper', + version: 2, + name: 'Die to a Chomper', + description: 'Slam into a Chomper enemy and die in Bit Jumper.', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/chomper.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:floater', + version: 2, + name: 'Die to a Floater', + description: 'Slam into a Floater enemy and die in Bit Jumper.', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/floater.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:smasher', + version: 2, + name: 'Smash 5 Enemies', + description: + 'Hit an enemy while using a power-up to smash it in Bit Jumper. Enemies include Spikers, Chompers, and Floaters.', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/smasher.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:balloon', + version: 2, + name: 'Balloon Power-Up x2', + description: + 'Collect the Balloon Power-Up twice in a single game of Bit Jumper.', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/balloon.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:helicopter', + version: 2, + name: 'Helicopter Power-Up x2', + description: + 'Collect the Helicopter Power-Up twice in a single game of Bit Jumper.', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/helicopter.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:rocket', + version: 2, + name: 'Rocket Power-Up x2', + description: + 'Collect the Rocket Power-Up twice in a single game of Bit Jumper.', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/rocket.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:1000', + version: 2, + name: 'Score 1000', + description: 'Score 1000 points in a single game of Bit Jumper', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/1000.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:2500', + version: 2, + name: 'Score 2500', + description: 'Score 2500 points in a single game of Bit Jumper', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/2500.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:5000', + version: 2, + name: 'Score 5000', + description: 'Score 5000 points in a single game of Bit Jumper', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/5000.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 2, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:7500', + version: 2, + name: 'Score 7500', + description: 'Score 7500 points in a single game of Bit Jumper', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/7500.png', + secret: false, + loot: [ + { + itemId: '5', + quantity: 3, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:10000', + version: 2, + name: 'Score 10000', + description: 'Score 10000 points in a single game of Bit Jumper', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/10000.png', + secret: false, + loot: [ + { + itemId: '8', + quantity: 1, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:15000', + version: 2, + name: 'Score 15000', + description: 'Score 15000 points in a single game of Bit Jumper', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/15000.png', + secret: false, + loot: [ + { + itemId: '8', + quantity: 2, + chance: 1, + }, + ], + }, + { + id: 'bit-jumper:20000', + version: 2, + name: 'Score 20000', + description: 'Score 20000 points in a single game of Bit Jumper', + iconUrl: + 'https://cdn.charity.games/_games/bit-jumper/assets/achievements/20000.png', + secret: false, + loot: [ + { + itemId: '8', + quantity: 3, + chance: 1, + }, + ], + }, + ], + tasks: [ + { type: 'score', score: 500 }, + { type: 'score', score: 1000 }, + { type: 'score', score: 2500 }, + ], + }, { version: 1, id: 'hide-and-seek', @@ -39,7 +299,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'hide-and-seek:perfect', version: 1, - gameId: 'hide-and-seek', name: 'Play a Perfect Game', description: 'Find all three hidden characters without making a mistake', @@ -57,7 +316,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'hide-and-seek:found-one', version: 1, - gameId: 'hide-and-seek', name: 'Find One', description: 'Find at least one hidden character during a game', iconUrl: @@ -74,7 +332,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'hide-and-seek:found-two', version: 1, - gameId: 'hide-and-seek', name: 'Find Two', description: 'Find at least two hidden characters during a game', iconUrl: @@ -91,7 +348,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'hide-and-seek:found-three', version: 1, - gameId: 'hide-and-seek', name: 'Find Three', description: 'Find all three hidden characters during a game', iconUrl: @@ -108,21 +364,21 @@ const integratedGames: SeedableGameSchema[] = [ ], tasks: [ { - name: 'Find one hidden character', + name: 'Play Hide & Seek (1)', description: 'Find one hidden character during a single game of Hide and Seek', type: 'score', score: 1, }, { - name: 'Find two hidden characters', + name: 'Play Hide & Seek (2)', description: 'Find two hidden characters during a single game of Hide and Seek', type: 'score', score: 2, }, { - name: 'Find all hidden characters', + name: 'Play Hide & Seek (3)', description: 'Find all three hidden characters during a single game of Hide and Seek', type: 'score', @@ -156,7 +412,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_BONUS_1', version: 1, - gameId: 'tall-tower', name: 'Bonus Prize - 1', description: 'Reach the bonus prize line with a single block', iconUrl: @@ -173,7 +428,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_BONUS_2', version: 1, - gameId: 'tall-tower', name: 'Bonus Prize - 2', description: 'Reach the bonus prize line with two blocks', iconUrl: @@ -190,7 +444,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_BONUS_3', version: 1, - gameId: 'tall-tower', name: 'Bonus Prize - 3', description: 'Reach the bonus prize line with three blocks', iconUrl: @@ -207,7 +460,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_MINOR_1', version: 1, - gameId: 'tall-tower', name: 'Minor Prize - 1', description: 'Reach the minor prize line with one block', iconUrl: @@ -224,7 +476,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_MINOR_2', version: 1, - gameId: 'tall-tower', name: 'Minor Prize - 2', description: 'Reach the minor prize line with two blocks', iconUrl: @@ -241,7 +492,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_MINOR_3', version: 1, - gameId: 'tall-tower', name: 'Minor Prize - 3', description: 'Reach the minor prize line with three blocks', iconUrl: @@ -258,7 +508,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_MAJOR_1', version: 1, - gameId: 'tall-tower', name: 'Major Prize - 1', description: 'Reach the major prize line with one block', iconUrl: @@ -275,7 +524,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_MAJOR_2', version: 1, - gameId: 'tall-tower', name: 'Major Prize - 2', description: 'Reach the major prize line with two blocks', iconUrl: @@ -292,7 +540,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'TALL_TOWER_MAJOR_3', version: 1, - gameId: 'tall-tower', name: 'Major Prize - 3', description: 'Reach the major prize line with three blocks', iconUrl: @@ -312,6 +559,10 @@ const integratedGames: SeedableGameSchema[] = [ type: 'score', score: 50, }, + { + type: 'score', + score: 100, + }, ], }, { @@ -334,28 +585,11 @@ const integratedGames: SeedableGameSchema[] = [ multiplier: 0.015, leaderboard: true, cloudStorage: true, - loot: [ - { - itemId: '4', - quantity: 1, - chance: 1, - }, - { - itemId: '2', - quantity: 1, - chance: 1, - }, - { - itemId: '10044', - quantity: 1, - chance: 1, - }, - ], + loot: [], achievements: [ { id: 'BLOCK_BASH_100_POINTS_GAME', version: 1, - gameId: 'block-bash', name: 'Score 100', description: 'Score 100 points in a single game', iconUrl: @@ -372,7 +606,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_500_POINTS_GAME', version: 1, - gameId: 'block-bash', name: 'Score 500', description: 'Score 500 points in a single game', iconUrl: @@ -389,7 +622,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_1000_POINTS_GAME', version: 2, - gameId: 'block-bash', name: 'Score 1,000', description: 'Score 1,000 points in a single game', iconUrl: @@ -411,7 +643,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_1500_POINTS_GAME', version: 2, - gameId: 'block-bash', name: 'Score 1,500', description: 'Score 1,500 points in a single game', iconUrl: @@ -433,7 +664,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_2000_POINTS_GAME', version: 2, - gameId: 'block-bash', name: 'Score 2,000', description: 'Score 2,000 points in a single game', iconUrl: @@ -455,7 +685,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_2500_POINTS_GAME', version: 1, - gameId: 'block-bash', name: 'Score 2,500', description: 'Score 2,500 points in a single game', iconUrl: @@ -477,7 +706,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_3000_POINTS_GAME', version: 2, - gameId: 'block-bash', name: 'Score 3,000', description: 'Score 3,000 points in a single game', iconUrl: @@ -499,7 +727,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_3500_POINTS_GAME', version: 1, - gameId: 'block-bash', name: 'Score 3,500', description: 'Score 3,500 points in a single game', iconUrl: @@ -521,7 +748,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_4000_POINTS_GAME', version: 2, - gameId: 'block-bash', name: 'Score 4,000', description: 'Score 4,000 points in a single game', iconUrl: @@ -543,7 +769,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_4500_POINTS_GAME', version: 1, - gameId: 'block-bash', name: 'Score 4,500', description: 'Score 4,500 points in a single game', iconUrl: @@ -565,7 +790,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_5000_POINTS_GAME', version: 2, - gameId: 'block-bash', name: 'Score 5,000', description: 'Score 5,000 points in a single game', iconUrl: @@ -587,7 +811,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_100_LINES', version: 1, - gameId: 'block-bash', name: 'Complete 100 Lines', description: 'Complete 100 lines across all games', iconUrl: @@ -604,7 +827,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_500_LINES', version: 1, - gameId: 'block-bash', name: 'Complete 500 Lines', description: 'Complete 500 lines across all games', iconUrl: @@ -621,7 +843,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_1000_LINES', version: 1, - gameId: 'block-bash', name: 'Complete 1,000 Lines', description: 'Complete 1,000 lines across all games', iconUrl: @@ -638,7 +859,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_2500_LINES', version: 1, - gameId: 'block-bash', name: 'Complete 2,500 Lines', description: 'Complete 2,500 lines across all games', iconUrl: @@ -655,7 +875,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_5000_LINES', version: 1, - gameId: 'block-bash', name: 'Complete 5,000 Lines', description: 'Complete 5,000 lines across all games', iconUrl: @@ -672,7 +891,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_10000_LINES', version: 1, - gameId: 'block-bash', name: 'Complete 10,000 Lines', description: 'Complete 10,000 lines across all games', iconUrl: @@ -689,7 +907,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_1000_BLOCKS_LIFETIME', version: 1, - gameId: 'block-bash', name: 'Place 1,000 Blocks', description: 'Place 1,000 blocks across all games', iconUrl: @@ -706,7 +923,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_5000_BLOCKS_LIFETIME', version: 1, - gameId: 'block-bash', name: 'Place 5,000 Blocks', description: 'Place 5,000 blocks across all games', iconUrl: @@ -723,7 +939,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_10000_BLOCKS_LIFETIME', version: 1, - gameId: 'block-bash', name: 'Place 10,00 Blocks', description: 'Place 10,000 blocks across all games', iconUrl: @@ -740,7 +955,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_20000_BLOCKS_LIFETIME', version: 1, - gameId: 'block-bash', name: 'Place 20,000 Blocks', description: 'Place 20,000 blocks across all games', iconUrl: @@ -757,7 +971,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_50000_BLOCKS_LIFETIME', version: 1, - gameId: 'block-bash', name: 'Place 50,000 Blocks', description: 'Place 50,000 blocks across all games', iconUrl: @@ -774,7 +987,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'BLOCK_BASH_100000_BLOCKS_LIFETIME', version: 1, - gameId: 'block-bash', name: 'Place 100,000 Blocks', description: 'Place 100,000 blocks across all games', iconUrl: @@ -822,68 +1034,11 @@ const integratedGames: SeedableGameSchema[] = [ score: 250, }, ], - loot: [ - { - itemId: '10044', - quantity: 1, - chance: 1, - }, - { - itemId: '10055', - quantity: 1, - chance: 1, - }, - { - itemId: '10058', - quantity: 1, - chance: 1, - }, - { - itemId: '10029', - quantity: 1, - chance: 1, - }, - { - itemId: '10047', - quantity: 1, - chance: 1, - }, - { - itemId: '10114', - quantity: 1, - chance: 1, - }, - { - itemId: '10115', - quantity: 1, - chance: 1, - }, - { - itemId: '10116', - quantity: 1, - chance: 1, - }, - { - itemId: '10117', - quantity: 1, - chance: 1, - }, - { - itemId: '10118', - quantity: 1, - chance: 1, - }, - { - itemId: '10119', - quantity: 1, - chance: 1, - }, - ], + loot: [], achievements: [ { id: 'DINO_DASH_SCORE_100', version: 1, - gameId: 'dino-dash', name: 'Score 100', description: 'Score over 100 points in a single game', iconUrl: @@ -900,7 +1055,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_SCORE_500', version: 1, - gameId: 'dino-dash', name: 'Score 500', description: 'Score over 500 points in a single game', iconUrl: @@ -917,7 +1071,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_SCORE_1000', version: 1, - gameId: 'dino-dash', name: 'Score 1000', description: 'Score over 1000 points in a single game', iconUrl: @@ -939,7 +1092,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_SCORE_2500', version: 1, - gameId: 'dino-dash', name: 'Score 2500', description: 'Score over 2500 points in a single game', iconUrl: @@ -961,7 +1113,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_SCORE_5000', version: 1, - gameId: 'dino-dash', name: 'Score 5000', description: 'Score over 5000 points in a single game', iconUrl: @@ -993,7 +1144,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_COINS_1', version: 1, - gameId: 'dino-dash', name: 'Collect 1 Coin', description: 'Collect 1 coin across all games', iconUrl: @@ -1010,7 +1160,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_COINS_10', version: 1, - gameId: 'dino-dash', name: 'Collect 10 Coins', description: 'Collect 10 coins across all games', iconUrl: @@ -1027,7 +1176,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_COINS_50', version: 1, - gameId: 'dino-dash', name: 'Collect 50 Coins', description: 'Collect 50 coins across all games', iconUrl: @@ -1044,7 +1192,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_COINS_100', version: 1, - gameId: 'dino-dash', name: 'Collect 100 Coins', description: 'Collect 100 coins across all games', iconUrl: @@ -1066,7 +1213,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_COINS_250', version: 1, - gameId: 'dino-dash', name: 'Collect 250 Coins', description: 'Collect 250 coins across all games', iconUrl: @@ -1088,7 +1234,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_DAMAGE_1', version: 1, - gameId: 'dino-dash', name: 'Take 1 Damage', description: 'Take 1 damage across all games', iconUrl: @@ -1105,7 +1250,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_DAMAGE_10', version: 1, - gameId: 'dino-dash', name: 'Take 10 Damage', description: 'Take 10 damage across all games', iconUrl: @@ -1122,7 +1266,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_DAMAGE_25', version: 1, - gameId: 'dino-dash', name: 'Take 25 Damage', description: 'Take 25 damage across all games', iconUrl: @@ -1139,7 +1282,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_DAMAGE_50', version: 1, - gameId: 'dino-dash', name: 'Take 50 Damage', description: 'Take 50 damage across all games', iconUrl: @@ -1156,7 +1298,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_DAMAGE_100', version: 1, - gameId: 'dino-dash', name: 'Take 100 Damage', description: 'Take 100 damage across all games', iconUrl: @@ -1173,7 +1314,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_JUMPS_125', version: 2, - gameId: 'dino-dash', name: 'Jump 125 Times', description: 'Jump 125 time across all games', iconUrl: @@ -1190,7 +1330,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_JUMPS_250', version: 2, - gameId: 'dino-dash', name: 'Jump 250 Times', description: 'Jump 250 times across all games', iconUrl: @@ -1207,7 +1346,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_JUMPS_1250', version: 2, - gameId: 'dino-dash', name: 'Jump 1250 Times', description: 'Jump 1250 times across all games', iconUrl: @@ -1224,7 +1362,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_JUMPS_3000', version: 2, - gameId: 'dino-dash', name: 'Jump 3000 Times', description: 'Jump 3000 times across all games', iconUrl: @@ -1241,7 +1378,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_JUMPS_5000', version: 2, - gameId: 'dino-dash', name: 'Jump 5000 Times', description: 'Jump 5000 times across all games', iconUrl: @@ -1258,7 +1394,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_UNLOCK_BILLY', version: 1, - gameId: 'dino-dash', name: 'Unlock Billy the Brontosaurus', description: 'Visit the shop and purchase the character: Billy the Brontosaurus', @@ -1276,7 +1411,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_UNLOCK_TERRY', version: 1, - gameId: 'dino-dash', name: 'Unlock Terry the Triceratops', description: 'Visit the shop and purchase the character: Terry the Triceratops', @@ -1294,7 +1428,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'DINO_DASH_UNLOCK_CARLY', version: 1, - gameId: 'dino-dash', name: 'Unlock Carly the Carnotaurus', description: 'Visit the shop and purchase the character: Carly the Carnotaurus', @@ -1339,18 +1472,11 @@ const integratedGames: SeedableGameSchema[] = [ score: 10, }, ], - loot: [ - { - itemId: '2', - quantity: 2, - chance: 0.1, - }, - ], + loot: [], achievements: [ { id: 'COLOR_RUN_SCORE_1', version: 3, - gameId: 'color-run', name: 'First Score', description: 'Score 1 point in the game', iconUrl: @@ -1367,7 +1493,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'COLOR_RUN_SCORE_5', version: 3, - gameId: 'color-run', name: 'Score 5', description: 'Score 5 points in the game', iconUrl: @@ -1384,7 +1509,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'COLOR_RUN_SCORE_10', version: 3, - gameId: 'color-run', name: 'Score 10', description: 'Score 10 points in the game', iconUrl: @@ -1401,7 +1525,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'COLOR_RUN_SCORE_25', version: 3, - gameId: 'color-run', name: 'Score 25', description: 'Score 25 points in the game', iconUrl: @@ -1423,7 +1546,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'COLOR_RUN_SCORE_50', version: 3, - gameId: 'color-run', name: 'Score 50', description: 'Score 50 points in the game', iconUrl: @@ -1445,7 +1567,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'COLOR_RUN_SCORE_75', version: 3, - gameId: 'color-run', name: 'Score 75', description: 'Score 75 points in the game', iconUrl: @@ -1467,7 +1588,6 @@ const integratedGames: SeedableGameSchema[] = [ { id: 'COLOR_RUN_SCORE_100', version: 3, - gameId: 'color-run', name: 'Score 100', description: 'Score 100 points in the game', iconUrl: diff --git a/libs/phaser/plugins/src/lib/charity-games.ts b/libs/phaser/plugins/src/lib/charity-games.ts index 50bf6eec..4745c5bd 100644 --- a/libs/phaser/plugins/src/lib/charity-games.ts +++ b/libs/phaser/plugins/src/lib/charity-games.ts @@ -26,6 +26,7 @@ export const isValidOrigin = (origin: string) => { return origin === '*' || validOrigins.includes(origin); }; +// TODO: Move charity games plugin towards an API-first approach. export class CharityGamesPlugin extends Phaser.Plugins.BasePlugin { static KEY = 'CharityGamesPlugin'; storage: StorageAPI; @@ -37,6 +38,7 @@ export class CharityGamesPlugin extends Phaser.Plugins.BasePlugin { rewards: RewardAPI; events: Phaser.Events.EventEmitter = new Phaser.Events.EventEmitter(); isInitialized = false; + isDisabled = false; storageKey = 'storage'; constructor(pluginManager: Phaser.Plugins.PluginManager) { @@ -54,6 +56,8 @@ export class CharityGamesPlugin extends Phaser.Plugins.BasePlugin { } #preload() { + if (this.isDisabled) return; + if (window.parent === window) { throw new Error('No parent window'); } @@ -85,25 +89,29 @@ export class CharityGamesPlugin extends Phaser.Plugins.BasePlugin { }); } - async initialize() { - // 10 seconds timeout for initialization - const { signal, cancel } = createTimeout(10 * SECONDS); + async initialize(): Promise { + this.isDisabled = process.env['DISABLE_CHARITY_GAMES_SDK'] === 'true'; + if (this.isDisabled) console.info('Charity Games SDK is disabled'); + + const { signal, cancel } = createTimeout(10 * (this.isDisabled ? 0 : 10)); try { this.#preload(); this.#emit('initializing', 0.33); - await this.session.start(signal); + await this.session.initialize(signal); this.#emit('initializing', 0.66); - await this.storage.load(signal); + await this.storage.initialize(signal); this.#emit('initializing', 1); - await this.achievements.load(signal); + await this.achievements.initialize(signal); this.isInitialized = true; this.#emit('initialized', { ok: true }); } catch (error) { - console.error('Failed to initialize', error); + if (!this.isDisabled) { + console.error('Failed to initialize', error); + } this.isInitialized = true; this.#emit('initialized', { ok: false }); } finally { @@ -112,6 +120,7 @@ export class CharityGamesPlugin extends Phaser.Plugins.BasePlugin { } send(event: T, payload: GameEventPayload) { + if (this.isDisabled) return; window.parent.postMessage({ event, payload }, '*'); } @@ -142,7 +151,7 @@ class SessionAPI { this.id = null; } - async start(signal: AbortSignal) { + async initialize(signal: AbortSignal) { const { sessionId } = await cancelableRequest(this.plugin, signal)( 'start-session', 'session-started' @@ -167,7 +176,7 @@ class StorageAPI { this.ignored = keys; } - async load(signal: AbortSignal) { + async initialize(signal: AbortSignal) { const { storage } = await cancelableRequest(this.plugin, signal)( 'load-storage', 'storage-loaded' @@ -221,7 +230,7 @@ class StorageAPI { class LeaderboardsAPI { constructor(private plugin: CharityGamesPlugin) {} - async submit(score: number) { + submit(score: number) { this.plugin.send('submit-score', { sessionId: this.plugin.session.id, score, @@ -233,7 +242,7 @@ class AchievementsAPI { cached: string[] = []; constructor(private plugin: CharityGamesPlugin) {} - async load(signal: AbortSignal) { + async initialize(signal: AbortSignal) { const act = await cancelableRequest(this.plugin, signal)( 'load-achievements', 'achievements-loaded' diff --git a/libs/util/functions/.eslintrc.json b/libs/util/functions/.eslintrc.json new file mode 100644 index 00000000..3230caf3 --- /dev/null +++ b/libs/util/functions/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/libs/util/functions/README.md b/libs/util/functions/README.md new file mode 100644 index 00000000..e8772ab2 --- /dev/null +++ b/libs/util/functions/README.md @@ -0,0 +1,11 @@ +# util-functions + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build util-functions` to build the library. + +## Running unit tests + +Run `nx test util-functions` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/util/functions/jest.config.ts b/libs/util/functions/jest.config.ts new file mode 100644 index 00000000..b0ba8c98 --- /dev/null +++ b/libs/util/functions/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'util-functions', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/util/functions', +}; diff --git a/libs/util/functions/package.json b/libs/util/functions/package.json new file mode 100644 index 00000000..a7ec2a46 --- /dev/null +++ b/libs/util/functions/package.json @@ -0,0 +1,11 @@ +{ + "name": "util/functions", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts", + "private": true +} diff --git a/libs/util/functions/project.json b/libs/util/functions/project.json new file mode 100644 index 00000000..d82f09dc --- /dev/null +++ b/libs/util/functions/project.json @@ -0,0 +1,26 @@ +{ + "name": "util-functions", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/util/functions/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/util/functions", + "main": "libs/util/functions/src/index.ts", + "tsConfig": "libs/util/functions/tsconfig.lib.json", + "assets": ["libs/util/functions/*.md"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/util/functions/jest.config.ts" + } + } + } +} diff --git a/libs/util/functions/src/index.ts b/libs/util/functions/src/index.ts new file mode 100644 index 00000000..83f2d786 --- /dev/null +++ b/libs/util/functions/src/index.ts @@ -0,0 +1,22 @@ +export const once = (fn: () => void) => { + let called = false; + return () => { + if (called) return; + called = true; + fn(); + }; +}; + +// eslint-disable-next-line @typescript-eslint/ban-types +export const lucky = (chance: number, callback: T) => { + if (chance < 1 || chance > 100) { + throw new Error('Chance must be an integer between 1 and 100 inclusive'); + } + return (x: number, y: number) => { + // calculate luck between 1 and 100 inclusive + const luck = Math.ceil(Math.random() * 100); + if (luck > chance) return; + + callback(x, y); + }; +}; diff --git a/libs/util/functions/tsconfig.json b/libs/util/functions/tsconfig.json new file mode 100644 index 00000000..8122543a --- /dev/null +++ b/libs/util/functions/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/util/functions/tsconfig.lib.json b/libs/util/functions/tsconfig.lib.json new file mode 100644 index 00000000..4befa7f0 --- /dev/null +++ b/libs/util/functions/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/util/functions/tsconfig.spec.json b/libs/util/functions/tsconfig.spec.json new file mode 100644 index 00000000..69a251f3 --- /dev/null +++ b/libs/util/functions/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/util/types/src/lib/game-schema.ts b/libs/util/types/src/lib/game-schema.ts index adbfcdec..470e8bf9 100644 --- a/libs/util/types/src/lib/game-schema.ts +++ b/libs/util/types/src/lib/game-schema.ts @@ -22,11 +22,13 @@ export const basicGameAchievementSchema = z.object({ iconUrl: z.string(), }); -export const seedableGameAchievementSchema = basicGameAchievementSchema.extend({ - secret: z.boolean(), - version: z.number(), - loot: seedableLootSchema.array(), -}); +export const seedableGameAchievementSchema = basicGameAchievementSchema + .extend({ + secret: z.boolean(), + version: z.number(), + loot: seedableLootSchema.array(), + }) + .omit({ gameId: true }); export type SeedableGameAchievementSchema = z.infer< typeof seedableGameAchievementSchema diff --git a/nx.json b/nx.json index 1a48357f..636fe27b 100644 --- a/nx.json +++ b/nx.json @@ -112,6 +112,14 @@ "options": { "targetName": "eslint:lint" } + }, + { + "plugin": "@nx/webpack/plugin", + "options": { + "buildTargetName": "build", + "serveTargetName": "serve", + "previewTargetName": "preview" + } } ] } diff --git a/package-lock.json b/package-lock.json index 07101101..10a6317c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,7 +155,8 @@ "validate-npm-package-name": "^5.0.0", "verdaccio": "^5.0.4", "vite": "^5.0.0", - "vitest": "^1.3.1" + "vitest": "^1.3.1", + "webpack-cli": "^5.1.4" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -14492,6 +14493,7 @@ "version": "4.36.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "license": "MIT", "peer": true, "funding": { "type": "github", @@ -14502,6 +14504,7 @@ "version": "4.36.1", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "license": "MIT", "peer": true, "dependencies": { "@tanstack/query-core": "4.36.1", @@ -14664,6 +14667,7 @@ "funding": [ "https://trpc.io/sponsor" ], + "license": "MIT", "peer": true, "peerDependencies": { "@tanstack/react-query": "^4.18.0", @@ -16439,9 +16443,10 @@ "license": "ISC" }, "node_modules/@videojs/http-streaming": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.1.tgz", - "integrity": "sha512-G7YrgNEq9ETaUmtkoTnTuwkY9U+xP7Xncedzgxio/Rmz2Gn2zmodEbBIVQinb2UDznk7X8uY5XBr/Ew6OD/LWg==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.2.tgz", + "integrity": "sha512-eCfQp61w00hg7Y9npmLnsJ6UvDF+SsFYzu7mQJgVXxWpVm9AthYWA3KQEKA7F7Sy6yzlm/Sps8BHs5ItelNZgQ==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -16465,6 +16470,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -16824,6 +16830,53 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -16992,6 +17045,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz", "integrity": "sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -17004,6 +17058,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -23601,6 +23656,16 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.17.0", "dev": true, @@ -26068,6 +26133,16 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -29832,6 +29907,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.1.0.tgz", "integrity": "sha512-7N+pk79EH4oLKPEYdgRXgAsKDyA/VCo0qCHlUwacttQA0WqsjZQYmNfywMvjlY9MpEBVZEt0jKFd73Kv15EBYQ==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -29843,6 +29919,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -30880,6 +30957,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz", "integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -30941,6 +31019,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.3.tgz", "integrity": "sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@babel/runtime": "^7.11.2", @@ -35650,6 +35729,19 @@ "node": ">=0.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -38370,6 +38462,7 @@ "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.63.0.tgz", "integrity": "sha512-OMlgrTCPzE/ibtRMoeLVhOY0RcNuNWh0rhAVqeKnk/QwcuUKQbnqhZ1kg2vzD8VU/6h3FoPTq4RJPHgLBvX6Bw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@adobe/css-tools": "~4.3.3", @@ -38430,6 +38523,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">= 8" @@ -40454,13 +40548,14 @@ } }, "node_modules/video.js": { - "version": "8.16.1", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.16.1.tgz", - "integrity": "sha512-yAhxu4Vhyx5DdOgPn2PcRKHx3Vzs9tpvCWA0yX+sv5bIeBkg+IWdEX+MHGZgktgDQ/R8fJDxDbEASyvxXnFn1A==", + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.17.3.tgz", + "integrity": "sha512-zhhmE0LNxJRA603/48oYzF7GYdT+rQRscvcsouYxFE71aKhalHLBP6S9/XjixnyjcrYgwIx8OQo6eSjcbbAW0Q==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "3.13.1", + "@videojs/http-streaming": "3.13.2", "@videojs/vhs-utils": "^4.0.0", "@videojs/xhr": "2.7.0", "aes-decrypter": "^4.0.1", @@ -40478,6 +40573,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.5.5", @@ -40504,6 +40600,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "license": "Apache-2.0", "peer": true, "dependencies": { "global": "^4.4.0" @@ -40520,6 +40617,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==", + "license": "Apache-2.0", "peer": true }, "node_modules/videojs-ima": { @@ -41545,6 +41643,62 @@ } } }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/webpack-dev-middleware": { "version": "6.1.2", "license": "MIT", diff --git a/package.json b/package.json index 699ca960..25de2c2f 100644 --- a/package.json +++ b/package.json @@ -180,9 +180,10 @@ "validate-npm-package-name": "^5.0.0", "verdaccio": "^5.0.4", "vite": "^5.0.0", - "vitest": "^1.3.1" + "vitest": "^1.3.1", + "webpack-cli": "^5.1.4" }, "nx": { "includedScripts": [] } -} \ No newline at end of file +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 0e5a6697..850ee771 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -324,6 +324,7 @@ "@worksheets/util/data": ["libs/util/data/src/index.ts"], "@worksheets/util/enums": ["libs/util/enums/src/index.ts"], "@worksheets/util/errors": ["libs/util/errors/src/index.ts"], + "@worksheets/util/functions": ["libs/util/functions/src/index.ts"], "@worksheets/util/generators": ["libs/util/generators/src/index.ts"], "@worksheets/util/html": ["libs/util/html/src/index.ts"], "@worksheets/util/integrations": ["libs/util/integrations/src/index.ts"],