Compare commits
10 Commits
1149f17b20
...
ba2199c60b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba2199c60b | ||
|
|
c3c6ba2f33 | ||
|
|
1ddd5c781d | ||
|
|
37a6581a27 | ||
|
|
c1a093cb45 | ||
|
|
b3e8bc4889 | ||
|
|
f4842a9f32 | ||
|
|
5d13cb900e | ||
|
|
c66cc42118 | ||
|
|
cc6a474a88 |
27
.drone.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
steps:
|
||||
- name: build
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: reg_user
|
||||
password:
|
||||
from_secret: reg_password
|
||||
registry: dockerreg.gltronic.ovh
|
||||
repo: dockerreg.gltronic.ovh/tronio
|
||||
- name: deploy
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
from_secret: deploy_host
|
||||
username:
|
||||
from_secret: deploy_user
|
||||
password:
|
||||
from_secret: deploy_password
|
||||
port: 22
|
||||
script:
|
||||
- cd docker/perso
|
||||
- docker-compose pull tronio
|
||||
- docker-compose up -d tronio
|
||||
3
.gitignore
vendored
@@ -21,4 +21,5 @@ pnpm-debug.log*
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
tronio.jar
|
||||
server/dist
|
||||
client/dist
|
||||
24
Dockerfile
@@ -1,17 +1,17 @@
|
||||
# https://medium.com/bb-tutorials-and-thoughts/packaging-your-vue-js-app-with-nodejs-backend-for-production-83abe213532c
|
||||
|
||||
FROM node:10 AS ui-build
|
||||
FROM node:latest AS client-build
|
||||
WORKDIR /usr/src/app
|
||||
COPY client/ ./client/
|
||||
RUN cd client && npm install && npm run build
|
||||
|
||||
FROM node:10 AS server-build
|
||||
WORKDIR /root/
|
||||
COPY --from=ui-build /usr/src/app/client/dist ./server/dist
|
||||
COPY server/package*.json ./server/
|
||||
RUN cd server && npm install
|
||||
COPY server/src ./server/src
|
||||
FROM node:latest AS server-build
|
||||
WORKDIR /usr/src/app
|
||||
COPY server/ ./server/
|
||||
RUN cd server && npm install && npm run build
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["node", "./server/src/server.js"]
|
||||
FROM node:latest AS serve
|
||||
WORKDIR /usr/src/app
|
||||
COPY server/package*.json ./
|
||||
RUN npm install --only=production
|
||||
COPY --from=server-build /usr/src/app/server/dist ./distServer
|
||||
COPY --from=client-build /usr/src/app/client/dist ./distClient
|
||||
CMD ["node", "./distServer/app.js"]
|
||||
|
||||
21567
client/package-lock.json
generated
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 887 B After Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
client/public/img/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="641.000000pt" height="641.000000pt" viewBox="0 0 641.000000 641.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,641.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2999 6406 c-2 -2 -40 -6 -84 -10 -44 -4 -93 -9 -110 -11 -16 -3 -48
|
||||
-7 -70 -10 -76 -10 -160 -27 -265 -51 -58 -14 -113 -27 -123 -29 -47 -10 -324
|
||||
-108 -403 -143 -124 -55 -290 -138 -324 -162 -15 -11 -31 -20 -35 -20 -5 0
|
||||
-212 -135 -285 -187 -119 -84 -373 -316 -490 -448 -343 -385 -598 -871 -717
|
||||
-1364 -28 -119 -31 -133 -39 -181 -3 -19 -7 -42 -9 -50 -2 -8 -7 -37 -11 -65
|
||||
-3 -27 -8 -59 -10 -69 -12 -63 -19 -219 -19 -411 1 -219 5 -287 29 -465 3 -21
|
||||
18 -102 42 -224 37 -195 156 -523 271 -750 90 -176 260 -444 332 -523 9 -10
|
||||
23 -28 32 -40 44 -64 242 -273 359 -378 299 -271 697 -505 1080 -635 109 -37
|
||||
143 -48 153 -50 1 0 43 -12 92 -25 50 -14 104 -28 120 -31 17 -3 62 -11 100
|
||||
-19 39 -7 92 -16 118 -20 27 -4 58 -8 69 -10 62 -12 209 -19 403 -19 239 0
|
||||
317 6 500 35 92 14 116 18 165 29 25 6 56 12 69 15 13 2 38 9 56 15 18 6 42
|
||||
12 54 15 114 22 416 138 599 230 387 194 709 450 1017 806 55 63 155 197 155
|
||||
207 0 6 4 12 9 14 29 11 233 367 302 528 22 52 47 109 54 125 50 114 126 371
|
||||
160 540 15 74 35 198 41 250 2 22 7 67 11 100 15 140 7 559 -13 705 -36 252
|
||||
-122 589 -194 755 -9 22 -32 74 -50 115 -71 163 -206 412 -259 476 -4 5 -14
|
||||
21 -21 34 -7 13 -17 29 -21 34 -5 6 -41 54 -80 106 -382 509 -946 912 -1554
|
||||
1110 -217 70 -396 109 -670 145 -45 6 -531 16 -536 11z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<Game v-if="isLoggedIn && isSocketConnected"/>
|
||||
<Game v-if="isLoggedIn && isSocketConnected" v-bind:showDebug="debugInfos" v-bind:showLeaderboard="leaderboard"/>
|
||||
<div v-else class="container mainMenu">
|
||||
<img src="./assets/logo.png" alt="TronIo logo" width="150px"/>
|
||||
<h1 class="title">tron.io</h1>
|
||||
<b-button @click="loginPrompt">Start</b-button>
|
||||
<div>
|
||||
<h1 class="gameTitle">tron.i</h1>
|
||||
<img src="./assets/logo_invert.png" alt="TronIo logo" width="110px" class="gameLogo"/>
|
||||
</div>
|
||||
<b-button @click="loginPrompt" class="startButton">Start</b-button>
|
||||
<hr>
|
||||
<b-field label="Music" position="is-centered">
|
||||
<b-radio-button v-model="music" native-value="1">
|
||||
@@ -14,6 +16,8 @@
|
||||
None
|
||||
</b-radio-button>
|
||||
</b-field>
|
||||
<b-switch v-model="debugInfos">Debug info</b-switch>
|
||||
<b-switch v-model="leaderboard">Leaderboard</b-switch>
|
||||
</div>
|
||||
<b-loading v-model="isLoading"/>
|
||||
</div>
|
||||
@@ -31,7 +35,9 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
music: '2'
|
||||
music: '2',
|
||||
debugInfos: false,
|
||||
leaderboard: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -79,11 +85,27 @@ export default {
|
||||
</script>
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
color: rgb(235, 226, 233);
|
||||
background-color: #2a374a;
|
||||
}
|
||||
|
||||
.gameLogo {
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.gameTitle {
|
||||
display: inline-block;
|
||||
font-family: TRON;
|
||||
font-size: 110px;
|
||||
color: #cf0cb4ff;
|
||||
}
|
||||
|
||||
.startButton {
|
||||
margin-top: 75px;
|
||||
}
|
||||
|
||||
.mainMenu {
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 23 KiB |
BIN
client/src/assets/logo_invert.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
client/src/assets/sprite/explosion.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
client/src/assets/style/TRON.TTF
Normal file
@@ -2,8 +2,13 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic&display=swap");
|
||||
|
||||
@font-face {
|
||||
font-family: 'Gravity Regular';
|
||||
src: url('Gravity-Regular.otf') format('opentype');
|
||||
font-family: 'TRON';
|
||||
src: url('TRON.TTF') format('opentype');
|
||||
}
|
||||
|
||||
html {
|
||||
color: rgb(235, 226, 233);
|
||||
background-color: #2a374a;
|
||||
}
|
||||
|
||||
hr {
|
||||
@@ -68,6 +73,10 @@ a {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
.select {
|
||||
&:after,
|
||||
select {
|
||||
|
||||
@@ -12,10 +12,10 @@ $blue: #3498db;
|
||||
$purple: #8e44ad;
|
||||
$red: #e74c3c;
|
||||
$white-ter: #ecf0f1;
|
||||
$primary: $purple;
|
||||
$primary: #cf0cb4ff;
|
||||
$yellow-invert: #fff;
|
||||
|
||||
$family-sans-serif: "Gravity Regular", "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
$family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
"Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
$family-monospace: "Inconsolata", "Consolas", "Monaco", monospace;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="game">
|
||||
<b-modal v-model="playerIsDead">
|
||||
<h1 class="title">DED</h1>
|
||||
<h1 class="titleDed">DED</h1>
|
||||
<h2 class="title">Score: {{player.score}}</h2>
|
||||
<h3 class="subtitle">Best score: {{player.bestScore}}</h3>
|
||||
<b-button @click="respawn">Respawn</b-button>
|
||||
<b-button @click="quit">Quit</b-button>
|
||||
</b-modal>
|
||||
<canvas
|
||||
class="game-canvas"
|
||||
@@ -16,11 +17,12 @@
|
||||
<script>
|
||||
import { send } from '@/store/socketPlugin'
|
||||
import { render } from '@/game/render.js'
|
||||
// import { update } from '@/game/update.js'
|
||||
import { sound } from '@/game/sound.js'
|
||||
import { Explosion } from '@/game/sprites.js'
|
||||
|
||||
export default {
|
||||
name: 'Game',
|
||||
props: ['showDebug', 'showLeaderboard'],
|
||||
data () {
|
||||
return {
|
||||
mouse: {
|
||||
@@ -34,8 +36,10 @@ export default {
|
||||
stats: {
|
||||
totalWalls: 0,
|
||||
lastUpdateTime: {},
|
||||
lastFrame: 0
|
||||
lastFrame: 0,
|
||||
lastSpriteFrame: 0
|
||||
},
|
||||
deadPlayers: [],
|
||||
renderTimer: null
|
||||
}
|
||||
},
|
||||
@@ -52,6 +56,9 @@ export default {
|
||||
player () {
|
||||
return this.$store.state.game.player
|
||||
},
|
||||
sprites () {
|
||||
return this.$store.state.game.sprites
|
||||
},
|
||||
players () {
|
||||
const pastUpdate = this.$store.state.game.updates[0]
|
||||
const nextUpdate = this.$store.state.game.updates[1]
|
||||
@@ -69,7 +76,6 @@ export default {
|
||||
return this.$store.state.game.leaderboard
|
||||
},
|
||||
playerIsDead () {
|
||||
if (this.player.state === 'DEAD') sound.explosion()
|
||||
return this.player.state === 'DEAD'
|
||||
}
|
||||
},
|
||||
@@ -78,7 +84,8 @@ export default {
|
||||
this.canvas.addEventListener('mousemove', this.mouseEvent)
|
||||
this.canvas.addEventListener('touchmove', this.touchEvent)
|
||||
this.canvas.addEventListener('resize', this.setCanvasSize)
|
||||
// this.renderTimer = setInterval(this.render, 1000 / 120)
|
||||
// this.renderTimer = setInterval(this.render, 1000 / 60)
|
||||
// this.renderTimer = setInterval(this.renderSprites, 1000 / 30)
|
||||
this.render()
|
||||
},
|
||||
methods: {
|
||||
@@ -108,19 +115,64 @@ export default {
|
||||
if (player.state === 'DEAD') this.context.globalAlpha = 1
|
||||
|
||||
this.stats.totalWalls += player.walls.length
|
||||
|
||||
// Check if player was dead before for one time event
|
||||
if (player.state === 'DEAD' && !this.deadPlayers.includes(player.id)) {
|
||||
this.deadPlayers.push(player.id)
|
||||
sound.explosion()
|
||||
this.sprites.push(new Explosion(player.x, player.y))
|
||||
}
|
||||
|
||||
// Remove player from dead list
|
||||
if (player.state !== 'DEAD' && this.deadPlayers.includes(player.id)) {
|
||||
const i = this.deadPlayers.indexOf(player.id)
|
||||
this.deadPlayers.splice(i, 1)
|
||||
}
|
||||
})
|
||||
|
||||
render.leaderboard(this.context, this.canvas, this.leaderboard)
|
||||
this.renderSprites()
|
||||
|
||||
render.mouse(this.context, this.mouse, this.player)
|
||||
render.debug(this.context, this.camera, this.mouse, this.canvas, this.stats)
|
||||
|
||||
if (this.showLeaderboard) {
|
||||
render.leaderboard(this.context, this.canvas, this.leaderboard)
|
||||
}
|
||||
|
||||
if (this.showDebug) {
|
||||
render.debug(this.context, this.camera, this.mouse, this.canvas, this.stats)
|
||||
}
|
||||
|
||||
this.stats.lastFrame = performance.now()
|
||||
|
||||
const nextUpdate = this.$store.state.game.updates[1]
|
||||
if (nextUpdate !== undefined) this.stats.lastUpdateTime = nextUpdate.time
|
||||
|
||||
requestAnimationFrame(this.render, this.canvas)
|
||||
},
|
||||
renderSprites () {
|
||||
let nextFrame = false
|
||||
// Check if needed to show next frame for animations
|
||||
if ((performance.now() - this.stats.lastSpriteFrame) > 50) {
|
||||
nextFrame = true
|
||||
this.stats.lastSpriteFrame = performance.now()
|
||||
}
|
||||
|
||||
this.sprites.forEach((sprite, index, object) => {
|
||||
render.sprite(this.context, this.canvas, this.camera, sprite)
|
||||
if (nextFrame) {
|
||||
if (sprite.framesCounter === sprite.frames) {
|
||||
if (sprite.removeAfterAnimation) {
|
||||
object.splice(index, 1)
|
||||
} else {
|
||||
// console.log('SPRITE ', index, 'frame reset')
|
||||
sprite.framesCounter = 0
|
||||
}
|
||||
} else {
|
||||
// console.log('SPRITE ', index, 'frame++', sprite.framesCounter)
|
||||
sprite.framesCounter++
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
mouseEvent (event) {
|
||||
var rect = this.canvas.getBoundingClientRect()
|
||||
this.mouse.x = event.clientX - rect.left
|
||||
@@ -169,7 +221,21 @@ export default {
|
||||
},
|
||||
respawn () {
|
||||
send({ type: 'respawn' })
|
||||
},
|
||||
quit () {
|
||||
send({ type: 'logout' })
|
||||
this.$store.dispatch('game/logout')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.titleDed {
|
||||
display: inline-block;
|
||||
font-family: TRON;
|
||||
font-size: 70px;
|
||||
color: #cf0cb4ff;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -54,7 +54,7 @@ export const render = {
|
||||
context.beginPath()
|
||||
context.lineWidth = settings.wallSize
|
||||
context.strokeStyle = player.color
|
||||
// context.shadowBlur = 0
|
||||
// context.shadowBlur = 0.5
|
||||
// context.shadowColor = player.color
|
||||
player.walls.forEach(wall => {
|
||||
const canvasX = canvas.width / 2 + wall.x - camera.x
|
||||
@@ -120,14 +120,36 @@ export const render = {
|
||||
context.stroke()
|
||||
},
|
||||
leaderboard (context, canvas, leaderboard) {
|
||||
context.globalAlpha = 0.1
|
||||
context.fillStyle = 'white'
|
||||
context.fillRect(canvas.width - 160, 35, 120, 20 + leaderboard.length * 5)
|
||||
context.globalAlpha = 1
|
||||
|
||||
context.fillStyle = 'white'
|
||||
context.textAlign = 'end'
|
||||
context.fillText('Leaderboard: ', canvas.width - 50, 10)
|
||||
context.fillText('Leaderboard: ', canvas.width - 40, 30)
|
||||
var i = 1
|
||||
leaderboard.forEach(player => {
|
||||
context.fillStyle = player.color
|
||||
context.fillText(player.name + ' - ' + player.score + ' (' + player.bestScore + ')', canvas.width - 50, 15 + i * 10)
|
||||
context.fillText(player.name + ' - ' + player.score + ' (' + player.bestScore + ')', canvas.width - 50, 40 + i * 10)
|
||||
i++
|
||||
})
|
||||
},
|
||||
sprite (context, canvas, camera, sprite) {
|
||||
const canvasX = canvas.width / 2 + sprite.x - camera.x
|
||||
const canvasY = canvas.height / 2 + sprite.y - camera.y
|
||||
|
||||
const posX = canvasX - sprite.sizeX * 2
|
||||
const posY = canvasY - sprite.sizeY * 2
|
||||
const sizeL = sprite.sizeX * 4
|
||||
const sizeH = sprite.sizeY * 4
|
||||
|
||||
if (sprite.isAnimated) {
|
||||
const frameX = sprite.heightX * (sprite.framesCounter % 8)
|
||||
const frameY = sprite.heightY * Math.floor(sprite.framesCounter / 8)
|
||||
context.drawImage(sprite.image, frameX, frameY, sprite.heightX, sprite.heightY, posX, posY, sizeL, sizeH)
|
||||
} else {
|
||||
context.drawImage(sprite.image, posX, posY, sizeL, sizeH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
client/src/game/sprites.js
Normal file
@@ -0,0 +1,41 @@
|
||||
export class Player {
|
||||
|
||||
}
|
||||
|
||||
export class Sprite {
|
||||
x = 0;
|
||||
y = 0;
|
||||
sizeX = 0;
|
||||
sizeY = 0;
|
||||
heightX = 0;
|
||||
heightY = 0;
|
||||
image = new Image();
|
||||
isAnimated = false;
|
||||
removeAfterAnimation = false;
|
||||
frames = 0;
|
||||
framesCounter = 0;
|
||||
}
|
||||
|
||||
export class Wall {
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
export class Explosion extends Sprite {
|
||||
constructor (x, y) {
|
||||
super()
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.sizeX = 50
|
||||
this.sizeY = 50
|
||||
this.heightX = 256
|
||||
this.heightY = 256
|
||||
this.image = new Image()
|
||||
this.isAnimated = true
|
||||
this.removeAfterAnimation = true
|
||||
this.frames = 32
|
||||
this.framesCounter = 0
|
||||
|
||||
this.image.src = require('@/assets/sprite/explosion.png')
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const state = {
|
||||
color: 'red',
|
||||
state: 'DEAD'
|
||||
},
|
||||
sprites: [],
|
||||
leaderboard: [],
|
||||
updates: [],
|
||||
firstUpdateTime: null,
|
||||
@@ -36,6 +37,10 @@ const actions = {
|
||||
commit('SET_PLAYER', player)
|
||||
commit('SET_LOGIN', true)
|
||||
},
|
||||
logout ({ commit }) {
|
||||
commit('SET_LOGIN', false)
|
||||
commit('CLEAR_UPDATE')
|
||||
},
|
||||
settings ({ commit }, settings) {
|
||||
commit('SET_SETTINGS', settings)
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ToastProgrammatic as Toast } from 'buefy'
|
||||
|
||||
// const connection = new WebSocket('ws://localhost:3000/socket')
|
||||
const connection = new WebSocket('wss://tronio.gltronic.ovh/socket')
|
||||
// const connection = new WebSocket('ws://localhost:3000/socket')
|
||||
|
||||
export default function createSocketPlugin () {
|
||||
return store => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
version: '3'
|
||||
---
|
||||
version: "2"
|
||||
services:
|
||||
tronio:
|
||||
image: gltron/tronio
|
||||
image: dockerreg.gltronic.ovh/tronio
|
||||
container_name: tronio
|
||||
ports:
|
||||
- 8006:8080
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=8080
|
||||
- SERVER=tronio.gltronic.ovh
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"standard"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
},
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>tronio</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1599131612586</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
2240
server/package-lock.json
generated
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "tronio-server",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"description": "Tron.io game server",
|
||||
"main": "server.js",
|
||||
"main": "dist/app.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"start": "tsc && node dist/app.js",
|
||||
"build": "tsc",
|
||||
"lint": "echo \"Warning: no lint specified\"",
|
||||
"test": "echo \"Warning: no test specified\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,10 +21,10 @@
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.12.1",
|
||||
"eslint-config-standard": "^16.0.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1"
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/node": "^14.14.22",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/ws": "^7.4.0",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
138
server/src/Game.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import WebSocket from 'ws';
|
||||
import { Player } from './models/Player';
|
||||
import { GameSettings } from './models/GameSettings';
|
||||
import { GameWebSocket } from './models/GameWebSocket';
|
||||
|
||||
export class Game {
|
||||
private gameSettings = new GameSettings();
|
||||
private players = new Map<string, Player>();
|
||||
private sockets = new Map<string, WebSocket>();
|
||||
|
||||
private computeUpdates = false;
|
||||
private updateInterval = setInterval(() => this.step(), 10000000);
|
||||
private lastUpdateTime = Date.now();
|
||||
private doUpdate = true;
|
||||
|
||||
constructor () {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
|
||||
public login (connection: GameWebSocket, name: string): void {
|
||||
console.log('[GAME] Player', name, 'connected |', this.players.size + 1);
|
||||
const id = uuidv4();
|
||||
connection.id = id;
|
||||
connection.name = name;
|
||||
this.sockets.set(id, connection);
|
||||
this.players.set(id, new Player(id, name));
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'login',
|
||||
player: this.players.get(id)
|
||||
}));
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'gameSettings',
|
||||
gameSettings: this.gameSettings
|
||||
}));
|
||||
|
||||
if (!this.computeUpdates) {
|
||||
console.log('[GAME] Starting updates');
|
||||
this.updateInterval = setInterval(() => this.step(), 1000 / 60);
|
||||
this.computeUpdates = true;
|
||||
}
|
||||
}
|
||||
|
||||
public logout (connection: GameWebSocket): void {
|
||||
console.log('[GAME] Player', connection.name, 'loggedout |', this.players.size - 1);
|
||||
this.players.delete(connection.id);
|
||||
|
||||
if (this.players.size === 0) {
|
||||
console.log('[GAME] Stoping updates');
|
||||
clearInterval(this.updateInterval);
|
||||
this.computeUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect (connection: GameWebSocket): void {
|
||||
console.log('[GAME] Player', connection.name, 'disconnected |', this.sockets.size - 1);
|
||||
this.sockets.delete(connection.id);
|
||||
this.logout(connection);
|
||||
}
|
||||
|
||||
public respawn (connection: GameWebSocket): void {
|
||||
console.log('[GAME] Player ' + connection.name + ' respawned');
|
||||
this.players.get(connection.id)?.reset();
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'gamePlayerSpawn',
|
||||
player: this.players.get(connection.id)
|
||||
}))
|
||||
}
|
||||
|
||||
public update (connection: GameWebSocket, player: Player): void {
|
||||
const playerToUpdate = this.players.get(connection.id);
|
||||
if (playerToUpdate) playerToUpdate.angle = player.angle;
|
||||
}
|
||||
|
||||
public kill (player: Player): void {
|
||||
console.log('[GAME] Player', player.name, 'died');
|
||||
player.kill();
|
||||
this.sockets.get(player.id)?.send(JSON.stringify({
|
||||
type: 'gamePlayerDead',
|
||||
player: player
|
||||
}))
|
||||
}
|
||||
|
||||
private step (): void {
|
||||
const currentTime = Date.now()
|
||||
const durationSinceLastUpdate = (currentTime - this.lastUpdateTime) / 1000;
|
||||
const tickToSimulate = (durationSinceLastUpdate * 60) / 1000;
|
||||
this.lastUpdateTime = currentTime;
|
||||
|
||||
// console.log('UPDATE ' + currentTime + ' doUpdate ' + doUpdate)
|
||||
|
||||
this.players.forEach((player, id) => {
|
||||
if (player.state === 'DEAD') return
|
||||
|
||||
if (player.isOutOfBorders()) this.kill(player);
|
||||
|
||||
this.players.forEach((player2, id2) => {
|
||||
for (let i = 0; i < player2.walls.length - 2; i++) {
|
||||
// Prevent self destroy on last wall
|
||||
if (player === player2 && i >= player2.walls.length - 1) break
|
||||
|
||||
const wallA = player2.walls[i]
|
||||
const wallB = player2.walls[i + 1]
|
||||
|
||||
if (player.isCloseToWall(wallA, wallB)) {
|
||||
if (player.isCrossingLine(wallA, wallB)) {
|
||||
if (player !== player2) player2.score += 300
|
||||
this.kill(player);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.players.forEach((player, id) => {
|
||||
player.step(tickToSimulate);
|
||||
});
|
||||
|
||||
if (this.doUpdate) this.broadcastUpdate();
|
||||
this.doUpdate = !this.doUpdate;
|
||||
}
|
||||
|
||||
private broadcastUpdate () {
|
||||
const update = {
|
||||
type: 'gameUpdate',
|
||||
players: [ ...this.players.values() ],
|
||||
time: this.lastUpdateTime
|
||||
}
|
||||
|
||||
this.sockets.forEach((connection, id) => {
|
||||
connection.send(JSON.stringify(update));
|
||||
});
|
||||
}
|
||||
}
|
||||
36
server/src/app.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import express from 'express';
|
||||
import { Game } from './Game';
|
||||
import WebSocket, { Server } from 'ws';
|
||||
import { GameWebSocket } from './models/GameWebSocket';
|
||||
|
||||
const app = express();
|
||||
const WebSocketServer = Server;
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.static('distClient/'));
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Tron.io running on port ${port}`);
|
||||
});
|
||||
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
const game = new Game();
|
||||
|
||||
wss.on('connection', function (connection: WebSocket) {
|
||||
connection.on('close', () => game.disconnect(connection as GameWebSocket));
|
||||
|
||||
connection.on('message', ( message ) => {
|
||||
try {
|
||||
const data = JSON.parse(message as string);
|
||||
switch (data.type) {
|
||||
case 'login': game.login(connection as GameWebSocket, data.name); break;
|
||||
case 'logout': game.logout(connection as GameWebSocket); break;
|
||||
case 'respawn': game.respawn(connection as GameWebSocket); break;
|
||||
case 'update': game.update(connection as GameWebSocket, data.player); break;
|
||||
}
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
const Player = require('./models/player')
|
||||
const gameSettings = require('./models/gameSettings')
|
||||
|
||||
const players = {}
|
||||
const sockets = {}
|
||||
|
||||
let updateInterval = -1
|
||||
|
||||
let lastUpdateTime = Date.now()
|
||||
let doUpdate = true
|
||||
|
||||
function login (connection, name) {
|
||||
const id = uuidv4()
|
||||
connection.id = id
|
||||
sockets[id] = connection
|
||||
players[id] = new Player(id, name)
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'login',
|
||||
player: players[id]
|
||||
}))
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'gameSettings',
|
||||
gameSettings: gameSettings
|
||||
}))
|
||||
|
||||
if (updateInterval === -1) updateInterval = setInterval(() => step(), 1000 / 60)
|
||||
}
|
||||
|
||||
function logout (connection) {
|
||||
delete sockets[connection.id]
|
||||
delete players[connection.id]
|
||||
|
||||
if (Object.keys(players).length === 0) {
|
||||
clearInterval(updateInterval)
|
||||
updateInterval = -1
|
||||
}
|
||||
}
|
||||
|
||||
function respawn (connection) {
|
||||
players[connection.id].reset()
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'gamePlayerSpawn',
|
||||
player: players[connection.id]
|
||||
}))
|
||||
}
|
||||
|
||||
function update (connection, player) {
|
||||
players[connection.id].angle = player.angle
|
||||
}
|
||||
|
||||
function kill (player) {
|
||||
player.kill()
|
||||
sockets[player.id].send(JSON.stringify({
|
||||
type: 'gamePlayerDead',
|
||||
player: player
|
||||
}))
|
||||
}
|
||||
|
||||
function step () {
|
||||
const currentTime = Date.now()
|
||||
const durationSinceLastUpdate = (currentTime - lastUpdateTime) / 1000
|
||||
const tickToSimulate = (durationSinceLastUpdate * 60) / 1000
|
||||
lastUpdateTime = currentTime
|
||||
|
||||
// console.log('UPDATE ' + currentTime + ' doUpdate ' + doUpdate)
|
||||
|
||||
Object.values(players).forEach((player) => {
|
||||
if (player.isOutOfBorders()) kill(player)
|
||||
|
||||
Object.values(players).forEach((player2) => {
|
||||
if (player.state === 'DEAD') return
|
||||
|
||||
for (let i = 0; i < player2.walls.length - 2; i++) {
|
||||
// Prevent self destroy on last wall
|
||||
if (player === player2 && i >= player2.walls.length - 1) break
|
||||
|
||||
const wallA = player2.walls[i]
|
||||
const wallB = player2.walls[i + 1]
|
||||
|
||||
if (player.isCloseToWall(wallA, wallB)) {
|
||||
if (player.isCrossingLine(wallA, wallB)) {
|
||||
if (player !== player2) player2.score += 300
|
||||
kill(player)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Object.values(players).forEach((player) => {
|
||||
player.step(tickToSimulate)
|
||||
})
|
||||
|
||||
if (doUpdate) broadcastUpdate()
|
||||
doUpdate = !doUpdate
|
||||
}
|
||||
|
||||
function broadcastUpdate () {
|
||||
const update = {
|
||||
type: 'gameUpdate',
|
||||
players: Object.values(players),
|
||||
time: lastUpdateTime
|
||||
}
|
||||
|
||||
Object.values(sockets).forEach((connection) => {
|
||||
connection.send(JSON.stringify(update))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
logout,
|
||||
respawn,
|
||||
update
|
||||
}
|
||||
10
server/src/models/GameSettings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class GameSettings {
|
||||
playerSize = 10;
|
||||
playerSpeed = 5;
|
||||
playerTurnSpeed = 10;
|
||||
wallSize = 8;
|
||||
wallUpdate = 3;
|
||||
arenaSize = 1000;
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
6
server/src/models/GameWebSocket.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import WebSocket from 'ws';
|
||||
|
||||
export interface GameWebSocket extends WebSocket {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
80
server/src/models/Player.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Wall } from './Wall';
|
||||
import { GameSettings } from './GameSettings';
|
||||
|
||||
export class Player {
|
||||
|
||||
private gameSettings = new GameSettings();
|
||||
public bestScore = 0;
|
||||
public angle = 0;
|
||||
public score = 0;
|
||||
public color = '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6);
|
||||
public x = this.gameSettings.arenaSize * (0.25 + Math.random() * 0.5);
|
||||
public y = this.gameSettings.arenaSize * (0.25 + Math.random() * 0.5);
|
||||
public walls: Wall[] = [];
|
||||
public lastWall = 0;
|
||||
public state = 'ALIVE';
|
||||
|
||||
constructor (public id: string, public name: string) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.score = 0;
|
||||
this.color = '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6);
|
||||
this.x = this.gameSettings.arenaSize * (0.25 + Math.random() * 0.5);
|
||||
this.y = this.gameSettings.arenaSize * (0.25 + Math.random() * 0.5);
|
||||
this.walls = [];
|
||||
this.lastWall = 0;
|
||||
this.state = 'ALIVE';
|
||||
}
|
||||
|
||||
kill () {
|
||||
this.state = 'DEAD';
|
||||
if (this.bestScore < this.score) this.bestScore = this.score;
|
||||
}
|
||||
|
||||
isOutOfBorders () {
|
||||
return this.x - this.gameSettings.playerSize < 0 ||
|
||||
this.x + this.gameSettings.playerSize > this.gameSettings.arenaSize ||
|
||||
this.y - this.gameSettings.playerSize < 0 ||
|
||||
this.y + this.gameSettings.playerSize > this.gameSettings.arenaSize
|
||||
}
|
||||
|
||||
isCloseToWall (wallA: Wall, wallB: Wall) {
|
||||
const xar = Math.min(wallA.x, wallB.x) - this.gameSettings.playerSize
|
||||
const yar = Math.min(wallA.y, wallB.y) - this.gameSettings.playerSize
|
||||
const xbr = Math.min(wallA.x, wallB.x) + this.gameSettings.playerSize
|
||||
const ybr = Math.min(wallA.y, wallB.y) + this.gameSettings.playerSize
|
||||
|
||||
return ((this.x >= xar && this.x <= xbr) && (this.y >= yar && this.y <= ybr))
|
||||
}
|
||||
|
||||
isCrossingLine (wallA: Wall, wallB: Wall) {
|
||||
const xa = wallA.x
|
||||
const ya = wallA.y
|
||||
const xb = wallB.x
|
||||
const yb = wallB.y
|
||||
const xc = this.x
|
||||
const yc = this.y
|
||||
const radius = this.gameSettings.playerSize
|
||||
|
||||
return Math.abs((yb - ya) * xc - (xb - xa) * yc + xb * ya - yb * xa) / Math.sqrt(Math.pow(xb - xa, 2) + Math.pow(yb - ya, 2)) < radius
|
||||
}
|
||||
|
||||
step (tickToSimulate: number) {
|
||||
if (this.state === 'DEAD') return
|
||||
|
||||
for (let i = 0; i < tickToSimulate; i++) {
|
||||
this.lastWall++
|
||||
|
||||
if (this.lastWall > this.gameSettings.wallUpdate) {
|
||||
this.walls.push(new Wall(this.x, this.y))
|
||||
this.score++
|
||||
this.lastWall = 0
|
||||
}
|
||||
|
||||
this.x = this.x + this.gameSettings.playerSpeed * Math.cos(this.angle)
|
||||
this.y = this.y + this.gameSettings.playerSpeed * Math.sin(this.angle)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
server/src/models/Wall.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class Wall {
|
||||
constructor (public x: number, public y: number) { }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = Object.freeze({
|
||||
playerSize: 10,
|
||||
playerSpeed: 5,
|
||||
playerTurnSpeed: 10,
|
||||
wallSize: 8,
|
||||
wallUpdate: 5,
|
||||
arenaSize: 1000
|
||||
})
|
||||
@@ -1,74 +0,0 @@
|
||||
const Wall = require('./wall')
|
||||
const gameSettings = require('./gameSettings')
|
||||
|
||||
class Player {
|
||||
constructor (id, name) {
|
||||
this.id = id
|
||||
this.name = name
|
||||
this.bestScore = 0
|
||||
this.angle = 0
|
||||
this.reset()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.score = 0
|
||||
this.color = '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6)
|
||||
this.x = gameSettings.arenaSize * (0.25 + Math.random() * 0.5)
|
||||
this.y = gameSettings.arenaSize * (0.25 + Math.random() * 0.5)
|
||||
this.walls = []
|
||||
this.lastWall = 0
|
||||
this.state = 'ALIVE'
|
||||
}
|
||||
|
||||
kill () {
|
||||
this.state = 'DEAD'
|
||||
if (this.bestScore < this.score) this.bestScore = this.score
|
||||
}
|
||||
|
||||
isOutOfBorders () {
|
||||
return this.x - gameSettings.playerSize < 0 ||
|
||||
this.x + gameSettings.playerSize > gameSettings.arenaSize ||
|
||||
this.y - gameSettings.playerSize < 0 ||
|
||||
this.y + gameSettings.playerSize > gameSettings.arenaSize
|
||||
}
|
||||
|
||||
isCloseToWall (wallA, wallB) {
|
||||
const xar = Math.min(wallA.x, wallB.x) - gameSettings.playerSize
|
||||
const yar = Math.min(wallA.y, wallB.y) - gameSettings.playerSize
|
||||
const xbr = Math.min(wallA.x, wallB.x) + gameSettings.playerSize
|
||||
const ybr = Math.min(wallA.y, wallB.y) + gameSettings.playerSize
|
||||
|
||||
return ((this.x >= xar && this.x <= xbr) && (this.y >= yar && this.y <= ybr))
|
||||
}
|
||||
|
||||
isCrossingLine (wallA, wallB) {
|
||||
const xa = wallA.x
|
||||
const ya = wallA.y
|
||||
const xb = wallB.x
|
||||
const yb = wallB.y
|
||||
const xc = this.x
|
||||
const yc = this.y
|
||||
const radius = gameSettings.playerSize
|
||||
|
||||
return Math.abs((yb - ya) * xc - (xb - xa) * yc + xb * ya - yb * xa) / Math.sqrt(Math.pow(xb - xa, 2) + Math.pow(yb - ya, 2)) < radius
|
||||
}
|
||||
|
||||
step (tickToSimulate) {
|
||||
if (this.state === 'DEAD') return
|
||||
|
||||
for (let i = 0; i < tickToSimulate; i++) {
|
||||
this.lastWall++
|
||||
|
||||
if (this.lastWall > gameSettings.wallUpdate) {
|
||||
this.walls.push(new Wall(this.x, this.y))
|
||||
this.score++
|
||||
this.lastWall = 0
|
||||
}
|
||||
|
||||
this.x = this.x + gameSettings.playerSpeed * Math.cos(this.angle)
|
||||
this.y = this.y + gameSettings.playerSpeed * Math.sin(this.angle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Player
|
||||
@@ -1,8 +0,0 @@
|
||||
class Wall {
|
||||
constructor (x, y) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Wall
|
||||
@@ -1,33 +0,0 @@
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const game = require('./game')
|
||||
const WebSocketServer = require('ws').Server
|
||||
const port = process.env.PORT || 3000
|
||||
|
||||
app.use(express.static('dist'))
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Tron.io running on port ${port}`)
|
||||
})
|
||||
|
||||
const wss = new WebSocketServer({ server })
|
||||
|
||||
wss.on('connection', function (connection) {
|
||||
connection.on('close', () => game.logout(connection))
|
||||
|
||||
connection.on('message', (message) => {
|
||||
let data
|
||||
|
||||
try {
|
||||
data = JSON.parse(message)
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case 'login': game.login(connection, data.name); break
|
||||
case 'respawn': game.respawn(connection); break
|
||||
case 'update': game.update(connection, data.player); break
|
||||
}
|
||||
})
|
||||
})
|
||||
11
server/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||