Move the game to subfolder
4
game/.eslintignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
dist
|
||||
19
game/.eslintrc.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint'
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
'@typescript-eslint/ban-ts-ignore': 0,
|
||||
'@typescript-eslint/no-namespace': { 'allowDeclarations': true },
|
||||
'@typescript-eslint/member-delimiter-style': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0
|
||||
}
|
||||
}
|
||||
4
game/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/.cache
|
||||
/dist
|
||||
/node_modules
|
||||
/.DS_Store
|
||||
21
game/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 ourcade
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18062
game/package-lock.json
generated
Normal file
38
game/package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "hs3jam-miner",
|
||||
"version": "1.0.0",
|
||||
"description": "Simple Phaser 3 game for the Hackerspace Trójmiasto's little game jam",
|
||||
"scripts": {
|
||||
"start": "parcel src/index.html -p 8000",
|
||||
"build": "parcel build src/index.html --out-dir dist --no-source-maps",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx"
|
||||
},
|
||||
"author": "Grzegorz Kupczyk",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ourcade/phaser3-parcel-template.git"
|
||||
},
|
||||
"homepage": "https://github.com/ourcade/phaser3-parcel-template",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^2.29.0",
|
||||
"@typescript-eslint/parser": "^2.29.0",
|
||||
"eslint": "^6.8.0",
|
||||
"minimist": ">=1.2.2",
|
||||
"parcel-bundler": "^1.12.5",
|
||||
"parcel-plugin-clean-easy": "^1.0.2",
|
||||
"parcel-plugin-static-files-copy": "^2.4.3",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"phaser": "^3.55.2"
|
||||
},
|
||||
"parcelCleanPaths": [
|
||||
"dist"
|
||||
],
|
||||
"staticFiles": {
|
||||
"staticPath": "public",
|
||||
"watcherGlob": "**"
|
||||
}
|
||||
}
|
||||
1364
game/public/assets/img/asteroids.json
Normal file
BIN
game/public/assets/img/asteroids.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
game/public/assets/img/boom.png
Normal file
|
After Width: | Height: | Size: 596 KiB |
BIN
game/public/assets/img/bullet.png
Normal file
|
After Width: | Height: | Size: 610 B |
BIN
game/public/assets/img/hs3-logo.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
game/public/assets/img/nebula01.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
game/public/assets/img/nebula02.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
game/public/assets/img/nebula03.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
game/public/assets/img/nebula04.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
game/public/assets/img/nebula05.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
game/public/assets/img/nebula06.png
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
game/public/assets/img/nebula07.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
game/public/assets/img/nebula08.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
game/public/assets/img/nebula09.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
game/public/assets/img/phaser3-logo.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
game/public/assets/img/ship.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
game/public/assets/img/stars.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
138
game/readme.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
This is a fork of [phaser3-typescript-parcel-template](https://github.com/ourcade/phaser3-typescript-parcel-template)
|
||||
|
||||

|
||||
|
||||
# Phaser 3 + TypeScript + Parcel Template
|
||||
> For people who want to spend time making Phaser 3 games in TypeScript instead of configuring build tools.
|
||||
|
||||

|
||||
|
||||
This is a TypeScript specific fork of [phaser3-parcel-template](https://github.com/ourcade/phaser3-parcel-template).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You'll need [Node.js](https://nodejs.org/en/), [npm](https://www.npmjs.com/), and [Parcel](https://parceljs.org/) installed.
|
||||
|
||||
It is highly recommended to use [Node Version Manager](https://github.com/nvm-sh/nvm) (nvm) to install Node.js and npm.
|
||||
|
||||
For Windows users there is [Node Version Manager for Windows](https://github.com/coreybutler/nvm-windows).
|
||||
|
||||
Install Node.js and `npm` with `nvm`:
|
||||
|
||||
```bash
|
||||
nvm install node
|
||||
|
||||
nvm use node
|
||||
```
|
||||
|
||||
Replace 'node' with 'latest' for `nvm-windows`.
|
||||
|
||||
Then install Parcel:
|
||||
|
||||
```bash
|
||||
npm install -g parcel-bundler
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
Clone this repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ourcade/phaser3-typescript-parcel-template.git
|
||||
```
|
||||
|
||||
This will create a folder named `phaser3-typescript-parcel-template`. You can specify a different folder name like this:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ourcade/phaser3-typescript-parcel-template.git my-folder-name
|
||||
```
|
||||
|
||||
Go into your new project folder and install dependencies:
|
||||
|
||||
```bash
|
||||
cd phaser3-typescript-parcel-template # or 'my-folder-name'
|
||||
npm install
|
||||
```
|
||||
|
||||
Start development server:
|
||||
|
||||
```
|
||||
npm run start
|
||||
```
|
||||
|
||||
To create a production build:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
Production files will be placed in the `dist` folder. Then upload those files to a web server. 🎉
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── dist
|
||||
├── node_modules
|
||||
├── public
|
||||
├── src
|
||||
│ ├── scenes
|
||||
│ │ ├── HelloWorldScene.ts
|
||||
│ ├── index.html
|
||||
│ ├── main.ts
|
||||
├── package.json
|
||||
```
|
||||
|
||||
The contents of this template is the basic [Phaser 3 getting started example](http://phaser.io/tutorials/getting-started-phaser3/part5).
|
||||
|
||||
This template assumes you will want to organize your code into multiple files and use TypeScript.
|
||||
|
||||
TypeScript files are intended for the `src` folder. `main.ts` is the entry point referenced by `index.html`.
|
||||
|
||||
Other than that there is no opinion on how you should structure your project. There is a `scenes` folder in `src` where the `HelloWorldScene.ts` lives but you can do whatever you want.
|
||||
|
||||
## Static Assets
|
||||
|
||||
Any static assets like images or audio files should be placed in the `public` folder. It'll then be served at http://localhost:8000/images/my-image.png
|
||||
|
||||
Example `public` structure:
|
||||
|
||||
```
|
||||
public
|
||||
├── images
|
||||
│ ├── my-image.png
|
||||
├── music
|
||||
│ ├── ...
|
||||
├── sfx
|
||||
│ ├── ...
|
||||
```
|
||||
|
||||
They can then be loaded by Phaser with `this.image.load('my-image', 'images/my-image.png')`.
|
||||
|
||||
## TypeScript ESLint
|
||||
|
||||
This template uses a basic `typescript-eslint` set up for code linting.
|
||||
|
||||
It does not aim to be opinionated.
|
||||
|
||||
## Dev Server Port
|
||||
|
||||
You can change the dev server's port number by modifying the `start` script in `package.json`. We use Parcel's `-p` option to specify the port number.
|
||||
|
||||
The script looks like this:
|
||||
|
||||
```
|
||||
parcel src/index.html -p 8000
|
||||
```
|
||||
|
||||
Change 8000 to whatever you want.
|
||||
|
||||
## Other Notes
|
||||
|
||||
[parcel-plugin-clean-easy](https://github.com/lifuzhao100/parcel-plugin-clean-easy) is used to ensure only the latest files are in the `dist` folder. You can modify this behavior by changing `parcelCleanPaths` in `package.json`.
|
||||
|
||||
[parcel-plugin-static-files](https://github.com/elwin013/parcel-plugin-static-files-copy#readme) is used to copy static files from `public` into the output directory and serve it. You can add additional paths by modifying `staticFiles` in `package.json`.
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](https://github.com/ourcade/phaser3-typescript-parcel-template/blob/master/LICENSE)
|
||||
97
game/src/classes/Asteroid.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import Phaser from 'phaser'
|
||||
import Bullet from '../classes/Bullet'
|
||||
|
||||
export default class Asteroid extends Phaser.Physics.Arcade.Sprite {
|
||||
// asteroid can't kill player
|
||||
// if younger than unbornAge in ms
|
||||
static unbornAge = 1000
|
||||
|
||||
readonly wrapMargin = 30
|
||||
readonly scaleMin = 0.7
|
||||
readonly scaleMax = 1.3
|
||||
readonly scaleRotationFactor = 0.4
|
||||
|
||||
age = 0
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
super(
|
||||
scene,
|
||||
Phaser.Math.RND.integerInRange(0, scene.physics.world.bounds.width),
|
||||
Phaser.Math.RND.integerInRange(0, scene.physics.world.bounds.height),
|
||||
'asteroids'
|
||||
)
|
||||
|
||||
scene.add.existing(this)
|
||||
scene.physics.add.existing(this)
|
||||
|
||||
this.setCircle(30, 30, 30)
|
||||
this.setScale(Phaser.Math.RND.realInRange(this.scaleMin, this.scaleMax))
|
||||
this.setVelocity(
|
||||
Phaser.Math.RND.realInRange(-100, 100),
|
||||
Phaser.Math.RND.realInRange(-100, 100)
|
||||
)
|
||||
this.setRandomShade()
|
||||
|
||||
this.anims.play(Asteroid.getRandomAnimationName())
|
||||
this.anims.timeScale = 1 + (this.scaleMax - this.scale) * this.scaleRotationFactor
|
||||
if (Phaser.Math.RND.integer() % 2 == 0)
|
||||
this.anims.reverse()
|
||||
|
||||
this.setAlpha(0)
|
||||
scene.tweens.addCounter({
|
||||
from: 0,
|
||||
to: 1,
|
||||
duration: Asteroid.unbornAge,
|
||||
onUpdate: (tween) => {
|
||||
const value = tween.getValue()
|
||||
this.setAlpha(value)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
preUpdate(time: number, delta: number) {
|
||||
super.preUpdate(time, delta)
|
||||
|
||||
this.scene.physics.world.wrap(this, this.scale * this.wrapMargin)
|
||||
this.age += delta
|
||||
}
|
||||
|
||||
gotHit(me, bullet) {
|
||||
if (!(bullet instanceof Bullet)) return
|
||||
if (bullet.active == false) return
|
||||
|
||||
me.scene.events.emit("asteroid:destroy")
|
||||
me.destroy() // TODO: use objects pool
|
||||
bullet.setActive(false)
|
||||
bullet.setVisible(false)
|
||||
}
|
||||
|
||||
private setRandomShade() {
|
||||
const color = new Phaser.Display.Color()
|
||||
color.randomGray(0xa0) //the darkest possible dye is 0xa0a0a0
|
||||
this.setTint(color.color)
|
||||
}
|
||||
|
||||
static createAnimations(scene: Phaser.Scene) {
|
||||
["a", "b", "c", "d"].forEach((animationName, i) => {
|
||||
const frames = scene.anims.generateFrameNames('asteroids', {
|
||||
start: 0, end: 15,
|
||||
zeroPad: 4,
|
||||
prefix: `${animationName}4`
|
||||
});
|
||||
|
||||
scene.anims.create({
|
||||
key: `asteroid${i}`,
|
||||
frames: frames,
|
||||
frameRate: 16,
|
||||
repeat: -1
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
static getRandomAnimationName() {
|
||||
const animationsAmount = 3
|
||||
const id = Phaser.Math.RND.integerInRange(0, animationsAmount)
|
||||
return `asteroid${id}`
|
||||
}
|
||||
}
|
||||
36
game/src/classes/Bullet.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Phaser from 'phaser'
|
||||
|
||||
export default class Bullet extends Phaser.Physics.Arcade.Sprite {
|
||||
readonly lifetime = 4000 //ms
|
||||
readonly speed = 600
|
||||
|
||||
age = 0
|
||||
|
||||
constructor(scene, x, y) {
|
||||
super(scene, x, y, 'bullet')
|
||||
}
|
||||
|
||||
fire(x: number, y: number, rotation: number) {
|
||||
this.body.reset(x, y)
|
||||
this.rotation = rotation
|
||||
|
||||
this.setActive(true)
|
||||
this.setVisible(true)
|
||||
|
||||
const v = new Phaser.Math.Vector2(0, -this.speed)
|
||||
v.rotate(rotation)
|
||||
this.setVelocity(v.x, v.y)
|
||||
this.age = 0
|
||||
}
|
||||
|
||||
preUpdate(time: number, delta: number) {
|
||||
super.preUpdate(time, delta)
|
||||
|
||||
this.age += delta
|
||||
|
||||
if (this.age >= this.lifetime) {
|
||||
this.setActive(false)
|
||||
this.setVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
game/src/classes/Bullets.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import Phaser from 'phaser'
|
||||
import Bullet from './Bullet'
|
||||
import Ship from './Ship'
|
||||
|
||||
export default class Bullets extends Phaser.Physics.Arcade.Group {
|
||||
readonly cooldown: number //ms
|
||||
|
||||
lastShoot = 0
|
||||
|
||||
constructor(scene: Phaser.Scene, fireRate: number) {
|
||||
super(scene.physics.world, scene)
|
||||
|
||||
this.createMultiple({
|
||||
frameQuantity: 30,
|
||||
key: 'bullet',
|
||||
active: false,
|
||||
visible: false,
|
||||
classType: Bullet
|
||||
});
|
||||
|
||||
this.cooldown = 1000 / fireRate
|
||||
}
|
||||
|
||||
fireBullet(time: number, shooter: Ship) {
|
||||
let bullet = this.getFirstDead(false)
|
||||
|
||||
if (bullet && this.lastShoot + this.cooldown <= time) {
|
||||
bullet.fire(shooter.x, shooter.y, shooter.rotation)
|
||||
this.lastShoot = time
|
||||
}
|
||||
}
|
||||
}
|
||||
59
game/src/classes/DifficultyManager.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import PlayScene from '../scenes/PlayScene'
|
||||
|
||||
export default class DifficultyManager {
|
||||
readonly spawnInterval = 1500 //ms
|
||||
readonly maxAsteroids = 300
|
||||
readonly level = 30
|
||||
readonly nextLevelRequirementIncrease = 10
|
||||
|
||||
private scene: PlayScene
|
||||
private difficultyLevel = 1
|
||||
private points = 0
|
||||
|
||||
private spawnAtOnce = 1
|
||||
private nextLevelRequirement = 10
|
||||
|
||||
constructor(scene: PlayScene) {
|
||||
this.scene = scene
|
||||
|
||||
scene.events.on('asteroid:destroy', () => {
|
||||
this.points += 1
|
||||
this.nextLevelRequirement -= 1
|
||||
this.scene.events.emit("getpoint", this.difficultyLevel)
|
||||
|
||||
if (this.nextLevelRequirement <= 0)
|
||||
this.levelUp()
|
||||
})
|
||||
|
||||
scene.time.addEvent({
|
||||
delay: this.spawnInterval,
|
||||
callback: this.spawnAsteroids,
|
||||
callbackScope: this,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
|
||||
levelUp() {
|
||||
this.difficultyLevel += 1
|
||||
this.spawnAtOnce += 1
|
||||
this.nextLevelRequirement = this.difficultyLevel * this.nextLevelRequirementIncrease
|
||||
this.scene.events.emit("lvlup", this.difficultyLevel)
|
||||
}
|
||||
|
||||
spawnAsteroids() {
|
||||
for (let i = 0; i < this.spawnAtOnce; ++i)
|
||||
this.scene.spawnAsteroid()
|
||||
}
|
||||
|
||||
getMaxAsteroids() {
|
||||
return Math.min(this.difficultyLevel * 5, this.maxAsteroids)
|
||||
}
|
||||
|
||||
getLevel() {
|
||||
return this.difficultyLevel
|
||||
}
|
||||
|
||||
getPoints() {
|
||||
return this.points
|
||||
}
|
||||
}
|
||||
86
game/src/classes/Ship.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import Phaser from 'phaser'
|
||||
import Bullets from './Bullets'
|
||||
import Thruster from './Thruster'
|
||||
import Asteroid from './Asteroid'
|
||||
|
||||
export default class Ship extends Phaser.Physics.Arcade.Sprite {
|
||||
readonly acceleration = 5
|
||||
readonly dragForce = 0.6
|
||||
readonly maxSpeed = 300
|
||||
|
||||
readonly colliderRadiusRatio = 0.43
|
||||
readonly wrapMargin = 10
|
||||
|
||||
readonly fireRate = 5 //shoots/s
|
||||
|
||||
bullets: Bullets
|
||||
thruster: Thruster
|
||||
life = 3
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
super(
|
||||
scene,
|
||||
scene.physics.world.bounds.centerX,
|
||||
scene.physics.world.bounds.centerY,
|
||||
'ship'
|
||||
)
|
||||
|
||||
scene.add.existing(this)
|
||||
scene.physics.add.existing(this)
|
||||
|
||||
scene.input.on('pointermove', (pointer) => {
|
||||
this.rotation = Phaser.Math.Angle.BetweenPoints(this, pointer) + Math.PI / 2
|
||||
})
|
||||
|
||||
this.setDamping(true)
|
||||
this.setDrag(this.dragForce)
|
||||
this.body.setCircle(this.width * this.colliderRadiusRatio)
|
||||
|
||||
this.bullets = new Bullets(scene, this.fireRate)
|
||||
this.thruster = new Thruster(scene, this)
|
||||
this.setMaxVelocity(this.maxSpeed)
|
||||
}
|
||||
|
||||
preUpdate(time: number, delta: number) {
|
||||
super.preUpdate(time, delta)
|
||||
|
||||
const keyUp = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W)
|
||||
const keyDown = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S)
|
||||
const keyLeft = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A)
|
||||
const keyRight = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D)
|
||||
|
||||
const vec = new Phaser.Math.Vector2(0, 0)
|
||||
|
||||
if (keyUp.isDown) vec.y = -this.acceleration
|
||||
if (keyDown.isDown) vec.y = this.acceleration
|
||||
if (keyRight.isDown) vec.x = this.acceleration
|
||||
if (keyLeft.isDown) vec.x = -this.acceleration
|
||||
|
||||
if (this.scene.input.activePointer.leftButtonDown())
|
||||
this.bullets!.fireBullet(time, this);
|
||||
|
||||
this.rotation = Phaser.Math.Angle.BetweenPoints(
|
||||
this,
|
||||
this.scene.input.activePointer
|
||||
) + Math.PI / 2
|
||||
|
||||
this.body.velocity.add(vec)
|
||||
this.scene.physics.world.wrap(this, this.wrapMargin)
|
||||
this.thruster.update(time, delta)
|
||||
}
|
||||
|
||||
gotHit(_, asteroid) {
|
||||
if (!(asteroid instanceof Asteroid)) return
|
||||
|
||||
if (asteroid.age > Asteroid.unbornAge) {
|
||||
this.life -= 1
|
||||
this.scene.cameras.main.shake(100, 0.02)
|
||||
asteroid.destroy() //TODO: use objects pool
|
||||
this.scene.events.emit("ship:gothit")
|
||||
}
|
||||
|
||||
if (!this.life){
|
||||
this.scene.events.emit("ship:destroyed")
|
||||
}
|
||||
}
|
||||
}
|
||||
59
game/src/classes/Thruster.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import Phaser from 'phaser'
|
||||
import Utils from './Utils'
|
||||
|
||||
export default class Thruster {
|
||||
readonly angleSpan = 40
|
||||
readonly frequency = 100
|
||||
readonly speed = 100
|
||||
readonly framesIndex = 30
|
||||
|
||||
emitter: Phaser.GameObjects.Particles.ParticleEmitter
|
||||
parent: Phaser.Physics.Arcade.Sprite
|
||||
|
||||
constructor(scene: Phaser.Scene, parent: Phaser.Physics.Arcade.Sprite) {
|
||||
this.emitter = scene.add.particles('particles')
|
||||
.createEmitter({
|
||||
frame: [25, 26, 27, 28, 29, 30, 31, 32, 34, 35],
|
||||
speed: this.speed,
|
||||
frequency: this.frequency,
|
||||
scale: { start: 0.2, end: 0 },
|
||||
blendMode: 'ADD',
|
||||
follow: parent
|
||||
})
|
||||
|
||||
this.parent = parent
|
||||
}
|
||||
|
||||
update(time: number, delta: number) {
|
||||
const thrustDirection = Phaser.Math.RadToDeg(
|
||||
this.parent.body.velocity.clone().negate().angle()
|
||||
)
|
||||
|
||||
this.emitter.angle.start = thrustDirection - this.angleSpan / 2
|
||||
this.emitter.angle.end = thrustDirection + this.angleSpan / 2
|
||||
|
||||
const thrustPower = this.getThrustPower()
|
||||
|
||||
this.emitter.setFrequency(thrustPower.frequency)
|
||||
this.emitter.setAlpha(thrustPower.opacity)
|
||||
}
|
||||
|
||||
getThrustPower() {
|
||||
const velocityRange = [0, 320] as const
|
||||
const frequencyRange = [300, 0] as const
|
||||
const opacityRange = [0, 1] as const
|
||||
|
||||
return {
|
||||
frequency: Utils.clampMap(
|
||||
this.parent.body.velocity.length(),
|
||||
...velocityRange,
|
||||
...frequencyRange
|
||||
),
|
||||
opacity: Utils.clampMap(
|
||||
this.parent.body.velocity.length(),
|
||||
...velocityRange,
|
||||
...opacityRange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
49
game/src/classes/Utils.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export default {
|
||||
map(value: number,
|
||||
sourceMin: number,
|
||||
sourceMax: number,
|
||||
targetMin: number,
|
||||
targetMax: number) {
|
||||
|
||||
const sourceRange = sourceMax - sourceMin
|
||||
const targetRange = targetMax - targetMin
|
||||
return (value - sourceMin) / sourceRange * targetRange + targetMin
|
||||
},
|
||||
|
||||
clamp(value: number,
|
||||
min: number,
|
||||
max: number) {
|
||||
let leftLimit = Math.min
|
||||
let rightLimit = Math.max
|
||||
|
||||
if (max < min) {
|
||||
leftLimit = Math.max
|
||||
rightLimit = Math.max
|
||||
}
|
||||
|
||||
return leftLimit(
|
||||
rightLimit(min, value),
|
||||
max)
|
||||
},
|
||||
|
||||
clampMap(value: number,
|
||||
sourceMin: number,
|
||||
sourceMax: number,
|
||||
targetMin: number,
|
||||
targetMax: number) {
|
||||
|
||||
const v = this.map(value,
|
||||
sourceMin,
|
||||
sourceMax,
|
||||
targetMin,
|
||||
targetMax
|
||||
)
|
||||
|
||||
const min = Math.min(targetMin, targetMax)
|
||||
const max = Math.max(targetMin, targetMax)
|
||||
|
||||
return this.clamp(
|
||||
v, min, max
|
||||
)
|
||||
}
|
||||
}
|
||||
124
game/src/gui.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
declare interface Window { myStuff: any }
|
||||
|
||||
(() => {
|
||||
const elements = {
|
||||
bar: {
|
||||
logged: document.getElementById("loggedbar"),
|
||||
loggedout: document.getElementById("loggedoutbar"),
|
||||
},
|
||||
buttons: {
|
||||
login: document.getElementById("login"),
|
||||
logout: document.getElementById("logout"),
|
||||
signup: document.getElementById("signup"),
|
||||
},
|
||||
name: document.getElementById("name"),
|
||||
key: document.getElementById("key"),
|
||||
}
|
||||
|
||||
function checkIfLogged() {
|
||||
const token = localStorage.getItem("token")
|
||||
const name = localStorage.getItem("name")
|
||||
if (token !== null) {
|
||||
window.myStuff.token = token
|
||||
window.myStuff.name = name
|
||||
console.log("my stuff", window.myStuff)
|
||||
|
||||
elements.bar.logged!.style.display = ""
|
||||
elements.bar.loggedout!.style.display = "none"
|
||||
elements.name!.innerText = name!;
|
||||
elements.key!.innerText = token!;
|
||||
} else {
|
||||
console.log("not logged in")
|
||||
elements.bar.logged!.style.display = "none"
|
||||
elements.bar.loggedout!.style.display = ""
|
||||
}
|
||||
}
|
||||
|
||||
function loginByKey() {
|
||||
const key = prompt("Type in the #key")
|
||||
|
||||
if (!key) {
|
||||
alert("Login cancelled")
|
||||
return
|
||||
}
|
||||
|
||||
//TODO: fetch data from server
|
||||
const response = {
|
||||
token: Math.random().toString() + '.abc',
|
||||
name: 'Hagis'
|
||||
}
|
||||
|
||||
localStorage.setItem('token', response.token)
|
||||
localStorage.setItem('name', response.name)
|
||||
|
||||
window.myStuff.token = response.token
|
||||
window.myStuff.name = response.name
|
||||
|
||||
elements.bar.logged!.style.display = ''
|
||||
elements.bar.loggedout!.style.display = 'none'
|
||||
elements.name!.innerText = response.name
|
||||
elements.key!.innerText = response.token
|
||||
}
|
||||
|
||||
function signup() {
|
||||
const nickname = prompt("Your nickname:")
|
||||
|
||||
if (!nickname) {
|
||||
alert("Signup cancelled")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Fetch data from server
|
||||
|
||||
//mock bad request
|
||||
if (nickname == 'Hgs') {
|
||||
alert("The name is occupied by someone else, try again with another nickname")
|
||||
return
|
||||
}
|
||||
|
||||
const response = {
|
||||
token: Math.random().toString() + '.abc',
|
||||
name: nickname!
|
||||
}
|
||||
|
||||
localStorage.setItem('token', response.token)
|
||||
localStorage.setItem('name', response.name)
|
||||
|
||||
window.myStuff.token = response.token
|
||||
window.myStuff.name = response.name
|
||||
|
||||
elements.bar.logged!.style.display = ''
|
||||
elements.bar.loggedout!.style.display = 'none'
|
||||
elements.name!.innerText = response.name
|
||||
elements.key!.innerText = response.token
|
||||
}
|
||||
|
||||
function logout() {
|
||||
const sure = confirm("Are you sure you want to logout? " +
|
||||
"You won't be able to login again without #key. " +
|
||||
"Make sure you copied key before log out!")
|
||||
|
||||
if (!sure)
|
||||
return
|
||||
|
||||
localStorage.clear()
|
||||
window.myStuff = {}
|
||||
|
||||
elements.bar.logged!.style.display = 'none'
|
||||
elements.bar.loggedout!.style.display = ''
|
||||
elements.name!.innerText = ''
|
||||
elements.key!.innerText = ''
|
||||
}
|
||||
|
||||
function setup() {
|
||||
window.myStuff = {}
|
||||
|
||||
elements.buttons.login!.addEventListener("click", loginByKey)
|
||||
elements.buttons.logout!.addEventListener("click", logout)
|
||||
elements.buttons.signup!.addEventListener("click", signup)
|
||||
|
||||
checkIfLogged()
|
||||
}
|
||||
|
||||
setup()
|
||||
})()
|
||||
25
game/src/index.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>SPACE SMASHER 9001!</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="bar">
|
||||
<div id="loggedbar" class="logged-in" style="display: none">
|
||||
Logged in as <span id="name">username</span>
|
||||
<span id="key" class="key">#asdasdasdasd</span>
|
||||
<button id="logout">Log out</button>
|
||||
</div>
|
||||
<div id="loggedoutbar" class="logged-out" style="display: none">
|
||||
Not logged in
|
||||
<button id="login">Log in with #key</button>
|
||||
<button id="signup">Sign up</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="main.ts"></script>
|
||||
<script src="gui.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
22
game/src/main.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import Phaser, { Game } from 'phaser'
|
||||
|
||||
import PlayScene from './scenes/PlayScene'
|
||||
import GameOverScene from './scenes/GameOverScene'
|
||||
import StartScene from './scenes/StartScene'
|
||||
|
||||
const config: Phaser.Types.Core.GameConfig = {
|
||||
type: Phaser.AUTO,
|
||||
|
||||
width: 800,
|
||||
height: 600,
|
||||
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: {
|
||||
debug: false
|
||||
}
|
||||
},
|
||||
scene: [StartScene, PlayScene, GameOverScene]
|
||||
}
|
||||
|
||||
export default new Phaser.Game(config)
|
||||
43
game/src/scenes/GameOverScene.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import Phaser from 'phaser'
|
||||
|
||||
export default class PlayScene extends Phaser.Scene {
|
||||
|
||||
constructor() {
|
||||
super('game-over-scene')
|
||||
}
|
||||
|
||||
preload() {
|
||||
this.load.image('phaser-logo', 'assets/img/phaser3-logo.png')
|
||||
this.load.image('hs3-logo', 'assets/img/hs3-logo.png')
|
||||
}
|
||||
|
||||
create(data) {
|
||||
console.log("Show end screen with data:", data)
|
||||
|
||||
this.add.image(250, 550, 'phaser-logo').setScale(0.5)
|
||||
this.add.image(550, 550, 'hs3-logo').setScale(0.5)
|
||||
|
||||
this.add.text(
|
||||
this.cameras.main.centerX,
|
||||
this.cameras.main.centerY - 100,
|
||||
"KONIEC GRY", {
|
||||
font: '64px Verdana',
|
||||
}).setOrigin(0.5, 0.5)
|
||||
|
||||
const rank = 0
|
||||
const pts = data.points || 0
|
||||
const lvl = data.level || 0
|
||||
const time = Math.ceil(data.elapsedTime || 0)
|
||||
|
||||
this.add.text(this.cameras.main.centerX, this.cameras.main.centerY, [
|
||||
"Miejsce w rankingu: #" + rank,
|
||||
"Punkty: " + pts,
|
||||
"Poziom: " + lvl,
|
||||
"Czas Gry: " + time + 's',
|
||||
], {
|
||||
font: '32px Verdana',
|
||||
align: 'center',
|
||||
color: 'cyan'
|
||||
}).setOrigin(0.5, 0.5)
|
||||
}
|
||||
}
|
||||
113
game/src/scenes/PlayScene.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import Phaser from 'phaser'
|
||||
import Ship from '../classes/Ship'
|
||||
import Asteroid from '../classes/Asteroid'
|
||||
import DifficultyManager from '../classes/DifficultyManager'
|
||||
|
||||
export default class PlayScene extends Phaser.Scene {
|
||||
readonly maxAsteroids = 70
|
||||
|
||||
player?: Ship
|
||||
rotFrames?: Phaser.Types.Animations.AnimationFrame[]
|
||||
asteroids?: Phaser.GameObjects.Group
|
||||
difficulty?: DifficultyManager
|
||||
points = 0
|
||||
progressLabel?: Phaser.GameObjects.Text
|
||||
background?: Phaser.GameObjects.Image
|
||||
backgroundOrder: number[] = []
|
||||
backgroundId = 0
|
||||
startTimestamp: number = 0
|
||||
auth: any
|
||||
|
||||
constructor() {
|
||||
super('play-scene')
|
||||
|
||||
this.backgroundOrder = []
|
||||
for (let i = 1; i < 10; ++i)
|
||||
this.backgroundOrder.push(i)
|
||||
Phaser.Utils.Array.Shuffle(this.backgroundOrder)
|
||||
}
|
||||
|
||||
preload() {
|
||||
for (let i = 1; i < 10; i++)
|
||||
this.load.image(`sky${i}`, `assets/img/nebula0${i}.png`)
|
||||
this.load.image('ship', 'assets/img/ship.png')
|
||||
this.load.image('bullet', 'assets/img/bullet.png')
|
||||
this.load.spritesheet('particles', 'assets/img/boom.png', { frameWidth: 192, frameHeight: 192 })
|
||||
this.load.multiatlas('asteroids', 'assets/img/asteroids.json', 'assets/img');
|
||||
}
|
||||
|
||||
create(data) {
|
||||
console.log("Started game with", data)
|
||||
this.auth = data
|
||||
|
||||
Asteroid.createAnimations(this)
|
||||
this.difficulty = new DifficultyManager(this)
|
||||
this.events.on('getpoint', this.updateLabel, this)
|
||||
this.events.on('lvlup', this.updateLabel, this)
|
||||
this.events.on('lvlup', this.changeBackground, this)
|
||||
this.events.on('ship:gothit', this.updateLabel, this)
|
||||
this.events.on('ship:destroyed', this.gameOver, this)
|
||||
|
||||
this.background = this.add.image(400, 300, 'sky1')
|
||||
this.asteroids = this.add.group()
|
||||
this.player = new Ship(this)
|
||||
this.progressLabel = this.add.text(5, 5, "", {
|
||||
font: '32px Verdana',
|
||||
color: 'cyan'
|
||||
})
|
||||
this.progressLabel.setDepth(1)
|
||||
|
||||
this.startTimestamp = this.time.now
|
||||
this.changeBackground()
|
||||
this.updateLabel()
|
||||
this.hookupCollisions()
|
||||
}
|
||||
|
||||
gameOver() {
|
||||
console.log("%cU ded", "color:red")
|
||||
console.log("points: ", this.difficulty!.getPoints())
|
||||
this.scene.start('game-over-scene', {
|
||||
points: this.difficulty!.getPoints(),
|
||||
level: this.difficulty!.getLevel(),
|
||||
elapsedTime: (this.time.now - this.startTimestamp) / 1000
|
||||
})
|
||||
}
|
||||
|
||||
changeBackground() {
|
||||
const currentId = this.backgroundOrder[this.backgroundId]
|
||||
const key = 'sky' + currentId.toString()
|
||||
|
||||
this.background!.setTexture(key)
|
||||
this.backgroundId = (++this.backgroundId) % this.backgroundOrder.length
|
||||
}
|
||||
|
||||
spawnAsteroid() {
|
||||
if (this.asteroids!.getLength() < this.difficulty!.getMaxAsteroids())
|
||||
this.asteroids!.add(new Asteroid(this))
|
||||
}
|
||||
|
||||
updateLabel() {
|
||||
const lvl = this.difficulty!.getLevel()
|
||||
const pts = this.difficulty!.getPoints()
|
||||
const lives = this.player!.life
|
||||
|
||||
const str = `Level: ${lvl}\tPoints: ${pts}\tHP: ${lives}`
|
||||
this.progressLabel!.text = str
|
||||
}
|
||||
|
||||
hookupCollisions() {
|
||||
// player - asteroids
|
||||
this.physics.add.overlap(
|
||||
this.player!, this.asteroids!, // colliders
|
||||
this.player!.gotHit, // callback
|
||||
undefined, // callback filter
|
||||
this.player // 'this' for callback
|
||||
)
|
||||
|
||||
// bullets - asteroids
|
||||
this.physics.add.overlap(
|
||||
this.asteroids!, this.player!.bullets,
|
||||
Asteroid.prototype.gotHit
|
||||
)
|
||||
}
|
||||
}
|
||||
40
game/src/scenes/StartScene.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import Phaser from 'phaser'
|
||||
|
||||
export default class StartScene extends Phaser.Scene {
|
||||
|
||||
constructor() {
|
||||
super('start-scene')
|
||||
}
|
||||
|
||||
preload() {
|
||||
this.load.image('phaser-logo', 'assets/img/phaser3-logo.png')
|
||||
this.load.image('hs3-logo', 'assets/img/hs3-logo.png')
|
||||
}
|
||||
|
||||
create() {
|
||||
this.add.image(250, 550, 'phaser-logo').setScale(0.5)
|
||||
this.add.image(550, 550, 'hs3-logo').setScale(0.5)
|
||||
|
||||
this.add.text(
|
||||
this.cameras.main.centerX,
|
||||
this.cameras.main.centerY - 100,
|
||||
"SPACE SMASHER 9001!", {
|
||||
font: '64px Verdana',
|
||||
}).setOrigin(0.5, 0.5)
|
||||
|
||||
this.add.text(this.cameras.main.centerX, this.cameras.main.centerY, [
|
||||
"Naciśnij przycisk aby zacząć",
|
||||
"",
|
||||
"Zaloguj się przed rozpoczęciem gry, aby zachować rekord",
|
||||
"albo graj jako gość (bez rankingu)"
|
||||
], {
|
||||
font: '21px Verdana',
|
||||
align: 'center',
|
||||
color: 'cyan'
|
||||
}).setOrigin(0.5, 0.5)
|
||||
|
||||
this.input.on('pointerup', () => {
|
||||
this.scene.start('play-scene', window.myStuff);
|
||||
});
|
||||
}
|
||||
}
|
||||
31
game/src/style.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
html,body{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
background-color: black;
|
||||
font-family: "Verdana", "Geneva", "Tahoma", "sans-serif";
|
||||
}
|
||||
|
||||
body {
|
||||
background-image: url('../public/assets/img/stars.png');
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 100%;
|
||||
background-color: aqua;
|
||||
width: 800px;
|
||||
margin:auto;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.key {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 800px;
|
||||
margin: auto;
|
||||
box-shadow: 0 0 35px rgba(0,255,255,0.3);
|
||||
}
|
||||
31
game/tsconfig.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "es6",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"node_module/phaser/types"
|
||||
],
|
||||
"types": [
|
||||
"phaser"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||