Move the game to subfolder

This commit is contained in:
KGrzeg 2021-10-17 18:22:57 +02:00
parent a724d80bb7
commit a86c5cdda0
39 changed files with 0 additions and 0 deletions

View 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}`
}
}

View 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)
}
}
}

View 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
}
}
}

View 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
View 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")
}
}
}

View 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
View 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
View 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
View 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
View 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)

View 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)
}
}

View 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
)
}
}

View 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
View 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);
}