dockered
This commit is contained in:
69
django/frontend/static/js/3D/buffers.js
Normal file
69
django/frontend/static/js/3D/buffers.js
Normal file
@ -0,0 +1,69 @@
|
||||
function initBuffers(gl)
|
||||
{
|
||||
const vertexBuffer = initVertexBuffer(gl);
|
||||
const indexBuffer = initIndexBuffer(gl);
|
||||
const normalBuffer = initNormalBuffer(gl);
|
||||
return { vertex: vertexBuffer, index : indexBuffer, normal: normalBuffer };
|
||||
}
|
||||
|
||||
function initVertexBuffer(gl)
|
||||
{
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
const positions = [
|
||||
// Front face
|
||||
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
|
||||
// Back face
|
||||
-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
|
||||
// Top face
|
||||
-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,
|
||||
// Bottom face
|
||||
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
|
||||
// Right face
|
||||
1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
|
||||
// Left face
|
||||
-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0
|
||||
];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
||||
return positionBuffer;
|
||||
}
|
||||
|
||||
function initNormalBuffer(gl)
|
||||
{
|
||||
const normalBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
||||
const vertexNormals = [
|
||||
// Front
|
||||
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
|
||||
// Back
|
||||
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0,
|
||||
// Top
|
||||
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
|
||||
// Bottom
|
||||
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,
|
||||
// Right
|
||||
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,
|
||||
// Left
|
||||
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
|
||||
];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW);
|
||||
return normalBuffer;
|
||||
}
|
||||
|
||||
function initIndexBuffer(gl)
|
||||
{
|
||||
const indexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||
const indices = [
|
||||
0, 1, 2, 0, 2, 3, // front
|
||||
4, 5, 6, 4, 6, 7, // back
|
||||
8, 9, 10, 8, 10, 11, // top
|
||||
12, 13, 14, 12, 14, 15, // bottom
|
||||
16, 17, 18, 16, 18, 19, // right
|
||||
20, 21, 22, 20, 22, 23, // left
|
||||
];
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
||||
return indexBuffer;
|
||||
}
|
||||
|
||||
export { initBuffers };
|
42
django/frontend/static/js/3D/cube.js
Normal file
42
django/frontend/static/js/3D/cube.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { shaderInfos } from "../3D/shaders.js"
|
||||
|
||||
function renderCube(ctx, x, y, z, angle = 0, sx = 1, sy = 1, sz = 1)
|
||||
{
|
||||
const modelMatrix = mat4.create();
|
||||
|
||||
mat4.translate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[x, y, z]
|
||||
);
|
||||
|
||||
mat4.rotate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
angle,
|
||||
[0, 1, 0],
|
||||
);
|
||||
|
||||
mat4.scale(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[sx, sy, sz]
|
||||
);
|
||||
|
||||
mat4.translate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[-1, 0, 0] // wtf, this works ?
|
||||
);
|
||||
|
||||
const normalMatrix = mat4.create();
|
||||
mat4.invert(normalMatrix, modelMatrix);
|
||||
mat4.transpose(normalMatrix, normalMatrix);
|
||||
|
||||
ctx.uniformMatrix4fv(shaderInfos.uniformLocations.modelMatrix, false, modelMatrix);
|
||||
ctx.uniformMatrix4fv(shaderInfos.uniformLocations.normalMatrix, false, normalMatrix);
|
||||
|
||||
ctx.drawElements(ctx.TRIANGLES, 36, ctx.UNSIGNED_SHORT, 0);
|
||||
}
|
||||
|
||||
export { renderCube };
|
28
django/frontend/static/js/3D/maths/gl-matrix-min.js
vendored
Normal file
28
django/frontend/static/js/3D/maths/gl-matrix-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
87
django/frontend/static/js/3D/shaders.js
Normal file
87
django/frontend/static/js/3D/shaders.js
Normal file
@ -0,0 +1,87 @@
|
||||
const vertex_shader_source = `
|
||||
attribute vec4 aPos;
|
||||
attribute vec3 aNormal;
|
||||
|
||||
uniform mat4 uMod;
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProj;
|
||||
uniform mat4 uNormalMat;
|
||||
|
||||
varying highp vec3 vLighting;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = uProj * uView * uMod * aPos;
|
||||
|
||||
highp vec3 ambientLight = vec3(0.3, 0.3, 0.3);
|
||||
highp vec3 directionalLightColor = vec3(1, 1, 1);
|
||||
highp vec3 directionalVector = vec3(-10, 2, -10);
|
||||
|
||||
highp vec4 transformedNormal = uNormalMat * vec4(aNormal, 1.0);
|
||||
|
||||
highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
|
||||
vLighting = ambientLight + (directionalLightColor * directional);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragment_shader_source = `
|
||||
varying highp vec3 vLighting;
|
||||
|
||||
void main()
|
||||
{
|
||||
highp vec3 color = vec3(1.0, 1.0, 1.0);
|
||||
gl_FragColor = vec4(color * vLighting, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export function initShaderProgram(gl)
|
||||
{
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertex_shader_source);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragment_shader_source);
|
||||
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, vertexShader);
|
||||
gl.attachShader(prog, fragmentShader);
|
||||
gl.linkProgram(prog);
|
||||
|
||||
shaderInfos = {
|
||||
program: prog,
|
||||
attribLocations: {
|
||||
vertexPosition: gl.getAttribLocation(prog, "aPos"),
|
||||
vertexNormal: gl.getAttribLocation(prog, "aNormal"),
|
||||
},
|
||||
uniformLocations: {
|
||||
projectionMatrix: gl.getUniformLocation(prog, "uProj"),
|
||||
modelMatrix: gl.getUniformLocation(prog, "uMod"),
|
||||
viewMatrix: gl.getUniformLocation(prog, "uView"),
|
||||
normalMatrix: gl.getUniformLocation(prog, "uNormalMat"),
|
||||
},
|
||||
};
|
||||
|
||||
if(!gl.getProgramParameter(prog, gl.LINK_STATUS))
|
||||
{
|
||||
alert(`Unable to initialize the shader program: ${gl.getProgramInfoLog(prog)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return prog;
|
||||
}
|
||||
|
||||
function loadShader(gl, type, source)
|
||||
{
|
||||
const shader = gl.createShader(type);
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
|
||||
{
|
||||
alert(`An error occurred while compiling the shaders: ${gl.getShaderInfoLog(shader)}`);
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
export let shaderInfos;
|
51
django/frontend/static/js/api/AExchangable.js
Normal file
51
django/frontend/static/js/api/AExchangable.js
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
|
||||
export class AExchangeable
|
||||
{
|
||||
/**
|
||||
* This abstract class implement import and export method useful to export/import data to/from the server
|
||||
* @param {[String]} fieldNameList
|
||||
*/
|
||||
export(fieldNameList = [])
|
||||
{
|
||||
let valueList = [];
|
||||
|
||||
fieldNameList.forEach(fieldName => {
|
||||
let value;
|
||||
|
||||
if (this[fieldName] instanceof AExchangeable)
|
||||
value = this[fieldName].export();
|
||||
else
|
||||
value = this[fieldName];
|
||||
});
|
||||
|
||||
return valueList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
import(data)
|
||||
{
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
|
||||
if (Array.isArray(value))
|
||||
{
|
||||
for (let i = 0; i < value.length; i++)
|
||||
{
|
||||
if (this[key][i] instanceof AExchangeable)
|
||||
this[key][i].import(value[i]);
|
||||
else
|
||||
this[key][i] = value[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this[key] instanceof AExchangeable)
|
||||
this[key].import(value);
|
||||
else
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
93
django/frontend/static/js/api/Account.js
Normal file
93
django/frontend/static/js/api/Account.js
Normal file
@ -0,0 +1,93 @@
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @returns {Response}
|
||||
*/
|
||||
async create(username, password)
|
||||
{
|
||||
let response = await this.client._post("/api/accounts/register", {username: username, password: password});
|
||||
|
||||
if (response.status === 201)
|
||||
await this.client._update_logged(true);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} password
|
||||
* @returns {?Promise<Object>}
|
||||
*/
|
||||
async delete(password)
|
||||
{
|
||||
const response = await this.client._delete("/api/accounts/delete", {password: password});
|
||||
|
||||
if (response.ok) {
|
||||
this.client._update_logged(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} newUsername
|
||||
* @returns {?Promise<Object>}
|
||||
*/
|
||||
async updateUsername(newUsername)
|
||||
{
|
||||
const data = {
|
||||
username: newUsername
|
||||
};
|
||||
const response = await this.client._patch_json(`/api/accounts/update_profile`, data);
|
||||
const respondeData = await response.json();
|
||||
|
||||
if (response.status === 200) {
|
||||
this.client.me.username = respondeData.username;
|
||||
document.getElementById('navbarDropdownButton').innerHTML = respondeData.username;
|
||||
document.getElementById('myProfileLink').href = '/profiles/' + respondeData.username;
|
||||
return null;
|
||||
}
|
||||
return respondeData['authorize'] || respondeData['detail'] || respondeData['username']?.join(' ') || 'Error.';
|
||||
}
|
||||
|
||||
async updatePassword(currentPassword, newPassword, newPassword2)
|
||||
{
|
||||
const data = {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
new_password2: newPassword2
|
||||
};
|
||||
const response = await this.client._put('/api/accounts/update_password', data);
|
||||
if (response.ok)
|
||||
return null;
|
||||
|
||||
const responseData = await response.json();
|
||||
const formatedData = {};
|
||||
if (responseData['current_password'])
|
||||
formatedData['currentPasswordDetail'] = responseData['current_password'];
|
||||
if (responseData['new_password'])
|
||||
formatedData['newPasswordDetail'] = responseData['new_password'];
|
||||
if (responseData['new_password2'])
|
||||
formatedData['newPassword2Detail'] = responseData['new_password2'];
|
||||
if (formatedData == {})
|
||||
formatedData['passwordDetail'] = 'Error';
|
||||
return formatedData;
|
||||
}
|
||||
}
|
||||
|
||||
export { Account };
|
258
django/frontend/static/js/api/Client.js
Normal file
258
django/frontend/static/js/api/Client.js
Normal file
@ -0,0 +1,258 @@
|
||||
import { Account } from "./Account.js";
|
||||
import { MatchMaking } from "./Matchmaking.js";
|
||||
import { Profiles } from "./Profiles.js";
|
||||
import { MyProfile } from "./MyProfile.js";
|
||||
import Notice from "./Notice.js";
|
||||
import LanguageManager from './LanguageManager.js';
|
||||
|
||||
function getCookie(name)
|
||||
{
|
||||
let cookie = {};
|
||||
document.cookie.split(';').forEach(function(el) {
|
||||
let split = el.split('=');
|
||||
cookie[split[0].trim()] = split.slice(1).join("=");
|
||||
});
|
||||
return cookie[name];
|
||||
}
|
||||
|
||||
class Client
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {String} url
|
||||
*/
|
||||
constructor(url)
|
||||
{
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this._url = url;
|
||||
|
||||
/**
|
||||
* @type {Account}
|
||||
*/
|
||||
this.account = new Account(this);
|
||||
|
||||
/**
|
||||
* @type {Profiles}
|
||||
*/
|
||||
this.profiles = new Profiles(this);
|
||||
|
||||
/**
|
||||
* @type {MatchMaking}
|
||||
*/
|
||||
this.matchmaking = new MatchMaking(this);
|
||||
|
||||
/**
|
||||
* @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthenticated()
|
||||
*/
|
||||
this._logged = undefined;
|
||||
|
||||
/**
|
||||
* @type {Notice}
|
||||
*/
|
||||
this.notice = new Notice(this);
|
||||
|
||||
this.lang = new LanguageManager();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The only right way to determine is the user is logged
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async isAuthenticated()
|
||||
{
|
||||
if (this._logged == undefined)
|
||||
this._logged = await this._test_logged();
|
||||
return this._logged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a GET request to %uri%
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _get(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept-Language': this.lang.currentLang
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _post(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a DELETE request
|
||||
* @param {String} uri
|
||||
* @param {String} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _delete(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PUT request with json
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _put(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
"Content-Type": "application/json",
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PATCH request with json
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _patch_json(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
"Content-Type": "application/json",
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PATCH request with file
|
||||
* @param {String} uri
|
||||
* @param {*} file
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _patch_file(uri, file)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: file,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change logged state. Use It if you recv an 403 error
|
||||
* @param {Promise<?>} state
|
||||
* @returns
|
||||
*/
|
||||
async _update_logged(state)
|
||||
{
|
||||
if (this._logged == state)
|
||||
return;
|
||||
|
||||
if (state)
|
||||
{
|
||||
this.me = new MyProfile(this);
|
||||
await this.me.init();
|
||||
this.notice.start();
|
||||
document.getElementById('navbarLoggedOut').classList.add('d-none');
|
||||
document.getElementById('navbarLoggedIn').classList.remove('d-none');
|
||||
document.getElementById('navbarDropdownButton').innerHTML = this.me.username;
|
||||
document.getElementById('myProfileLink').href = '/profiles/' + this.me.username;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.me = undefined;
|
||||
this.notice.stop();
|
||||
document.getElementById('navbarLoggedOut').classList.remove('d-none');
|
||||
document.getElementById('navbarLoggedIn').classList.add('d-none');
|
||||
document.getElementById('navbarDropdownButton').innerHTML = 'Me';
|
||||
document.getElementById('myProfileLink').href = '';
|
||||
}
|
||||
this._logged = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggin the user
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async login(username, password)
|
||||
{
|
||||
let response = await this._post("/api/accounts/login", {username: username, password: password});
|
||||
if (response.status == 200)
|
||||
await this._update_logged(true);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user
|
||||
* @returns {Promise<?>}
|
||||
*/
|
||||
async logout()
|
||||
{
|
||||
await this._get("/api/accounts/logout");
|
||||
await this._update_logged(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is logged. NEVER USE IT, USE isAuthenticated()
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async _test_logged()
|
||||
{
|
||||
let response = await this._get("/api/accounts/logged");
|
||||
|
||||
await this._update_logged(response.status === 200);
|
||||
return response.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
export {Client};
|
74
django/frontend/static/js/api/LanguageManager.js
Normal file
74
django/frontend/static/js/api/LanguageManager.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { reloadView } from '../index.js';
|
||||
|
||||
export default class LanguageManager {
|
||||
constructor() {
|
||||
this.availableLanguages = ['en', 'fr', 'tp', 'cr'];
|
||||
|
||||
this.dict = null;
|
||||
this.currentLang = 'en';
|
||||
this.chosenLang = localStorage.getItem('preferedLanguage') || this.currentLang;
|
||||
if (this.chosenLang !== this.currentLang && this.availableLanguages.includes(this.chosenLang)) {
|
||||
this.loading = this.translatePage();
|
||||
this.currentLang = this.chosenLang;
|
||||
} else {
|
||||
this.loading = this.loadDict(this.chosenLang);
|
||||
}
|
||||
document.getElementById('languageDisplay').innerHTML =
|
||||
document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML;
|
||||
}
|
||||
|
||||
async translatePage() {
|
||||
if (this.currentLang === this.chosenLang)
|
||||
return;
|
||||
|
||||
await this.loadDict(this.chosenLang);
|
||||
if (!this.dict)
|
||||
return 1;
|
||||
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
let key = el.getAttribute('data-i18n');
|
||||
el.innerHTML = this.dict[key];
|
||||
});
|
||||
await reloadView();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async changeLanguage(lang) {
|
||||
if (lang === this.currentLang || !this.availableLanguages.includes(lang))
|
||||
return 1;
|
||||
|
||||
this.chosenLang = lang;
|
||||
if (await this.translatePage() !== 0)
|
||||
return 1;
|
||||
|
||||
this.currentLang = this.chosenLang;
|
||||
localStorage.setItem('preferedLanguage', lang);
|
||||
document.getElementById('languageDisplay').innerHTML =
|
||||
document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML;
|
||||
return 0;
|
||||
}
|
||||
|
||||
async loadDict(lang) {
|
||||
let dictUrl = `${location.origin}/static/js/lang/${lang}.json`;
|
||||
let response = await fetch(dictUrl);
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log(`No translation found for language ${lang}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dict = await response.json();
|
||||
}
|
||||
|
||||
async waitLoading() {
|
||||
await this.loading;
|
||||
}
|
||||
|
||||
get(key, defaultTxt) {
|
||||
if (!this.dict)
|
||||
return defaultTxt;
|
||||
|
||||
return this.dict[key] || defaultTxt;
|
||||
}
|
||||
}
|
61
django/frontend/static/js/api/Matchmaking.js
Normal file
61
django/frontend/static/js/api/Matchmaking.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
class MatchMaking
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client)
|
||||
{
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this.searching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CallableFunction} receive_func
|
||||
* @param {CallableFunction} disconnect_func
|
||||
* @param {Number} mode The number of players in a game
|
||||
* @returns {Promise<?>}
|
||||
*/
|
||||
async start(receive_func, disconnect_func, game_type, mode)
|
||||
{
|
||||
if (!await this.client.isAuthenticated())
|
||||
return null;
|
||||
|
||||
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${game_type}/${mode}`;
|
||||
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this.searching = true;
|
||||
|
||||
this.receive_func = receive_func;
|
||||
this.disconnect_func = disconnect_func;
|
||||
|
||||
this._socket.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
receive_func(data);
|
||||
};
|
||||
|
||||
this._socket.onclose = this.onclose.bind(this);
|
||||
}
|
||||
|
||||
onclose(event)
|
||||
{
|
||||
this.stop();
|
||||
this.disconnect_func(event);
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._socket)
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
this.searching = false;
|
||||
}
|
||||
}
|
||||
|
||||
export {MatchMaking};
|
144
django/frontend/static/js/api/MyProfile.js
Normal file
144
django/frontend/static/js/api/MyProfile.js
Normal file
@ -0,0 +1,144 @@
|
||||
import { Client } from "./Client.js";
|
||||
import { Profile } from "./Profile.js";
|
||||
|
||||
class MyProfile extends Profile
|
||||
{
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
super(client, "../me");
|
||||
|
||||
/**
|
||||
* @type {[Profile]}
|
||||
*/
|
||||
this.blockedUsers = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.friendList = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.incomingFriendRequests = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.outgoingFriendRequests = [];
|
||||
}
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
await this.getBlockedUsers();
|
||||
// await this.getFriends();
|
||||
// await this.getIncomingFriendRequests()
|
||||
// await this.getOutgoingFriendRequests()
|
||||
}
|
||||
|
||||
async getBlockedUsers() {
|
||||
const response = await this.client._get('/api/profiles/block');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.blockedUsers.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
|
||||
}
|
||||
|
||||
async getFriends() {
|
||||
const response = await this.client._get('/api/profiles/friends');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.friendList.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
|
||||
}
|
||||
async getIncomingFriendRequests() {
|
||||
const response = await this.client._get('/api/profiles/incoming_friend_requests');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.incomingFriendRequests.push(
|
||||
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
|
||||
));
|
||||
}
|
||||
async getOutgoingFriendRequests() {
|
||||
const response = await this.client._get('/api/profiles/outgoing_friend_requests');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.outgoingFriendRequests.push(
|
||||
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isFriend(profile) {
|
||||
for (const user of this.friendList) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isBlocked(profile) {
|
||||
for (const user of this.blockedUsers) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_hasIncomingRequestFrom(profile) {
|
||||
for (const user of this.incomingFriendRequests) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_hasOutgoingRequestTo(profile) {
|
||||
for (const user of this.outgoingFriendRequests) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {File} selectedFile
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async changeAvatar(selectedFile)
|
||||
{
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', selectedFile);
|
||||
|
||||
const response = await this.client._patch_file(`/api/profiles/settings`, formData);
|
||||
const responseData = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.avatar = responseData.avatar;
|
||||
return null;
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
async deleteAvatar() {
|
||||
const response = await this.client._delete('/api/profiles/settings');
|
||||
const responseData = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.avatar = responseData.avatar;
|
||||
return null;
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export {MyProfile};
|
101
django/frontend/static/js/api/Notice.js
Normal file
101
django/frontend/static/js/api/Notice.js
Normal file
@ -0,0 +1,101 @@
|
||||
import {Client} from './Client.js';
|
||||
import {createNotification} from '../utils/noticeUtils.js'
|
||||
import { lastView } from '../index.js';
|
||||
import ProfilePageView from '../views/ProfilePageView.js';
|
||||
import Search from '../views/Search.js';
|
||||
|
||||
export default class Notice {
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client) {
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this.url = location.origin.replace('http', 'ws') + '/ws/notice';
|
||||
}
|
||||
|
||||
start() {
|
||||
this._socket = new WebSocket(this.url);
|
||||
|
||||
this._socket.onclose = _ => this._socket = undefined;
|
||||
this._socket.onmessage = message => {
|
||||
const data = JSON.parse(message.data);
|
||||
//console.log(data)
|
||||
|
||||
if (data.type === 'friend_request') {
|
||||
this.friend_request(data.author);
|
||||
} else if (data.type === 'new_friend') {
|
||||
this.new_friend(data.friend);
|
||||
} else if (data.type === 'friend_removed') {
|
||||
this.friend_removed(data.friend);
|
||||
} else if (data.type === 'friend_request_canceled') {
|
||||
this.friend_request_canceled(data.author);
|
||||
} else if (data.type === 'online') {
|
||||
this.online(data.user)
|
||||
} else if (data.type === 'offline') {
|
||||
this.offline(data.user)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this._socket) {
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
_setOnlineStatus(user, status) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === user.id) {
|
||||
lastView.profile.online = status;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
else if (lastView instanceof Search) {
|
||||
lastView.display_specific_user(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
online(user) {
|
||||
this._setOnlineStatus(user, true)
|
||||
}
|
||||
|
||||
offline(user) {
|
||||
this._setOnlineStatus(user, false)
|
||||
}
|
||||
|
||||
friend_request(author) {
|
||||
createNotification('Friend Request', `<strong>${author.username}</strong> sent you a friend request.`);
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
|
||||
lastView.profile.hasIncomingRequest = true;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
new_friend(friend) {
|
||||
createNotification('New Friend', `<strong>${friend.username}</strong> accepted your friend request.`);
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === friend.id) {
|
||||
lastView.profile.isFriend = true;
|
||||
lastView.profile.hasIncomingRequest = false;
|
||||
lastView.profile.hasOutgoingRequest = false;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
friend_removed(exFriend) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === exFriend.id) {
|
||||
lastView.profile.isFriend = false;
|
||||
lastView.profile.online = null;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
friend_request_canceled(author) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
|
||||
lastView.profile.hasIncomingRequest = false;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
}
|
101
django/frontend/static/js/api/Profile.js
Normal file
101
django/frontend/static/js/api/Profile.js
Normal file
@ -0,0 +1,101 @@
|
||||
import { AExchangeable } from "./AExchangable.js";
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
export class Profile extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client, username, id, avatar)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this.username = username;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this.avatar = avatar;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
**/
|
||||
this.online = null;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.isFriend;
|
||||
this.isBlocked;
|
||||
this.hasIncomingRequest;
|
||||
this.hasOutgoingRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async init()
|
||||
{
|
||||
let response;
|
||||
if (this.username !== undefined)
|
||||
response = await this.client._get(`/api/profiles/user/${this.username}`);
|
||||
else
|
||||
response = await this.client._get(`/api/profiles/id/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
const responseData = await response.json();
|
||||
this.id = responseData.id;
|
||||
this.username = responseData.username;
|
||||
this.avatar = responseData.avatar;
|
||||
this.online = responseData.online
|
||||
|
||||
if (!this.client.me || this.client.me.id === this.id)
|
||||
return;
|
||||
|
||||
this.hasIncomingRequest = responseData.has_incoming_request;
|
||||
this.hasOutgoingRequest = responseData.has_outgoing_request;
|
||||
this.isFriend = responseData.is_friend;
|
||||
this.isBlocked = this.client.me._isBlocked(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<[Object]>}
|
||||
*/
|
||||
async getGameHistory()
|
||||
{
|
||||
const response = await this.client._get(`/api/games/history/${this.id}`);
|
||||
const response_data = await response.json();
|
||||
|
||||
const games = [];
|
||||
|
||||
response_data.forEach(game_data => {
|
||||
games.push(game_data);
|
||||
});
|
||||
|
||||
return games;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList = [])
|
||||
{
|
||||
super.export([...["username", "avatar", "id"], ...additionalFieldList])
|
||||
}
|
||||
}
|
59
django/frontend/static/js/api/Profiles.js
Normal file
59
django/frontend/static/js/api/Profiles.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { Profile } from "./Profile.js";
|
||||
|
||||
class Profiles
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<[Profile]>}
|
||||
*/
|
||||
async all()
|
||||
{
|
||||
let response = await this.client._get("/api/profiles/");
|
||||
let response_data = await response.json();
|
||||
|
||||
let profiles = [];
|
||||
response_data.forEach((profile) => {
|
||||
profiles.push(new Profile(this.client, profile.username, profile.id, profile.avatar));
|
||||
});
|
||||
return profiles;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} username
|
||||
* @returns {?Promise<Profile>}
|
||||
*/
|
||||
async getProfile(username)
|
||||
{
|
||||
let profile = new Profile(this.client, username);
|
||||
if (await profile.init())
|
||||
return null;
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} id
|
||||
* @returns {Profile}
|
||||
*/
|
||||
async getProfileId(id)
|
||||
{
|
||||
let profile = new Profile(this.client, undefined, id);
|
||||
if (await profile.init())
|
||||
return null;
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
export {Profiles};
|
22
django/frontend/static/js/api/chat/Ask.js
Normal file
22
django/frontend/static/js/api/chat/Ask.js
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
class Ask {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async ask_game(asked) {
|
||||
response = await this.client._post(`/api/chat/ask/`);
|
||||
}
|
||||
|
||||
async ask_game_canceled() {
|
||||
|
||||
}
|
||||
|
||||
async ask_game_accepted() {
|
||||
|
||||
}
|
||||
|
||||
async ask_game_refused() {
|
||||
|
||||
}
|
||||
}
|
66
django/frontend/static/js/api/chat/Channel.js
Normal file
66
django/frontend/static/js/api/chat/Channel.js
Normal file
@ -0,0 +1,66 @@
|
||||
import {Message} from "./Message.js";
|
||||
|
||||
class Channel {
|
||||
constructor(client, channel, members, messages, reload) {
|
||||
this.client = client;
|
||||
this.channel = channel;
|
||||
this.members = members;
|
||||
this.messages = [];
|
||||
if (messages != undefined)
|
||||
this.updateMessages(messages);
|
||||
|
||||
this.connect(reload);
|
||||
}
|
||||
|
||||
// reload = function to use when we receive a message
|
||||
connect(reload) {
|
||||
const url = location.origin.replace('http', 'ws') +
|
||||
'/ws/chat/' +
|
||||
this.channel;
|
||||
|
||||
this.chatSocket = new WebSocket(url);
|
||||
this.chatSocket.onmessage = (event) =>{
|
||||
let data = JSON.parse(event.data);
|
||||
|
||||
this.messages.push(new Message(
|
||||
this.channel,
|
||||
data.author,
|
||||
data.content,
|
||||
data.time,
|
||||
));
|
||||
|
||||
reload();
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.chatSocket.close();
|
||||
}
|
||||
|
||||
updateMessages(messages)
|
||||
{
|
||||
this.messages = [];
|
||||
|
||||
messages.forEach((message) => {
|
||||
this.messages.push(new Message(
|
||||
message.channel,
|
||||
message.author,
|
||||
message.content,
|
||||
message.time,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
async sendMessageChannel(message, receivers_id) {
|
||||
|
||||
if (this.chatSocket == undefined)
|
||||
return;
|
||||
|
||||
this.chatSocket.send(JSON.stringify({
|
||||
'message':message,
|
||||
'receivers_id':receivers_id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export {Channel};
|
23
django/frontend/static/js/api/chat/Channels.js
Normal file
23
django/frontend/static/js/api/chat/Channels.js
Normal file
@ -0,0 +1,23 @@
|
||||
import {Channel} from "./Channel.js";
|
||||
|
||||
export default class Channels {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.channel = undefined;
|
||||
}
|
||||
|
||||
async createChannel(members_id, reload) {
|
||||
|
||||
const response = await this.client._post("/api/chat/", {
|
||||
members_id:members_id
|
||||
});
|
||||
|
||||
if (response.status >= 300)
|
||||
return undefined;
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data)
|
||||
|
||||
this.channel = new Channel(this.client, data.id, members_id, data.messages, reload);
|
||||
}
|
||||
}
|
10
django/frontend/static/js/api/chat/Message.js
Normal file
10
django/frontend/static/js/api/chat/Message.js
Normal file
@ -0,0 +1,10 @@
|
||||
class Message {
|
||||
constructor(channel, author, content, time) {
|
||||
this.channel = channel;
|
||||
this.author = author;
|
||||
this.content = content;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
export {Message};
|
159
django/frontend/static/js/api/game/AGame.js
Normal file
159
django/frontend/static/js/api/game/AGame.js
Normal file
@ -0,0 +1,159 @@
|
||||
import { AExchangeable } from "../AExchangable.js";
|
||||
import { APlayer } from "./APlayer.js";
|
||||
import { Client } from "../Client.js"
|
||||
import { sleep } from "../../utils/sleep.js";
|
||||
import { Profile } from "../Profile.js";
|
||||
|
||||
export class AGame extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* Abstract class to create commununication between client and server
|
||||
* @param {Client} client
|
||||
* @param {Number} id
|
||||
* @param {CallableFunction} receiveHandler
|
||||
* @param {CallableFunction} disconntectHandler
|
||||
* @param {"tictactoe" | "pong"} gameType
|
||||
*/
|
||||
constructor(client, id, receiveHandler, disconntectHandler, gameType)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* ex: Tictactoe, Pong
|
||||
* @type {String}
|
||||
*/
|
||||
this.gameType = gameType;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._receiveHandler = receiveHandler;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._disconntectHandler = disconntectHandler;
|
||||
|
||||
/**
|
||||
* @type {Profile}
|
||||
*/
|
||||
this.winner;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.startTimestamp;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.stopTimestamp;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.started;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.finished;
|
||||
|
||||
/**
|
||||
* @type {[APlayer]}
|
||||
*/
|
||||
this.players = [];
|
||||
}
|
||||
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get(`/api/games/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
this.import(response_data);
|
||||
}
|
||||
|
||||
getState()
|
||||
{
|
||||
return ["waiting", "started", "finished"][this.started + this.finished];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send string to the server, must be excuted after .join()
|
||||
* @param {String} data
|
||||
*/
|
||||
send(data)
|
||||
{
|
||||
if (this._socket === undefined || this._socket.readyState !== WebSocket.OPEN)
|
||||
return;
|
||||
this._socket.send(data);
|
||||
}
|
||||
|
||||
async join()
|
||||
{
|
||||
if (this.finished === true)
|
||||
{
|
||||
console.error("The Game is not currently ongoing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/games/${this.gameType}/${this.id}`;
|
||||
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this._socket.onmessage = async (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
await this._receiveHandler(data);
|
||||
};
|
||||
|
||||
this._socket.onclose = async () => {
|
||||
this._socket = undefined;
|
||||
await this._disconntectHandler();
|
||||
};
|
||||
}
|
||||
|
||||
leave()
|
||||
{
|
||||
if (this._socket)
|
||||
{
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be redefine using your own APlayer inherited
|
||||
* @param {Object} data
|
||||
|
||||
import(data)
|
||||
{
|
||||
super.import(data);
|
||||
|
||||
// just an example code
|
||||
|
||||
/*
|
||||
this.players.length = 0;
|
||||
|
||||
data.players.forEach(player_data => {
|
||||
let player = new APlayer(this.client, this);
|
||||
player.import(player_data);
|
||||
this.players.push(player);
|
||||
});
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
39
django/frontend/static/js/api/game/APlayer.js
Normal file
39
django/frontend/static/js/api/game/APlayer.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { Client } from "../Client.js";
|
||||
import { Profile } from "../Profile.js";
|
||||
import { AGame } from "./AGame.js";
|
||||
|
||||
export class APlayer extends Profile
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {AGame} game
|
||||
* @param {Number} id
|
||||
* @param {String} username
|
||||
* @param {String} avatar
|
||||
* @param {Boolean} isConnected
|
||||
*/
|
||||
constructor (client, game, id, username, avatar, isConnected)
|
||||
{
|
||||
super(client, username, id, avatar);
|
||||
|
||||
/**
|
||||
* @type {AGame}
|
||||
*/
|
||||
this.game = game
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.isConnected = isConnected;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList = [])
|
||||
{
|
||||
super.export([...additionalFieldList, ...["isConnected"]])
|
||||
}
|
||||
}
|
32
django/frontend/static/js/api/game/pong/Point.js
Normal file
32
django/frontend/static/js/api/game/pong/Point.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
|
||||
class Point extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Number} x
|
||||
* @param {Number} y
|
||||
*/
|
||||
constructor(x, y)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.x = x;
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {[String]}
|
||||
*/
|
||||
export(additionalFieldList)
|
||||
{
|
||||
super.export([...additionalFieldList, "x", "y"])
|
||||
}
|
||||
}
|
||||
|
||||
export { Point };
|
83
django/frontend/static/js/api/game/pong/PongBall.js
Normal file
83
django/frontend/static/js/api/game/pong/PongBall.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { renderCube} from "../../../3D/cube.js"
|
||||
import { Position } from "./Position.js";
|
||||
import { Point } from "./Point.js";
|
||||
|
||||
export class PongBall extends AExchangeable
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {PongGame} game
|
||||
* @param {Position} position
|
||||
* @param {Number} angle
|
||||
* @param {Number} speed
|
||||
* @param {Number} size
|
||||
*/
|
||||
constructor(game, size, position = new Position(new Point(game.config.CENTER_X, game.config.CENTER_Y), 0), angle, speed)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game = game;
|
||||
|
||||
/**
|
||||
* @type {Position}
|
||||
*/
|
||||
this.position = position;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.size = size;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.angle = angle;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
draw(ctx)
|
||||
{
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.rect(this.position.location.x - this.size / 2, this.position.location.y - this.size / 2, this.size, this.size);
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const size = this.size * 3;
|
||||
const posx = (this.position.location.x - this.size / 2) - this.game.config.MAP_SIZE_X / 2;
|
||||
const posy = (this.position.location.y - this.size / 2) - this.game.config.MAP_SIZE_Y / 2;
|
||||
renderCube(ctx, posx, 0, posy, 0, size, size, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert('Unknown rendering context type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
render(ctx)
|
||||
{
|
||||
let distance = this.speed * (this.game.time.deltaTime() / 1000);
|
||||
|
||||
this.position.location.x = this.position.location.x + distance * Math.cos(this.angle);
|
||||
this.position.location.y = this.position.location.y - distance * Math.sin(this.angle);
|
||||
|
||||
this.draw(ctx);
|
||||
}
|
||||
}
|
94
django/frontend/static/js/api/game/pong/PongConfig.js
Normal file
94
django/frontend/static/js/api/game/pong/PongConfig.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
|
||||
export class PongConfig extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_SIZE_X;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_SIZE_Y;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.WALL_RATIO;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.PADDLE_SPEED_PER_SECOND_MAX;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.PADDLE_RATIO;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.BALL_SIZE;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.BALL_SPEED_INC;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.BALL_SPEED_START;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.STROKE_THICKNESS;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.GAME_MAX_SCORE;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_CENTER_X;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_CENTER_Y;
|
||||
}
|
||||
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get("/api/games/");
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
this.import(response_data);
|
||||
|
||||
this.MAP_CENTER_X = this.MAP_SIZE_X / 2;
|
||||
this.MAP_CENTER_Y = this.MAP_SIZE_Y / 2;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
230
django/frontend/static/js/api/game/pong/PongGame.js
Normal file
230
django/frontend/static/js/api/game/pong/PongGame.js
Normal file
@ -0,0 +1,230 @@
|
||||
import { Time } from "./Time.js";
|
||||
import { AGame } from "../AGame.js";
|
||||
import { PongConfig } from "./PongConfig.js";
|
||||
import { PongPlayer } from "./PongPlayer.js";
|
||||
import { Client } from "../../Client.js";
|
||||
import { PongBall } from "./PongBall.js";
|
||||
import { sleep } from "../../../utils/sleep.js";
|
||||
import { Wall } from "./Wall.js"
|
||||
import { Position } from "./Position.js";
|
||||
|
||||
export class PongGame extends AGame
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {CallableFunction} goal_handler
|
||||
* @param {CallableFunction} finish_handler
|
||||
* @param {CallableFunction} disconnect_handler
|
||||
* @param {CallableFunction} startHandler
|
||||
* @param {Number} id
|
||||
*/
|
||||
constructor(client, id, disconnectHandler, goalHandler, startHandler, finishHandler)
|
||||
{
|
||||
super(client, id, undefined, disconnectHandler, "pong");
|
||||
|
||||
this._receiveHandler = this._receive;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._goalHandler = goalHandler;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._startHandler = startHandler;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._finishHandler = finishHandler
|
||||
|
||||
/**
|
||||
* @type {Time}
|
||||
*/
|
||||
this.time;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this._inited = false;
|
||||
|
||||
/**
|
||||
* @type {PongConfig}
|
||||
*/
|
||||
this.config;
|
||||
|
||||
/**
|
||||
* @type {Ball}
|
||||
*/
|
||||
this.ball = new PongBall(this, undefined, new Position(), 0, 0);
|
||||
|
||||
/**
|
||||
* @type {[Wall]}
|
||||
*/
|
||||
this.walls = [];
|
||||
|
||||
/**
|
||||
* @type {[PongPlayer]}
|
||||
*/
|
||||
this.players = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<Number>}
|
||||
*/
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get(`/api/games/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
response_data.players.forEach((player_data) => {
|
||||
let player = new PongPlayer(this.client, this)
|
||||
this.players.push(player);
|
||||
});
|
||||
|
||||
this.import(response_data);
|
||||
|
||||
if (this.finished === true)
|
||||
return 0;
|
||||
|
||||
this.config = new PongConfig(this.client);
|
||||
|
||||
let ret = await this.config.init();
|
||||
|
||||
if (ret !== 0)
|
||||
return ret;
|
||||
|
||||
this.time = new Time();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
drawSides(ctx)
|
||||
{
|
||||
this.walls.forEach(wall => {
|
||||
wall.draw(ctx);
|
||||
});
|
||||
|
||||
this.players.forEach(player => {
|
||||
player.draw(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
render(ctx)
|
||||
{
|
||||
ctx.clearRect(0, 0, this.config.MAP_SIZE_Y, this.config.MAP_SIZE_Y);
|
||||
|
||||
this.drawSides(ctx);
|
||||
this.ball.render(ctx);
|
||||
|
||||
ctx.strokeStyle = "#000000";
|
||||
ctx.lineWidth = this.config.STROKE_THICKNESS;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
send(data)
|
||||
{
|
||||
super.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
async _receive(data)
|
||||
{
|
||||
console.log(data)
|
||||
|
||||
if (this._inited === false && data.detail === "init_game")
|
||||
{
|
||||
this._initGame(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.detail === "update_player")
|
||||
this._updatePlayer(data);
|
||||
else if (data.detail === "update_ball")
|
||||
this._updateBall(data);
|
||||
else if (data.detail === "goal")
|
||||
await this._receiveGoal(data);
|
||||
else if (data.detail === "finish")
|
||||
await this._receiveFinish(data);
|
||||
else if (data.detail === "start")
|
||||
await this._receiveStart();
|
||||
}
|
||||
|
||||
async _receiveFinish(data)
|
||||
{
|
||||
const winner = this.players.find(player => player.id === data.winner_id)
|
||||
this.finished = true;
|
||||
await this._finishHandler(winner);
|
||||
}
|
||||
|
||||
|
||||
async _receiveStart()
|
||||
{
|
||||
this.started = true;
|
||||
await this._startHandler();
|
||||
}
|
||||
|
||||
async _receiveGoal(data)
|
||||
{
|
||||
const player = this.players.find((player) => player.id === data.player_id);
|
||||
|
||||
if (player === undefined)
|
||||
{
|
||||
console.error("error: unknown player.")
|
||||
return
|
||||
}
|
||||
|
||||
player.score.push(data.timestamp)
|
||||
console.log(player)
|
||||
|
||||
await this._goalHandler(player);
|
||||
}
|
||||
|
||||
_updatePlayer(data)
|
||||
{
|
||||
let player = this.players.find((player) => player.id === data.id);
|
||||
|
||||
player.import(data);
|
||||
}
|
||||
|
||||
_updateBall(data)
|
||||
{
|
||||
this.ball.import(data);
|
||||
}
|
||||
|
||||
_initGame(data)
|
||||
{
|
||||
data.walls.forEach((wall_data) => {
|
||||
this.walls.push(new Wall(this));
|
||||
});
|
||||
|
||||
this.import(data);
|
||||
|
||||
this._inited = true;
|
||||
}
|
||||
|
||||
async waitInit()
|
||||
{
|
||||
while (this._inited !== true)
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
110
django/frontend/static/js/api/game/pong/PongMyPlayer.js
Normal file
110
django/frontend/static/js/api/game/pong/PongMyPlayer.js
Normal file
@ -0,0 +1,110 @@
|
||||
import { PongPlayer } from "./PongPlayer.js";
|
||||
import { Client } from "../../Client.js";
|
||||
import { Segment } from "./Segment.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { Position } from "./Position.js";
|
||||
|
||||
export class PongMyPlayer extends PongPlayer
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {PongGame} game
|
||||
* @param {Segment} rail
|
||||
* @param {[Number]} score
|
||||
* @param {Position} position
|
||||
*/
|
||||
constructor(client, game, score, rail, position = new Position(0.5))
|
||||
{
|
||||
super(client, game, client.me.id, client.me.username, client.me.avatar, score, rail, position, true);
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game;
|
||||
|
||||
this.upKeys = [];
|
||||
this.downKeys = [];
|
||||
|
||||
if (rail.start.x != rail.stop.x)
|
||||
{
|
||||
if (rail.start.x < rail.stop.x)
|
||||
{
|
||||
this.upKeys.push("a");
|
||||
this.downKeys.push("d");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.upKeys.push("d");
|
||||
this.downKeys.push("a");
|
||||
}
|
||||
}
|
||||
if (rail.start.y != rail.stop.y)
|
||||
{
|
||||
if (rail.start.y < rail.stop.y)
|
||||
{
|
||||
this.upKeys.push("w");
|
||||
this.downKeys.push("s");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.upKeys.push("s");
|
||||
this.downKeys.push("w");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[string]} keys_pressed
|
||||
*/
|
||||
updatePaddle(keys_pressed)
|
||||
{
|
||||
let new_location = this.position.location;
|
||||
|
||||
keys_pressed.forEach(key => {
|
||||
if (this.downKeys.includes(key))
|
||||
new_location += this.game.config.PADDLE_SPEED_PER_SECOND_MAX * this.game.time.deltaTimeSecond();
|
||||
if (this.upKeys.includes(key))
|
||||
new_location -= this.game.config.PADDLE_SPEED_PER_SECOND_MAX * this.game.time.deltaTimeSecond();
|
||||
});
|
||||
|
||||
new_location = Math.max(0 + this.game.config.PADDLE_RATIO / 2, new_location);
|
||||
new_location = Math.min(1 - this.game.config.PADDLE_RATIO / 2, new_location);
|
||||
|
||||
if (this.position.location === new_location)
|
||||
return;
|
||||
|
||||
this.position.location = new_location;
|
||||
|
||||
this._sendPaddlePosition();
|
||||
}
|
||||
|
||||
_sendPaddlePosition()
|
||||
{
|
||||
this.game.send({"detail": "update_my_paddle_pos", ...{"time": this.game.time._currentFrame, "position": this.position}});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Position} newPosition
|
||||
*/
|
||||
updatePos(newPosition)
|
||||
{
|
||||
let position_verified = newPosition;
|
||||
|
||||
let time_diff = (this.time._current_frame - newPosition.time) / 1000;
|
||||
|
||||
let sign = this.position.location - newPosition.location >= 0 ? 1 : -1;
|
||||
|
||||
let distance = Math.abs(this.position.location - newPosition.location);
|
||||
|
||||
let distance_max = time_diff * this.game.config.paddle_speed_per_second_max;
|
||||
|
||||
if (distance > distance_max)
|
||||
position_verified.location = distance_max * sign;
|
||||
|
||||
this.position = position_verified;
|
||||
}
|
||||
}
|
102
django/frontend/static/js/api/game/pong/PongPlayer.js
Normal file
102
django/frontend/static/js/api/game/pong/PongPlayer.js
Normal file
@ -0,0 +1,102 @@
|
||||
import { APlayer } from "../APlayer.js";
|
||||
import { Point } from "./Point.js";
|
||||
import { Segment } from "./Segment.js";
|
||||
import { Client } from "../../Client.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { Position } from "./Position.js";
|
||||
|
||||
export class PongPlayer extends APlayer
|
||||
{
|
||||
/**
|
||||
* @param {Number} id
|
||||
* @param {PongGame} game
|
||||
* @param {Segment} rail
|
||||
* @param {[Number]} score
|
||||
* @param {Position} position
|
||||
* @param {Boolean} isConnected
|
||||
* @param {String} username
|
||||
* @param {String} avatar
|
||||
* @param {Client} client
|
||||
* @param {Boolean} isEliminated
|
||||
*/
|
||||
constructor(client, game, id, username, avatar, score = [], rail = new Segment(game), position = new Position(0.5), isConnected, isEliminated)
|
||||
{
|
||||
super(client, game, id, username, avatar, isConnected)
|
||||
|
||||
/**
|
||||
* @type {Position}
|
||||
*/
|
||||
this.position = position;
|
||||
|
||||
/**
|
||||
* @type {[Number]}
|
||||
*/
|
||||
this.score = score;
|
||||
|
||||
/**
|
||||
* @type {Segment}
|
||||
*/
|
||||
this.rail = rail;
|
||||
|
||||
/**
|
||||
* @type {PongPlayer}
|
||||
*/
|
||||
this.game = game;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.isEliminated = isEliminated;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} new_position
|
||||
*/
|
||||
updatePos(new_position)
|
||||
{
|
||||
this.position = new_position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
draw(ctx)
|
||||
{
|
||||
if (this.isConnected === false || this.isEliminated === true)
|
||||
{
|
||||
ctx.moveTo(this.rail.start.x, this.rail.start.y);
|
||||
ctx.lineTo(this.rail.stop.x, this.rail.stop.y);
|
||||
return;
|
||||
}
|
||||
|
||||
const diffX = this.rail.stop.x - this.rail.start.x,
|
||||
diffY = this.rail.stop.y - this.rail.start.y;
|
||||
|
||||
const railLength = this.rail.len(),
|
||||
paddleLength = railLength * this.game.config.PADDLE_RATIO;
|
||||
|
||||
const paddleCenter = new Point(this.rail.start.x + diffX * this.position.location,
|
||||
this.rail.start.y + diffY * this.position.location);
|
||||
|
||||
const paddleStartX = paddleCenter.x - (diffX * (paddleLength / 2 / railLength)),
|
||||
paddleStartY = paddleCenter.y - (diffY * (paddleLength / 2 / railLength)),
|
||||
paddleStopX = paddleCenter.x + (diffX * (paddleLength / 2 / railLength)),
|
||||
paddleStopY = paddleCenter.y + (diffY * (paddleLength / 2 / railLength));
|
||||
|
||||
let paddleStart = new Point(paddleStartX, paddleStartY),
|
||||
paddleStop = new Point (paddleStopX, paddleStopY);
|
||||
|
||||
let paddle = new Segment(this.game, paddleStart, paddleStop);
|
||||
|
||||
paddle.draw(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList = [])
|
||||
{
|
||||
super.export([...additionalFieldList, "position", "rail", "score"])
|
||||
}
|
||||
}
|
31
django/frontend/static/js/api/game/pong/Position.js
Normal file
31
django/frontend/static/js/api/game/pong/Position.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
import { Point } from "./Point.js";
|
||||
|
||||
export class Position extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Point | Number} location
|
||||
* @param {Number} time
|
||||
*/
|
||||
constructor(location = new Point(), time)
|
||||
{
|
||||
super();
|
||||
/**
|
||||
* @type {Point | Number}
|
||||
*/
|
||||
this.location = location;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList)
|
||||
{
|
||||
super.export([...additionalFieldList + "location", "time"]);
|
||||
}
|
||||
}
|
80
django/frontend/static/js/api/game/pong/Segment.js
Normal file
80
django/frontend/static/js/api/game/pong/Segment.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { Point } from "./Point.js";
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { renderCube } from "../../../3D/cube.js";
|
||||
|
||||
class Segment extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Point} start
|
||||
* @param {Point} stop
|
||||
* @param {PongGame} game
|
||||
*/
|
||||
constructor(game, start = new Point(), stop = new Point())
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
this.start = start;
|
||||
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
this.stop = stop;
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game = game
|
||||
|
||||
}
|
||||
|
||||
angle()
|
||||
{
|
||||
let x = this.start.x - this.stop.x,
|
||||
y = this.start.y - this.stop.y;
|
||||
|
||||
return Math.atan2(y, x);
|
||||
}
|
||||
|
||||
len()
|
||||
{
|
||||
let x = this.start.x - this.stop.x,
|
||||
y = this.start.y - this.stop.y;
|
||||
|
||||
return (x ** 2 + y ** 2) ** (1 / 2);
|
||||
}
|
||||
|
||||
draw(ctx)
|
||||
{
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.moveTo(this.start.x, this.start.y);
|
||||
ctx.lineTo(this.stop.x, this.stop.y);
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const size = this.game.config.BALL_SIZE * 2;
|
||||
const sizex = this.len() / 2;
|
||||
const posx = (this.start.x - this.game.config.MAP_CENTER_X);
|
||||
const posy = (this.start.y - this.game.config.MAP_CENTER_Y);
|
||||
renderCube(ctx, posx, 0, posy, -this.angle(), sizex, size, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert('Unknown rendering context type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList)
|
||||
{
|
||||
super.export([...additionalFieldList, "start", "stop"]);
|
||||
}
|
||||
}
|
||||
|
||||
export { Segment }
|
42
django/frontend/static/js/api/game/pong/Time.js
Normal file
42
django/frontend/static/js/api/game/pong/Time.js
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
|
||||
class Time
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this._lastFrame = undefined;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this._currentFrame = undefined;
|
||||
}
|
||||
|
||||
deltaTime()
|
||||
{
|
||||
if (this._lastFrame === undefined)
|
||||
return 0;
|
||||
return (this._currentFrame - this._lastFrame);
|
||||
}
|
||||
|
||||
deltaTimeSecond()
|
||||
{
|
||||
return this.deltaTime() / 1000;
|
||||
}
|
||||
|
||||
get_fps()
|
||||
{
|
||||
return 1 / this.deltaTimeSecond();
|
||||
}
|
||||
|
||||
new_frame()
|
||||
{
|
||||
this._lastFrame = this._currentFrame;
|
||||
this._currentFrame = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
export { Time };
|
44
django/frontend/static/js/api/game/pong/Wall.js
Normal file
44
django/frontend/static/js/api/game/pong/Wall.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { Point } from "./Point.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { Segment } from "./Segment.js";
|
||||
import { renderCube} from "../../../3D/cube.js"
|
||||
|
||||
export class Wall extends Segment
|
||||
{
|
||||
|
||||
/**
|
||||
* @param {PongGame} game
|
||||
* @param {Point} start
|
||||
* @param {Point} stop
|
||||
*/
|
||||
constructor(game, start, stop)
|
||||
{
|
||||
super(game, start, stop)
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game = game
|
||||
}
|
||||
|
||||
draw(ctx)
|
||||
{
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.moveTo(this.start.x, this.start.y);
|
||||
ctx.lineTo(this.stop.x, this.stop.y);
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const size = this.game.config.BALL_SIZE * 2;
|
||||
const sizeX = this.len() / 2;
|
||||
const posX = (this.start.x - this.game.config.MAP_CENTER_X);
|
||||
const posY = (this.start.y - this.game.config.MAP_CENTER_Y);
|
||||
renderCube(ctx, posX, 0, posY, -this.angle(), sizeX, size, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert('Unknown rendering context type');
|
||||
}
|
||||
}
|
||||
}
|
334
django/frontend/static/js/api/game/tictactoe/TicTacToeGame.js
Normal file
334
django/frontend/static/js/api/game/tictactoe/TicTacToeGame.js
Normal file
@ -0,0 +1,334 @@
|
||||
import { client, lang } from "../../../index.js";
|
||||
|
||||
import { AGame } from "../AGame.js";
|
||||
|
||||
class TicTacToe
|
||||
{
|
||||
constructor(height, width, gap, rectsize, canvas, game_id)
|
||||
{
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
this.gap = gap;
|
||||
this.rectsize = rectsize;
|
||||
this.map = [[],[],[],[],[],[],[],[],[]];
|
||||
for (let i = 0; i < 9; i++)
|
||||
for (let j = 0; j < 9; j++)
|
||||
this.map[i].push(-1);
|
||||
this.game_id = game_id;
|
||||
this.game = new AGame(client, game_id, this.onReceive.bind(this), this.uninit.bind(this), "tictactoe")
|
||||
this.canvas = canvas
|
||||
this.context = this.canvas.getContext("2d");
|
||||
this.sign;
|
||||
this.currentMorpion = 4;
|
||||
this.turn;
|
||||
}
|
||||
|
||||
async init()
|
||||
{
|
||||
await this.game.join();
|
||||
this.canvas.addEventListener("mousedown", (event, morpion = this) => this.onClick(event, morpion));
|
||||
}
|
||||
|
||||
async uninit()
|
||||
{
|
||||
this.canvas.removeEventListener("mousedown", (event, morpion = this) => this.onClick(event, morpion));
|
||||
this.game.leave()
|
||||
}
|
||||
|
||||
async onReceive(messageData)
|
||||
{
|
||||
switch (messageData.detail)
|
||||
{
|
||||
case 'x':
|
||||
case 'o':
|
||||
this.sign = messageData.detail;
|
||||
this.turn = messageData.detail == "x";
|
||||
break;
|
||||
|
||||
case 'game_start':
|
||||
this.game.started = true;
|
||||
this.game.finished = false;
|
||||
if (this.turn)
|
||||
this.setOutline(4, false);
|
||||
this.printTimer();
|
||||
break;
|
||||
|
||||
case 'game_move':
|
||||
if (messageData.targetMorpion === undefined || messageData.targetCase === undefined)
|
||||
return ;
|
||||
this.map[messageData.targetMorpion][messageData.targetCase] = (this.sign == "x") ? 1 : 0;
|
||||
this.printSign(messageData.targetMorpion, messageData.targetCase, (this.sign == "x") ? "o" : "x");
|
||||
this.setOutline(this.currentMorpion, false);
|
||||
this.printTimer();
|
||||
break;
|
||||
|
||||
case 'game_end':
|
||||
this.game.finished = true;
|
||||
this.canvas.removeEventListener("mousedown", (event, morpion = this) => this.onClick(event, morpion));
|
||||
this.printWin(messageData.winning_sign);
|
||||
break;
|
||||
case 'catchup':
|
||||
this.map = messageData.morpion;
|
||||
for (let i = 0; i < 9; i++)
|
||||
{
|
||||
for (let j = 0; j < 9; j++)
|
||||
{
|
||||
if (this.map[i][j] != -1)
|
||||
this.printSign(i, j, this.map[i][j])
|
||||
}
|
||||
}
|
||||
this.turn = (messageData.turn == this.sign);
|
||||
this.currentMorpion = messageData.currentMorpion;
|
||||
if (this.turn)
|
||||
this.setOutline(this.currentMorpion, false);
|
||||
}
|
||||
}
|
||||
|
||||
printWin(winning_sign)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "white";
|
||||
this.context.fillRect(this.width / 2 - 200, this.height - this.gap + 10, 400, 80);
|
||||
this.context.closePath();
|
||||
this.context.beginPath();
|
||||
this.context.font = `20px sans-serif`;
|
||||
this.context.fillStyle = (winning_sign == "o") ? "red" : "green";
|
||||
this.context.fillText((winning_sign == "o") ? lang.get("morpionWin") + "O" : lang.get("morpionWin") + "X", this.width / 2 - 85, this.height - this.gap / 2 + 10, 180);
|
||||
}
|
||||
|
||||
printTimer()
|
||||
{
|
||||
let sec = 20;
|
||||
let turn = this.turn
|
||||
if (this.turn)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "white";
|
||||
this.context.font = `20px Verdana`;
|
||||
this.context.fillText(sec, this.width / 2, this.gap / 2);
|
||||
this.context.closePath();
|
||||
sec--;
|
||||
}
|
||||
let id = setInterval(() =>
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "#1a1a1d";
|
||||
this.context.fillRect(this.width / 2 - 40, 0, this.width / 2 + 40, this.gap - 10)
|
||||
this.context.closePath();
|
||||
if (sec == 0 || turn != this.turn || this.game.finished)
|
||||
{
|
||||
clearInterval(id);
|
||||
if (sec == 0 && !this.turn && this.game.finished == false)
|
||||
this.game.send(JSON.stringify({"timerIsDue" : this.sign}))
|
||||
return;
|
||||
}
|
||||
if (this.turn)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "white";
|
||||
this.context.font = `20px Verdana`;
|
||||
this.context.fillText(sec, this.width / 2, this.gap / 2);
|
||||
this.context.closePath();
|
||||
}
|
||||
sec--;
|
||||
}, 1000
|
||||
)
|
||||
}
|
||||
checkWin()
|
||||
{
|
||||
for (let i = 0; i < 9; i++)
|
||||
{
|
||||
for (let j = 0; j < 3; j++)
|
||||
{
|
||||
if (this.map[i][j] == this.map[i][j + 3] && this.map[i][j + 3] == this.map[i][j + 6])
|
||||
return (this.map[i][j])
|
||||
}
|
||||
for (let j = 0; i < 9; i += 3)
|
||||
{
|
||||
if (this.map[i][j] == this.map[i][j + 1] && this.map[i][j + 1] == this.map[i][j + 2])
|
||||
return (this.map[i][j])
|
||||
}
|
||||
if (this.map[i][0] == this.map[i][4] && this.map[i][4] == this.map[i][8])
|
||||
return (this.map[i][0]);
|
||||
if (this.map[i][6] == this.map[i][4] && this.map[i][4] == this.map[i][2])
|
||||
return (this.map[i][6]);
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
onClick(event, morpion)
|
||||
{
|
||||
let x = event.offsetX;
|
||||
let y = event.offsetY;
|
||||
let targetMorpion, targetCase;
|
||||
|
||||
if (this.game.finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
targetMorpion = morpion.findPlace(x, this) + morpion.findPlace(y, this) * 3;
|
||||
if (morpion.findPlace(x, this) < 0 || morpion.findPlace(y, this) < 0)
|
||||
return -1;
|
||||
targetCase = morpion.findSquare(x, this.rectsize * 3 * morpion.findPlace(x, this) + this.gap, this) + morpion.findSquare(y, this.rectsize * 3 * morpion. findPlace(y, this) + this.gap, this) * 3;
|
||||
|
||||
if (morpion.checkCase(targetMorpion, targetCase))
|
||||
{
|
||||
morpion.setOutline(this.currentMorpion, true);
|
||||
morpion.sendCase(targetMorpion, targetCase);
|
||||
morpion.printTimer()
|
||||
}
|
||||
else
|
||||
morpion.incorrectCase();
|
||||
}
|
||||
|
||||
checkCase(targetMorpion, targetCase)
|
||||
{
|
||||
return (this.map[targetMorpion][targetCase] == -1 && this.turn == true && targetMorpion == this.currentMorpion);
|
||||
}
|
||||
|
||||
incorrectCase()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
sendCase(targetMorpion, targetCase)
|
||||
{
|
||||
this.map[targetMorpion][targetCase] = (this.sign == "x") ? 0 : 1;
|
||||
this.currentMorpion = targetCase;
|
||||
this.printSign(targetMorpion, targetCase, this.sign);
|
||||
this.game.send(JSON.stringify({"targetMorpion" : targetMorpion, "targetCase" : targetCase, "sign" : this.sign}));
|
||||
this.turn = !this.turn;
|
||||
}
|
||||
|
||||
printSign(targetMorpion, targetCase, sign)
|
||||
{
|
||||
let targetX = (this.gap + targetMorpion % 3 * this.rectsize * 3) + (targetCase % 3 * this.rectsize);
|
||||
let targetY = (this.gap + Math.floor(targetMorpion / 3) * this.rectsize * 3) + (Math.floor(targetCase / 3) * this.rectsize);
|
||||
if (sign == "x")
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = "green";
|
||||
this.context.moveTo(targetX + 10, targetY + 10);
|
||||
this.context.lineTo(targetX + 40, targetY + 40);
|
||||
this.context.moveTo(targetX + 40, targetY + 10);
|
||||
this.context.lineTo(targetX + 10, targetY + 40);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = "red";
|
||||
targetX += this.rectsize / 2;
|
||||
targetY += this.rectsize / 2;
|
||||
this.context.arc(targetX, targetY, 15, 0, 2 * Math.PI);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
if (sign != this.sign)
|
||||
this.turn = true;
|
||||
}
|
||||
|
||||
findPlace(x, morpion)
|
||||
{
|
||||
if (x <= this.gap || x >= this.gap + this.rectsize * 9)
|
||||
return -1;
|
||||
if (x <= this.gap + this.rectsize * 3)
|
||||
return 0;
|
||||
if (x >= this.gap + this.rectsize * 3 && x <= this.gap + this.rectsize * 6)
|
||||
return 1;
|
||||
if (x >= this.gap + this.rectsize * 6)
|
||||
return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
findSquare(x, gap, morpion)
|
||||
{
|
||||
if (x <= gap + this.rectsize)
|
||||
return 0;
|
||||
if (x >= gap + this.rectsize && x <= gap + this.rectsize * 2)
|
||||
return 1;
|
||||
if (x >= gap + this.rectsize * 2)
|
||||
return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
setOutline(targetMorpion, clear)
|
||||
{
|
||||
let targetX = (this.gap + targetMorpion % 3 * this.rectsize * 3);
|
||||
let targetY = (this.gap + Math.floor(targetMorpion / 3) * this.rectsize * 3);
|
||||
if (this.game.finished)
|
||||
return;
|
||||
if (!clear)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = (this.sign == "x") ? "green" : "red";
|
||||
this.context.rect(targetX, targetY, this.rectsize * 3, this.rectsize * 3)
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(230 230 230)`;
|
||||
this.context.rect(targetX, targetY, this.rectsize * 3, this.rectsize * 3)
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
DrawSuperMorpion()
|
||||
{
|
||||
this.context.fillStyle = "#1a1a1d";
|
||||
this.context.roundRect(0, 0, this.canvas.width, this.canvas.height, 20);
|
||||
this.context.fill();
|
||||
for (let i = 1, x = this.gap, y = this.gap; i <= 9; i++)
|
||||
{
|
||||
this.DrawMorpion(x, y);
|
||||
x += this.rectsize * 3;
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
y += this.rectsize * 3;
|
||||
x = this.gap;
|
||||
}
|
||||
}
|
||||
this.context.lineWidth = 6;
|
||||
for (let i = 0; i < 4; i++)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(230 230 230)`;
|
||||
this.context.moveTo(this.gap + i * this.rectsize * 3, this.gap - 3);
|
||||
this.context.lineTo(this.gap + i * this.rectsize * 3, this.canvas.height - this.gap + 3);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
};
|
||||
for (let i = 0; i < 4; i++)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(230 230 230)`;
|
||||
this.context.moveTo(this.gap, this.gap + i * this.rectsize * 3);
|
||||
this.context.lineTo(this.canvas.height - this.gap, this.gap + i * this.rectsize * 3);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
DrawMorpion(start_x, start_y)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(200 200 200)`;
|
||||
for (let i = 1, x = 0, y = 0; i <= 9; i++)
|
||||
{
|
||||
this.context.strokeRect(start_x + x, start_y + y, this.rectsize, this.rectsize);
|
||||
x += this.rectsize;
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
y += this.rectsize;
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
export { TicTacToe };
|
6314
django/frontend/static/js/bootstrap/bootstrap.bundle.js
vendored
Normal file
6314
django/frontend/static/js/bootstrap/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
django/frontend/static/js/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
7
django/frontend/static/js/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4447
django/frontend/static/js/bootstrap/bootstrap.esm.js
vendored
Normal file
4447
django/frontend/static/js/bootstrap/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
django/frontend/static/js/bootstrap/bootstrap.esm.js.map
Normal file
1
django/frontend/static/js/bootstrap/bootstrap.esm.js.map
Normal file
File diff suppressed because one or more lines are too long
7
django/frontend/static/js/bootstrap/bootstrap.esm.min.js
vendored
Normal file
7
django/frontend/static/js/bootstrap/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4494
django/frontend/static/js/bootstrap/bootstrap.js
vendored
Normal file
4494
django/frontend/static/js/bootstrap/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
django/frontend/static/js/bootstrap/bootstrap.js.map
Normal file
1
django/frontend/static/js/bootstrap/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
django/frontend/static/js/bootstrap/bootstrap.min.js
vendored
Normal file
7
django/frontend/static/js/bootstrap/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
django/frontend/static/js/bootstrap/bootstrap.min.js.map
Normal file
1
django/frontend/static/js/bootstrap/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
1
django/frontend/static/js/chartjs/chart.umd.min.js
vendored
Normal file
1
django/frontend/static/js/chartjs/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
162
django/frontend/static/js/index.js
Normal file
162
django/frontend/static/js/index.js
Normal file
@ -0,0 +1,162 @@
|
||||
import { Client } from "./api/Client.js";
|
||||
|
||||
import Search from "./views/Search.js";
|
||||
import HomeView from "./views/HomeView.js";
|
||||
import LogoutView from "./views/accounts/LogoutView.js";
|
||||
|
||||
import PongOnlineView from "./views/PongOnlineView.js"
|
||||
import { PongOfflineView } from "./views/PongOfflineView.js"
|
||||
|
||||
import { TicTacToeOnlineView } from "./views/TicTacToeOnlineView.js"
|
||||
import { TicTacToeOfflineView } from "./views/TicTacToeOfflineView.js"
|
||||
|
||||
import PageNotFoundView from './views/PageNotFoundView.js' ;
|
||||
|
||||
import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js";
|
||||
import SettingsView from "./views/SettingsView.js";
|
||||
import ProfilePageView from "./views/ProfilePageView.js";
|
||||
import MatchMakingView from "./views/MatchMakingView.js";
|
||||
import AuthenticationView from "./views/accounts/AuthenticationView.js";
|
||||
|
||||
let client = new Client(location.origin);
|
||||
let lang = client.lang;
|
||||
|
||||
let lastView;
|
||||
let lastPageUrlBeforeLogin;
|
||||
|
||||
const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");
|
||||
|
||||
const getParams = match => {
|
||||
|
||||
const values = match.result.slice(1);
|
||||
const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(result => result[1]);
|
||||
return Object.fromEntries(keys.map((key, i) => {
|
||||
return [key, values[i]];
|
||||
}));
|
||||
};
|
||||
|
||||
const navigateTo = async (uri) => {
|
||||
|
||||
history.pushState(null, null, uri);
|
||||
|
||||
if (await router(uri) !== 0)
|
||||
return;
|
||||
|
||||
let link = document.querySelector('a[href=\'' + location.pathname + '\']');
|
||||
if (link) {
|
||||
document.querySelector('[data-link].active')?.classList.remove('active');
|
||||
link.classList.add('active');
|
||||
}
|
||||
};
|
||||
|
||||
const reloadView = async _ => {
|
||||
await lastView?.leavePage();
|
||||
await renderView(lastView);
|
||||
};
|
||||
|
||||
async function renderView(view)
|
||||
{
|
||||
let content = await view?.getHtml();
|
||||
if (content == null)
|
||||
return 1;
|
||||
|
||||
view.setTitle();
|
||||
document.querySelector("#app").innerHTML = content;
|
||||
|
||||
let error_code = await view.postInit();
|
||||
|
||||
if (error_code === 404)
|
||||
renderView(new PageNotFoundView());
|
||||
else if (error_code === 403)
|
||||
this._client._update_logged(false);
|
||||
}
|
||||
|
||||
const router = async(uri) => {
|
||||
|
||||
const routes = [
|
||||
{ path: "/", view: HomeView},
|
||||
{ path: "/profiles/:username", view: ProfilePageView },
|
||||
{ path: "/login", view: AuthenticationView },
|
||||
{ path: "/register", view: AuthenticationView },
|
||||
{ path: "/logout", view: LogoutView },
|
||||
{ path: "/search", view: Search },
|
||||
{ path: "/home", view: HomeView },
|
||||
{ path: "/settings", view: SettingsView },
|
||||
{ path: "/matchmaking", view: MatchMakingView },
|
||||
{ path: "/games/pong/offline", view: PongOfflineView },
|
||||
{ path: "/games/pong/:id", view: PongOnlineView },
|
||||
{ path: "/games/tictactoe/offline", view: TicTacToeOfflineView },
|
||||
{ path: "/games/tictactoe/:id", view: TicTacToeOnlineView },
|
||||
];
|
||||
|
||||
// Test each route for potential match
|
||||
const potentialMatches = routes.map(route => {
|
||||
return {
|
||||
route: route,
|
||||
result: uri.match(pathToRegex(route.path))
|
||||
};
|
||||
});
|
||||
|
||||
let match = potentialMatches.find(potentialMatch => potentialMatch.result !== null);
|
||||
|
||||
if (!match) {
|
||||
match = {
|
||||
route: {
|
||||
path: uri,
|
||||
view: PageNotFoundView
|
||||
},
|
||||
result: [uri]
|
||||
};
|
||||
}
|
||||
|
||||
if (lastView !== undefined)
|
||||
await lastView.leavePage();
|
||||
|
||||
const view = new match.route.view(getParams(match), lastPageUrlBeforeLogin);
|
||||
|
||||
if (!(view instanceof AuthenticationView) && ! (view instanceof LogoutView))
|
||||
lastPageUrlBeforeLogin = uri;
|
||||
|
||||
if (view instanceof AbstractRedirectView && await view.redirect())
|
||||
return 1;
|
||||
|
||||
lastView = view;
|
||||
|
||||
if (await renderView(view))
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", function() {router(location.pathname);});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
document.body.addEventListener("click", e => {
|
||||
if (e.target.matches("[data-link]")) {
|
||||
e.preventDefault();
|
||||
document.querySelector('[data-link].nav-link.active')?.classList.remove('active');
|
||||
if (e.target.classList.contains('nav-link'))
|
||||
e.target.classList.add('active');
|
||||
navigateTo(e.target.href.slice(location.origin.length));
|
||||
}
|
||||
});
|
||||
|
||||
//Languages
|
||||
await lang.waitLoading();
|
||||
Array.from(document.getElementById('languageSelector').children).forEach(el => {
|
||||
el.onclick = async _ => {
|
||||
if (await lang.changeLanguage(el.value))
|
||||
return;
|
||||
document.querySelector('#languageSelector > .active')?.classList.remove('active');
|
||||
el.classList.add('active');
|
||||
};
|
||||
});
|
||||
document.querySelector(`#languageSelector > [value=${lang.chosenLang}]`)
|
||||
?.classList.add('active');
|
||||
|
||||
await client.isAuthenticated();
|
||||
router(location.pathname);
|
||||
document.querySelector('a[href=\'' + location.pathname + '\']')?.classList.add('active');
|
||||
});
|
||||
|
||||
export { client, lang, lastView, navigateTo, reloadView };
|
85
django/frontend/static/js/lang/cr.json
Normal file
85
django/frontend/static/js/lang/cr.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"navbarSearch": "cherchbeh",
|
||||
"navbarHome": "Quoicoubouse",
|
||||
"navbarLogin": "Quoicouconnec",
|
||||
"navbarRegister": "Quoicougistré",
|
||||
"navbarProfile": "Mon crampté Profile",
|
||||
"navbarSettings": "Roue Crampté",
|
||||
"navbarLogout": "Déconnexion crampté",
|
||||
"homeWindowTitle": "Quoicoubouse",
|
||||
"homeTitle": "Quoicoubouse",
|
||||
"homeOnline": "Jouer en crampté",
|
||||
"homeOffline": "Jouer hors crampté",
|
||||
"homeSettings": "Roue Crampté",
|
||||
"homeLogout": "Déconnexion crampté",
|
||||
"loginWindowTitle": "Quoicouconnec",
|
||||
"loginFormTitle": "Quoicouconnec",
|
||||
"loginFormUsername": "Nom d'crampté",
|
||||
"loginFormPassword": "Mot de crampté",
|
||||
"loginFormButton": "Quoicouconnec",
|
||||
"loginNoAccount": "Pas de compte encore crampté?",
|
||||
"loginRegister": "Quoicougistré",
|
||||
"errorEmptyField": "Ce champ ne peut pas être vide crampté.",
|
||||
"logoutWindowTitle": "Déconnexion crampté",
|
||||
"registerWindowTitle": "Quoicougistré",
|
||||
"registerFormTitle": "Quoicougistré",
|
||||
"registerFormUsername": "Nom d'crampté",
|
||||
"registerFormPassword": "Mot de crampté",
|
||||
"registerFormButton": "Quoicougistré",
|
||||
"registerAlreadyAccount": "Déjà un compte crampté?",
|
||||
"registerLogin": "Quoicouconnec",
|
||||
"404WindowTitle": "Pas crampté",
|
||||
"SearchWindowTitle": "cherchbeh",
|
||||
"profileAddFriend": "Demander le cramptéman",
|
||||
"profileRemoveFriend": "Supprimer le cramptéman",
|
||||
"profileDenyRequest": "Refuser le cramptéman",
|
||||
"profileAcceptRequest": "Accepter le cramptéman",
|
||||
"profileUnblock": "Quoicoudebloquer",
|
||||
"profileBlock": "Quoicoubloquer",
|
||||
"gameGoalTaken": "Tu es quoicoucringe",
|
||||
"gamePlayersListName": "Crampteurs",
|
||||
"ticTacToe": "Quoicoumorpion hors crampté",
|
||||
"gamemodeChoice" : "Mode de crampté",
|
||||
"ticTacToeOnline" : "Quoicoumorpion crampté",
|
||||
"ticTacToeTitle": "Cramption",
|
||||
"ruleTitle" : "Règles cramptés",
|
||||
"ruleBase" : "cramptun. Vous devez quouicougagner sur une des 9 quoicougrilles pour gagner la croustipartie",
|
||||
"ruleMovement" : "quoicoudeux. Vous quoicommencez sur le morpion quoicoucentral, et jouez sur le quoicoumorpion correspondant a votre croustichoix a votre prochain cramptour",
|
||||
"ruleDraw" : "cramptrois. Si votre quoicouchoix rempli entièrement un quoicoumorpion et provoque une cramptégalité, vous perdez",
|
||||
"morpionWin" : "Le quoicougagnant est : ",
|
||||
"matchmakingTitle": "Matchmaking crampté",
|
||||
"matchmakingStartSearch": "Cramptrouver une partie",
|
||||
"matchmakingStopSearch": "Crampter le matchmaking",
|
||||
"matchmakingNbPlayers": "Nombre de crampteurs",
|
||||
"TournamentCreateTitle": "Créer un cramptournoi",
|
||||
"TournamentCreateButton": "Créer le cramptournoi",
|
||||
"TournamentCreateTournamentName": "Nom du cramptournoi",
|
||||
"TournamentCreateNbPlayerByGame": "Nombre de crampteurs en crampté",
|
||||
"TournamentCreateNbPlayer": "Nombre de crampteurs dans le cramptournoi",
|
||||
|
||||
"settingsWindowTitle": "Roue crampté",
|
||||
"settingsAvatarTitle": "Avatar",
|
||||
"settingsAvatarSave": "Cramptregistrer",
|
||||
"settingsAvatarDelete": "Décrampté",
|
||||
"settingsAvatarSaved": "Avatar crampté.",
|
||||
"settingsAvatarDeleted": "Avatar décrampté.",
|
||||
"settingsAvatarDeleteError": "Une cramptérreur s'est produite.",
|
||||
"settingsAvatarTooLarge": "L'image est trop crampté",
|
||||
"settingsTitle": "Paramètres de crampté",
|
||||
"settingsUsername": "Nom d'crampté",
|
||||
"settingsUsernameSave": "Cramptregistrer",
|
||||
"settingsUsernameSaved": "Nom d'crampté cramptregistré.",
|
||||
"settingsPasswordTitle": "Modifier le crampté de passe",
|
||||
"settingsCurrentPassword": "Mot de crampté actuel",
|
||||
"settingsNewPassword": "Nouveau crampté de passe",
|
||||
"settingsRepeatNewPassword": "Répéter nouveau crampte de passe",
|
||||
"settingsPasswordSave": "Cramptregistrer",
|
||||
"settingsPasswordSaved": "Nouveau crampté de passe enregistré.",
|
||||
"settingsDeleteButton": "Supprimer le crousticompte",
|
||||
"settingsDeleteTitle": "Êtes vous cramptsûr ? Cette action est permanente !!",
|
||||
"settingsDeleteConfirm": "Crampté de passe nécessaire pour confirmer la suppression:",
|
||||
"settingsDeleteInput": "Cranpté de passe",
|
||||
"settingsDeleteCancel": "Annuler",
|
||||
"settingsDeleteSuccess": "Crousticompte décrampté avec succès."
|
||||
}
|
||||
|
83
django/frontend/static/js/lang/en.json
Normal file
83
django/frontend/static/js/lang/en.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"navbarSearch": "Search",
|
||||
"navbarHome": "Home",
|
||||
"navbarLogin": "Login",
|
||||
"navbarRegister": "Register",
|
||||
"navbarProfile": "My Profile",
|
||||
"navbarSettings": "Settings",
|
||||
"navbarLogout": "Logout",
|
||||
"homeWindowTitle": "Home",
|
||||
"homeTitle": "Home",
|
||||
"homeOnline": "Play online",
|
||||
"homeOffline": "Play offline",
|
||||
"homeSettings": "Settings",
|
||||
"homeLogout": "Logout",
|
||||
"loginWindowTitle": "Login",
|
||||
"loginFormTitle": "Login",
|
||||
"loginFormUsername": "Username",
|
||||
"loginFormPassword": "Password",
|
||||
"loginFormButton": "Login",
|
||||
"loginNoAccount": "No account yet?",
|
||||
"loginRegister": "Register",
|
||||
"errorEmptyField": "This field may not be blank.",
|
||||
"logoutWindowTitle": "Logout",
|
||||
"registerWindowTitle": "Register",
|
||||
"registerFormTitle": "Register",
|
||||
"registerFormUsername": "Username",
|
||||
"registerFormPassword": "Password",
|
||||
"registerFormButton": "Register",
|
||||
"registerAlreadyAccount": "Already have an account?",
|
||||
"registerLogin": "Login",
|
||||
"404WindowTitle": "Not Found",
|
||||
"SearchWindowTitle": "Search",
|
||||
"profileAddFriend": "Ask Friend",
|
||||
"profileRemoveFriend": "Remove Friend",
|
||||
"profileDenyRequest": "Decline Friend",
|
||||
"profileAcceptRequest": "Accept Friend",
|
||||
"profileUnblock": "Unblock",
|
||||
"profileBlock": "Block",
|
||||
"gameGoalTaken": "Goal Taken",
|
||||
"gamePlayersListName": "Players",
|
||||
"ticTacToe": "TicTacToe offline",
|
||||
"ticTacToeTitle": "TicTacToe",
|
||||
"gamemodeChoice" : "Gamemode",
|
||||
"ruleTitle" : "Rules",
|
||||
"ruleBase" : "1. Win on one of the 9 tictactoe to win the game",
|
||||
"ruleMovement" : "2. You start on the central tictactoe, and play on the one corresponding to your choice on the next turn",
|
||||
"ruleDraw" : "3. If your play cause a tictactoe to be full and a draw, you lose the game",
|
||||
"morpionWin" : "The winner is : ",
|
||||
"matchmakingTitle": "Matchmaking",
|
||||
"matchmakingStartSearch": "Find a game",
|
||||
"matchmakingStopSearch": "Stop matchmaking",
|
||||
"matchmakingNbPlayers": "Number of players",
|
||||
"TournamentCreateTitle": "Create tournament",
|
||||
"TournamentCreateButton": "Create tournament",
|
||||
"TournamentCreateTournamentName": "Tournament Name",
|
||||
"TournamentCreateNbPlayerByGame": "Number of player in a game",
|
||||
"TournamentCreateNbPlayer": "Number of players in the tournament",
|
||||
|
||||
"settingsWindowTitle": "Settings",
|
||||
"settingsAvatarTitle": "Avatar",
|
||||
"settingsAvatarSave": "Save",
|
||||
"settingsAvatarDelete": "Delete",
|
||||
"settingsAvatarSaved": "Avatar saved.",
|
||||
"settingsAvatarDeleted": "Avatar deleted.",
|
||||
"settingsAvatarDeleteError": "Something went wrong.",
|
||||
"settingsAvatarTooLarge": "Image is too large.",
|
||||
"settingsTitle": "Account settings",
|
||||
"settingsUsername": "Username",
|
||||
"settingsUsernameSave": "Save",
|
||||
"settingsUsernameSaved": "Username saved.",
|
||||
"settingsPasswordTitle": "Change password",
|
||||
"settingsCurrentPassword": "Current password",
|
||||
"settingsNewPassword": "New password",
|
||||
"settingsRepeatNewPassword": "Repeat new password",
|
||||
"settingsPasswordSave": "Save",
|
||||
"settingsPasswordSaved": "New password has been saved.",
|
||||
"settingsDeleteButton": "Delete account",
|
||||
"settingsDeleteTitle": "Are you sure ? This action is permanent !!",
|
||||
"settingsDeleteConfirm": "Please enter your password to confirm deletion:",
|
||||
"settingsDeleteInput": "Password",
|
||||
"settingsDeleteCancel": "Cancel",
|
||||
"settingsDeleteSuccess": "Account successfully deleted."
|
||||
}
|
83
django/frontend/static/js/lang/fr.json
Normal file
83
django/frontend/static/js/lang/fr.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"navbarSearch": "Recherche",
|
||||
"navbarHome": "Maison",
|
||||
"navbarLogin": "Connexion",
|
||||
"navbarRegister": "S'inscrire",
|
||||
"navbarProfile": "Mon Profil",
|
||||
"navbarSettings": "Paramètres",
|
||||
"navbarLogout": "Déconnexion",
|
||||
"homeWindowTitle": "Maison",
|
||||
"homeTitle": "Maison",
|
||||
"homeOnline": "Jouer en ligne",
|
||||
"homeOffline": "Jouer hors ligne",
|
||||
"homeSettings": "Paramètres",
|
||||
"homeLogout": "Déconnexion",
|
||||
"loginWindowTitle": "Connexion",
|
||||
"loginFormTitle": "Connexion",
|
||||
"loginFormUsername": "Nom d'utilisateur",
|
||||
"loginFormPassword": "Mot de passe",
|
||||
"loginFormButton": "Connexion",
|
||||
"loginNoAccount": "Pas de compte?",
|
||||
"loginRegister": "S'inscrire",
|
||||
"errorEmptyField": "Ce champ ne peut être vide.",
|
||||
"logoutWindowTitle": "Déconnexion",
|
||||
"registerWindowTitle": "S'inscrire",
|
||||
"registerFormTitle": "S'inscrire",
|
||||
"registerFormUsername": "Nom d'utilisateur",
|
||||
"registerFormPassword": "Mot de passe",
|
||||
"registerFormButton": "S'inscrire",
|
||||
"registerAlreadyAccount": "Déjà un compte?",
|
||||
"registerLogin": "Connexion",
|
||||
"404WindowTitle": "Pas trouvé",
|
||||
"SearchWindowTitle": "Recherche",
|
||||
"profileAddFriend": "Demander en ami",
|
||||
"profileRemoveFriend": "Retirer l'ami",
|
||||
"profileDenyRequest": "Refuser l'ami",
|
||||
"profileAcceptRequest": "Accepter l'ami",
|
||||
"profileUnblock": "Débloquer",
|
||||
"profileBlock": "Bloquer",
|
||||
"gameGoalTaken": "But pris",
|
||||
"gamePlayersListName": "Joueurs",
|
||||
"ticTacToe" : "Morpion hors ligne",
|
||||
"ticTacToeTitle": "Morpion",
|
||||
"gamemodeChoice" : "Mode de jeu",
|
||||
"ruleTitle" : "Règles",
|
||||
"ruleBase" : "1. Vous devez gagner sur une des 9 grilles pour gagner la partie",
|
||||
"ruleMovement" : "2. Vous commencez sur le morpion central, et jouez sur le morpion correspondant a votre choix a votre prochain tour",
|
||||
"ruleDraw" : "3. Si votre choix rempli entièrement un morpion et provoque une égalité, vous perdez",
|
||||
"morpionWin" : "Le gagnant est : ",
|
||||
"matchmakingTitle": "Matchmaking",
|
||||
"matchmakingStartSearch": "Trouver une partie",
|
||||
"matchmakingStopSearch": "Arrêter le matchmaking",
|
||||
"matchmakingNbPlayers": "Nombre de joueurs",
|
||||
"TournamentCreateTitle": "Créer un tournoi",
|
||||
"TournamentCreateButton": "Créer le tournoi",
|
||||
"TournamentCreateTournamentName": "Nom du tournoi",
|
||||
"TournamentCreateNbPlayerByGame": "Nombre de joueurs en jeu",
|
||||
"TournamentCreateNbPlayer": "Nombre de joueurs dans le tournoi",
|
||||
|
||||
"settingsWindowTitle": "Paramètres",
|
||||
"settingsAvatarTitle": "Avatar",
|
||||
"settingsAvatarSave": "Enregister",
|
||||
"settingsAvatarDelete": "Supprimer",
|
||||
"settingsAvatarSaved": "Avatar enregistré.",
|
||||
"settingsAvatarDeleted": "Avatar supprimé.",
|
||||
"settingsAvatarDeleteError": "Une erreur s'est produite.",
|
||||
"settingsAvatarTooLarge": "L'image est trop lourde",
|
||||
"settingsTitle": "Paramètres de compte",
|
||||
"settingsUsername": "Nom d'utilisateur",
|
||||
"settingsUsernameSave": "Enregister",
|
||||
"settingsUsernameSaved": "Now d'utilisateur enregistré.",
|
||||
"settingsPasswordTitle": "Modifier le mot de passe",
|
||||
"settingsCurrentPassword": "Mot de passe actuel",
|
||||
"settingsNewPassword": "Nouveau mot de passe",
|
||||
"settingsRepeatNewPassword": "Répéter nouveau mot de passe",
|
||||
"settingsPasswordSave": "Enregister",
|
||||
"settingsPasswordSaved": "Nouveau mot de passe enregistré.",
|
||||
"settingsDeleteButton": "Supprimer le compte",
|
||||
"settingsDeleteTitle": "Êtes vous sûr ? Cette action est permanente !!",
|
||||
"settingsDeleteConfirm": "Mot de passe nécessaire pour confirmer la suppression:",
|
||||
"settingsDeleteInput": "Mot de passe",
|
||||
"settingsDeleteCancel": "Annuler",
|
||||
"settingsDeleteSuccess": "Compte supprimé avec succès."
|
||||
}
|
83
django/frontend/static/js/lang/tp.json
Normal file
83
django/frontend/static/js/lang/tp.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"navbarSearch": "Lukin",
|
||||
"navbarHome": "Tomo",
|
||||
"navbarLogin": "Open",
|
||||
"navbarRegister": "Sitelen",
|
||||
"navbarProfile": "Sitelen mi",
|
||||
"navbarSettings": "Nasin",
|
||||
"navbarLogout": "Tawa ala",
|
||||
"homeWindowTitle": "Tomo",
|
||||
"homeTitle": "Tomo",
|
||||
"homeOnline": "Mute tawa",
|
||||
"homeOffline": "Mute lon",
|
||||
"homeSettings": "Nasin",
|
||||
"homeLogout": "Tawa ala",
|
||||
"loginWindowTitle": "Open",
|
||||
"loginFormTitle": "Open",
|
||||
"loginFormUsername": "nimi pi jan Open",
|
||||
"loginFormPassword": "nimi nasa",
|
||||
"loginFormButton": "Open",
|
||||
"loginNoAccount": "sina wile ala wile jo e nimi pi jan Open?",
|
||||
"loginRegister": "Sitelen",
|
||||
"errorEmptyField": "nimi ni li wile sitelen.",
|
||||
"logoutWindowTitle": "Tawa ala",
|
||||
"registerWindowTitle": "Sitelen",
|
||||
"registerFormTitle": "Sitelen",
|
||||
"registerFormUsername": "nimi pi jan sin",
|
||||
"registerFormPassword": "nimi nasa",
|
||||
"registerFormButton": "Sitelen",
|
||||
"registerAlreadyAccount": "sina jo ala jo e nimi pi jan sin?",
|
||||
"registerLogin": "Open",
|
||||
"404WindowTitle": "Ala o lukin e ni",
|
||||
"SearchWindowTitle": "Lukin",
|
||||
"profileAddFriend": "kama jo e jan",
|
||||
"profileRemoveFriend": "tawa ala e jan",
|
||||
"profileDenyRequest": "ante e ijo ni",
|
||||
"profileAcceptRequest": "kama jo e ijo ni",
|
||||
"profileUnblock": "Tawa ala e nimi pi jan ni",
|
||||
"profileBlock": "Tawa e nimi pi jan ni",
|
||||
"gameGoalTaken": "Wile pali",
|
||||
"gamePlayersListName": "Musi",
|
||||
"ticTacToe": "TicTacToe offline",
|
||||
"ticTacToeTitle": "TicTacToe",
|
||||
"gamemodeChoice" : "sitelen sitelen",
|
||||
"ruleTitle" : "Rules",
|
||||
"ruleBase" : "1. Win on wan pi the 9 tictactoe tawa win the game",
|
||||
"ruleMovement" : "2. Sina open on the central tictactoe, en play on the wan corresponding tawa your choice on the next turn",
|
||||
"ruleDraw" : "3. If your play cause a tictactoe tawa be full en a draw, sina lose the game",
|
||||
"morpionWin" : "jan li jo e poka sina : ",
|
||||
"matchmakingTitle": "Matchmaking",
|
||||
"matchmakingStartSearch": "lukin e ilo musi",
|
||||
"matchmakingStopSearch": "o pini e pana sona e jan pi pana sona e jan ante.",
|
||||
"matchmakingNbPlayers": "nanpa pi jan ante",
|
||||
"TournamentCreateTitle": "o pana e musi ante e musi",
|
||||
"TournamentCreateButton": "jo ala pona li jo e ijo li pali e ijo li pana e ijo li toki e ijo li kama jo e ijo li kama pali e ijo li kama pana e ijo li kama toki e ijo li kama jo e ijo li kama pali e ijo li kama pana e ijo",
|
||||
"TournamentCreateTournamentName": "ilo kipisi",
|
||||
"TournamentCreateNbPlayerByGame": "ilo jan lon poki pi lon anpa en sike pimeja li kama.",
|
||||
"TournamentCreateNbPlayer": "nanpa pi jan pona lon soweli musi",
|
||||
|
||||
"settingsWindowTitle": "Ilo open",
|
||||
"settingsAvatarTitle": "Jan ante",
|
||||
"settingsAvatarSave": "O lukin",
|
||||
"settingsAvatarDelete": "O pana e tawa ala",
|
||||
"settingsAvatarSaved": "Jan ante li kama.",
|
||||
"settingsAvatarDeleted": "Jan ante li pakala e ijo.",
|
||||
"settingsAvatarDeleteError": "Ijo li pakala.",
|
||||
"settingsAvatarTooLarge": "Ijo li suli mute.",
|
||||
"settingsTitle": "Ilo pi ijo sina",
|
||||
"settingsUsername": "Nimi jan",
|
||||
"settingsUsernameSave": "O lukin",
|
||||
"settingsUsernameSaved": "Nimi jan li kama.",
|
||||
"settingsPasswordTitle": "O ante e nimi pi toki anu nimi",
|
||||
"settingsCurrentPassword": "Nimi pi tenpo ni",
|
||||
"settingsNewPassword": "Nimi sin",
|
||||
"settingsRepeatNewPassword": "Nimi sin",
|
||||
"settingsPasswordSave": "O lukin",
|
||||
"settingsPasswordSaved": "Nimi sin li kama.",
|
||||
"settingsDeleteButton": "O pakala e ilo mi",
|
||||
"settingsDeleteTitle": "Sina wile ala wile ? Ni li ike mute !!",
|
||||
"settingsDeleteConfirm": "O toki e nimi sina lon insa.",
|
||||
"settingsDeleteInput": "Nimi",
|
||||
"settingsDeleteCancel": "O pana tawa ala",
|
||||
"settingsDeleteSuccess": "Ijo sina li pakala mute."
|
||||
}
|
BIN
django/frontend/static/js/sound/tictactoe/incorrectbuzzer.mp3
Normal file
BIN
django/frontend/static/js/sound/tictactoe/incorrectbuzzer.mp3
Normal file
Binary file not shown.
BIN
django/frontend/static/js/sound/tictactoe/play-move.mp3
Normal file
BIN
django/frontend/static/js/sound/tictactoe/play-move.mp3
Normal file
Binary file not shown.
20
django/frontend/static/js/utils/formUtils.js
Normal file
20
django/frontend/static/js/utils/formUtils.js
Normal file
@ -0,0 +1,20 @@
|
||||
export function clearIds(property_name, elements_id)
|
||||
{
|
||||
elements_id.forEach(element_id => {
|
||||
let element = document.getElementById(element_id);
|
||||
element[property_name] = "";
|
||||
});
|
||||
}
|
||||
|
||||
export function clearElements(prop, elements) {
|
||||
elements.forEach(element => element[prop] = '');
|
||||
}
|
||||
|
||||
export function fill_errors(errors, property_name)
|
||||
{
|
||||
Object.keys(errors).forEach(error_field =>
|
||||
{
|
||||
let element = document.getElementById(error_field);
|
||||
element[property_name] = errors[error_field];
|
||||
});
|
||||
}
|
61
django/frontend/static/js/utils/graph.js
Normal file
61
django/frontend/static/js/utils/graph.js
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
export function generateRandomColor()
|
||||
{
|
||||
return `#${Math.floor(Math.random()*16777215).toString(16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {[Number]} data
|
||||
*/
|
||||
export function transformData(data)
|
||||
{
|
||||
let newData = [];
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
newData.push({x: Math.round(data[index] / 1000),
|
||||
y: index + 1});
|
||||
}
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function range(start, stop, step = 1)
|
||||
{
|
||||
if (stop === undefined)
|
||||
{
|
||||
stop = start;
|
||||
start = 0;
|
||||
}
|
||||
let newArr = [];
|
||||
for (let i = start; i <= stop; i += step)
|
||||
newArr.push(i);
|
||||
|
||||
return newArr;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {[Object]} dataset
|
||||
*/
|
||||
export function get_labels(dataset)
|
||||
{
|
||||
let labelsSet = new Set();
|
||||
|
||||
dataset.forEach(player_data => {
|
||||
player_data.data.forEach(data => {
|
||||
labelsSet.add(data.x);
|
||||
});
|
||||
});
|
||||
|
||||
let labels = Array.from(labelsSet);
|
||||
|
||||
labels.sort(function(a, b){return b - a;});
|
||||
|
||||
labels.reverse();
|
||||
|
||||
return labels;
|
||||
}
|
18
django/frontend/static/js/utils/noticeUtils.js
Normal file
18
django/frontend/static/js/utils/noticeUtils.js
Normal file
@ -0,0 +1,18 @@
|
||||
export function createNotification(title = 'New notification', content, delay = 3000) {
|
||||
|
||||
const toastElement = document.createElement('div');
|
||||
toastElement.classList.add('toast');
|
||||
toastElement.role = 'alert';
|
||||
toastElement.setAttribute('data-bs-delay', delay);
|
||||
toastElement.innerHTML =
|
||||
`<div class='toast-header'>
|
||||
<strong class='me-auto'>${title}</strong>
|
||||
<button type='button' class='btn-close' data-bs-dismiss='toast'></button>
|
||||
</div>
|
||||
<div class='toast-body'>${content}</div>`
|
||||
toastElement.addEventListener('hidden.bs.toast', e => e.target.remove());
|
||||
new bootstrap.Toast(toastElement).show();
|
||||
|
||||
const toastContainer = document.getElementById('toastContainer');
|
||||
toastContainer.insertBefore(toastElement, toastContainer.firstChild);
|
||||
}
|
6
django/frontend/static/js/utils/sleep.js
Normal file
6
django/frontend/static/js/utils/sleep.js
Normal file
@ -0,0 +1,6 @@
|
||||
function sleep(ms)
|
||||
{
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export {sleep}
|
20
django/frontend/static/js/views/HomeView.js
Normal file
20
django/frontend/static/js/views/HomeView.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { lang } from "../index.js";
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractAuthenticatedView {
|
||||
constructor(params) {
|
||||
super(params, 'homeWindowTitle');
|
||||
this.redirect_url = "/login";
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return /* HTML */ `
|
||||
<h1>${lang.get('homeTitle', 'Home')}</h1>
|
||||
<a href="/matchmaking" data-link>${lang.get('homeOnline', 'Play online')}</a>
|
||||
<a href="/games/pong/offline" data-link>${lang.get('homeOffline', 'Play offline')}</a>
|
||||
<a href="/games/tictactoe/offline" data-link>${lang.get('ticTacToe')}</a>
|
||||
<a href="/settings" data-link>${lang.get('homeSettings', 'Settings')}</a>
|
||||
<a href="/logout" data-link>${lang.get('homeLogout', 'Logout')}</a>
|
||||
`;
|
||||
}
|
||||
}
|
143
django/frontend/static/js/views/MatchMakingView.js
Normal file
143
django/frontend/static/js/views/MatchMakingView.js
Normal file
@ -0,0 +1,143 @@
|
||||
import { client, lang, navigateTo } from "../index.js";
|
||||
import { clearIds, fill_errors } from "../utils/formUtils.js";
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractAuthenticatedView {
|
||||
|
||||
constructor(params)
|
||||
{
|
||||
super(params, "Matchmaking");
|
||||
}
|
||||
|
||||
async toggle_search()
|
||||
{
|
||||
clearIds("innerText", ["detail"]);
|
||||
if (client.matchmaking.searching)
|
||||
{
|
||||
client.matchmaking.stop();
|
||||
this.button.innerHTML = lang.get("matchmakingStartSearch");
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), this.game_type_input.value, this.nb_players_input.value);
|
||||
|
||||
this.button.innerHTML = lang.get("matchmakingStopSearch");
|
||||
}
|
||||
}
|
||||
|
||||
ondisconnect(event)
|
||||
{
|
||||
this.button.innerHTML = lang.get("matchmakingStartSearch");
|
||||
}
|
||||
|
||||
onreceive(data)
|
||||
{
|
||||
if (data.detail === "game_found")
|
||||
{
|
||||
navigateTo(`/games/${data.game_type}/${data.game_id}`);
|
||||
return;
|
||||
}
|
||||
this.display_data(data);
|
||||
}
|
||||
|
||||
display_data(data)
|
||||
{
|
||||
clearIds("innerText", ["detail"]);
|
||||
fill_errors(data, "innerText");
|
||||
}
|
||||
|
||||
addEnterEvent()
|
||||
{
|
||||
[this.nb_players_input, this.game_type_input].forEach((input) => {
|
||||
|
||||
input.addEventListener('keydown', async ev => {
|
||||
|
||||
if (ev.key !== 'Enter')
|
||||
return;
|
||||
|
||||
await this.toggle_search.bind(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addChangeNbPlayersEvent()
|
||||
{
|
||||
let update = () => {
|
||||
this.button.disabled = (this.nb_players_input.value < 2 || this.nb_players_input.value > 4);
|
||||
};
|
||||
|
||||
["change", "oninput"].forEach((event_name) => {
|
||||
this.nb_players_input.addEventListener(event_name, update);
|
||||
});
|
||||
}
|
||||
|
||||
addChangegame_typeEvent()
|
||||
{
|
||||
let nb_players_div = document.getElementById("nb-players-div");
|
||||
|
||||
this.game_type_input.addEventListener("change", () => {
|
||||
|
||||
if (this.game_type_input.value === "tictactoe")
|
||||
{
|
||||
nb_players_div.style.display = 'none';
|
||||
this.nb_players_input.value = 2;
|
||||
}
|
||||
else
|
||||
nb_players_div.style.display = 'block';
|
||||
|
||||
client.matchmaking.stop();
|
||||
clearIds("innerText", ["detail"]);
|
||||
});
|
||||
}
|
||||
|
||||
addEvents()
|
||||
{
|
||||
this.addEnterEvent();
|
||||
this.addChangegame_typeEvent();
|
||||
this.addChangeNbPlayersEvent();
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
this.button = document.getElementById("toggle-search");
|
||||
this.nb_players_input = document.getElementById("nb-players-input");
|
||||
this.game_type_input = document.getElementById("game-type-input");
|
||||
|
||||
this.button.onclick = this.toggle_search.bind(this);
|
||||
|
||||
this.addEvents()
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return /* HTML */ `
|
||||
<div class='container-fluid'>
|
||||
<div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
|
||||
<h4 class='text-center fw-semibold mb-4' id="title">${lang.get("matchmakingTitle")}</h4>
|
||||
<div>
|
||||
<div class='form-floating mb-2' id='game_type-div'>
|
||||
<select class='form-control' id='game-type-input'>
|
||||
<option value='pong'>Pong</option>
|
||||
<option value='tictactoe'>ticTacToe</option>
|
||||
</select>
|
||||
<label for='game-type-input'>${lang.get("gamemodeChoice")}</label>
|
||||
</div>
|
||||
<div class='form-floating mb-2' id='nb-players-div'>
|
||||
<input type='number' min='2' value='2' max='4' class='form-control' id='nb-players-input' placeholder='${lang.get("matchmakingNbPlayers")}'>
|
||||
<label for='nb-players-input' id='username-label'>${lang.get("matchmakingNbPlayers")}</label>
|
||||
<span class='text-danger' id='username'></span>
|
||||
</div>
|
||||
<div class='d-flex'>
|
||||
<button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='toggle-search'>${lang.get("matchmakingStartSearch")}</button>
|
||||
<span class='text-danger my-auto mx-2' id='detail'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async leavePage()
|
||||
{
|
||||
await client.matchmaking.stop();
|
||||
}
|
||||
}
|
16
django/frontend/static/js/views/PageNotFoundView.js
Normal file
16
django/frontend/static/js/views/PageNotFoundView.js
Normal file
@ -0,0 +1,16 @@
|
||||
import AbstractView from "./abstracts/AbstractView.js";
|
||||
import { lang } from '../index.js';
|
||||
|
||||
export default class extends AbstractView {
|
||||
constructor(params) {
|
||||
super(params, '404WindowTitle');
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return `
|
||||
<h1>404 Bozo</h1>
|
||||
<img src="https://media.giphy.com/media/pm0BKtuBFpdM4/giphy.gif">
|
||||
<p>Git gud</p>
|
||||
`;
|
||||
}
|
||||
}
|
656
django/frontend/static/js/views/PongOfflineView.js
Normal file
656
django/frontend/static/js/views/PongOfflineView.js
Normal file
@ -0,0 +1,656 @@
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
import "../3D/maths/gl-matrix-min.js"
|
||||
import { initShaderProgram, shaderInfos } from "../3D/shaders.js"
|
||||
import { initBuffers } from "../3D/buffers.js"
|
||||
import { renderCube } from "../3D/cube.js"
|
||||
|
||||
export class PongOfflineView extends AbstractAuthenticatedView {
|
||||
constructor(params) {
|
||||
super(params, 'Game');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init(round) {
|
||||
this.game = null;
|
||||
this.player1 = "Player 1";
|
||||
this.player2 = "Player 2";
|
||||
this.player3 = "Player 3";
|
||||
this.player4 = "Player 4";
|
||||
|
||||
this.winner1 = undefined;
|
||||
this.winner2 = undefined;
|
||||
|
||||
this.final_winner = undefined;
|
||||
this.round = -1;
|
||||
|
||||
this.game_mode = 1; // 1 is 2D, 2 is 3D
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return `
|
||||
<link rel="stylesheet" href="/static/css/gameOffline.css">
|
||||
<h1>Game</h1>
|
||||
<button id='startGameButton'>Start Game</button>
|
||||
<button id='resetGameButton'>Reset Game</button>
|
||||
<button id='stopGameButton'>Stop Game</button>
|
||||
<button id='startTournament'>Start Tournament</button>
|
||||
<button id='gameMode'>Switch to 3D</button>
|
||||
<div id="display"></div>
|
||||
<div id="display-button"></div>
|
||||
<svg id="tree" height="3000" width="3000"></svg>
|
||||
`;
|
||||
}
|
||||
|
||||
async postInit() {
|
||||
this.app = document.getElementById("display");
|
||||
document.getElementById('startGameButton').onclick = this.startGame.bind(this);
|
||||
document.getElementById('resetGameButton').onclick = this.resetGame.bind(this);
|
||||
document.getElementById('resetGameButton').hidden = 1;
|
||||
document.getElementById('stopGameButton').onclick = this.stopGame.bind(this);
|
||||
document.getElementById('stopGameButton').hidden = 1;
|
||||
document.getElementById('startTournament').onclick = this.startTournament.bind(this);
|
||||
document.getElementById('gameMode').hidden = 1;
|
||||
document.getElementById('gameMode').onclick = this.toggleGameMode.bind(this);
|
||||
}
|
||||
|
||||
async leavePage() {
|
||||
this.game?.cleanup();
|
||||
}
|
||||
|
||||
async toggleGameMode()
|
||||
{
|
||||
if(this.game_mode === 1) // 3D
|
||||
{
|
||||
this.game_mode = 2;
|
||||
document.getElementById("gameMode").innerText = "Switch to 2D";
|
||||
}
|
||||
else if(this.game_mode === 2) // 2D
|
||||
{
|
||||
this.game_mode = 1;
|
||||
document.getElementById("gameMode").innerText = "Switch to 3D";
|
||||
}
|
||||
this.game.changeGameMode(this.game_mode);
|
||||
}
|
||||
|
||||
startTournament() {
|
||||
let startTournament = document.getElementById("startTournament");
|
||||
let player1 = document.createElement("input");
|
||||
player1.id="player1"; player1.type="text"; player1.name="message"; player1.placeholder="Player 1"; player1.maxLength=10; player1.value = "";
|
||||
startTournament.before(player1);
|
||||
|
||||
let player2 = document.createElement("input");
|
||||
player2.id="player2"; player2.type="text"; player2.name="message"; player2.placeholder="Player 2"; player2.maxLength=10; player2.value = "";
|
||||
player1.after(player2);
|
||||
|
||||
let player3 = document.createElement("input");
|
||||
player3.id="player3"; player3.type="text"; player3.name="message"; player3.placeholder="Player 3"; player3.maxLength=10; player3.value = "";
|
||||
player2.after(player3);
|
||||
|
||||
let player4 = document.createElement("input");
|
||||
player4.id="player4"; player4.type="text"; player4.name="message"; player4.placeholder="Player 4"; player4.maxLength=10; player4.value = "";
|
||||
player3.after(player4);
|
||||
|
||||
startTournament.onclick = () => {
|
||||
if (player1.value.length > 0)
|
||||
this.player1 = player1.value;
|
||||
if (player2.value.length > 0)
|
||||
this.player2 = player2.value;
|
||||
if (player3.value.length > 0)
|
||||
this.player3 = player3.value;
|
||||
if (player4.value.length > 0)
|
||||
this.player4 = player4.value;
|
||||
|
||||
player1.remove();
|
||||
player2.remove();
|
||||
player3.remove();
|
||||
player4.remove();
|
||||
|
||||
this.round = 0;
|
||||
|
||||
this.startGame(this.player1, this.player2);
|
||||
startTournament.onclick = this.startTournament.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
createButton()
|
||||
{
|
||||
this.up1 = document.createElement("button");
|
||||
this.up1.textContent = "↑";
|
||||
this.up1.id = "up1";
|
||||
|
||||
this.down1 = document.createElement("button");
|
||||
this.down1.textContent = "↓";
|
||||
this.down1.id = "down1";
|
||||
|
||||
this.up2 = document.createElement("button");
|
||||
this.up2.textContent = "↑";
|
||||
this.up2.id = "up2";
|
||||
|
||||
this.down2 = document.createElement("button");
|
||||
this.down2.textContent = "↓";
|
||||
this.down2.id = "down2";
|
||||
|
||||
this.up1.setAttribute("user_id", 1);
|
||||
this.up1.setAttribute("direction", "up");
|
||||
this.up2.setAttribute("user_id", 2);
|
||||
this.up2.setAttribute("direction", "up");
|
||||
|
||||
this.down1.setAttribute("user_id", 1);
|
||||
this.down1.setAttribute("direction", "down");
|
||||
this.down2.setAttribute("user_id", 2);
|
||||
this.down2.setAttribute("direction", "down");
|
||||
|
||||
document.addEventListener('touchstart', this.keyDownHandler);
|
||||
document.addEventListener('touchend', this.keyUpHandler);
|
||||
[this.up1, this.up2, this.down1, this.down2].forEach(button => {
|
||||
button.onmousedown = this.game.keyDownHandler.bind(this.game);
|
||||
button.onmouseup = this.game.keyUpHandler.bind(this.game);
|
||||
button.ontouchstart = this.game.keyDownHandler.bind(this.game);
|
||||
button.ontouchend = this.game.keyUpHandler.bind(this.game);
|
||||
});
|
||||
|
||||
document.getElementById("display-button").append(this.up1, this.down1);
|
||||
document.getElementById("display-button").append(this.up2, this.down2);
|
||||
}
|
||||
|
||||
resetGame(player1, player2) {
|
||||
if (this.round >= 0)
|
||||
this.round = 0;
|
||||
|
||||
this.winner1 = undefined;
|
||||
this.winner2 = undefined;
|
||||
this.final_winner = undefined;
|
||||
|
||||
this.startGame(player1, player2)
|
||||
}
|
||||
|
||||
async startGame(player1, player2) {
|
||||
if (player1 == null || player2 == null) {
|
||||
player1 = this.player1;
|
||||
player2 = this.player2;
|
||||
}
|
||||
if (this.game == null) {
|
||||
this.game = new Game(this.game_mode, this, player1, player2);
|
||||
this.createButton();
|
||||
}
|
||||
else {
|
||||
this.app.removeChild(this.game.canvas);
|
||||
this.app.removeChild(this.game.scoresDisplay);
|
||||
this.game.cleanup();
|
||||
this.game = new Game(this.game_mode, this, player1, player2);
|
||||
this.createButton();
|
||||
}
|
||||
|
||||
if (this.round >= 0)
|
||||
await this.display_tree_tournament();
|
||||
|
||||
document.getElementById('startTournament').hidden = 1;
|
||||
document.getElementById('startGameButton').hidden = 1;
|
||||
document.getElementById('stopGameButton').hidden = 0;
|
||||
document.getElementById('resetGameButton').hidden = 0;
|
||||
document.getElementById('gameMode').hidden = 0;
|
||||
}
|
||||
|
||||
stopGame() {
|
||||
if (!this.game)
|
||||
return;
|
||||
this.app.removeChild(this.game.canvas);
|
||||
this.app.removeChild(this.game.scoresDisplay);
|
||||
this.game.cleanup();
|
||||
this.init();
|
||||
document.getElementById('startGameButton').innerHTML = 'Start Game';
|
||||
document.getElementById('startTournament').hidden = 0;
|
||||
document.getElementById('startGameButton').hidden = 0;
|
||||
document.getElementById('stopGameButton').hidden = 1;
|
||||
document.getElementById('resetGameButton').hidden = 1;
|
||||
document.getElementById('gameMode').hidden = 1;
|
||||
this.app.style.maxWidth = null;
|
||||
|
||||
}
|
||||
|
||||
async display_tree_tournament() {
|
||||
let players = [this.player1, this.player2, this.player3, this.player4, this.winner1, this.winner2, this.final_winner];
|
||||
const svg = document.getElementById('tree');
|
||||
|
||||
svg.innerHTML = '';
|
||||
|
||||
let width = 150;
|
||||
let height = 40;
|
||||
let gap_y = height + 25;
|
||||
let gap_x = width + 25;
|
||||
let start_y = height / 2;
|
||||
|
||||
let rounds = Math.log2(4) + 1;
|
||||
|
||||
let k = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
let number_square = 4 / Math.pow(2, i)
|
||||
for(let j = 0; j < number_square; j++) {
|
||||
const y = start_y + gap_y * j * Math.pow(2, i) + (gap_y / 2 * Math.pow(2, i));
|
||||
svg.appendChild(await this.create_square(gap_x * i, y, width, height, "white", "black", players[k]));
|
||||
svg.appendChild(await this.create_text(gap_x * i, y, width, height, "white", "black", players[k]));
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create_square(x, y, width, height, fill, stroke, text) {
|
||||
const square = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
square.setAttribute("id", "square")
|
||||
square.setAttribute("x", x);
|
||||
square.setAttribute("y", y);
|
||||
square.setAttribute("width", width);
|
||||
square.setAttribute("height", height);
|
||||
square.setAttribute("fill", fill);
|
||||
square.setAttribute("stroke", stroke);
|
||||
|
||||
return square
|
||||
}
|
||||
|
||||
async create_text(x, y, width, height, fill, stroke, text) {
|
||||
const textElement = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
textElement.setAttribute("id", "textElement")
|
||||
textElement.setAttribute("x", x);
|
||||
textElement.setAttribute("y", y + height / 2 + 5);
|
||||
textElement.setAttribute("font-family", "Arial")
|
||||
textElement.setAttribute("font-size", "20")
|
||||
textElement.setAttribute("fill", stroke);
|
||||
textElement.setAttribute("stroke", fill);
|
||||
|
||||
if (text == undefined)
|
||||
text = "";
|
||||
textElement.appendChild(document.createTextNode(text));
|
||||
|
||||
return textElement;
|
||||
}
|
||||
}
|
||||
|
||||
class Game {
|
||||
constructor(game_mode, this_pong, player_name1, player_name2) {
|
||||
|
||||
this.pong = this_pong;
|
||||
|
||||
//Global variables
|
||||
this.def = {
|
||||
CANVASHEIGHT: 270,
|
||||
CANVASWIDTH: 480,
|
||||
PADDLEHEIGHT: 70,
|
||||
PADDLEWIDTH: 10,
|
||||
PADDLEMARGIN: 5,
|
||||
PADDLESPEED: 3,
|
||||
BALLRADIUS: 5,
|
||||
BALLSPEED: 2,
|
||||
BALLSPEEDINCR: 0.15,
|
||||
MAXBOUNCEANGLE: 10 * (Math.PI / 12),
|
||||
MAXSCORE: 2
|
||||
};
|
||||
|
||||
this.app = document.getElementById("display");
|
||||
this.app.style.maxWidth = Number(this.def.CANVASWIDTH) + "px";
|
||||
|
||||
this.canvas = null;
|
||||
this.context = null;
|
||||
|
||||
this.player_name1 = player_name1;
|
||||
this.player_name2 = player_name2;
|
||||
|
||||
this.scoresDisplay = document.createElement('p');
|
||||
this.scoresDisplay.innerHTML = `${this.player_name1}: 0 - ${this.player_name2}: 0`;
|
||||
this.app.appendChild(this.scoresDisplay);
|
||||
|
||||
this.players = [
|
||||
{
|
||||
paddle: new Paddle(this.def.PADDLEMARGIN, this.def),
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
paddle: new Paddle(this.def.CANVASWIDTH - this.def.PADDLEMARGIN - this.def.PADDLEWIDTH, this.def),
|
||||
score: 0
|
||||
}
|
||||
];
|
||||
this.ballStartSide = 0;
|
||||
this.ballRespawned = false;
|
||||
this.ball = new Ball(this.def, this.ballStartSide, this.context);
|
||||
|
||||
this.interval = setInterval(this.updateGame.bind(this), 10);
|
||||
|
||||
this.keys = [];
|
||||
this.keyUpHandler = this.keyUpHandler.bind(this);
|
||||
this.keyDownHandler = this.keyDownHandler.bind(this);
|
||||
document.addEventListener('keydown', this.keyDownHandler);
|
||||
document.addEventListener('keyup', this.keyUpHandler);
|
||||
|
||||
// stufs for 3D
|
||||
this.shader_prog = null;
|
||||
this.buffers = null;
|
||||
this.cam_pos = [0, 150, -10];
|
||||
this.cam_target = [0, 0, 0];
|
||||
this.cam_up = [0, 0, -1];
|
||||
|
||||
this.initGame(game_mode);
|
||||
}
|
||||
|
||||
initGame(mode)
|
||||
{
|
||||
if(mode === 1)
|
||||
this.init2D();
|
||||
else if(mode === 2)
|
||||
this.initWebGL();
|
||||
}
|
||||
|
||||
initWebGL()
|
||||
{
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.width = this.def.CANVASWIDTH;
|
||||
this.canvas.height = this.def.CANVASHEIGHT;
|
||||
this.canvas.id = "gameCanvas";
|
||||
this.canvas.style.border = '1px solid #d3d3d3';
|
||||
this.canvas.style.backgroundColor = '#f1f1f1';
|
||||
this.app.appendChild(this.canvas);
|
||||
|
||||
this.context = this.canvas.getContext("webgl");
|
||||
|
||||
if(this.context === null)
|
||||
{
|
||||
alert("Unable to initialize WebGL. Your browser or machine may not support it. You may also be a bozo");
|
||||
return;
|
||||
}
|
||||
|
||||
this.shader_prog = initShaderProgram(this.context);
|
||||
this.buffers = initBuffers(this.context);
|
||||
|
||||
this.context.enable(this.context.CULL_FACE);
|
||||
this.context.cullFace(this.context.BACK);
|
||||
}
|
||||
|
||||
init2D()
|
||||
{
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.id = 'gameCanvas';
|
||||
this.canvas.width = this.def.CANVASWIDTH;
|
||||
this.canvas.height = this.def.CANVASHEIGHT;
|
||||
this.canvas.style.border = '1px solid #d3d3d3';
|
||||
this.canvas.style.backgroundColor = '#f1f1f1';
|
||||
this.context = this.canvas.getContext('2d');
|
||||
this.app.appendChild(this.canvas);
|
||||
}
|
||||
|
||||
changeGameMode(mode)
|
||||
{
|
||||
this.app.removeChild(this.canvas);
|
||||
this.initGame(mode);
|
||||
}
|
||||
|
||||
finish(winner) {
|
||||
this.cleanup();
|
||||
if (this.pong.round >= 0) {
|
||||
if (this.pong.round == 0) {
|
||||
this.pong.winner1 = winner;
|
||||
this.pong.startGame(this.pong.player3, this.pong.player4)
|
||||
this.pong.round++;
|
||||
}
|
||||
else if (this.pong.round == 1) {
|
||||
this.pong.winner2 = winner;
|
||||
this.pong.startGame(this.pong.winner1, this.pong.winner2)
|
||||
this.pong.round++;
|
||||
}
|
||||
else {
|
||||
this.pong.final_winner = winner;
|
||||
this.pong.display_tree_tournament();
|
||||
this.scoresDisplay.innerHTML = winner + ' wins!! GGS';
|
||||
}
|
||||
}
|
||||
else
|
||||
this.scoresDisplay.innerHTML = winner + ' wins!! GGS';
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
clearInterval(this.interval);
|
||||
document.removeEventListener('keydown', this.keyDownHandler);
|
||||
document.removeEventListener('keyup', this.keyUpHandler);
|
||||
this.canvas.style.display = 'none';
|
||||
["up1", "up2", "down1", "down2", "player_name1", "player_name2"].forEach(button => {
|
||||
if (document.getElementById(button) != null)
|
||||
document.getElementById(button).remove();
|
||||
});
|
||||
let svg = document.getElementById("tree");
|
||||
while (svg.firstChild)
|
||||
svg.removeChild(svg.firstChild);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
updateGame() {
|
||||
//Paddle movement
|
||||
if ((this.keys.includes('s') || this.keys.includes('down1')) &&
|
||||
this.players[0].paddle.y + this.def.PADDLEHEIGHT < this.def.CANVASHEIGHT - this.def.PADDLEMARGIN)
|
||||
this.players[0].paddle.y += this.def.PADDLESPEED;
|
||||
if ((this.keys.includes('w') || this.keys.includes('up1')) &&
|
||||
this.players[0].paddle.y > 0 + this.def.PADDLEMARGIN)
|
||||
this.players[0].paddle.y -= this.def.PADDLESPEED;
|
||||
|
||||
if ((this.keys.includes('ArrowDown') || this.keys.includes('down2')) &&
|
||||
this.players[1].paddle.y + this.def.PADDLEHEIGHT < this.def.CANVASHEIGHT - this.def.PADDLEMARGIN)
|
||||
this.players[1].paddle.y += this.def.PADDLESPEED;
|
||||
if ((this.keys.includes('ArrowUp') || this.keys.includes('up2')) &&
|
||||
this.players[1].paddle.y > 0 + this.def.PADDLEMARGIN)
|
||||
this.players[1].paddle.y -= this.def.PADDLESPEED;
|
||||
|
||||
//GOOAAAAL
|
||||
if (this.ball.x <= 0)
|
||||
this.updateScore.bind(this)(this.players[0].score, ++this.players[1].score);
|
||||
else if (this.ball.x >= this.def.CANVASWIDTH)
|
||||
this.updateScore.bind(this)(++this.players[0].score, this.players[1].score);
|
||||
|
||||
//Ball collisions
|
||||
if (this.detectCollision(this.players[0].paddle, this.ball))
|
||||
this.calculateBallVelocity(this.players[0].paddle.getCenter().y, this.ball);
|
||||
else if (this.detectCollision(this.players[1].paddle, this.ball))
|
||||
this.calculateBallVelocity(this.players[1].paddle.getCenter().y, this.ball, -1);
|
||||
|
||||
if (this.ball.y - this.ball.radius <= 0)
|
||||
this.ball.vy *= -1;
|
||||
else if (this.ball.y + this.ball.radius >= this.canvas.height)
|
||||
this.ball.vy *= -1;
|
||||
|
||||
if (!this.ballRespawned) {
|
||||
this.ball.x += this.ball.vx;
|
||||
this.ball.y += this.ball.vy;
|
||||
}
|
||||
|
||||
if(this.context instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
this.clear();
|
||||
}
|
||||
else if(this.context instanceof WebGLRenderingContext)
|
||||
{
|
||||
this.context.clearColor(0.1, 0.1, 0.1, 1.0);
|
||||
this.context.clearDepth(1.0);
|
||||
this.context.enable(this.context.DEPTH_TEST);
|
||||
this.context.depthFunc(this.context.LEQUAL);
|
||||
this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
|
||||
|
||||
const projectionMatrix = mat4.create();
|
||||
const viewMatrix = mat4.create();
|
||||
|
||||
mat4.perspective(projectionMatrix, (90 * Math.PI) / 180, this.context.canvas.clientWidth / this.context.canvas.clientHeight, 0.1, 10000000.0);
|
||||
mat4.lookAt(viewMatrix, this.cam_pos, this.cam_target, this.cam_up);
|
||||
|
||||
this.setPositionAttribute();
|
||||
this.setNormalAttribute();
|
||||
|
||||
this.context.useProgram(shaderInfos.program);
|
||||
|
||||
this.context.uniformMatrix4fv(shaderInfos.uniformLocations.projectionMatrix, false, projectionMatrix);
|
||||
this.context.uniformMatrix4fv(shaderInfos.uniformLocations.viewMatrix, false, viewMatrix);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert('Unknown rendering context type');
|
||||
}
|
||||
this.players[0].paddle.update(this.context);
|
||||
this.players[1].paddle.update(this.context);
|
||||
this.ball.update(this.context);
|
||||
}
|
||||
|
||||
updateScore(p1Score, p2Score) {
|
||||
if (p1Score > this.def.MAXSCORE) {
|
||||
this.finish(this.player_name1);
|
||||
}
|
||||
else if (p2Score > this.def.MAXSCORE) {
|
||||
this.finish(this.player_name2);
|
||||
} else {
|
||||
this.scoresDisplay.innerHTML = `${this.player_name1}: ${p1Score} - ${this.player_name2}: ${p2Score}`;
|
||||
this.ballStartSide = 1 - this.ballStartSide;
|
||||
this.ball = new Ball(this.def, this.ballStartSide, this.context);
|
||||
this.ballRespawned = true;
|
||||
new Promise(r => setTimeout(r, 300))
|
||||
.then(_ => this.ballRespawned = false);
|
||||
}
|
||||
}
|
||||
|
||||
detectCollision(paddle, ball) {
|
||||
let paddleCenter = paddle.getCenter();
|
||||
let dx = Math.abs(ball.x - paddleCenter.x);
|
||||
let dy = Math.abs(ball.y - paddleCenter.y);
|
||||
if (dx <= ball.radius + paddle.width / 2 &&
|
||||
dy <= ball.radius + paddle.height / 2)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
calculateBallVelocity(paddleCenterY, ball, side = 1) {
|
||||
let relativeIntersectY = paddleCenterY - ball.y;
|
||||
let normRelIntersectY = relativeIntersectY / this.def.PADDLEHEIGHT / 2;
|
||||
let bounceAngle = normRelIntersectY * this.def.MAXBOUNCEANGLE;
|
||||
|
||||
ball.speed += this.def.BALLSPEEDINCR;
|
||||
ball.vx = ball.speed * side * Math.cos(bounceAngle);
|
||||
ball.vy = ball.speed * -Math.sin(bounceAngle);
|
||||
}
|
||||
|
||||
keyUpHandler(ev) {
|
||||
let attributes = ev.originalTarget.attributes;
|
||||
|
||||
let key = ev.key === undefined ? `${attributes.direction.value}${attributes.user_id.value}` : ev.key;
|
||||
|
||||
const idx = this.keys.indexOf(key);
|
||||
if (idx != -1)
|
||||
this.keys.splice(idx, 1);
|
||||
}
|
||||
|
||||
keyDownHandler(ev) {
|
||||
let attributes = ev.originalTarget.attributes;
|
||||
|
||||
let key = ev.key === undefined ? `${attributes.direction.value}${attributes.user_id.value}` : ev.key;
|
||||
|
||||
if (!this.keys.includes(key))
|
||||
this.keys.push(key);
|
||||
}
|
||||
|
||||
setNormalAttribute()
|
||||
{
|
||||
const numComponents = 3;
|
||||
const type = this.context.FLOAT;
|
||||
const normalize = false;
|
||||
const stride = 0;
|
||||
const offset = 0;
|
||||
this.context.bindBuffer(this.context.ARRAY_BUFFER, this.buffers.normal);
|
||||
this.context.vertexAttribPointer(
|
||||
shaderInfos.attribLocations.vertexNormal,
|
||||
numComponents,
|
||||
type,
|
||||
normalize,
|
||||
stride,
|
||||
offset,
|
||||
);
|
||||
this.context.enableVertexAttribArray(shaderInfos.attribLocations.vertexNormal);
|
||||
}
|
||||
|
||||
setPositionAttribute()
|
||||
{
|
||||
const numComponents = 3;
|
||||
const type = this.context.FLOAT;
|
||||
const normalize = false;
|
||||
const stride = 0;
|
||||
const offset = 0;
|
||||
this.context.bindBuffer(this.context.ARRAY_BUFFER, this.buffers.vertex);
|
||||
this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, this.buffers.index);
|
||||
this.context.vertexAttribPointer(
|
||||
shaderInfos.attribLocations.vertexPosition,
|
||||
numComponents,
|
||||
type,
|
||||
normalize,
|
||||
stride,
|
||||
offset
|
||||
);
|
||||
this.context.enableVertexAttribArray(shaderInfos.attribLocations.vertexPosition);
|
||||
}
|
||||
}
|
||||
|
||||
class Paddle {
|
||||
constructor(paddleSide, def) {
|
||||
this.width = def.PADDLEWIDTH;
|
||||
this.height = def.PADDLEHEIGHT;
|
||||
this.x = paddleSide;
|
||||
this.y = def.CANVASHEIGHT / 2 - this.height / 2;
|
||||
this.def = def;
|
||||
this.update();
|
||||
}
|
||||
|
||||
update(ctx) {
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(this.x, this.y, this.width, this.height);
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const posx = (this.x - this.def.CANVASWIDTH / 2);
|
||||
const posy = (this.y - this.def.CANVASHEIGHT / 3);
|
||||
renderCube(ctx, posx, 0, posy, 0, this.width, this.width, this.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
getCenter() {
|
||||
return {
|
||||
x: this.x + this.width / 2,
|
||||
y: this.y + this.height / 2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Ball {
|
||||
constructor(def, startSide, ctx) {
|
||||
this.radius = def.BALLRADIUS;
|
||||
this.speed = def.BALLSPEED;
|
||||
this.x = def.CANVASWIDTH / 2;
|
||||
this.y = def.CANVASHEIGHT / 2;
|
||||
this.vy = 0;
|
||||
if (startSide === 0)
|
||||
this.vx = -this.speed;
|
||||
else
|
||||
this.vx = this.speed;
|
||||
this.def = def;
|
||||
this.update(ctx);
|
||||
}
|
||||
|
||||
update(ctx) {
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const size = this.radius;
|
||||
const posx = (this.x - this.radius / 2) - this.def.CANVASWIDTH / 2;
|
||||
const posy = (this.y - this.radius / 2) - this.def.CANVASHEIGHT / 2;
|
||||
renderCube(ctx, posx, 0, posy, 0, size, size, size);
|
||||
}
|
||||
}
|
||||
}
|
261
django/frontend/static/js/views/PongOnlineView.js
Normal file
261
django/frontend/static/js/views/PongOnlineView.js
Normal file
@ -0,0 +1,261 @@
|
||||
import { client, reloadView } from "../index.js";
|
||||
import { PongGame } from "../api/game/pong/PongGame.js";
|
||||
import { PongPlayer } from "../api/game/pong/PongPlayer.js";
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
import { Profile } from "../api/Profile.js";
|
||||
import { PongMyPlayer } from "../api/game/pong/PongMyPlayer.js";
|
||||
|
||||
export default class PongOnlineView extends AbstractAuthenticatedView
|
||||
{
|
||||
constructor(params)
|
||||
{
|
||||
super(params, 'Pong');
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.game_id = params.id;
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game;
|
||||
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
this.canva;
|
||||
|
||||
/**
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.gameboard;
|
||||
|
||||
/**
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
this.gamestate;
|
||||
|
||||
/**
|
||||
* @type {HTMLTableElement}
|
||||
*/
|
||||
this.scoreboard;
|
||||
|
||||
/**
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
this.app = document.getElementById("app");
|
||||
|
||||
/**
|
||||
* @type {[]}
|
||||
*/
|
||||
this.keysPressed
|
||||
}
|
||||
|
||||
createMyPlayer()
|
||||
{
|
||||
let index = this.game.players.findIndex((player) => player.id === client.me.id);
|
||||
|
||||
if (index === -1)
|
||||
return;
|
||||
|
||||
let myPlayer = this.game.players[index];
|
||||
|
||||
if (myPlayer.isEliminated)
|
||||
return;
|
||||
|
||||
this.keysPressed = [];
|
||||
|
||||
this.myPlayer = new PongMyPlayer(client,
|
||||
this.game,
|
||||
myPlayer.score,
|
||||
myPlayer.rail,
|
||||
myPlayer.position,
|
||||
);
|
||||
|
||||
myPlayer = this.myPlayer;
|
||||
|
||||
this.registerKey();
|
||||
}
|
||||
|
||||
keyReleaseHandler(event)
|
||||
{
|
||||
const idx = this.keysPressed.indexOf(event.key);
|
||||
if (idx != -1)
|
||||
this.keysPressed.splice(idx, 1);
|
||||
}
|
||||
|
||||
keyPressHandler(event)
|
||||
{
|
||||
console.log("bozo")
|
||||
if (!this.keysPressed.includes(event.key))
|
||||
this.keysPressed.push(event.key);
|
||||
}
|
||||
|
||||
registerKey()
|
||||
{
|
||||
this.keyPressHandler = this.keyPressHandler.bind(this);
|
||||
this.keyReleaseHandler = this.keyReleaseHandler.bind(this);
|
||||
document.addEventListener('keydown', this.keyPressHandler);
|
||||
document.addEventListener('keyup', this.keyReleaseHandler);
|
||||
}
|
||||
|
||||
unregisterKey()
|
||||
{
|
||||
document.removeEventListener('keydown', this.keyPressHandler);
|
||||
document.removeEventListener('keyup', this.keyReleaseHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PongPlayer} player
|
||||
*/
|
||||
async onGoal(player)
|
||||
{
|
||||
document.getElementById(`score-${player.id}`).innerText = player.score.length;
|
||||
}
|
||||
|
||||
async onStart()
|
||||
{
|
||||
this.gamestate.innerHTML = this.game.getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PongPlayer} winner
|
||||
*/
|
||||
async onFinish(winner)
|
||||
{
|
||||
this.gamestate.innerHTML = this.game.getState();
|
||||
this.destroyGameboard();
|
||||
this.displayWinner(winner)
|
||||
}
|
||||
|
||||
async onDisconnect()
|
||||
{
|
||||
await reloadView();
|
||||
}
|
||||
|
||||
createScoreboard()
|
||||
{
|
||||
this.scoreboard = document.createElement("table");
|
||||
|
||||
this.app.appendChild(this.scoreboard);
|
||||
|
||||
let row = document.createElement("tr");
|
||||
|
||||
this.game.players.forEach(player => {
|
||||
let th = document.createElement("th");
|
||||
th.innerText = player.username;
|
||||
row.appendChild(th);
|
||||
});
|
||||
|
||||
this.scoreboard.appendChild(row);
|
||||
|
||||
row = document.createElement("tr");
|
||||
|
||||
this.game.players.forEach(player => {
|
||||
let th = document.createElement("th");
|
||||
th.id = `score-${player.id}`;
|
||||
th.innerText = player.score.length;
|
||||
this.scoreboard.appendChild(th);
|
||||
});
|
||||
|
||||
this.scoreboard.appendChild(row);
|
||||
}
|
||||
|
||||
createGameboard()
|
||||
{
|
||||
this.canva = document.createElement("canvas");
|
||||
|
||||
this.app.appendChild(this.canva)
|
||||
|
||||
this.canva.height = this.game.config.MAP_SIZE_Y
|
||||
this.canva.width = this.game.config.MAP_SIZE_X
|
||||
|
||||
this.gameboard = this.canva.getContext('2d');
|
||||
}
|
||||
|
||||
destroyGameboard()
|
||||
{
|
||||
this.app.removeChild(this.canva);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Profile} winner
|
||||
*/
|
||||
displayWinner(winner)
|
||||
{
|
||||
const winnerStand = document.createElement("h1");
|
||||
|
||||
document.getElementById("app").appendChild(winnerStand);
|
||||
|
||||
winnerStand.innerText = `winner: ${winner.username}`;
|
||||
}
|
||||
|
||||
async connect()
|
||||
{
|
||||
await this.game.join();
|
||||
await this.game.waitInit();
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
this.gamestate = document.getElementById("gamestate");
|
||||
|
||||
this.game = new PongGame(client, this.game_id, this.onStart.bind(this), this.onGoal.bind(this), this.onStart.bind(this), this.onFinish.bind(this));
|
||||
|
||||
const errorCode = await this.game.init();
|
||||
if (errorCode)
|
||||
return errorCode;
|
||||
|
||||
this.gamestate.innerHTML = this.game.getState();
|
||||
|
||||
if (this.game.finished)
|
||||
{
|
||||
this.displayWinner(this.game.winner);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.connect();
|
||||
this.createScoreboard();
|
||||
this.createGameboard();
|
||||
this.createMyPlayer();
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let loop_id = setInterval(() => {
|
||||
if (this.game === undefined)
|
||||
clearInterval(loop_id);
|
||||
this.myPlayer?.updatePaddle(this.keysPressed);
|
||||
this.renderGame();
|
||||
this.game?.time?.new_frame();
|
||||
//clearInterval(loop_id);
|
||||
// 1 sec fps
|
||||
}, 1000 / 60);
|
||||
}
|
||||
|
||||
renderGame()
|
||||
{
|
||||
this.gameboard.beginPath();
|
||||
this.game.render(this.gameboard);
|
||||
this.gameboard.strokeStyle = "#000000";
|
||||
this.gameboard.lineWidth = 1;
|
||||
this.gameboard.stroke();
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
return /* HTML */ `
|
||||
<h1 id='gamestate'></h1>
|
||||
`;
|
||||
}
|
||||
|
||||
async leavePage()
|
||||
{
|
||||
this.unregisterKey();
|
||||
this.game.leave()
|
||||
}
|
||||
}
|
234
django/frontend/static/js/views/ProfilePageView.js
Normal file
234
django/frontend/static/js/views/ProfilePageView.js
Normal file
@ -0,0 +1,234 @@
|
||||
import AbstractView from "./abstracts/AbstractView.js";
|
||||
import { client, lang } from "../index.js";
|
||||
|
||||
export default class extends AbstractView {
|
||||
constructor(params) {
|
||||
super(params, `${decodeURI(params.username)} - Profile`);
|
||||
this.username = decodeURI(params.username);
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
document.title = this.titleKey;
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
if (!this.profile)
|
||||
return 404;
|
||||
|
||||
const games = await this.profile.getGameHistory();
|
||||
|
||||
await this.fillHistory(games);
|
||||
await this.fillStatistics(games);
|
||||
|
||||
if (this.profile.id === client.me.id)
|
||||
return;
|
||||
|
||||
const addFriendButton = document.getElementById('addFriendButton'),
|
||||
removeFriendButton = document.getElementById('removeFriendButton'),
|
||||
blockButton = document.getElementById('blockButton'),
|
||||
unblockButton = document.getElementById('unblockButton');
|
||||
|
||||
this.loadFriendshipStatus();
|
||||
if (this.profile.isBlocked)
|
||||
unblockButton.classList.remove('d-none');
|
||||
else
|
||||
blockButton.classList.remove('d-none');
|
||||
|
||||
addFriendButton.onclick = _ => this.addFriend();
|
||||
removeFriendButton.onclick = _ => this.removeFriend();
|
||||
unblockButton.onclick = _ => this.unblockUser();
|
||||
blockButton.onclick = _ => this.blockUser();
|
||||
}
|
||||
|
||||
loadFriendshipStatus() {
|
||||
const addFriendButton = document.getElementById('addFriendButton'),
|
||||
removeFriendButton = document.getElementById('removeFriendButton'),
|
||||
statusIndicator = document.getElementById('statusIndicator');
|
||||
|
||||
if (this.profile.hasIncomingRequest) {
|
||||
removeFriendButton.classList.add('d-none');
|
||||
addFriendButton.classList.remove('d-none');
|
||||
addFriendButton.innerHTML = 'Accept Request';
|
||||
} else if (this.profile.hasOutgoingRequest) {
|
||||
addFriendButton.classList.add('d-none');
|
||||
removeFriendButton.classList.remove('d-none');
|
||||
removeFriendButton.classList.replace('btn-danger', 'btn-secondary');
|
||||
removeFriendButton.innerHTML = 'Cancel Request';
|
||||
} else if (this.profile.isFriend) {
|
||||
addFriendButton.classList.add('d-none');
|
||||
removeFriendButton.classList.remove('d-none');
|
||||
removeFriendButton.classList.replace('btn-secondary', 'btn-danger');
|
||||
removeFriendButton.innerHTML = 'Remove Friend';
|
||||
} else {
|
||||
addFriendButton.innerHTML = 'Add Friend';
|
||||
removeFriendButton.classList.add('d-none');
|
||||
addFriendButton.classList.remove('d-none');
|
||||
}
|
||||
|
||||
statusIndicator.classList.remove('bg-success', 'bg-danger');
|
||||
if (this.profile.online === true)
|
||||
statusIndicator.classList.add('bg-success');
|
||||
else if (this.profile.online === false)
|
||||
statusIndicator.classList.add('bg-danger');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[Object]} games
|
||||
*/
|
||||
async fillStatistics(games)
|
||||
{
|
||||
let winrateDiv = document.getElementById("winrate");
|
||||
|
||||
let win = 0;
|
||||
let lose = 0;
|
||||
|
||||
games.forEach(game => {
|
||||
if (game.finished === false)
|
||||
return
|
||||
|
||||
if (client.me.id === game.winner.id)
|
||||
win++;
|
||||
else
|
||||
lose++;
|
||||
});
|
||||
|
||||
winrateDiv.innerText = `winrate: ${win + lose === 0 ? "🤓" : win / (win + lose)}`
|
||||
}
|
||||
|
||||
async fillHistory(games)
|
||||
{
|
||||
let game_list = document.getElementById("game-list");
|
||||
|
||||
games.forEach(game => {
|
||||
|
||||
let a = document.createElement("a");
|
||||
a.href = `/games/${game.game_type}/${game.id}`;
|
||||
a.setAttribute("data-link", true);
|
||||
|
||||
let game_item = document.createElement("div");
|
||||
game_item.className = "game-item";
|
||||
game_item.style.backgroundColor = "grey";
|
||||
if (game.started)
|
||||
game_item.style.backgroundColor = "yellow";
|
||||
if (game.finished)
|
||||
game_item.style.backgroundColor = this.profile.id === game.winner.id ? "green" : "red";
|
||||
|
||||
game.players.forEach(player => {
|
||||
let player_score = document.createElement("a");
|
||||
|
||||
player_score.href = `/profiles/${player.username}`;
|
||||
player_score.innerText = `${player.username}`;
|
||||
player_score.setAttribute("data-link", true);
|
||||
|
||||
game_item.appendChild(player_score);
|
||||
});
|
||||
|
||||
a.appendChild(game_item);
|
||||
game_list.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
|
||||
this.profile = await client.profiles.getProfile(this.username);
|
||||
if (!this.profile)
|
||||
return '';
|
||||
|
||||
return /* HTML */ `
|
||||
<div>
|
||||
<div class='mb-3' id='profileInfo'>
|
||||
<h1>${this.username}<span id='statusIndicator' style='height:0.75em; width:0.75em' class='ms-2 rounded-circle border d-inline-block'></span></h1>
|
||||
<a href=${this.profile.avatar} target='_blank'>
|
||||
<img class='img-thumbnail' src=${this.profile.avatar} style='width:auto; max-height:20vh; min-height:10vh'>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button class='btn btn-sm btn-success d-none' id='addFriendButton'>Add Friend</button>
|
||||
<button class='btn btn-sm btn-danger d-none' id='removeFriendButton'>Remove Friend</button>
|
||||
<button class='btn btn-sm btn-danger d-none' id='blockButton'>Block</button>
|
||||
<button class='btn btn-sm btn-secondary d-none' id='unblockButton'>Unblock</button>
|
||||
</div>
|
||||
<h1>Games</h1>
|
||||
<div>
|
||||
<h1 id='winrate'></h1>
|
||||
</div>
|
||||
<div>
|
||||
<link rel="stylesheet" href="/static/css/gameHistory.css">
|
||||
<div id="game-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async addFriend() {
|
||||
const removeFriendButton = document.getElementById('removeFriendButton');
|
||||
|
||||
const response = await client._post(`/api/profiles/friends/${this.profile.id}`);
|
||||
const body = await response.json();
|
||||
console.log(body);
|
||||
|
||||
if (response.ok) {
|
||||
removeFriendButton.classList.remove('d-none');
|
||||
document.getElementById('addFriendButton').classList.add('d-none');
|
||||
}
|
||||
if (response.status === 200) {
|
||||
removeFriendButton.innerHTML = 'Cancel Request';
|
||||
removeFriendButton.classList.replace('btn-danger', 'btn-secondary');
|
||||
this.profile.hasOutgoingRequest = true;
|
||||
} else if (response.status === 201) {
|
||||
removeFriendButton.innerHTML = 'Remove Friend';
|
||||
removeFriendButton.classList.replace('btn-secondary', 'btn-danger');
|
||||
this.profile.isFriend = true;
|
||||
this.profile.hasIncomingRequest = false;
|
||||
}
|
||||
}
|
||||
|
||||
async removeFriend() {
|
||||
const addFriendButton = document.getElementById('addFriendButton'),
|
||||
statusIndicator = document.getElementById('statusIndicator');
|
||||
|
||||
const response = await client._delete(`/api/profiles/friends/${this.profile.id}`);
|
||||
const body = await response.json();
|
||||
console.log(body);
|
||||
|
||||
if (response.ok) {
|
||||
addFriendButton.innerHTML = 'Add Friend';
|
||||
addFriendButton.classList.remove('d-none');
|
||||
statusIndicator.classList.remove('bg-danger', 'bg-success');
|
||||
document.getElementById('removeFriendButton').classList.add('d-none');
|
||||
}
|
||||
if (response.status === 200) {
|
||||
this.profile.hasOutgoingRequest = false;
|
||||
} else if (response.status === 201) {
|
||||
this.profile.isFriend = false;
|
||||
this.profile.online = null;
|
||||
}
|
||||
}
|
||||
|
||||
async blockUser() {
|
||||
const response = await client._post(`/api/profiles/block/${this.profile.id}`);
|
||||
const body = await response.json();
|
||||
console.log(body);
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('blockButton').classList.add('d-none');
|
||||
document.getElementById('unblockButton').classList.remove('d-none');
|
||||
client.me.blockedUsers.push(this.profile);
|
||||
this.profile.isBlocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
async unblockUser() {
|
||||
const response = await client._delete(`/api/profiles/block/${this.profile.id}`);
|
||||
const body = await response.json();
|
||||
console.log(body);
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('unblockButton').classList.add('d-none');
|
||||
document.getElementById('blockButton').classList.remove('d-none');
|
||||
client.me.blockedUsers = client.me.blockedUsers.filter(profile => profile.id !== this.profile.id);
|
||||
this.profile.isBlocked = false;
|
||||
}
|
||||
}
|
||||
}
|
350
django/frontend/static/js/views/Search.js
Normal file
350
django/frontend/static/js/views/Search.js
Normal file
@ -0,0 +1,350 @@
|
||||
import AbstractView from "./abstracts/AbstractView.js";
|
||||
import { client, lang } from "../index.js";
|
||||
import Channels from '../api/chat/Channels.js'
|
||||
|
||||
export default class extends AbstractView {
|
||||
constructor(params) {
|
||||
super(params, 'SearchWindowTitle');
|
||||
this.channelManager = new Channels(client);
|
||||
}
|
||||
|
||||
async postInit() {
|
||||
this.profiles = await client.profiles.all();
|
||||
this.logged = await client.isAuthenticated();
|
||||
|
||||
this.logged = await client.isAuthenticated();
|
||||
this.profiles = await client.profiles.all();
|
||||
|
||||
document.getElementById('username-input').oninput = () => this.display_users(this.logged, this.profiles);
|
||||
|
||||
let search = document.getElementById("input_user");
|
||||
if (search != undefined)
|
||||
search.oninput = () => this.display_users();
|
||||
|
||||
this.last_add_chat = undefined;
|
||||
|
||||
this.display_users();
|
||||
this.display_chat();
|
||||
}
|
||||
|
||||
async display_users() {
|
||||
|
||||
const search = document.getElementById("username-input").value.toLowerCase();
|
||||
|
||||
let list_users = document.getElementById('user-list');
|
||||
list_users.innerHTML = "";
|
||||
|
||||
this.profiles.filter(user => user.username.toLowerCase().startsWith(search) == true).forEach(async (user) => {
|
||||
|
||||
if (user.id == null) {
|
||||
console.log("list User one with id null;");
|
||||
return;
|
||||
}
|
||||
|
||||
var new_user = document.createElement("li");
|
||||
|
||||
// username
|
||||
let username = document.createElement("a");
|
||||
username.setAttribute('data-link', '');
|
||||
username.id = `username${user.id}`;
|
||||
username.href = `/profiles/${user.username}`;
|
||||
if (this.logged && user.id == client.me.id)
|
||||
username.style.color = "green";
|
||||
else {
|
||||
let profile = await client.profiles.getProfileId(user.id);
|
||||
let online = profile.online;
|
||||
if (online == undefined)
|
||||
username.style.color = "gray";
|
||||
else if (online == true)
|
||||
username.style.color = "green";
|
||||
else
|
||||
username.style.color = "red";
|
||||
}
|
||||
username.appendChild(document.createTextNode(user.username));
|
||||
new_user.appendChild(username);
|
||||
|
||||
// space
|
||||
new_user.appendChild(document.createTextNode(" "));
|
||||
|
||||
// button chat
|
||||
if (this.logged && client.me.id != user.id) {
|
||||
let add_chat = document.createElement("a");
|
||||
add_chat.id = "add_chat_off";
|
||||
add_chat.onclick = async () => {
|
||||
if (this.channelManager.channel != undefined) {
|
||||
|
||||
// Permet de savoir si c'est le même channel
|
||||
// Check to know if it's the same channel
|
||||
this.channelManager.channel.members_id.forEach((member_id) => {
|
||||
if (member_id == user.id)
|
||||
this.channelManager.channel = undefined;
|
||||
});
|
||||
if (this.channelManager.channel == undefined) {
|
||||
add_chat.id = "add_chat_off";
|
||||
this.last_add_chat = undefined;
|
||||
return await this.hide_chat();
|
||||
}
|
||||
|
||||
await this.channelManager.channel.disconnect();
|
||||
}
|
||||
await this.channelManager.createChannel([client.me.id , user.id], () => this.reload_display_messages());
|
||||
this.display_chat();
|
||||
if (this.last_add_chat != undefined)
|
||||
this.last_add_chat.id = "add_chat_off";
|
||||
this.last_add_chat = add_chat;
|
||||
add_chat.id = "add_chat_on";
|
||||
};
|
||||
add_chat.appendChild(document.createTextNode("Chat"));
|
||||
new_user.appendChild(add_chat);
|
||||
|
||||
new_user.appendChild(document.createTextNode(" "));
|
||||
|
||||
}
|
||||
|
||||
// break line
|
||||
new_user.appendChild(document.createElement("br"));
|
||||
|
||||
// avatar
|
||||
var img = document.createElement("img");
|
||||
img.src = user.avatar;
|
||||
new_user.appendChild(img);
|
||||
|
||||
|
||||
list_users.appendChild(new_user);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async display_specific_user(id) {
|
||||
|
||||
let user = document.getElementById("username" + id);
|
||||
if (user == undefined)
|
||||
return ;
|
||||
|
||||
if (this.logged && id == client.me.id)
|
||||
user.style.color = "green";
|
||||
else {
|
||||
let profile = await client.profiles.getProfileId(id);
|
||||
let online = profile.online;
|
||||
if (online == undefined)
|
||||
user.style.color = "gray";
|
||||
else if (online == true)
|
||||
user.style.color = "green";
|
||||
else
|
||||
user.style.color = "red";
|
||||
}
|
||||
}
|
||||
|
||||
async display_chat()
|
||||
{
|
||||
let reloads = ["members", "messages"];
|
||||
reloads.forEach(reload => {
|
||||
if (document.getElementById(reload) != undefined)
|
||||
document.getElementById(reload).remove();
|
||||
});
|
||||
|
||||
if (this.channelManager.channel === undefined || this.logged === false)
|
||||
return ;
|
||||
|
||||
let chats = document.getElementById("chats");
|
||||
|
||||
if (document.getElementById("chat") == null) {
|
||||
let chat = document.createElement("div");
|
||||
chat.id = "chat";
|
||||
chats.appendChild(chat);
|
||||
}
|
||||
|
||||
// nom des membres du channel
|
||||
let members = await this.display_members(chat);
|
||||
|
||||
// L'affiche des messages
|
||||
let messages = await this.display_messages(chat);
|
||||
|
||||
// Input pour rentrer un message
|
||||
let chat_input = document.getElementById("chat-input") || document.createElement("input");
|
||||
chat_input.id="chat_input";
|
||||
chat_input.type="text";
|
||||
chat_input.name="message";
|
||||
chat_input.placeholder="message bozo";
|
||||
chat_input.maxLength=255;
|
||||
chat.appendChild(chat_input);
|
||||
|
||||
let members_id = this.channelManager.channel.members_id;
|
||||
|
||||
chat_input.onkeydown = async () => {
|
||||
if (event.keyCode == 13 && this.channelManager.channel != undefined) {
|
||||
//let chat_input = document.getElementById("chat-input");
|
||||
let chat_text = chat_input.value;
|
||||
|
||||
let receivers_id = [];
|
||||
members_id.forEach((member_id) => {
|
||||
if (member_id != client.me.id)
|
||||
receivers_id.push(this.profiles.filter(user => user.id == member_id)[0].id);
|
||||
});
|
||||
await this.channelManager.channel.sendMessageChannel(chat_text, receivers_id);
|
||||
// Reset
|
||||
chat_input.value = "";
|
||||
}
|
||||
};
|
||||
chat_input.focus();
|
||||
|
||||
|
||||
// Scroll to the bottom of messages
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
|
||||
// this.display_invite();
|
||||
|
||||
}
|
||||
|
||||
async display_messages(chat) {
|
||||
|
||||
let messages = document.createElement("div");
|
||||
|
||||
messages.id = "messages";
|
||||
chat.appendChild(messages);
|
||||
|
||||
// les messages, réecriture seulement du dernier
|
||||
this.channelManager.channel.messages.forEach((message) => {
|
||||
let text = document.createElement("p");
|
||||
let date = new Date(message.time);
|
||||
text.title = date.toLocaleString("fr-FR");
|
||||
text.appendChild(document.createTextNode(message.content));
|
||||
console.log(message.author, client.me.id)
|
||||
text.id = message.author === client.me.id ? "you" : "other";
|
||||
|
||||
messages.appendChild(text);
|
||||
});
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async reload_display_messages() {
|
||||
let messages = document.getElementById("messages");
|
||||
|
||||
let i = 0;
|
||||
this.channelManager.channel.messages.forEach((message) => {
|
||||
if (messages.children[i] == null || message.content != messages.children[i].innerText) {
|
||||
let text = document.createElement("p");
|
||||
let date = new Date(message.time);
|
||||
text.title = date.toLocaleString("fr-FR");
|
||||
text.appendChild(document.createTextNode(message.content));
|
||||
text.id = message.author === client.me.id ? "you" : "other";
|
||||
|
||||
messages.appendChild(text);
|
||||
}
|
||||
i++;
|
||||
});
|
||||
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
|
||||
async display_members(chat) {
|
||||
|
||||
let members_id = this.channelManager.channel.members_id;
|
||||
|
||||
let members = document.createElement("h2");
|
||||
members.id = "members";
|
||||
let usernames = "";
|
||||
members_id.forEach((member_id) => {
|
||||
if (member_id != client.me.id) {
|
||||
if (usernames.length > 0)
|
||||
usernames += ", ";
|
||||
usernames += (this.profiles.filter(user => user.id == member_id)[0].username);
|
||||
}
|
||||
});
|
||||
members.textContent = usernames;
|
||||
chat.appendChild(members);
|
||||
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
async display_invite() {
|
||||
|
||||
let chat = document.getElementById("chat");
|
||||
|
||||
if (chat == undefined)
|
||||
return ;
|
||||
|
||||
let members_id = this.channelManager.channel.members_id;
|
||||
let others = members_id.filter(id => id !== client.me.id);
|
||||
|
||||
let invite = document.getElementById("invite") || document.createElement("button");
|
||||
let yes = document.getElementById("yes") || document.createElement("button");
|
||||
let no = document.getElementById("no") || document.createElement("button");
|
||||
|
||||
let invitedBy;
|
||||
|
||||
if (invitedBy == undefined) {
|
||||
|
||||
if (yes && no) {
|
||||
yes.remove();
|
||||
no.remove();
|
||||
}
|
||||
|
||||
// Button to send invite to play
|
||||
invite.id = "invite";
|
||||
invite.style.background = "orange";
|
||||
invite.innerText = "invite";
|
||||
invite.title = "Invite to play a game";
|
||||
invite.onclick = async () => {
|
||||
await client.notice.send_invite(others);
|
||||
};
|
||||
chat.appendChild(invite);
|
||||
}
|
||||
else {
|
||||
|
||||
if (invite)
|
||||
invite.remove();
|
||||
|
||||
yes.id = "yes";
|
||||
yes.style.background = "green";
|
||||
yes.title = "Accept to play a game";
|
||||
yes.onclick = async () => {
|
||||
await client.notice.accept_invite(invitedBy);
|
||||
};
|
||||
|
||||
no.id = "no";
|
||||
no.style.background = "red";
|
||||
no.title = "Refuse to play a game";
|
||||
no.onclick = async () => {
|
||||
await client.notice.refuse_invite(invitedBy);
|
||||
};
|
||||
|
||||
chat.appendChild(yes);
|
||||
chat.appendChild(no);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async hide_chat() {
|
||||
|
||||
let closes = ["chat", "invite"];
|
||||
closes.forEach(close => {
|
||||
if (document.getElementById(close))
|
||||
document.getElementById(close).remove();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async leavePage() {
|
||||
if (this.channelManager.channel != undefined)
|
||||
this.channelManager.channel.disconnect();
|
||||
this.channelManager.channel = undefined;
|
||||
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return `
|
||||
<link rel="stylesheet" href="/static/css/search.css">
|
||||
|
||||
<div id="chats">
|
||||
<div id="users">
|
||||
<input id="username-input" type="text" name="message" placeholder="userbozo"/>
|
||||
<ul id='user-list'>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
332
django/frontend/static/js/views/SettingsView.js
Normal file
332
django/frontend/static/js/views/SettingsView.js
Normal file
@ -0,0 +1,332 @@
|
||||
import {client, lang, navigateTo} from '../index.js';
|
||||
import {clearElements, fill_errors} from '../utils/formUtils.js'
|
||||
import AbstractAuthenticatedView from './abstracts/AbstractAuthenticatedView.js';
|
||||
|
||||
export default class extends AbstractAuthenticatedView
|
||||
{
|
||||
constructor(params)
|
||||
{
|
||||
super(params, 'settingsWindowTitle');
|
||||
this.PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024; // 2MB
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
this.avatarInit();
|
||||
this.usernameInit();
|
||||
this.passwordInit();
|
||||
this.deleteInit();
|
||||
}
|
||||
|
||||
deleteInit() {
|
||||
const deleteInput = document.getElementById('deleteInput');
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
|
||||
deleteModal.addEventListener('shown.bs.modal', _ => {
|
||||
deleteInput.focus();
|
||||
});
|
||||
deleteModal.addEventListener('hidden.bs.modal', _ => {
|
||||
deleteInput.value = '';
|
||||
});
|
||||
deleteInput.onkeydown = e => {
|
||||
if (e.key === 'Enter')
|
||||
this.deleteAccount();
|
||||
}
|
||||
document.getElementById('deleteButton').onclick = this.deleteAccount;
|
||||
}
|
||||
|
||||
passwordInit() {
|
||||
document.getElementById('currentPasswordInput').onkeydown = e => {
|
||||
if (e.key === 'Enter')
|
||||
this.savePassword();
|
||||
};
|
||||
document.getElementById('newPasswordInput').onkeydown = e => {
|
||||
if (e.key === 'Enter')
|
||||
this.savePassword();
|
||||
};
|
||||
document.getElementById('newPassword2Input').onkeydown = e => {
|
||||
if (e.key === 'Enter')
|
||||
this.savePassword();
|
||||
};
|
||||
document.getElementById('passwordSave').onclick = this.savePassword;
|
||||
}
|
||||
|
||||
usernameInit() {
|
||||
const usernameInput = document.getElementById('usernameInput');
|
||||
const usernameSave = document.getElementById('usernameSave');
|
||||
|
||||
usernameInput.oninput = e => {
|
||||
const value = e.target.value;
|
||||
if (value != client.me.username && value.length)
|
||||
usernameSave.classList.remove('disabled');
|
||||
else
|
||||
usernameSave.classList.add('disabled');
|
||||
}
|
||||
usernameSave.onclick = _ => this.saveUsername();
|
||||
}
|
||||
|
||||
avatarInit() {
|
||||
const avatar = document.getElementById('avatar');
|
||||
const avatarInput = document.getElementById('avatarInput');
|
||||
const avatarUpload = document.getElementById('avatarUpload');
|
||||
const avatarDelete = document.getElementById('avatarDelete');
|
||||
|
||||
avatar.onclick = _ => avatarInput.click();
|
||||
avatarInput.onchange = function () {
|
||||
const selectedFile = this.files[0];
|
||||
if (!selectedFile)
|
||||
return;
|
||||
|
||||
avatar.src = URL.createObjectURL(selectedFile);
|
||||
avatarUpload.classList.remove('d-none');
|
||||
}
|
||||
avatarUpload.onclick = _ => this.saveAvatar();
|
||||
avatarDelete.onclick = _ => this.deleteAvatar();
|
||||
}
|
||||
|
||||
async displayAvatar() {
|
||||
let avatar = document.getElementById('avatar');
|
||||
avatar.src = client.me.avatar + '?t=' + new Date().getTime();
|
||||
}
|
||||
|
||||
async savePassword() {
|
||||
const currentPasswordInput = document.getElementById('currentPasswordInput');
|
||||
const currentPassword = currentPasswordInput.value;
|
||||
const currentPasswordDetail = document.getElementById('currentPasswordDetail');
|
||||
const newPasswordInput = document.getElementById('newPasswordInput');
|
||||
const newPassword = newPasswordInput.value;
|
||||
const newPasswordDetail = document.getElementById('newPasswordDetail');
|
||||
const newPassword2Input = document.getElementById('newPassword2Input');
|
||||
const newPassword2 = newPassword2Input.value;
|
||||
const newPassword2Detail = document.getElementById('newPassword2Detail');
|
||||
const passwordDetail = document.getElementById('passwordDetail');
|
||||
|
||||
clearElements('innerHTML', [currentPasswordDetail,
|
||||
newPasswordDetail,
|
||||
newPassword2Detail,
|
||||
passwordDetail
|
||||
]);
|
||||
currentPasswordInput.classList.remove('is-invalid');
|
||||
newPasswordInput.classList.remove('is-invalid');
|
||||
newPassword2Input.classList.remove('is-invalid');
|
||||
|
||||
if (!currentPassword.length) {
|
||||
currentPasswordDetail.innerHTML = lang.get('errorEmptyField');
|
||||
currentPasswordInput.classList.add('is-invalid');
|
||||
}
|
||||
if (!newPassword.length) {
|
||||
newPasswordDetail.innerHTML = lang.get('errorEmptyField');
|
||||
newPasswordInput.classList.add('is-invalid');
|
||||
}
|
||||
if (!newPassword2.length) {
|
||||
newPassword2Detail.innerHTML = lang.get('errorEmptyField');
|
||||
newPassword2Input.classList.add('is-invalid');
|
||||
}
|
||||
if (!currentPassword.length || !newPassword.length || !newPassword2.length)
|
||||
return;
|
||||
|
||||
const error = await client.account.updatePassword(currentPassword, newPassword, newPassword2);
|
||||
if (!error) {
|
||||
passwordDetail.classList.remove('text-danger');
|
||||
passwordDetail.classList.add('text-success');
|
||||
passwordDetail.innerHTML = lang.get('settingsPasswordSaved');
|
||||
setTimeout(_ => passwordDetail.innerHTML = '', 3000);
|
||||
clearElements('value', [currentPasswordInput, newPasswordInput, newPassword2Input]);
|
||||
} else {
|
||||
passwordDetail.classList.add('text-danger');
|
||||
passwordDetail.classList.remove('text-success');
|
||||
fill_errors(error, 'innerHTML');
|
||||
if (error.currentPasswordDetail)
|
||||
currentPasswordInput.classList.add('is-invalid');
|
||||
if (error.newPasswordDetail)
|
||||
newPasswordInput.classList.add('is-invalid');
|
||||
if (error.newPassword2Detail)
|
||||
newPassword2Input.classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
async saveUsername()
|
||||
{
|
||||
const usernameInput = document.getElementById('usernameInput');
|
||||
const username = usernameInput.value;
|
||||
const usernameDetail = document.getElementById('usernameDetail');
|
||||
|
||||
if (!username.length || username === client.me.username)
|
||||
return;
|
||||
|
||||
const error = await client.account.updateUsername(username);
|
||||
if (!error) {
|
||||
usernameDetail.classList.remove('text-danger', 'd-none');
|
||||
usernameDetail.classList.add('text-success');
|
||||
usernameDetail.innerHTML = lang.get('settingsUsernameSaved');
|
||||
setTimeout(_ => usernameDetail.classList.add('d-none'), 2000);
|
||||
document.getElementById('usernameSave').classList.add('disabled');
|
||||
} else {
|
||||
usernameDetail.classList.remove('text-success', 'd-none');
|
||||
usernameDetail.classList.add('text-danger');
|
||||
usernameDetail.innerHTML = error;
|
||||
document.getElementById('usernameSave').classList.add('disabled');
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async saveAvatar()
|
||||
{
|
||||
const avatarInput = document.getElementById('avatarInput');
|
||||
const selectedFile = avatarInput.files[0];
|
||||
const avatarDetail = document.getElementById('avatarDetail');
|
||||
|
||||
if (!selectedFile)
|
||||
return;
|
||||
|
||||
if (selectedFile.size > this.PROFILE_PICTURE_MAX_SIZE) {
|
||||
avatarDetail.classList.remove('text-success');
|
||||
avatarDetail.classList.add('text-danger');
|
||||
avatarDetail.innerHTML = lang.get('settingsAvatarTooLarge');
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await client.me.changeAvatar(selectedFile);
|
||||
if (!error) {
|
||||
avatarDetail.classList.remove('text-danger');
|
||||
avatarDetail.classList.add('text-success');
|
||||
avatarDetail.innerHTML = lang.get('settingsAvatarSaved');
|
||||
setTimeout(_ => avatarDetail.innerHTML = '', 2000);
|
||||
document.getElementById('avatarDelete').classList.remove('d-none');
|
||||
document.getElementById('avatarUpload').classList.add('d-none');
|
||||
avatarInput.value = null;
|
||||
} else {
|
||||
avatarDetail.classList.remove('text-success');
|
||||
avatarDetail.classList.add('text-danger');
|
||||
avatarDetail.innerHTML = error.avatar[0];
|
||||
document.getElementById('avatarUpload').classList.add('d-none');
|
||||
avatarInput.value = null;
|
||||
console.log(error);
|
||||
}
|
||||
this.displayAvatar();
|
||||
}
|
||||
|
||||
async deleteAvatar() {
|
||||
const avatarDetail = document.getElementById('avatarDetail');
|
||||
|
||||
const error = await client.me.deleteAvatar();
|
||||
if (!error) {
|
||||
avatarDetail.classList.remove('text-danger');
|
||||
avatarDetail.classList.add('text-success');
|
||||
avatarDetail.innerHTML = lang.get('settingsAvatarDeleted');
|
||||
setTimeout(_ => avatarDetail.innerHTML = '', 2000);
|
||||
document.getElementById('avatarDelete').classList.add('d-none');
|
||||
} else {
|
||||
avatarDetail.classList.remove('text-success');
|
||||
avatarDetail.classList.add('text-danger');
|
||||
avatarDetail.innerHTML = lang.get('settingsAvatarDeleteError');
|
||||
}
|
||||
this.displayAvatar();
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
const passwordInput = document.getElementById('deleteInput');
|
||||
const password = passwordInput.value;
|
||||
const passwordDetail = document.getElementById('deleteDetail');
|
||||
|
||||
passwordInput.classList.remove('is-invalid');
|
||||
passwordDetail.innerHTML = '';
|
||||
|
||||
if (!password.length) {
|
||||
passwordInput.classList.add('is-invalid');
|
||||
passwordDetail.innerHTML = lang.get('errorEmptyField');
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await client.account.delete(password);
|
||||
if (!error) {
|
||||
passwordDetail.classList.replace('text-danger', 'text-success');
|
||||
passwordDetail.innerHTML = lang.get('settingsDeleteSuccess');
|
||||
setTimeout(_ => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
|
||||
navigateTo('/login');
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
passwordInput.classList.add('is-invalid');
|
||||
passwordDetail.innerHTML = error['password'];
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
const avatarUnchanged = client.me.avatar === '/static/avatars/default.avif';
|
||||
|
||||
return /* HTML */ `
|
||||
<div class='container-fluid col-lg-8 d-flex rounded border border-2 bg-light-subtle py-3'>
|
||||
<div class='row col-4 bg-body-tertiary border rounded p-2 m-2 d-flex justify-content-center align-items-center'>
|
||||
<h2 class='border-bottom'>${lang.get('settingsAvatarTitle')}</h2>
|
||||
<img id='avatar' class='rounded p-0' src=${client.me.avatar} style='cursor: pointer;'>
|
||||
<input id='avatarInput' class='d-none' type='file' accept='image/*'>
|
||||
<div class='d-flex gap-2 mt-1 px-0'>
|
||||
<span class='my-auto ms-1 me-auto' id='avatarDetail'></span>
|
||||
<button class='btn btn-primary d-none' id='avatarUpload'>${lang.get('settingsAvatarSave')}</button>
|
||||
<button class='btn btn-danger${avatarUnchanged ? ' d-none' : ''}' id='avatarDelete'>${lang.get('settingsAvatarDelete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h1 class='border-bottom ps-1 mb-3'>${lang.get('settingsTitle')}</h1>
|
||||
<div class='d-flex flex-column gap-2'>
|
||||
<div>
|
||||
<div class='input-group'>
|
||||
<div class='form-floating'>
|
||||
<input type='text' class='form-control' id='usernameInput' placeholder='${lang.get('settingsUsername')}' value=${client.me.username}>
|
||||
<label for='usernameInput'>${lang.get('settingsUsername')}</label>
|
||||
</div>
|
||||
<button class='input-group-text btn btn-success disabled' id='usernameSave'>${lang.get('settingsUsernameSave')}</button>
|
||||
</div>
|
||||
<span class='form-text' id='usernameDetail'></span>
|
||||
</div>
|
||||
<div class='flex-grow-1 p-1 border rounded bg-body-tertiary'>
|
||||
<h5 class='ps-1 border-bottom'>${lang.get('settingsPasswordTitle')}</h5>
|
||||
<div class='d-flex flex-column gap-1'>
|
||||
<div class='form-floating'>
|
||||
<input type='password' class='form-control' id='currentPasswordInput' placeholder='${lang.get('settingsCurrentPassword')}'>
|
||||
<label for='currentPasswordInput'>${lang.get('settingsCurrentPassword')}</label>
|
||||
</div>
|
||||
<span class='text-danger ps-1' id='currentPasswordDetail'></span>
|
||||
<div class='form-floating'>
|
||||
<input type='password' class='form-control' id='newPasswordInput' placeholder='${lang.get('settingsNewPassword')}'>
|
||||
<label for='newPassword2Input'>${lang.get('settingsNewPassword')}</label>
|
||||
</div>
|
||||
<span class='text-danger ps-1' id='newPasswordDetail'></span>
|
||||
<div class='form-floating'>
|
||||
<input type='password' class='form-control' id='newPassword2Input' placeholder='${'settingsRepeatNewPassword'}'>
|
||||
<label for='newPassword2Input'>${lang.get('settingsRepeatNewPassword')}</label>
|
||||
</div>
|
||||
<span class='text-danger ps-1' id='newPassword2Detail'></span>
|
||||
<div class='d-flex flex-row'>
|
||||
<span class='ps-1 my-auto text-danger' id='passwordDetail'></span>
|
||||
<button class='btn btn-success ms-auto' id='passwordSave'>${lang.get('settingsPasswordSave')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class='btn btn-outline-danger' data-bs-toggle='modal' data-bs-target='#deleteModal'>${lang.get('settingsDeleteButton')}</button>
|
||||
<div class='modal fade' id='deleteModal' tabindex='-1'>
|
||||
<div class='modal-dialog'>
|
||||
<div class='modal-content'>
|
||||
<div class='modal-header bg-danger bg-gradient'>
|
||||
<h1 class='modal-title fs-5'>${lang.get('settingsDeleteTitle')}</h1>
|
||||
<button class='btn-close' data-bs-dismiss='modal'></button>
|
||||
</div>
|
||||
<div class='modal-body'>
|
||||
<h6 class='form-label'>${lang.get('settingsDeleteConfirm')}</h6>
|
||||
<input type='password' class='form-control' id='deleteInput' placeholder='${lang.get('settingsDeleteInput')}'>
|
||||
<span class='form-text text-danger' id='deleteDetail'></span>
|
||||
</div>
|
||||
<div class='modal-footer'>
|
||||
<button type='button' class='btn btn-secondary' data-bs-dismiss='modal'>${lang.get('settingsDeleteCancel')}</button>
|
||||
<button type='button' class='btn btn-danger' id='deleteButton'>${lang.get('settingsDeleteButton')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
253
django/frontend/static/js/views/TicTacToeOfflineView.js
Normal file
253
django/frontend/static/js/views/TicTacToeOfflineView.js
Normal file
@ -0,0 +1,253 @@
|
||||
import { lang } from "../index.js";
|
||||
import AbstractView from "./abstracts/AbstractView.js";
|
||||
|
||||
export class TicTacToeOfflineView extends AbstractView
|
||||
{
|
||||
constructor(params)
|
||||
{
|
||||
super(params, lang.get('ticTacToe'));
|
||||
this.width = 510;
|
||||
this.height = 510;
|
||||
this.morpion = [[],[],[],[],[],[],[],[],[]];
|
||||
this.rectsize = 48;
|
||||
this.gap = 40;
|
||||
this.playerTurn = 0;
|
||||
this.playerOneCurrentTab = 4;
|
||||
this.playerTwoCurrentTab = 4;
|
||||
this.Winner = -1;
|
||||
}
|
||||
|
||||
async leavePage()
|
||||
{
|
||||
this.canvas.removeEventListener("mousedown", this.onClick, false);
|
||||
}
|
||||
|
||||
onClick(event)
|
||||
{
|
||||
let targetTabX, targetTabY, targetTab;
|
||||
let squareTab;
|
||||
let x = event.offsetX;
|
||||
let y = event.offsetY;
|
||||
|
||||
if (this.Winner != -1)
|
||||
return;
|
||||
|
||||
function findPlace(x, morpion)
|
||||
{
|
||||
if (x <= morpion.gap || x >= morpion.gap + morpion.rectsize * 9)
|
||||
return -1;
|
||||
if (x <= morpion.gap + morpion.rectsize * 3)
|
||||
return 0;
|
||||
if (x >= morpion.gap + morpion.rectsize * 3 && x <= morpion.gap + morpion.rectsize * 6)
|
||||
return 1;
|
||||
if (x >= morpion.gap + morpion.rectsize * 6)
|
||||
return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function drawNewCase(targetTab, squareTab, morpion)
|
||||
{
|
||||
let targetX = (morpion.gap + targetTab % 3 * morpion.rectsize * 3) + (squareTab % 3 * morpion.rectsize);
|
||||
let targetY = (morpion.gap + Math.floor(targetTab / 3) * morpion.rectsize * 3) + (Math.floor(squareTab / 3) * morpion.rectsize);
|
||||
if (!morpion.playerTurn)
|
||||
{
|
||||
morpion.ctx.beginPath();
|
||||
morpion.ctx.strokeStyle = "green";
|
||||
morpion.ctx.moveTo(targetX + 10, targetY + 10);
|
||||
morpion.ctx.lineTo(targetX + 40, targetY + 40);
|
||||
morpion.ctx.moveTo(targetX + 40, targetY + 10);
|
||||
morpion.ctx.lineTo(targetX + 10, targetY + 40);
|
||||
morpion.ctx.stroke();
|
||||
morpion.ctx.closePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
morpion.ctx.beginPath();
|
||||
morpion.ctx.strokeStyle = "red";
|
||||
targetX += morpion.rectsize / 2;
|
||||
targetY += morpion.rectsize / 2;
|
||||
morpion.ctx.arc(targetX, targetY, 15, 0, 2 * Math.PI);
|
||||
morpion.ctx.stroke();
|
||||
morpion.ctx.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
function PaintCurrentTab(morpion, currentTab, color)
|
||||
{
|
||||
let targetX = (morpion.gap + currentTab % 3 * morpion.rectsize * 3);
|
||||
let targetY = (morpion.gap + Math.floor(currentTab / 3) * morpion.rectsize * 3);
|
||||
|
||||
morpion.ctx.beginPath();
|
||||
morpion.ctx.strokeStyle = color;
|
||||
morpion.ctx.lineWidth = 6;
|
||||
morpion.ctx.strokeRect(targetX, targetY, morpion.rectsize * 3, morpion.rectsize * 3);
|
||||
morpion.ctx.closePath();
|
||||
|
||||
}
|
||||
|
||||
function highlightNewTab(morpion)
|
||||
{
|
||||
if (!morpion.playerTurn)
|
||||
PaintCurrentTab(morpion, morpion.playerOneCurrentTab, "green");
|
||||
else
|
||||
PaintCurrentTab(morpion, morpion.playerTwoCurrentTab, "red");
|
||||
}
|
||||
|
||||
function checkForWinningMove(morpion, targetTab)
|
||||
{
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < 3; i++)
|
||||
{
|
||||
if (morpion.morpion[targetTab][i] != -1 && (morpion.morpion[targetTab][i] === morpion.morpion[targetTab][i + 3] && morpion.morpion[targetTab][i + 3] === morpion.morpion[targetTab][i + 6]))
|
||||
morpion.Winner = !morpion.playerTurn;
|
||||
}
|
||||
for (let i = 0; i < morpion.morpion.length; i += 3)
|
||||
{
|
||||
if (morpion.morpion[targetTab][i] != -1 && (morpion.morpion[targetTab][i] === morpion.morpion[targetTab][i + 1] && morpion.morpion[targetTab][i + 1] === morpion.morpion[targetTab][i + 2]))
|
||||
morpion.Winner = !morpion.playerTurn;
|
||||
}
|
||||
if (morpion.morpion[targetTab][0] != -1 && (morpion.morpion[targetTab][0] === morpion.morpion[targetTab][4] && morpion.morpion[targetTab][4] === morpion.morpion[targetTab][8]))
|
||||
morpion.Winner = !morpion.playerTurn;
|
||||
if (morpion.morpion[targetTab][2] != -1 && (morpion.morpion[targetTab][2] === morpion.morpion[targetTab][4] && morpion.morpion[targetTab][4] === morpion.morpion[targetTab][6]))
|
||||
morpion.Winner = !morpion.playerTurn;
|
||||
morpion.morpion[targetTab].forEach((v) => (v !== -1 && count++));
|
||||
if (count == 9 && morpion.Winner == -1)
|
||||
morpion.Winner = morpion.playerTurn;
|
||||
}
|
||||
|
||||
function paintWinningNotification(morpion)
|
||||
{
|
||||
morpion.ctx.beginPath();
|
||||
morpion.ctx.fillStyle = "white";
|
||||
morpion.ctx.fillRect(morpion.width / 2 - 200, morpion.height - morpion.gap + 10, 400, 80);
|
||||
morpion.ctx.closePath();
|
||||
morpion.ctx.beginPath();
|
||||
morpion.ctx.fillStyle = (morpion.Winner) ? "red" : "green";
|
||||
morpion.ctx.fillText((morpion.Winner) ? "Winner is : O" : "Winner is : X", morpion.width / 2 - 30, morpion.height - morpion.gap / 2, 140);
|
||||
morpion.ctx.closePath();
|
||||
}
|
||||
|
||||
function updateMorpion(targetTab, squareTab, morpion)
|
||||
{
|
||||
if (morpion.playerTurn && targetTab != morpion.playerTwoCurrentTab)
|
||||
return -1;
|
||||
if (!morpion.playerTurn && targetTab != morpion.playerOneCurrentTab)
|
||||
return -1;
|
||||
if (morpion.morpion[targetTab][squareTab] == -1)
|
||||
morpion.morpion[targetTab][squareTab] = (morpion.playerTurn) ? 1 : 0;
|
||||
else
|
||||
return -1;
|
||||
drawNewCase(targetTab, squareTab, morpion);
|
||||
morpion.playerTurn = !morpion.playerTurn;
|
||||
PaintCurrentTab(morpion, targetTab, "white");
|
||||
if (!morpion.playerTurn)
|
||||
morpion.playerTwoCurrentTab = squareTab;
|
||||
else
|
||||
morpion.playerOneCurrentTab = squareTab;
|
||||
checkForWinningMove(morpion, targetTab);
|
||||
if (morpion.Winner != -1)
|
||||
{
|
||||
paintWinningNotification(morpion);
|
||||
return;
|
||||
}
|
||||
highlightNewTab(morpion);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findSquare(x, gap, morpion)
|
||||
{
|
||||
if (x <= gap + morpion.rectsize)
|
||||
return 0;
|
||||
if (x >= gap + morpion.rectsize && x <= gap + morpion.rectsize * 2)
|
||||
return 1;
|
||||
if (x >= gap + morpion.rectsize * 2)
|
||||
return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
targetTabX = findPlace(x, this);
|
||||
targetTabY = findPlace(y, this);
|
||||
if (targetTabX < 0 || targetTabY < 0)
|
||||
return -1;
|
||||
targetTab = targetTabX + targetTabY * 3;
|
||||
squareTab = findSquare(x, this.rectsize * 3 * targetTabX + this.gap, this) + findSquare(y, this.rectsize * 3 * targetTabY + this.gap, this) * 3;
|
||||
updateMorpion(targetTab, squareTab, this);
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
for (let i = 0; i < 9; i++)
|
||||
for (let j = 0; j < 9; j++)
|
||||
this.morpion[i].push(-1);
|
||||
this.canvas = document.getElementById("Morpion");
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
this.ctx.fillStyle = "black";
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
for (let i = 1, x = this.gap, y = this.gap; i <= 9; i++)
|
||||
{
|
||||
this.DrawMorpion(x, y);
|
||||
x += this.rectsize * 3;
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
y += this.rectsize * 3;
|
||||
x = this.gap;
|
||||
}
|
||||
}
|
||||
this.ctx.lineWidth = 6;
|
||||
for (let i = 0; i < 4; i++)
|
||||
{
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = `rgb(230 230 230)`;
|
||||
this.ctx.moveTo(this.gap + i * this.rectsize * 3, this.gap - 3);
|
||||
this.ctx.lineTo(this.gap + i * this.rectsize * 3, this.canvas.height - this.gap + 3);
|
||||
this.ctx.stroke();
|
||||
this.ctx.closePath();
|
||||
};
|
||||
for (let i = 0; i < 4; i++)
|
||||
{
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = `rgb(230 230 230)`;
|
||||
this.ctx.moveTo(this.gap, this.gap + i * this.rectsize * 3);
|
||||
this.ctx.lineTo(this.canvas.height - this.gap, this.gap + i * this.rectsize * 3);
|
||||
this.ctx.stroke();
|
||||
this.ctx.closePath();
|
||||
}
|
||||
this.canvas.addEventListener("mousedown", (event) => { this.onClick(event) }, false);
|
||||
}
|
||||
|
||||
DrawMorpion(start_x, start_y)
|
||||
{
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = `rgb(200 200 200)`;
|
||||
for (let i = 1, x = 0, y = 0; i <= 9; i++)
|
||||
{
|
||||
this.ctx.strokeRect(start_x + x, start_y + y, this.rectsize, this.rectsize);
|
||||
x += this.rectsize;
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
y += this.rectsize;
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
this.ctx.closePath();
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
return `
|
||||
<link rel="stylesheet" href="/static/css/tictactoe.css">
|
||||
<h1>${lang.get('ticTacToe')}</h1>
|
||||
<div style="display: flex">
|
||||
<canvas id="Morpion" width="${this.width}" height="${this.height}"></canvas>
|
||||
</div>
|
||||
<div id="rule">
|
||||
<h2 style="padding-left: 50px"><B>${lang.get('ruleTitle')}</B></h2>
|
||||
<h5 style="padding-left: 30px">${lang.get('ruleBase')}<br><br></h5>
|
||||
<h5 style="padding-left: 30px">${lang.get('ruleMovement')}<br><br></h5>
|
||||
<h5 style="padding-left: 30px">${lang.get('ruleDraw')}</h5>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
48
django/frontend/static/js/views/TicTacToeOnlineView.js
Normal file
48
django/frontend/static/js/views/TicTacToeOnlineView.js
Normal file
@ -0,0 +1,48 @@
|
||||
import AbstractView from "./abstracts/AbstractView.js";
|
||||
import { lang } from "../index.js";
|
||||
import { TicTacToe } from "../api/game/tictactoe/TicTacToeGame.js"
|
||||
|
||||
export class TicTacToeOnlineView extends AbstractView
|
||||
{
|
||||
constructor(params, titleKey)
|
||||
{
|
||||
super(params, lang.get('ticTacToeTitle'));
|
||||
this.params = params;
|
||||
this.titleKey = titleKey;
|
||||
this.game_id = params.id;
|
||||
this.height = 510;
|
||||
this.width = 510;
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
this.Morpion = new TicTacToe(this.height, this.width, 30, 50, document.getElementById("Morpion"), this.game_id);
|
||||
this.Morpion.DrawSuperMorpion();
|
||||
await this.Morpion.init();
|
||||
}
|
||||
|
||||
async leavePage()
|
||||
{
|
||||
this.Morpion.uninit();
|
||||
}
|
||||
|
||||
setTitle()
|
||||
{
|
||||
document.title = lang.get(this.titleKey, this.titleKey);
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return `
|
||||
<link rel="stylesheet" href="/static/css/tictactoe.css">
|
||||
<div id="canva">
|
||||
<canvas id="Morpion" width="${this.width}" height="${this.height}"></canvas>
|
||||
</div>
|
||||
<div id ="rule">
|
||||
<h2 style="padding-left: 50px"><B>${lang.get('ruleTitle')}</B></h2>
|
||||
<h5 style="padding-left: 30px">${lang.get('ruleBase')}<br><br></h5>
|
||||
<h5 style="padding-left: 30px">${lang.get('ruleMovement')}<br><br></h5>
|
||||
<h5 style="padding-left: 30px">${lang.get('ruleDraw')}</h5>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { client, navigateTo } from "../../index.js";
|
||||
import AbstractRedirectView from "./AbstractRedirectView.js";
|
||||
|
||||
export default class extends AbstractRedirectView{
|
||||
constructor(params, titleKey, uri = "/login") {
|
||||
super(params, titleKey, uri);
|
||||
}
|
||||
|
||||
async redirect()
|
||||
{
|
||||
if (await client.isAuthenticated() === false)
|
||||
{
|
||||
navigateTo(this.redirect_url);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { client, navigateTo } from "../../index.js";
|
||||
import AbstractRedirectView from "./AbstractRedirectView.js";
|
||||
|
||||
export default class extends AbstractRedirectView{
|
||||
constructor(params, titleKey, uri = "/home") {
|
||||
super(params, titleKey, uri);
|
||||
}
|
||||
|
||||
async redirect()
|
||||
{
|
||||
if (await client.isAuthenticated() === false)
|
||||
return 0;
|
||||
navigateTo(this.redirect_url);
|
||||
return 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { navigateTo } from "../../index.js";
|
||||
import AbstractView from "./AbstractView.js";
|
||||
|
||||
export default class extends AbstractView{
|
||||
constructor(params, titleKey, uri)
|
||||
{
|
||||
super(params, titleKey);
|
||||
this.redirect_url = uri;
|
||||
}
|
||||
|
||||
async redirect()
|
||||
{
|
||||
navigateTo(this.redirect_url);
|
||||
}
|
||||
}
|
22
django/frontend/static/js/views/abstracts/AbstractView.js
Normal file
22
django/frontend/static/js/views/abstracts/AbstractView.js
Normal file
@ -0,0 +1,22 @@
|
||||
import {lang} from '../../index.js';
|
||||
|
||||
export default class {
|
||||
constructor(params, titleKey) {
|
||||
this.params = params;
|
||||
this.titleKey = titleKey;
|
||||
}
|
||||
|
||||
async postInit() {
|
||||
}
|
||||
|
||||
async leavePage() {
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
document.title = lang.get(this.titleKey, this.titleKey);
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return "";
|
||||
}
|
||||
}
|
176
django/frontend/static/js/views/accounts/AuthenticationView.js
Normal file
176
django/frontend/static/js/views/accounts/AuthenticationView.js
Normal file
@ -0,0 +1,176 @@
|
||||
import { client, lang, navigateTo } from "../../index.js";
|
||||
import { clearIds, fill_errors } from "../../utils/formUtils.js";
|
||||
import AbstractNonAuthenticatedView from "../abstracts/AbstractNonAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractNonAuthenticatedView
|
||||
{
|
||||
constructor(params, lastUrlBeforeLogin = '/home')
|
||||
{
|
||||
super(params, 'loginWindowTitle', lastUrlBeforeLogin);
|
||||
this.redirect_url = lastUrlBeforeLogin;
|
||||
}
|
||||
|
||||
async leavePage()
|
||||
{
|
||||
this.current_mode = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async postInit()
|
||||
{
|
||||
let element = document.getElementById("toggle-register-login");
|
||||
|
||||
element.onclick = this.toggle_register_login.bind(this);
|
||||
|
||||
let new_mode = location.pathname.slice(1);
|
||||
this.update_mode(new_mode);
|
||||
|
||||
document.getElementById("button").onclick = this.authentication.bind(this);
|
||||
|
||||
let username_input = document.getElementById('username-input'),
|
||||
password_input = document.getElementById('password-input');
|
||||
|
||||
[username_input, password_input].forEach(input => {
|
||||
input.addEventListener('keydown', async ev => {
|
||||
if (ev.key === 'Enter')
|
||||
await this.authentication.bind(this)();
|
||||
});
|
||||
});
|
||||
username_input.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field is normal
|
||||
* @param username {String}
|
||||
* @param password {String}
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
basic_verif(username, password)
|
||||
{
|
||||
if (username === '')
|
||||
document.getElementById('username').innerHTML = lang.get('errorEmptyField', 'This field may not be blank.');
|
||||
|
||||
if (password === '')
|
||||
document.getElementById('password').innerHTML = lang.get('errorEmptyField', 'This field may not be blank.');
|
||||
|
||||
if (username === '' || password === '')
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns { undefined }
|
||||
*/
|
||||
toggle_register_login(event)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
let new_mode = this.current_mode === "register" ? "login" : "register";
|
||||
|
||||
this.update_mode(new_mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} new_mode
|
||||
*/
|
||||
update_mode(new_mode)
|
||||
{
|
||||
if (new_mode === this.current_mode)
|
||||
return;
|
||||
|
||||
this.current_mode = new_mode;
|
||||
|
||||
let title = document.getElementById("title"),
|
||||
username_label = document.getElementById("username-label"),
|
||||
password_label = document.getElementById("password-label"),
|
||||
toggle_register_login = document.getElementById("toggle-register-login"),
|
||||
toggle_register_login_label = document.getElementById("toggle-register-login-label"),
|
||||
button = document.getElementById("button")
|
||||
;
|
||||
|
||||
let title_text = this.current_mode === "register" ? "registerFormTitle" : "loginFormTitle";
|
||||
title.innerText = lang.get(title_text, "ERROR LANG");
|
||||
|
||||
let username_label_text = this.current_mode === "register" ? "registerFormUsername" : "loginFormUsername";
|
||||
username_label.innerText = lang.get(username_label_text, "ERROR LANG");
|
||||
|
||||
let password_label_text = this.current_mode === "register" ? "registerFormPassword" : "loginFormPassword";
|
||||
password_label.innerText = lang.get(password_label_text, "ERROR LANG");
|
||||
|
||||
let toggle_register_login_label_text = this.current_mode === "register" ? "registerAlreadyAccount" : "loginNoAccount";
|
||||
toggle_register_login_label.innerText = lang.get(toggle_register_login_label_text, "ERROR LANG");
|
||||
|
||||
let toggle_register_login_text = this.current_mode === "register" ? "registerLogin" : "loginRegister";
|
||||
toggle_register_login.innerText = lang.get(toggle_register_login_text, "ERROR LANG");
|
||||
|
||||
let button_text = this.current_mode === "register" ? "registerFormButton" : "loginFormButton";
|
||||
button.innerText = lang.get(button_text, "ERROR LANG");
|
||||
|
||||
this.titleKey = this.current_mode === 'register' ? 'registerWindowTitle' : 'loginWindowTitle';
|
||||
this.setTitle();
|
||||
|
||||
document.getElementById('username-input').focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async authentication()
|
||||
{
|
||||
let username = document.getElementById("username-input").value,
|
||||
password = document.getElementById("password-input").value;
|
||||
|
||||
if (!this.basic_verif())
|
||||
return;
|
||||
|
||||
let response;
|
||||
|
||||
if (this.current_mode === "register")
|
||||
response = await client.account.create(username, password);
|
||||
else
|
||||
response = await client.login(username, password);
|
||||
|
||||
if (response.status === 200 || response.status === 201)
|
||||
{
|
||||
navigateTo(this.redirect_url);
|
||||
return;
|
||||
}
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
clearIds("innerHTML", ["username", "password", 'login']);
|
||||
fill_errors(response_data, "innerHTML");
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
return /* HTML */ `
|
||||
<div class='container-fluid'>
|
||||
<form class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
|
||||
<h4 class='text-center fw-semibold mb-4' id="title">Loading...</h4>
|
||||
<div class='form-floating mb-2'>
|
||||
<input type='text' class='form-control' id='username-input' placeholder='Username'>
|
||||
<label for='usernameInput' id='username-label'>Loading...</label>
|
||||
<span class='text-danger' id='username'></span>
|
||||
</div>
|
||||
<div class='form-floating'>
|
||||
<input type='password' class='form-control' id='password-input' placeholder='Password'>
|
||||
<label for='password-input' id='password-label'>Loading...</label>
|
||||
<span class='text-danger' id='password'></span>
|
||||
</div>
|
||||
<div class='d-flex'>
|
||||
<button type='button' class='btn btn-primary mt-3 mb-2' id='button'>Loading...</button>
|
||||
<span class='text-danger my-auto mx-2' id='login'></span>
|
||||
<div class='ms-auto mt-auto flex-row d-flex gap-2' id="toggle">
|
||||
<p id='toggle-register-login-label'>Loading...</p>
|
||||
<a id="toggle-register-login" href='#'>Loading...</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
15
django/frontend/static/js/views/accounts/LogoutView.js
Normal file
15
django/frontend/static/js/views/accounts/LogoutView.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { client, navigateTo } from "../../index.js";
|
||||
import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractAuthenticatedView
|
||||
{
|
||||
constructor(params, lastPageUrl = '/login') {
|
||||
super(params, 'logoutWindowTitle', lastPageUrl);
|
||||
this.lastPageUrl = lastPageUrl;
|
||||
}
|
||||
|
||||
async postInit() {
|
||||
await client.logout();
|
||||
navigateTo(this.lastPageUrl);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user