🎉
This commit is contained in:
commit
bddd51dd5d
14 changed files with 8441 additions and 0 deletions
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.idea
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 OneWayBlock
|
||||||
|
|
||||||
|
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.
|
||||||
11
README.md
Normal file
11
README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Clash of Coins - Graffiti Tutorial
|
||||||
|
|
||||||
|
This repository contains source code of Graffiti Tutorial Series. It consists of two main parts - `client` and `server`.
|
||||||
|
|
||||||
|
## Client
|
||||||
|
|
||||||
|
Client implements a rendering system which listens to mouse events or events emitted by the server and draws lines on the canvas. It supports variable brush size and color, and also has eraser mode.
|
||||||
|
|
||||||
|
1. Run `npm install` to install dependencies
|
||||||
|
2. Run `npm start` to launch a dev server
|
||||||
|
3. Open <http://localhost:8080>
|
||||||
4492
client/package-lock.json
generated
Normal file
4492
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
client/package.json
Normal file
26
client/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "graffiti-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "./build/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack-dev-server --mode development",
|
||||||
|
"start": "npm run dev",
|
||||||
|
"build": "webpack"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@webpack-cli/serve": "^2.0.5",
|
||||||
|
"html-webpack-plugin": "^5.6.0",
|
||||||
|
"webpack": "^5.89.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pixi/utils": "^7.3.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dat.gui": "^0.7.9",
|
||||||
|
"pixi.js": "^7.3.2",
|
||||||
|
"socket.io": "^4.7.2",
|
||||||
|
"socket.io-client": "^4.7.2",
|
||||||
|
"webpack-dev-server": "^4.15.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
215
client/src/App.js
Normal file
215
client/src/App.js
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
|
||||||
|
import * as PIXI from 'pixi.js';
|
||||||
|
import {io} from 'socket.io-client';
|
||||||
|
|
||||||
|
import {DrawingApp, Layer} from './DrawingApp';
|
||||||
|
|
||||||
|
const socket = io('ws://192.168.0.155:7777');
|
||||||
|
|
||||||
|
var id = "";
|
||||||
|
var drawing_app = null;
|
||||||
|
|
||||||
|
var game_info = document.getElementById('game_state_info');
|
||||||
|
var pixi_wrapper = document.getElementById('pixi-wrapper');
|
||||||
|
|
||||||
|
function setGameInfo(info) {
|
||||||
|
game_info.innerHTML = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log("\n*** CONNECTED TO SOCKET *** ", socket.id);
|
||||||
|
|
||||||
|
id = socket.id;
|
||||||
|
|
||||||
|
console.log("\n*** Startup! ***");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('join_room_btn').addEventListener("click", (e) => {
|
||||||
|
var room_code = document.getElementById('room_code').value;
|
||||||
|
var username = document.getElementById('username').value;
|
||||||
|
|
||||||
|
socket.emit('join_room', room_code, username);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('room', (room_id) => {
|
||||||
|
// join the room
|
||||||
|
console.log(room_id);
|
||||||
|
document.getElementById('game_room_code').innerHTML = room_id;
|
||||||
|
|
||||||
|
document.getElementById('game_setup').style.display = "none";
|
||||||
|
document.getElementById('game_info').style.display = "initial";
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('room_error', (error_msg) => {
|
||||||
|
alert(error_msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('create_room_btn').addEventListener("click", (e) => {
|
||||||
|
// Tell the server we want a new room
|
||||||
|
var username = document.getElementById('username').value;
|
||||||
|
socket.emit("create_room", username);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('player_data', (player_arr) => {
|
||||||
|
var count = player_arr.length;
|
||||||
|
document.getElementById('current_player_count').innerHTML = count;
|
||||||
|
|
||||||
|
var player_list = document.getElementById('player_list');
|
||||||
|
|
||||||
|
player_list.innerHTML = "";
|
||||||
|
|
||||||
|
player_arr.forEach((player) => {
|
||||||
|
player_list.innerHTML += "<li>" + player.player_name + "</li>";
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('start_game_btn').disabled = (count < 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('start_game_btn').addEventListener('click', (e) => {
|
||||||
|
socket.emit('start_game');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game_started', () => {
|
||||||
|
// game_data.game_leader =
|
||||||
|
console.log("yooo");
|
||||||
|
document.getElementById('pre_game_player_list').style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('leader_selected', (leader) => {
|
||||||
|
console.log(leader);
|
||||||
|
console.log(id);
|
||||||
|
setGameInfo(`Please wait while the leader ${leader.player_name} is selecting a topic to be painted`);
|
||||||
|
if(leader.id == id) {
|
||||||
|
setGameInfo("You are the game leader! Please choose a topic and a category that the topic fits in.");
|
||||||
|
document.getElementById('game_leader_input').style.display = "initial";
|
||||||
|
|
||||||
|
document.getElementById('submit_topic').addEventListener('click', (e) => {
|
||||||
|
var topic = document.getElementById('game_topic').value;
|
||||||
|
var category = document.getElementById('game_category').value;
|
||||||
|
|
||||||
|
socket.emit('topic_selected', topic, category);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('topic_selected', (topic, category) => {
|
||||||
|
console.log(topic, category);
|
||||||
|
document.getElementById('game_leader_input').style.display = "none";
|
||||||
|
setGameInfo(`Topic: ${topic}<br />Category: ${category}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game_finished', (leader, player_list) => {
|
||||||
|
var pl = document.getElementById("endgame_player_list");
|
||||||
|
|
||||||
|
pl.innerHTML = "";
|
||||||
|
|
||||||
|
player_list.forEach((player) => {
|
||||||
|
if (player.id == leader.id) {
|
||||||
|
pl.innerHTML += `<li style="font-weight: bold; color: black;">${player.player_name} (Leader)</li>`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pl.innerHTML += `<li style="color: ${player.player_color};">${player.player_name}</li>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pl.style.display = "initial";
|
||||||
|
|
||||||
|
var ink_bar = document.getElementById('myBar');
|
||||||
|
ink_bar.style.display = "none";
|
||||||
|
|
||||||
|
document.getElementById('current_user_text').style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('actually_start_game', (player_data) => {
|
||||||
|
// Get player with our id to get the correct color
|
||||||
|
drawing_app = new DrawingApp(pixi_wrapper, player_data);
|
||||||
|
window.drawing_app = drawing_app;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('draw_data', (round_num, data) => {
|
||||||
|
console.log("Draw Data: ", round_num);
|
||||||
|
var cur_layer = drawing_app.layers[round_num];
|
||||||
|
|
||||||
|
cur_layer.drawPointLine(data[0], data[1], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('advance_round', (round_num, current_player) => {
|
||||||
|
var ink_bar = document.getElementById('myBar');
|
||||||
|
ink_bar.style.width = "100%";
|
||||||
|
|
||||||
|
var current_user_text = `It's <span style="white-space:pre; font-weight: bold; color: ${current_player.player_color}">${current_player.player_name}</span>'s turn to draw!`
|
||||||
|
|
||||||
|
document.getElementById('current_user_text').innerHTML = current_user_text;
|
||||||
|
|
||||||
|
if(id == current_player.id) {
|
||||||
|
ink_bar.style.backgroundColor = current_player.player_color;
|
||||||
|
ink_bar.style.visibility = "visible";
|
||||||
|
|
||||||
|
const cur_layer = drawing_app.layers[round_num];
|
||||||
|
|
||||||
|
cur_layer.enable();
|
||||||
|
|
||||||
|
cur_layer.setLivePaintProgressCallback((old_pos, new_pos) => {
|
||||||
|
var ink_pct = cur_layer.getPaintPercentage();
|
||||||
|
|
||||||
|
ink_bar.style.width = ((1.0 - ink_pct) * 100) + "%";
|
||||||
|
|
||||||
|
console.log(ink_pct);
|
||||||
|
|
||||||
|
socket.emit("draw_data", [old_pos, new_pos]);
|
||||||
|
});
|
||||||
|
|
||||||
|
cur_layer.setFinishedPaintingCallback(() => {
|
||||||
|
cur_layer.disable();
|
||||||
|
socket.emit("round_finished");
|
||||||
|
});
|
||||||
|
|
||||||
|
/*player_data.forEach((p) => {
|
||||||
|
if(p.id == id) {
|
||||||
|
ink_bar.style.backgroundColor = p.player_color;
|
||||||
|
// can probably get the color from current_player
|
||||||
|
// since we only show this if we are the current player
|
||||||
|
// yeah...
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ink_bar.style.visibility = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Set current drawing layer, activate if WE are drawing
|
||||||
|
// If we are not drawing, still hook drawing data from server into
|
||||||
|
// drawing app
|
||||||
|
|
||||||
|
// TODO: Figure out how to best "unhook" these afterwards
|
||||||
|
});
|
||||||
|
|
||||||
|
/*function display_colors() {
|
||||||
|
var color_selection = document.getElementById('color_selection');
|
||||||
|
color_selection.innerHTML = "";
|
||||||
|
|
||||||
|
available_colors.forEach((element) => {
|
||||||
|
let newDiv = document.createElement('div');
|
||||||
|
newDiv.classList.add('color');
|
||||||
|
newDiv.style.backgroundColor = element;
|
||||||
|
|
||||||
|
color_selection.appendChild(newDiv);
|
||||||
|
});
|
||||||
|
};*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*const drawing_app = new DrawingApp(pixi_wrapper, {
|
||||||
|
'players': [
|
||||||
|
{'player_id': 'A', 'color': '#c35a00'},
|
||||||
|
{'player_id': 'B', 'color': '#290097'}
|
||||||
|
]
|
||||||
|
});*/
|
||||||
|
|
||||||
|
/*drawing_app.layers[0].setLivePaintProgressCallback((old_pos, new_pos) => {
|
||||||
|
console.log(old_pos);
|
||||||
|
socket.emit("draw_data", [old_pos, new_pos]);
|
||||||
|
})*/
|
||||||
|
|
||||||
|
// Test
|
||||||
|
|
||||||
61
client/src/BrushGenerator.js
Normal file
61
client/src/BrushGenerator.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import * as PIXI from 'pixi.js';
|
||||||
|
|
||||||
|
const fragment = `
|
||||||
|
uniform float size;
|
||||||
|
uniform float erase;
|
||||||
|
uniform vec3 color;
|
||||||
|
uniform float smoothing;
|
||||||
|
|
||||||
|
void main(){
|
||||||
|
vec2 uv = vec2(gl_FragCoord.xy) / size;
|
||||||
|
float dst = distance(uv, vec2(0.5, 0.5)) * 2.;
|
||||||
|
float alpha = max(0., 1. - dst);
|
||||||
|
alpha = pow(alpha, smoothing);
|
||||||
|
if(erase == 0.)
|
||||||
|
gl_FragColor = vec4(color, 1) * alpha;
|
||||||
|
else
|
||||||
|
gl_FragColor = vec4(alpha);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class BrushGenerator {
|
||||||
|
constructor(renderer) {
|
||||||
|
this.renderer = renderer;
|
||||||
|
|
||||||
|
this.filter = new PIXI.Filter(null, fragment, {
|
||||||
|
color: [0, 0, 0],
|
||||||
|
size: 16,
|
||||||
|
erase: 0,
|
||||||
|
smoothing: 0.01
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get(size, color, smoothing) {
|
||||||
|
this.filter.uniforms.size = size;
|
||||||
|
this.filter.uniforms.color = color;
|
||||||
|
this.filter.uniforms.smoothing = smoothing;
|
||||||
|
|
||||||
|
const texture = PIXI.RenderTexture.create({width: size, height: size});
|
||||||
|
texture.baseTexture.premultipliedAlpha = true;
|
||||||
|
const sprite = new PIXI.Sprite();
|
||||||
|
sprite.width = size;
|
||||||
|
sprite.height = size;
|
||||||
|
|
||||||
|
sprite.filters = [this.filter];
|
||||||
|
|
||||||
|
this.renderer.render(sprite, {
|
||||||
|
renderTexture: texture,
|
||||||
|
clear: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
hexToArray(color) {
|
||||||
|
const r = color >> 16;
|
||||||
|
const g = (color & 0x00ffff) >> 8;
|
||||||
|
const b = color & 0x0000ff;
|
||||||
|
|
||||||
|
return [r / 255, g / 255, b / 255];
|
||||||
|
}
|
||||||
|
}
|
||||||
202
client/src/DrawingApp.js
Normal file
202
client/src/DrawingApp.js
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import * as PIXI from 'pixi.js';
|
||||||
|
|
||||||
|
import BrushGenerator from './BrushGenerator';
|
||||||
|
import SpritePool from './SpritePool';
|
||||||
|
|
||||||
|
const max_ink_per_layer = 1000;
|
||||||
|
|
||||||
|
const layer_width = 512;
|
||||||
|
const layer_height = 512;
|
||||||
|
|
||||||
|
const brush_size = 16;
|
||||||
|
const brush_smoothing = 0.01;
|
||||||
|
|
||||||
|
export class Layer {
|
||||||
|
constructor(app, render_texture, brushGenerator, player_id, color) {
|
||||||
|
this.app = app;
|
||||||
|
this.render_texture = render_texture;
|
||||||
|
this.spritePool = new SpritePool();
|
||||||
|
|
||||||
|
this.ink_used = 0.0;
|
||||||
|
this.lifted = false;
|
||||||
|
|
||||||
|
this.player_id = player_id;
|
||||||
|
this.color = this.hexToRgb(color);
|
||||||
|
this.draw_buffer = new PIXI.Container();
|
||||||
|
|
||||||
|
this.sprite = new PIXI.Sprite(this.render_texture);
|
||||||
|
this.sprite.width = layer_width;
|
||||||
|
this.sprite.height = layer_height;
|
||||||
|
this.sprite.eventMode = 'none';
|
||||||
|
this.app.stage.addChildAt(this.sprite, 0);
|
||||||
|
|
||||||
|
this.brushTexture =
|
||||||
|
brushGenerator.get(brush_size, this.color, brush_smoothing);
|
||||||
|
|
||||||
|
this.drawingStarted = false;
|
||||||
|
this.lastPosition = null;
|
||||||
|
|
||||||
|
const onDown = (e) => {
|
||||||
|
const position = this.sprite.toLocal(e.data.global);
|
||||||
|
// position.x += 512;
|
||||||
|
// position.y += 512;
|
||||||
|
|
||||||
|
this.lastPosition = position;
|
||||||
|
this.drawingStarted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMove = (e) => {
|
||||||
|
const position = this.sprite.toLocal(e.data.global);
|
||||||
|
// position.x += 512;
|
||||||
|
// position.y += 512;
|
||||||
|
// console.log(position);
|
||||||
|
|
||||||
|
if (this.drawingStarted) {
|
||||||
|
this.drawPointLine(this.lastPosition, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastPosition = position;
|
||||||
|
};
|
||||||
|
const onUp = (e) => {
|
||||||
|
this.lifted = true;
|
||||||
|
this.drawingStarted = false;
|
||||||
|
this.finished_painting_cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sprite.on('mousedown', onDown);
|
||||||
|
this.sprite.on('touchstart', onDown);
|
||||||
|
this.sprite.on('mousemove', onMove);
|
||||||
|
this.sprite.on('touchmove', onMove);
|
||||||
|
this.sprite.on('mouseup', onUp);
|
||||||
|
this.sprite.on('touchend', onUp);
|
||||||
|
|
||||||
|
this.app.ticker.add(() => {
|
||||||
|
this.renderPoints();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPaintPercentage() {
|
||||||
|
return Math.min(this.ink_used / max_ink_per_layer, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hexToRgb(hex) {
|
||||||
|
var res = hex.match(/[a-f0-9]{2}/gi);
|
||||||
|
return res && res.length === 3 ? res.map(function(v) {
|
||||||
|
return parseInt(v, 16) / 255
|
||||||
|
}) :
|
||||||
|
[0, 0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
setFinishedPaintingCallback(callback) {
|
||||||
|
this.finished_painting_cb = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLivePaintProgressCallback(callback) {
|
||||||
|
this.live_paint_progress_cb = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPoint(x, y) {
|
||||||
|
const sprite = this.spritePool.get();
|
||||||
|
sprite.x = x;
|
||||||
|
sprite.y = y;
|
||||||
|
sprite.texture = this.brushTexture;
|
||||||
|
|
||||||
|
sprite.blendMode = PIXI.BLEND_MODES.NORMAL;
|
||||||
|
|
||||||
|
this.draw_buffer.addChild(sprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPointLine(oldPos, newPos, force = false) {
|
||||||
|
if (this.lifted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = {
|
||||||
|
x: oldPos.x - newPos.x,
|
||||||
|
y: oldPos.y - newPos.y,
|
||||||
|
};
|
||||||
|
const deltaLength = Math.sqrt(delta.x ** 2 + delta.y ** 2);
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
this.ink_used += deltaLength;
|
||||||
|
this.live_paint_progress_cb(oldPos, newPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ink_used >= max_ink_per_layer) {
|
||||||
|
this.lifted = true;
|
||||||
|
this.drawingStarted = false;
|
||||||
|
this.finished_painting_cb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pass socket.io to tell server?
|
||||||
|
// Or save to array and just pass to server on round-end. Should be easier?
|
||||||
|
|
||||||
|
this.drawPoint(newPos.x, newPos.y);
|
||||||
|
|
||||||
|
if (deltaLength >= brush_size / 8) {
|
||||||
|
const additionalPoints = Math.ceil(deltaLength / (brush_size / 8));
|
||||||
|
|
||||||
|
for (let i = 1; i < additionalPoints; i++) {
|
||||||
|
const pos = {
|
||||||
|
x: newPos.x + delta.x * (i / additionalPoints),
|
||||||
|
y: newPos.y + delta.y * (i / additionalPoints),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.drawPoint(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPoints() {
|
||||||
|
this.app.renderer.render(this.draw_buffer, {
|
||||||
|
renderTexture: this.render_texture,
|
||||||
|
clear: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.draw_buffer.children = [];
|
||||||
|
|
||||||
|
this.spritePool.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this.sprite.eventMode = 'static';
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
this.sprite.eventMode = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DrawingApp {
|
||||||
|
constructor(dom_elem, player_data) {
|
||||||
|
var self = this;
|
||||||
|
this.app = new PIXI.Application({
|
||||||
|
width: layer_width,
|
||||||
|
height: layer_height,
|
||||||
|
backgroundColor: 0xffffff,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.brushGenerator = new BrushGenerator(this.app.renderer);
|
||||||
|
this.brushTexture = null;
|
||||||
|
|
||||||
|
this.layers = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < player_data.length * 2; i++) {
|
||||||
|
const render_texture =
|
||||||
|
PIXI.RenderTexture.create({width: 1024, height: 1024});
|
||||||
|
var cur_player = player_data[i % player_data.length];
|
||||||
|
this.layers.push(new Layer(
|
||||||
|
this.app, render_texture, this.brushGenerator, cur_player.id,
|
||||||
|
cur_player.player_color));
|
||||||
|
}
|
||||||
|
|
||||||
|
dom_elem.appendChild(this.app.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.app;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock_all_layers() {}
|
||||||
|
}
|
||||||
29
client/src/SpritePool.js
Normal file
29
client/src/SpritePool.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as PIXI from 'pixi.js';
|
||||||
|
|
||||||
|
export default class SpritePool {
|
||||||
|
constructor() {
|
||||||
|
this.sprites = [];
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get() {
|
||||||
|
if(this.index < this.sprites.length) {
|
||||||
|
return this.sprites[this.index++];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY);
|
||||||
|
sprite.anchor.set(0.5);
|
||||||
|
this.sprites.push(sprite);
|
||||||
|
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
for(let i = 0; i < this.sprites.length; i++)
|
||||||
|
this.sprites[i].destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
162
client/src/index.html
Normal file
162
client/src/index.html
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Fake Artist</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: black;
|
||||||
|
font-size: 1.4vmax;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1.0vmax;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color_selection {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 25px 25px 25px 25px 25px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameinfo {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game_leader_input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pixi-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game_info {
|
||||||
|
color: #FFFFFF;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#myProgress {
|
||||||
|
width: 100%;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#myBar {
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pixi-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
/*display: initial;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#current_user_text {
|
||||||
|
color: black;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endgame_player_list {
|
||||||
|
display: none;
|
||||||
|
color: black;
|
||||||
|
background-color: white;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pl_container {
|
||||||
|
background-color: white;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<div id="game_setup">
|
||||||
|
<form>
|
||||||
|
<label>
|
||||||
|
Username:
|
||||||
|
<input id="username" type="text" placeholder="Username" maxlength="16" />
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
<form>
|
||||||
|
<label>
|
||||||
|
Room-Code: <input id="room_code" maxlength="4" type="text" placeholder="Room-Code" />
|
||||||
|
</label>
|
||||||
|
<input id="join_room_btn" type="button" value="Join Room" />
|
||||||
|
</form>
|
||||||
|
<form>
|
||||||
|
<input id="create_room_btn" type="button" value="Create Room" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
<div id="game_info">
|
||||||
|
<span id="game_room_code"></span>
|
||||||
|
<div id="pre_game_player_list" class="gameinfo">
|
||||||
|
<span id="current_player_count">X</span>/<span id="max_player_count">10</span>
|
||||||
|
<ul id="player_list">
|
||||||
|
</ul>
|
||||||
|
<form>
|
||||||
|
<input id="start_game_btn" type="button" value="Start" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span id="game_state_info"></span>
|
||||||
|
</div>
|
||||||
|
<div id="game_leader_input">
|
||||||
|
<form>
|
||||||
|
<label>
|
||||||
|
Topic: <input type="text" placeholder="Topic" id="game_topic" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Category: <input type="text" placeholder="Category" id="game_category" />
|
||||||
|
</label>
|
||||||
|
<input type="button" id="submit_topic" value="Start" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="game">
|
||||||
|
<div class="center" id="pl_container">
|
||||||
|
<ul id="endgame_player_list"></ul>
|
||||||
|
</div>
|
||||||
|
<div id="user_turn_info">
|
||||||
|
<div class="center" id="current_user_text"></div>
|
||||||
|
<div>
|
||||||
|
<div class="center" id="myProgress">
|
||||||
|
<div id="myBar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="pixi-wrapper"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
372
client/src/server/server.js
Normal file
372
client/src/server/server.js
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const http = require('http').Server(app);
|
||||||
|
const io = require('socket.io')(http);
|
||||||
|
const cors = require('cors');
|
||||||
|
|
||||||
|
const room_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
|
Array.prototype.remove = function() {
|
||||||
|
var what, a = arguments, L = a.length, ax;
|
||||||
|
while (L && this.length) {
|
||||||
|
what = a[--L];
|
||||||
|
while ((ax = this.indexOf(what)) !== -1) {
|
||||||
|
this.splice(ax, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function randomInt(low, high) {
|
||||||
|
return Math.floor(Math.random() * (high - low + 1) + low);
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomId() {
|
||||||
|
let out = '';
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
out += room_chars[randomInt(0, room_chars.length - 1)];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const available_colors = [
|
||||||
|
'#c35a00', '#290097', '#b8c400', '#8c8eff', '#47d058', '#cf0036', '#01cefd',
|
||||||
|
'#500027', '#006241', '#ffc4e4'
|
||||||
|
];
|
||||||
|
|
||||||
|
class Player {
|
||||||
|
id = null;
|
||||||
|
player_name = 'Player';
|
||||||
|
player_color = '';
|
||||||
|
|
||||||
|
constructor(id_, name_, player_color_) {
|
||||||
|
this.id = id_;
|
||||||
|
this.player_name = name_;
|
||||||
|
this.player_color = player_color_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GAME_STATE = {
|
||||||
|
PRE_GAME: 0,
|
||||||
|
TOPIC_SELECTION: 1,
|
||||||
|
DRAWING: 2,
|
||||||
|
VOTING: 3,
|
||||||
|
RESULTS: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
class Game {
|
||||||
|
room_id = '';
|
||||||
|
topic = '';
|
||||||
|
category = '';
|
||||||
|
players = []; // Player Class
|
||||||
|
actual_players = [];
|
||||||
|
current_game_state = 0;
|
||||||
|
current_player = null;
|
||||||
|
leader = null;
|
||||||
|
fake_artist = null;
|
||||||
|
current_round = 0;
|
||||||
|
player_turns = [];
|
||||||
|
disconnected_during_session = [];
|
||||||
|
|
||||||
|
constructor(room_id_) {
|
||||||
|
this.room_id = room_id_
|
||||||
|
}
|
||||||
|
|
||||||
|
add_player(player) {
|
||||||
|
this.players.push(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_player(player_id) {
|
||||||
|
this.players.splice(
|
||||||
|
this.players.findIndex(item => item.id === player_id), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var active_games = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
GameLoop:
|
||||||
|
- Lobby
|
||||||
|
- Wait for 5+ Players -> enable "start" button (any player can start the
|
||||||
|
game)
|
||||||
|
- Randomly select one Player to be "game leader"
|
||||||
|
- All other players have to draw
|
||||||
|
- They take turns, only getting to see the image once its their turn (may be
|
||||||
|
changed, depending) on how boring it gets to wait (maybe only let game leade
|
||||||
|
peek)
|
||||||
|
- Once everybody painted two strokes (two full rounds of the game)
|
||||||
|
Reveal the image to everybody
|
||||||
|
*/
|
||||||
|
|
||||||
|
initGame =
|
||||||
|
() => {
|
||||||
|
console.log('Initializing Game!');
|
||||||
|
let count = 0;
|
||||||
|
// Game.map = {};
|
||||||
|
// console.log("Map Created: ", Game.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.static('static'));
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile('index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log('User: ', socket.id, ' connected.');
|
||||||
|
|
||||||
|
socket.on('draw_data', (arr) => {
|
||||||
|
if (socket.room) {
|
||||||
|
var room_id = socket.room;
|
||||||
|
var game = active_games[room_id];
|
||||||
|
|
||||||
|
socket.broadcast.to(game.room_id)
|
||||||
|
.emit('draw_data', game.current_round - 1, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(arr[0], arr[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// socket.on('')
|
||||||
|
|
||||||
|
socket.on('create_room', (username) => {
|
||||||
|
if (username == '') {
|
||||||
|
socket.emit('room_error', 'Invalid username!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('New room');
|
||||||
|
var room_id = randomId();
|
||||||
|
console.log(room_id);
|
||||||
|
socket.join(room_id);
|
||||||
|
socket.emit('room', room_id);
|
||||||
|
console.log(io.sockets.adapter.rooms);
|
||||||
|
|
||||||
|
var game = new Game(room_id);
|
||||||
|
|
||||||
|
var player_color = available_colors[0];
|
||||||
|
|
||||||
|
for (var i = 0; i < available_colors.length; i++) {
|
||||||
|
var free = true;
|
||||||
|
for (var k = 0; k < game.players.length; k++) {
|
||||||
|
if (available_colors[i] == game.players[k].player_color) {
|
||||||
|
free = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (free) {
|
||||||
|
player_color = available_colors[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var player = new Player(socket.id, username, player_color);
|
||||||
|
|
||||||
|
game.add_player(player);
|
||||||
|
|
||||||
|
socket.room = room_id;
|
||||||
|
|
||||||
|
active_games[room_id] = game;
|
||||||
|
io.to(room_id).emit('player_data', active_games[room_id].players);
|
||||||
|
console.log(JSON.stringify(active_games[room_id]));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('start_game', () => {
|
||||||
|
if (socket.room) {
|
||||||
|
var room_id = socket.room;
|
||||||
|
var game = active_games[room_id];
|
||||||
|
|
||||||
|
game.game_state = GAME_STATE.TOPIC_SELECTION;
|
||||||
|
|
||||||
|
io.to(room_id).emit('game_started');
|
||||||
|
|
||||||
|
var leader =
|
||||||
|
game.players[Math.floor(Math.random() * game.players.length)];
|
||||||
|
game.leader = leader;
|
||||||
|
|
||||||
|
var remaining_players = game.players.filter(e => e !== leader);
|
||||||
|
game.actual_players = remaining_players;
|
||||||
|
game.player_turns = game.actual_players.concat(game.actual_players);
|
||||||
|
|
||||||
|
var fake_artist = remaining_players[Math.floor(
|
||||||
|
Math.random() * remaining_players.length)];
|
||||||
|
|
||||||
|
game.fake_artist = fake_artist;
|
||||||
|
|
||||||
|
io.to(room_id).emit('leader_selected', leader);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('topic_selected', (topic, category) => {
|
||||||
|
console.log(topic, category);
|
||||||
|
|
||||||
|
if (socket.room) {
|
||||||
|
var room_id = socket.room;
|
||||||
|
var game = active_games[room_id];
|
||||||
|
|
||||||
|
game.topic = topic;
|
||||||
|
game.category = category;
|
||||||
|
|
||||||
|
game.players.forEach((player) => {
|
||||||
|
console.log(player.id);
|
||||||
|
if (player.id != game.fake_artist.id) {
|
||||||
|
console.log('OK');
|
||||||
|
io.to(player.id).emit('topic_selected', topic, category);
|
||||||
|
} else {
|
||||||
|
console.log('FAKE');
|
||||||
|
io.to(player.id).emit('topic_selected', '???', category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
game.game_state = GAME_STATE.DRAWING;
|
||||||
|
io.to(socket.room).emit('actually_start_game', game.actual_players);
|
||||||
|
|
||||||
|
|
||||||
|
// FIXME: Super slow for some reason
|
||||||
|
// Maybe just do the following:
|
||||||
|
// Have a manual advance on the clients (button, ink empty, on mouse up)
|
||||||
|
// If someone disconnects, just manually advance, no timeout needed
|
||||||
|
game.current_round = 0;
|
||||||
|
|
||||||
|
// game.current_player = game.player_turns[game.current_round];
|
||||||
|
// socket.to(socket.room).emit('advance_round', game.current_round,
|
||||||
|
// game.current_player);
|
||||||
|
|
||||||
|
play_next_turn(game);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function play_next_turn(game) {
|
||||||
|
console.log('Round: ', game.current_round);
|
||||||
|
|
||||||
|
if (game.game_state == GAME_STATE.VOTING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.current_round >= game.player_turns.length) {
|
||||||
|
game.game_state = GAME_STATE.VOTING;
|
||||||
|
console.log('GAME FINISHED!');
|
||||||
|
io.to(game.room_id).emit('game_finished', game.leader, game.players);
|
||||||
|
game.game_state = GAME_STATE.VOTING;
|
||||||
|
// TODO: Pass fake artist after voting... implement voting in the first
|
||||||
|
// place
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//while(game.current_player)
|
||||||
|
|
||||||
|
game.current_player = game.player_turns[game.current_round];
|
||||||
|
io.to(game.room_id)
|
||||||
|
.emit('advance_round', game.current_round, game.current_player);
|
||||||
|
|
||||||
|
game.current_round++;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('round_finished', () => {
|
||||||
|
if (socket.room) {
|
||||||
|
var room_id = socket.room;
|
||||||
|
var game = active_games[room_id];
|
||||||
|
|
||||||
|
if (game.game_state != GAME_STATE.DRAWING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('round finished!');
|
||||||
|
play_next_turn(game);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// socket.
|
||||||
|
|
||||||
|
socket.on('join_room', (room_id, username) => {
|
||||||
|
if (username == '') {
|
||||||
|
socket.emit('room_error', 'Invalid username!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!active_games[room_id]) {
|
||||||
|
socket.emit('room_error', 'This room does not exist!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user_count = active_games[room_id].players.length;
|
||||||
|
|
||||||
|
if (user_count > 10) {
|
||||||
|
socket.emit('room_error', 'This room is full!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var game_state = active_games[room_id].current_game_state;
|
||||||
|
|
||||||
|
if (game_state != GAME_STATE.PRE_GAME) {
|
||||||
|
socket.emit('room_error', 'This game is already in progress!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var game = active_games[room_id];
|
||||||
|
|
||||||
|
socket.join(room_id);
|
||||||
|
|
||||||
|
var player_color = available_colors[0];
|
||||||
|
|
||||||
|
for (var i = 0; i < available_colors.length; i++) {
|
||||||
|
var free = true;
|
||||||
|
for (var k = 0; k < game.players.length; k++) {
|
||||||
|
if (available_colors[i] == game.players[k].player_color) {
|
||||||
|
free = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (free) {
|
||||||
|
player_color = available_colors[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var player = new Player(socket.id, username, player_color);
|
||||||
|
active_games[room_id].add_player(player);
|
||||||
|
|
||||||
|
socket.room = room_id;
|
||||||
|
|
||||||
|
socket.emit('room', room_id);
|
||||||
|
io.to(room_id).emit('player_data', active_games[room_id].players);
|
||||||
|
console.log(JSON.stringify(active_games[room_id]));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('User: ', socket.id, ' disconnected.');
|
||||||
|
|
||||||
|
if (socket.room) {
|
||||||
|
var room_id = socket.room;
|
||||||
|
var game = active_games[room_id];
|
||||||
|
game.remove_player(room_id);
|
||||||
|
io.to(room_id).emit('player_data', game.players);
|
||||||
|
|
||||||
|
console.log(socket.id, game.current_player);
|
||||||
|
|
||||||
|
if (game.game_state == GAME_STATE.DRAWING) {
|
||||||
|
game.disconnected_during_session.push(game.players[socket.id]);
|
||||||
|
|
||||||
|
/*console.log(game.player_turns);
|
||||||
|
game.player_turns = game.player_turns.filter(e => e.id != socket.id);
|
||||||
|
console.log(game.player_turns);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (socket.id == game.current_player.id) {
|
||||||
|
play_next_turn(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
http.listen(7777, '0.0.0.0', () => {
|
||||||
|
console.log('Listening on 7777');
|
||||||
|
})
|
||||||
|
|
||||||
|
initGame();
|
||||||
21
client/webpack.config.js
Normal file
21
client/webpack.config.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
const path = require('path');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/App.js',
|
||||||
|
output: {
|
||||||
|
filename: './index.js',
|
||||||
|
path: path.resolve(__dirname, 'build')
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: 'index.html',
|
||||||
|
template: path.resolve(__dirname, './src/index.html'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
static: path.join(__dirname, 'build'),
|
||||||
|
compress: true,
|
||||||
|
port: 8080
|
||||||
|
}
|
||||||
|
};
|
||||||
2801
client/yarn.lock
Normal file
2801
client/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "pixi-graffiti",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue