Игровая логика PvP: сервер (C++) + клиент (Quintus.js)
Сервер: Game — authoritative-тик 20 Гц через uSockets timer, движение/коллизии танков и пуль, разрушаемые кирпичные стены, respawn, победа/поражение/ничья, технический проигрыш при дисконнекте. game.ready запускает матч, когда оба игрока в pvp-комнате готовы. Клиент: заведён Quintus.js (MIT, vendored) — Tank/Bullet/Wall спрайты, рендер по снапшотам game.state, keyboard-инпут (WASD/стрелки/space), экран результата матча. Найден и исправлен use-after-free: колбэк onFinished_ синхронно удалял Game изнутри его же метода — перенесено на uWS::Loop::defer.
This commit is contained in:
@@ -88,3 +88,34 @@
|
|||||||
content: '- ';
|
content: '- ';
|
||||||
color: var(--color-accent-dim);
|
color: var(--color-accent-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#battlecity-canvas-container {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#battlecity-canvas-container canvas {
|
||||||
|
border: var(--border);
|
||||||
|
background: var(--color-bg);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#battle-result {
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#battle-back {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-accent-dim);
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#battle-back:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 6px var(--color-accent-dim);
|
||||||
|
}
|
||||||
|
|||||||
+7
-2
@@ -25,13 +25,18 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="game-root">
|
<main id="game-root">
|
||||||
<!-- TODO: лобби (этап 3) и игровое поле на Quintus.js (этапы 4-5) -->
|
<!-- TODO: coop-режим с AI и защитой базы — этап 5 -->
|
||||||
<h2 class="prompt">./battlecity</h2>
|
<h2 class="prompt">./battlecity</h2>
|
||||||
<div id="lobby"></div>
|
<div id="lobby"></div>
|
||||||
<div id="battlecity-canvas-container"></div>
|
<div id="battlecity-canvas-container" style="display: none"></div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/vendor/quintus/quintus.js"></script>
|
||||||
|
<script src="/vendor/quintus/quintus_2d.js"></script>
|
||||||
|
<script src="/vendor/quintus/quintus_sprites.js"></script>
|
||||||
|
<script src="/vendor/quintus/quintus_scenes.js"></script>
|
||||||
|
<script src="/vendor/quintus/quintus_input.js"></script>
|
||||||
<script type="module" src="/js/matrix-rain.js"></script>
|
<script type="module" src="/js/matrix-rain.js"></script>
|
||||||
<script type="module" src="/js/game/main.js"></script>
|
<script type="module" src="/js/game/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { ClientMessage, ServerMessage } from './protocol.js';
|
||||||
|
|
||||||
|
const TILE_PX = 32;
|
||||||
|
// Quintus транслирует и вращает контекст сам (Sprite.render -> matrix.setContextTransform)
|
||||||
|
// на основе p.x/p.y/p.angle — draw() получает уже трансформированный контекст
|
||||||
|
// и должен рисовать спрайт центрированным в (0,0), без своих translate/rotate.
|
||||||
|
const DIR_DEGREES = { up: 0, right: 90, down: 180, left: 270 };
|
||||||
|
|
||||||
|
let Q = null;
|
||||||
|
let net = null;
|
||||||
|
let myPlayerId = null;
|
||||||
|
let stage = null;
|
||||||
|
let tankSprites = new Map();
|
||||||
|
let bulletSprites = new Map();
|
||||||
|
let lastSentInput = null;
|
||||||
|
let seq = 0;
|
||||||
|
let polling = false;
|
||||||
|
|
||||||
|
function setupEntities() {
|
||||||
|
Q.Sprite.extend('Tank', {
|
||||||
|
init(p) {
|
||||||
|
this._super(p, { w: 26, h: 26, renderAlways: true });
|
||||||
|
},
|
||||||
|
draw(ctx) {
|
||||||
|
const p = this.p;
|
||||||
|
ctx.fillStyle = p.isMe ? '#00ff41' : '#9fa3a0';
|
||||||
|
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||||
|
ctx.fillStyle = '#0a0e0c';
|
||||||
|
ctx.fillRect(-3, -p.h / 2 - 8, 6, 12);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Q.Sprite.extend('Bullet', {
|
||||||
|
init(p) {
|
||||||
|
this._super(p, { w: 8, h: 8, renderAlways: true });
|
||||||
|
},
|
||||||
|
draw(ctx) {
|
||||||
|
ctx.fillStyle = '#c7c7c7';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0, 0, this.p.w / 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Q.Sprite.extend('Wall', {
|
||||||
|
init(p) {
|
||||||
|
this._super(p, { w: TILE_PX, h: TILE_PX });
|
||||||
|
},
|
||||||
|
draw(ctx) {
|
||||||
|
const p = this.p;
|
||||||
|
if (p.tile === 2) {
|
||||||
|
ctx.fillStyle = '#9fa3a0';
|
||||||
|
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = '#0a8f2c';
|
||||||
|
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||||
|
ctx.strokeStyle = '#0a0e0c';
|
||||||
|
ctx.strokeRect(-p.w / 2 + 1, -p.h / 2 + 1, p.w - 2, p.h - 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureQuintus(cols, rows) {
|
||||||
|
if (Q) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Q = window.Quintus().include('Sprites, Scenes, 2D, Input').setup({
|
||||||
|
width: cols * TILE_PX,
|
||||||
|
height: rows * TILE_PX,
|
||||||
|
});
|
||||||
|
Q.input.keyboardControls({
|
||||||
|
LEFT: 'left', RIGHT: 'right', UP: 'up', DOWN: 'down',
|
||||||
|
A: 'left', D: 'right', W: 'up', S: 'down',
|
||||||
|
SPACE: 'fire',
|
||||||
|
});
|
||||||
|
setupEntities();
|
||||||
|
Q.scene('battle', (stageRef) => {
|
||||||
|
stage = stageRef;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('battlecity-canvas-container').appendChild(Q.el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMap(map) {
|
||||||
|
for (let y = 0; y < map.length; y++) {
|
||||||
|
for (let x = 0; x < map[y].length; x++) {
|
||||||
|
const tile = map[y][x];
|
||||||
|
if (tile === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stage.insert(
|
||||||
|
new Q.Wall({
|
||||||
|
x: x * TILE_PX + TILE_PX / 2,
|
||||||
|
y: y * TILE_PX + TILE_PX / 2,
|
||||||
|
tile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollInput() {
|
||||||
|
if (!polling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const input = {
|
||||||
|
seq: seq++,
|
||||||
|
up: !!Q.inputs.up,
|
||||||
|
down: !!Q.inputs.down,
|
||||||
|
left: !!Q.inputs.left,
|
||||||
|
right: !!Q.inputs.right,
|
||||||
|
fire: !!Q.inputs.fire,
|
||||||
|
};
|
||||||
|
const key = `${input.up}${input.down}${input.left}${input.right}${input.fire}`;
|
||||||
|
if (key !== lastSentInput) {
|
||||||
|
lastSentInput = key;
|
||||||
|
net.send(ClientMessage.GAME_INPUT, input);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(pollInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onState(payload) {
|
||||||
|
for (const t of payload.tanks) {
|
||||||
|
let sprite = tankSprites.get(t.id);
|
||||||
|
if (!sprite) {
|
||||||
|
sprite = new Q.Tank({ x: t.x * TILE_PX, y: t.y * TILE_PX, isMe: t.id === myPlayerId });
|
||||||
|
stage.insert(sprite);
|
||||||
|
tankSprites.set(t.id, sprite);
|
||||||
|
}
|
||||||
|
sprite.p.x = t.x * TILE_PX;
|
||||||
|
sprite.p.y = t.y * TILE_PX;
|
||||||
|
sprite.p.angle = DIR_DEGREES[t.direction] || 0;
|
||||||
|
sprite.p.hidden = !t.alive;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenBullets = new Set();
|
||||||
|
for (const b of payload.bullets) {
|
||||||
|
seenBullets.add(b.id);
|
||||||
|
let sprite = bulletSprites.get(b.id);
|
||||||
|
if (!sprite) {
|
||||||
|
sprite = new Q.Bullet({ x: b.x * TILE_PX, y: b.y * TILE_PX });
|
||||||
|
stage.insert(sprite);
|
||||||
|
bulletSprites.set(b.id, sprite);
|
||||||
|
}
|
||||||
|
sprite.p.x = b.x * TILE_PX;
|
||||||
|
sprite.p.y = b.y * TILE_PX;
|
||||||
|
}
|
||||||
|
for (const [id, sprite] of bulletSprites) {
|
||||||
|
if (!seenBullets.has(id)) {
|
||||||
|
sprite.destroy();
|
||||||
|
bulletSprites.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOver(payload) {
|
||||||
|
polling = false;
|
||||||
|
const resultText = { win: 'Победа', lose: 'Поражение', draw: 'Ничья' }[payload.result] || payload.result;
|
||||||
|
const resultEl = document.getElementById('battle-result');
|
||||||
|
resultEl.textContent = `${resultText} — ${payload.reason}`;
|
||||||
|
document.getElementById('battle-back').style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBattle(networkInstance, payload, ownPlayerId) {
|
||||||
|
net = networkInstance;
|
||||||
|
myPlayerId = ownPlayerId;
|
||||||
|
|
||||||
|
const rows = payload.map.length;
|
||||||
|
const cols = payload.map[0].length;
|
||||||
|
|
||||||
|
document.getElementById('lobby').style.display = 'none';
|
||||||
|
const container = document.getElementById('battlecity-canvas-container');
|
||||||
|
container.style.display = 'block';
|
||||||
|
container.innerHTML = '<p id="battle-result"></p><button id="battle-back" style="display:none">выйти в лобби</button>';
|
||||||
|
|
||||||
|
ensureQuintus(cols, rows);
|
||||||
|
Q.stageScene('battle', 0);
|
||||||
|
tankSprites = new Map();
|
||||||
|
bulletSprites = new Map();
|
||||||
|
|
||||||
|
buildMap(payload.map);
|
||||||
|
|
||||||
|
net.on(ServerMessage.GAME_STATE, onState);
|
||||||
|
net.on(ServerMessage.GAME_OVER, onOver);
|
||||||
|
|
||||||
|
lastSentInput = null;
|
||||||
|
seq = 0;
|
||||||
|
polling = true;
|
||||||
|
requestAnimationFrame(pollInput);
|
||||||
|
|
||||||
|
document.getElementById('battle-back').addEventListener('click', () => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Network, ServerMessage } from './network.js';
|
import { Network, ServerMessage } from './network.js';
|
||||||
|
import { ClientMessage } from './protocol.js';
|
||||||
|
import { startBattle } from './battle.js';
|
||||||
|
|
||||||
const container = document.getElementById('lobby');
|
const container = document.getElementById('lobby');
|
||||||
|
|
||||||
let net;
|
let net;
|
||||||
|
let myPlayerId = null;
|
||||||
let rooms = [];
|
let rooms = [];
|
||||||
let currentRoom = null;
|
let currentRoom = null;
|
||||||
let errorMessage = '';
|
let errorMessage = '';
|
||||||
@@ -47,14 +50,19 @@ function renderLobbyView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderRoomView() {
|
function renderRoomView() {
|
||||||
|
const readyBtn =
|
||||||
|
currentRoom.mode === 'pvp'
|
||||||
|
? '<button data-action="ready">готов</button>'
|
||||||
|
: '<p class="lobby-error">coop ещё не реализован (этап 5)</p>';
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="lobby-panel">
|
<div class="lobby-panel">
|
||||||
${errorBanner()}
|
${errorBanner()}
|
||||||
<h3 class="prompt">room ${currentRoom.name}</h3>
|
<h3 class="prompt">room ${currentRoom.name}</h3>
|
||||||
<p>режим: <span class="tag">${currentRoom.mode}</span></p>
|
<p>режим: <span class="tag">${currentRoom.mode}</span></p>
|
||||||
<ul class="room-players">
|
<ul class="room-players">
|
||||||
${currentRoom.players.map((p) => `<li>${p.nickname}</li>`).join('')}
|
${currentRoom.players.map((p) => `<li>${p.nickname}${p.ready ? ' — готов' : ''}</li>`).join('')}
|
||||||
</ul>
|
</ul>
|
||||||
|
${readyBtn}
|
||||||
<button data-action="leave">выйти в лобби</button>
|
<button data-action="leave">выйти в лобби</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -78,6 +86,9 @@ container?.addEventListener('click', (e) => {
|
|||||||
case 'join':
|
case 'join':
|
||||||
net.joinRoom(btn.dataset.roomId);
|
net.joinRoom(btn.dataset.roomId);
|
||||||
break;
|
break;
|
||||||
|
case 'ready':
|
||||||
|
net.send(ClientMessage.GAME_READY);
|
||||||
|
break;
|
||||||
case 'leave':
|
case 'leave':
|
||||||
currentRoom = null;
|
currentRoom = null;
|
||||||
net.leaveRoom();
|
net.leaveRoom();
|
||||||
@@ -99,8 +110,9 @@ export function initLobby() {
|
|||||||
net = new Network();
|
net = new Network();
|
||||||
|
|
||||||
net.on('_open', () => net.hello(nickname));
|
net.on('_open', () => net.hello(nickname));
|
||||||
net.on(ServerMessage.HELLO_ACK, () => {
|
net.on(ServerMessage.HELLO_ACK, (payload) => {
|
||||||
connected = true;
|
connected = true;
|
||||||
|
myPlayerId = payload.player_id;
|
||||||
net.listRooms();
|
net.listRooms();
|
||||||
});
|
});
|
||||||
net.on('_close', () => {
|
net.on('_close', () => {
|
||||||
@@ -125,6 +137,10 @@ export function initLobby() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
net.on(ServerMessage.GAME_START, (payload) => {
|
||||||
|
startBattle(net, payload, myPlayerId);
|
||||||
|
});
|
||||||
|
|
||||||
net.on(ServerMessage.ERROR, (payload) => {
|
net.on(ServerMessage.ERROR, (payload) => {
|
||||||
console.error('[gameserver] error:', payload);
|
console.error('[gameserver] error:', payload);
|
||||||
errorMessage = payload.message;
|
errorMessage = payload.message;
|
||||||
|
|||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2011 Cykod LLC, http://coderdeck.com/
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
Vendored
+2292
File diff suppressed because it is too large
Load Diff
+550
@@ -0,0 +1,550 @@
|
|||||||
|
/*global Quintus:false, module:false */
|
||||||
|
|
||||||
|
var quintus2D = function(Quintus) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
Quintus["2D"] = function(Q) {
|
||||||
|
|
||||||
|
Q.component('viewport',{
|
||||||
|
added: function() {
|
||||||
|
this.entity.on('prerender',this,'prerender');
|
||||||
|
this.entity.on('render',this,'postrender');
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
this.offsetX = 0;
|
||||||
|
this.offsetY = 0;
|
||||||
|
this.centerX = Q.width/2;
|
||||||
|
this.centerY = Q.height/2;
|
||||||
|
this.scale = 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
extend: {
|
||||||
|
follow: function(sprite,directions,boundingBox) {
|
||||||
|
this.off('poststep',this.viewport,'follow');
|
||||||
|
this.viewport.directions = directions || { x: true, y: true };
|
||||||
|
this.viewport.following = sprite;
|
||||||
|
if(Q._isUndefined(boundingBox) && this.lists.TileLayer !== undefined) {
|
||||||
|
this.viewport.boundingBox = Q._detect(this.lists.TileLayer, function(layer) {
|
||||||
|
return layer.p.boundingBox ? { minX: 0, maxX: layer.p.w, minY: 0, maxY: layer.p.h } : null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.viewport.boundingBox = boundingBox;
|
||||||
|
}
|
||||||
|
this.on('poststep',this.viewport,'follow');
|
||||||
|
this.viewport.follow(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
unfollow: function() {
|
||||||
|
this.off('poststep',this.viewport,'follow');
|
||||||
|
},
|
||||||
|
|
||||||
|
centerOn: function(x,y) {
|
||||||
|
this.viewport.centerOn(x,y);
|
||||||
|
},
|
||||||
|
|
||||||
|
moveTo: function(x,y) {
|
||||||
|
return this.viewport.moveTo(x,y);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
follow: function(first) {
|
||||||
|
var followX = Q._isFunction(this.directions.x) ? this.directions.x(this.following) : this.directions.x;
|
||||||
|
var followY = Q._isFunction(this.directions.y) ? this.directions.y(this.following) : this.directions.y;
|
||||||
|
|
||||||
|
this[first === true ? 'centerOn' : 'softCenterOn'](
|
||||||
|
followX ?
|
||||||
|
this.following.p.x - this.offsetX :
|
||||||
|
undefined,
|
||||||
|
followY ?
|
||||||
|
this.following.p.y - this.offsetY :
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
offset: function(x,y) {
|
||||||
|
this.offsetX = x;
|
||||||
|
this.offsetY = y;
|
||||||
|
},
|
||||||
|
|
||||||
|
softCenterOn: function(x,y) {
|
||||||
|
if(x !== void 0) {
|
||||||
|
var dx = (x - Q.width / 2 / this.scale - this.x)/3;
|
||||||
|
if(this.boundingBox) {
|
||||||
|
if(this.x + dx < this.boundingBox.minX) {
|
||||||
|
this.x = this.boundingBox.minX / this.scale;
|
||||||
|
}
|
||||||
|
else if(this.x + dx > (this.boundingBox.maxX - Q.width) / this.scale) {
|
||||||
|
this.x = Math.max(this.boundingBox.maxX - Q.width, this.boundingBox.minX) / this.scale;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.x += dx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.x += dx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(y !== void 0) {
|
||||||
|
var dy = (y - Q.height / 2 / this.scale - this.y)/3;
|
||||||
|
if(this.boundingBox) {
|
||||||
|
if(this.y + dy < this.boundingBox.minY) {
|
||||||
|
this.y = this.boundingBox.minY / this.scale;
|
||||||
|
}
|
||||||
|
else if(this.y + dy > (this.boundingBox.maxY - Q.height) / this.scale) {
|
||||||
|
this.y = Math.max(this.boundingBox.maxY - Q.height, this.boundingBox.minY) / this.scale;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.y += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.y += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
centerOn: function(x,y) {
|
||||||
|
if(x !== void 0) {
|
||||||
|
this.x = x - Q.width / 2 / this.scale;
|
||||||
|
}
|
||||||
|
if(y !== void 0) {
|
||||||
|
this.y = y - Q.height / 2 / this.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
moveTo: function(x,y) {
|
||||||
|
if(x !== void 0) {
|
||||||
|
this.x = x;
|
||||||
|
}
|
||||||
|
if(y !== void 0) {
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
return this.entity;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
prerender: function() {
|
||||||
|
this.centerX = this.x + Q.width / 2 /this.scale;
|
||||||
|
this.centerY = this.y + Q.height / 2 /this.scale;
|
||||||
|
Q.ctx.save();
|
||||||
|
Q.ctx.translate(Math.floor(Q.width/2),Math.floor(Q.height/2));
|
||||||
|
Q.ctx.scale(this.scale,this.scale);
|
||||||
|
Q.ctx.translate(-Math.floor(this.centerX), -Math.floor(this.centerY));
|
||||||
|
},
|
||||||
|
|
||||||
|
postrender: function() {
|
||||||
|
Q.ctx.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Q.Sprite.extend("TileLayer",{
|
||||||
|
|
||||||
|
init: function(props) {
|
||||||
|
this._super(props,{
|
||||||
|
tileW: 32,
|
||||||
|
tileH: 32,
|
||||||
|
blockTileW: 10,
|
||||||
|
blockTileH: 10,
|
||||||
|
type: 1,
|
||||||
|
renderAlways: true
|
||||||
|
});
|
||||||
|
if(this.p.dataAsset) {
|
||||||
|
this.load(this.p.dataAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setDimensions();
|
||||||
|
|
||||||
|
this.blocks = [];
|
||||||
|
this.p.blockW = this.p.tileW * this.p.blockTileW;
|
||||||
|
this.p.blockH = this.p.tileH * this.p.blockTileH;
|
||||||
|
this.colBounds = {};
|
||||||
|
this.directions = [ 'top','left','right','bottom'];
|
||||||
|
this.tileProperties = {};
|
||||||
|
|
||||||
|
this.collisionObject = {
|
||||||
|
p: {
|
||||||
|
w: this.p.tileW,
|
||||||
|
h: this.p.tileH,
|
||||||
|
cx: this.p.tileW/2,
|
||||||
|
cy: this.p.tileH/2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tileCollisionObjects = {};
|
||||||
|
|
||||||
|
this.collisionNormal = { separate: []};
|
||||||
|
|
||||||
|
this._generateCollisionObjects();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Generate the tileCollisionObject overrides where needed
|
||||||
|
_generateCollisionObjects: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function returnPoint(pt) {
|
||||||
|
return [ pt[0] * self.p.tileW - self.p.tileW/2,
|
||||||
|
pt[1] * self.p.tileH - self.p.tileH/2
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.sheet() && this.sheet().frameProperties) {
|
||||||
|
var frameProperties = this.sheet().frameProperties;
|
||||||
|
for(var k in frameProperties) {
|
||||||
|
var colObj = this.tileCollisionObjects[k] = { p: Q._clone(this.collisionObject.p) };
|
||||||
|
Q._extend(colObj.p,frameProperties[k]);
|
||||||
|
|
||||||
|
if(colObj.p.points) {
|
||||||
|
colObj.p.points = Q._map(colObj.p.points, returnPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tileCollisionObjects[k] = colObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
load: function(dataAsset) {
|
||||||
|
var fileParts = dataAsset.split("."),
|
||||||
|
fileExt = fileParts[fileParts.length-1].toLowerCase(),
|
||||||
|
data;
|
||||||
|
|
||||||
|
if (fileExt === "json") {
|
||||||
|
data = Q._isString(dataAsset) ? Q.asset(dataAsset) : dataAsset;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw "file type not supported";
|
||||||
|
}
|
||||||
|
this.p.tiles = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
setDimensions: function() {
|
||||||
|
var tiles = this.p.tiles;
|
||||||
|
|
||||||
|
if(tiles) {
|
||||||
|
this.p.rows = tiles.length;
|
||||||
|
this.p.cols = tiles[0].length;
|
||||||
|
this.p.w = this.p.cols * this.p.tileW;
|
||||||
|
this.p.h = this.p.rows * this.p.tileH;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getTile: function(tileX,tileY) {
|
||||||
|
return this.p.tiles[tileY] && this.p.tiles[tileY][tileX];
|
||||||
|
},
|
||||||
|
|
||||||
|
getTileProperty: function(tile, prop) {
|
||||||
|
if(this.tileProperties[tile] !== undefined) {
|
||||||
|
return this.tileProperties[tile][prop];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getTileProperties: function(tile) {
|
||||||
|
if(this.tileProperties[tile] !== undefined) {
|
||||||
|
return this.tileProperties[tile];
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getTilePropertyAt: function(tileX, tileY, prop) {
|
||||||
|
return this.getTileProperty(this.getTile(tileX, tileY), prop);
|
||||||
|
},
|
||||||
|
|
||||||
|
getTilePropertiesAt: function(tileX, tileY) {
|
||||||
|
return this.getTileProperties(this.getTile(tileX, tileY));
|
||||||
|
},
|
||||||
|
|
||||||
|
tileHasProperty: function(tile, prop) {
|
||||||
|
return(this.getTileProperty(tile, prop) !== undefined);
|
||||||
|
},
|
||||||
|
|
||||||
|
setTile: function(x,y,tile) {
|
||||||
|
var p = this.p,
|
||||||
|
blockX = Math.floor(x/p.blockTileW),
|
||||||
|
blockY = Math.floor(y/p.blockTileH);
|
||||||
|
|
||||||
|
if(x >= 0 && x < this.p.cols &&
|
||||||
|
y >= 0 && y < this.p.rows) {
|
||||||
|
|
||||||
|
this.p.tiles[y][x] = tile;
|
||||||
|
|
||||||
|
if(this.blocks[blockY]) {
|
||||||
|
this.blocks[blockY][blockX] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tilePresent: function(tileX,tileY) {
|
||||||
|
return this.p.tiles[tileY] && this.collidableTile(this.p.tiles[tileY][tileX]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Overload this method to draw tiles at frame 0 or not draw
|
||||||
|
// tiles at higher number frames
|
||||||
|
drawableTile: function(tileNum) {
|
||||||
|
return tileNum > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Overload this method to control which tiles trigger a collision
|
||||||
|
// (defaults to all tiles > number 0)
|
||||||
|
collidableTile: function(tileNum) {
|
||||||
|
return tileNum > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getCollisionObject: function(tileX, tileY) {
|
||||||
|
var p = this.p,
|
||||||
|
tile = this.getTile(tileX, tileY),
|
||||||
|
colObj;
|
||||||
|
|
||||||
|
colObj = (this.tileCollisionObjects[tile] !== undefined) ?
|
||||||
|
this.tileCollisionObjects[tile] : this.collisionObject;
|
||||||
|
|
||||||
|
colObj.p.x = tileX * p.tileW + p.x + p.tileW/2;
|
||||||
|
colObj.p.y = tileY * p.tileH + p.y + p.tileH/2;
|
||||||
|
|
||||||
|
return colObj;
|
||||||
|
},
|
||||||
|
|
||||||
|
collide: function(obj) {
|
||||||
|
var p = this.p,
|
||||||
|
objP = obj.c || obj.p,
|
||||||
|
tileStartX = Math.floor((objP.x - objP.cx - p.x) / p.tileW),
|
||||||
|
tileStartY = Math.floor((objP.y - objP.cy - p.y) / p.tileH),
|
||||||
|
tileEndX = Math.ceil((objP.x - objP.cx + objP.w - p.x) / p.tileW),
|
||||||
|
tileEndY = Math.ceil((objP.y - objP.cy + objP.h - p.y) / p.tileH),
|
||||||
|
normal = this.collisionNormal,
|
||||||
|
col, colObj;
|
||||||
|
|
||||||
|
normal.collided = false;
|
||||||
|
|
||||||
|
for(var tileY = tileStartY; tileY<=tileEndY; tileY++) {
|
||||||
|
for(var tileX = tileStartX; tileX<=tileEndX; tileX++) {
|
||||||
|
if(this.tilePresent(tileX,tileY)) {
|
||||||
|
colObj = this.getCollisionObject(tileX, tileY);
|
||||||
|
|
||||||
|
col = Q.collision(obj,colObj);
|
||||||
|
|
||||||
|
if(col && col.magnitude > 0) {
|
||||||
|
if(colObj.p.sensor) {
|
||||||
|
colObj.tile = this.getTile(tileX,tileY);
|
||||||
|
if(obj.trigger) {
|
||||||
|
obj.trigger('sensor.tile',colObj);
|
||||||
|
}
|
||||||
|
} else if(!normal.collided || normal.magnitude < col.magnitude ) {
|
||||||
|
normal.collided = true;
|
||||||
|
normal.separate[0] = col.separate[0];
|
||||||
|
normal.separate[1] = col.separate[1];
|
||||||
|
normal.magnitude = col.magnitude;
|
||||||
|
normal.distance = col.distance;
|
||||||
|
normal.normalX = col.normalX;
|
||||||
|
normal.normalY = col.normalY;
|
||||||
|
normal.tileX = tileX;
|
||||||
|
normal.tileY = tileY;
|
||||||
|
normal.tile = this.getTile(tileX,tileY);
|
||||||
|
if(obj.p.collisions !== undefined) {
|
||||||
|
obj.p.collisions.push(normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normal.collided ? normal : false;
|
||||||
|
},
|
||||||
|
|
||||||
|
prerenderBlock: function(blockX,blockY) {
|
||||||
|
var p = this.p,
|
||||||
|
tiles = p.tiles,
|
||||||
|
sheet = this.sheet(),
|
||||||
|
blockOffsetX = blockX*p.blockTileW,
|
||||||
|
blockOffsetY = blockY*p.blockTileH;
|
||||||
|
|
||||||
|
if(blockOffsetX < 0 || blockOffsetX >= this.p.cols ||
|
||||||
|
blockOffsetY < 0 || blockOffsetY >= this.p.rows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas'),
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
canvas.width = p.blockW;
|
||||||
|
canvas.height= p.blockH;
|
||||||
|
this.blocks[blockY] = this.blocks[blockY] || {};
|
||||||
|
this.blocks[blockY][blockX] = canvas;
|
||||||
|
|
||||||
|
for(var y=0;y<p.blockTileH;y++) {
|
||||||
|
if(tiles[y+blockOffsetY]) {
|
||||||
|
for(var x=0;x<p.blockTileW;x++) {
|
||||||
|
if(this.drawableTile(tiles[y+blockOffsetY][x+blockOffsetX])) {
|
||||||
|
sheet.draw(ctx,
|
||||||
|
x*p.tileW,
|
||||||
|
y*p.tileH,
|
||||||
|
tiles[y+blockOffsetY][x+blockOffsetX]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
drawBlock: function(ctx, blockX, blockY) {
|
||||||
|
var p = this.p,
|
||||||
|
startX = Math.floor(blockX * p.blockW + p.x),
|
||||||
|
startY = Math.floor(blockY * p.blockH + p.y);
|
||||||
|
|
||||||
|
if(!this.blocks[blockY] || !this.blocks[blockY][blockX]) {
|
||||||
|
this.prerenderBlock(blockX,blockY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.blocks[blockY] && this.blocks[blockY][blockX]) {
|
||||||
|
ctx.drawImage(this.blocks[blockY][blockX],startX,startY);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
draw: function(ctx) {
|
||||||
|
var p = this.p,
|
||||||
|
viewport = this.stage.viewport,
|
||||||
|
scale = viewport ? viewport.scale : 1,
|
||||||
|
x = viewport ? viewport.x : 0,
|
||||||
|
y = viewport ? viewport.y : 0,
|
||||||
|
viewW = Q.width / scale,
|
||||||
|
viewH = Q.height / scale,
|
||||||
|
startBlockX = Math.floor((x - p.x) / p.blockW),
|
||||||
|
startBlockY = Math.floor((y - p.y) / p.blockH),
|
||||||
|
endBlockX = Math.floor((x + viewW - p.x) / p.blockW),
|
||||||
|
endBlockY = Math.floor((y + viewH - p.y) / p.blockH);
|
||||||
|
|
||||||
|
for(var iy=startBlockY;iy<=endBlockY;iy++) {
|
||||||
|
for(var ix=startBlockX;ix<=endBlockX;ix++) {
|
||||||
|
this.drawBlock(ctx,ix,iy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Q.gravityY = 9.8*100;
|
||||||
|
Q.gravityX = 0;
|
||||||
|
|
||||||
|
Q.component('2d',{
|
||||||
|
added: function() {
|
||||||
|
var entity = this.entity;
|
||||||
|
Q._defaults(entity.p,{
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
ax: 0,
|
||||||
|
ay: 0,
|
||||||
|
gravity: 1,
|
||||||
|
collisionMask: Q.SPRITE_DEFAULT
|
||||||
|
});
|
||||||
|
entity.on('step',this,"step");
|
||||||
|
entity.on('hit',this,'collision');
|
||||||
|
},
|
||||||
|
|
||||||
|
collision: function(col,last) {
|
||||||
|
var entity = this.entity,
|
||||||
|
p = entity.p,
|
||||||
|
magnitude = 0;
|
||||||
|
|
||||||
|
if(col.obj.p && col.obj.p.sensor) {
|
||||||
|
col.obj.trigger("sensor",entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.impact = 0;
|
||||||
|
var impactX = Math.abs(p.vx);
|
||||||
|
var impactY = Math.abs(p.vy);
|
||||||
|
|
||||||
|
p.x -= col.separate[0];
|
||||||
|
p.y -= col.separate[1];
|
||||||
|
|
||||||
|
// Top collision
|
||||||
|
if(col.normalY < -0.3) {
|
||||||
|
if(!p.skipCollide && p.vy > 0) { p.vy = 0; }
|
||||||
|
col.impact = impactY;
|
||||||
|
entity.trigger("bump.bottom",col);
|
||||||
|
entity.trigger("bump",col);
|
||||||
|
}
|
||||||
|
if(col.normalY > 0.3) {
|
||||||
|
if(!p.skipCollide && p.vy < 0) { p.vy = 0; }
|
||||||
|
col.impact = impactY;
|
||||||
|
|
||||||
|
entity.trigger("bump.top",col);
|
||||||
|
entity.trigger("bump",col);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(col.normalX < -0.3) {
|
||||||
|
if(!p.skipCollide && p.vx > 0) { p.vx = 0; }
|
||||||
|
col.impact = impactX;
|
||||||
|
entity.trigger("bump.right",col);
|
||||||
|
entity.trigger("bump",col);
|
||||||
|
}
|
||||||
|
if(col.normalX > 0.3) {
|
||||||
|
if(!p.skipCollide && p.vx < 0) { p.vx = 0; }
|
||||||
|
col.impact = impactX;
|
||||||
|
|
||||||
|
entity.trigger("bump.left",col);
|
||||||
|
entity.trigger("bump",col);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
step: function(dt) {
|
||||||
|
var p = this.entity.p,
|
||||||
|
dtStep = dt;
|
||||||
|
// TODO: check the entity's magnitude of vx and vy,
|
||||||
|
// reduce the max dtStep if necessary to prevent
|
||||||
|
// skipping through objects.
|
||||||
|
while(dtStep > 0) {
|
||||||
|
dt = Math.min(1/30,dtStep);
|
||||||
|
// Updated based on the velocity and acceleration
|
||||||
|
p.vx += p.ax * dt + (p.gravityX === void 0 ? Q.gravityX : p.gravityX) * dt * p.gravity;
|
||||||
|
p.vy += p.ay * dt + (p.gravityY === void 0 ? Q.gravityY : p.gravityY) * dt * p.gravity;
|
||||||
|
p.x += p.vx * dt;
|
||||||
|
p.y += p.vy * dt;
|
||||||
|
|
||||||
|
this.entity.stage.collide(this.entity);
|
||||||
|
dtStep -= dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Q.component('aiBounce', {
|
||||||
|
added: function() {
|
||||||
|
this.entity.on("bump.right",this,"goLeft");
|
||||||
|
this.entity.on("bump.left",this,"goRight");
|
||||||
|
},
|
||||||
|
|
||||||
|
goLeft: function(col) {
|
||||||
|
this.entity.p.vx = -col.impact;
|
||||||
|
if(this.entity.p.defaultDirection === 'right') {
|
||||||
|
this.entity.p.flip = 'x';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.entity.p.flip = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goRight: function(col) {
|
||||||
|
this.entity.p.vx = col.impact;
|
||||||
|
if(this.entity.p.defaultDirection === 'left') {
|
||||||
|
this.entity.p.flip = 'x';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.entity.p.flip = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if(typeof Quintus === 'undefined') {
|
||||||
|
module.exports = quintus2D;
|
||||||
|
} else {
|
||||||
|
quintus2D(Quintus);
|
||||||
|
}
|
||||||
+986
@@ -0,0 +1,986 @@
|
|||||||
|
/*global Quintus:false, module:false */
|
||||||
|
|
||||||
|
/**
|
||||||
|
Quintus HTML5 Game Engine - Input Module
|
||||||
|
|
||||||
|
The code in `quintus_input.js` defines the `Quintus.Input` module, which
|
||||||
|
concerns itself with game-type (pretty anything besides touchscreen input)
|
||||||
|
|
||||||
|
@module Quintus.Input
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
var quintusInput = function(Quintus) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quintus Input Module
|
||||||
|
*
|
||||||
|
* @class Quintus.Input
|
||||||
|
*/
|
||||||
|
Quintus.Input = function(Q) {
|
||||||
|
/**
|
||||||
|
* Provided key names mapped to key codes - add more names and key codes as necessary
|
||||||
|
*
|
||||||
|
* @for Quintus.Input
|
||||||
|
* @property KEY_NAMES
|
||||||
|
* @type Object
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
var KEY_NAMES = Q.KEY_NAMES = {
|
||||||
|
LEFT: 37, RIGHT: 39,
|
||||||
|
UP: 38, DOWN: 40,
|
||||||
|
|
||||||
|
ZERO : 48, ONE : 49, TWO : 50,
|
||||||
|
THREE : 51, FOUR : 52, FIVE : 53,
|
||||||
|
SIX : 54, SEVEN : 55, EIGHT : 56,
|
||||||
|
NINE : 57,
|
||||||
|
|
||||||
|
A : 65, B : 66, C : 67,
|
||||||
|
D : 68, E : 69, F : 70,
|
||||||
|
G : 71, H : 72, I : 73,
|
||||||
|
J : 74, K : 75, L : 76,
|
||||||
|
M : 77, N : 78, O : 79,
|
||||||
|
P : 80, Q : 81, R : 82,
|
||||||
|
S : 83, T : 84, U : 85,
|
||||||
|
V : 86, W : 87, X : 88,
|
||||||
|
Y : 89, Z : 90,
|
||||||
|
|
||||||
|
ENTER: 13,
|
||||||
|
ESC: 27,
|
||||||
|
BACKSPACE : 8,
|
||||||
|
TAB : 9,
|
||||||
|
SHIFT : 16,
|
||||||
|
CTRL : 17,
|
||||||
|
ALT : 18,
|
||||||
|
SPACE: 32,
|
||||||
|
|
||||||
|
HOME : 36, END : 35,
|
||||||
|
PGGUP : 33, PGDOWN : 34
|
||||||
|
};
|
||||||
|
|
||||||
|
var DEFAULT_KEYS = {
|
||||||
|
LEFT: 'left', RIGHT: 'right',
|
||||||
|
UP: 'up', DOWN: 'down',
|
||||||
|
SPACE: 'fire',
|
||||||
|
Z: 'fire',
|
||||||
|
X: 'action',
|
||||||
|
ENTER: 'confirm',
|
||||||
|
ESC: 'esc',
|
||||||
|
P: 'P',
|
||||||
|
S: 'S'
|
||||||
|
};
|
||||||
|
|
||||||
|
var DEFAULT_TOUCH_CONTROLS = [ ['left','<' ],
|
||||||
|
['right','>' ],
|
||||||
|
[],
|
||||||
|
['action','b'],
|
||||||
|
['fire', 'a' ]];
|
||||||
|
|
||||||
|
// Clockwise from midnight (a la CSS)
|
||||||
|
var DEFAULT_JOYPAD_INPUTS = [ 'up','right','down','left'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current state of bound inputs
|
||||||
|
*
|
||||||
|
* @for Quintus.Input
|
||||||
|
* @property Q.inputs
|
||||||
|
* @type Object
|
||||||
|
*/
|
||||||
|
Q.inputs = {};
|
||||||
|
Q.joypad = {};
|
||||||
|
|
||||||
|
var hasTouch = !!('ontouchstart' in window);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Convert a canvas point to a stage point, x dimension
|
||||||
|
*
|
||||||
|
* @method Q.canvasToStageX
|
||||||
|
* @for Quintus.Input
|
||||||
|
* @param {Float} x
|
||||||
|
* @param {Q.Stage} stage
|
||||||
|
* @returns {Integer} x
|
||||||
|
*/
|
||||||
|
Q.canvasToStageX = function(x,stage) {
|
||||||
|
x = x / Q.cssWidth * Q.width;
|
||||||
|
if(stage.viewport) {
|
||||||
|
x /= stage.viewport.scale;
|
||||||
|
x += stage.viewport.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Convert a canvas point to a stage point, y dimension
|
||||||
|
*
|
||||||
|
* @method Q.canvasToStageY
|
||||||
|
* @param {Float} y
|
||||||
|
* @param {Q.Stage} stage
|
||||||
|
* @returns {Integer} y
|
||||||
|
*/
|
||||||
|
Q.canvasToStageY = function(y,stage) {
|
||||||
|
y = y / Q.cssWidth * Q.width;
|
||||||
|
if(stage.viewport) {
|
||||||
|
y /= stage.viewport.scale;
|
||||||
|
y += stage.viewport.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return y;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Button and mouse input subsystem for Quintus.
|
||||||
|
* An instance of this class is auto-created as {{#crossLink "Q.input"}}{{/crossLink}}
|
||||||
|
*
|
||||||
|
* @class Q.InputSystem
|
||||||
|
* @extends Q.Evented
|
||||||
|
* @for Quintus.Input
|
||||||
|
*/
|
||||||
|
Q.InputSystem = Q.Evented.extend({
|
||||||
|
keys: {},
|
||||||
|
keypad: {},
|
||||||
|
keyboardEnabled: false,
|
||||||
|
touchEnabled: false,
|
||||||
|
joypadEnabled: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a key name or keycode to an action name (used by `keyboardControls`)
|
||||||
|
*
|
||||||
|
* @method bindKey
|
||||||
|
* @for Q.InputSystem
|
||||||
|
* @param {String or Integer} key - name or integer keycode for to bind
|
||||||
|
* @param {String} name - name of action to bind to
|
||||||
|
*/
|
||||||
|
bindKey: function(key,name) {
|
||||||
|
Q.input.keys[KEY_NAMES[key] || key] = name;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable keyboard controls by binding to events
|
||||||
|
*
|
||||||
|
* @for Q.InputSystem
|
||||||
|
* @method enableKeyboard
|
||||||
|
*/
|
||||||
|
enableKeyboard: function() {
|
||||||
|
if(this.keyboardEnabled) { return false; }
|
||||||
|
|
||||||
|
// Make selectable and remove an :focus outline
|
||||||
|
Q.el.tabIndex = 0;
|
||||||
|
Q.el.style.outline = 0;
|
||||||
|
|
||||||
|
Q.el.addEventListener("keydown",function(e) {
|
||||||
|
if(Q.input.keys[e.keyCode]) {
|
||||||
|
var actionName = Q.input.keys[e.keyCode];
|
||||||
|
Q.inputs[actionName] = true;
|
||||||
|
Q.input.trigger(actionName);
|
||||||
|
Q.input.trigger('keydown',e.keyCode);
|
||||||
|
}
|
||||||
|
if(!e.ctrlKey && !e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},false);
|
||||||
|
|
||||||
|
Q.el.addEventListener("keyup",function(e) {
|
||||||
|
if(Q.input.keys[e.keyCode]) {
|
||||||
|
var actionName = Q.input.keys[e.keyCode];
|
||||||
|
Q.inputs[actionName] = false;
|
||||||
|
Q.input.trigger(actionName + "Up");
|
||||||
|
Q.input.trigger('keyup',e.keyCode);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
},false);
|
||||||
|
|
||||||
|
if(Q.options.autoFocus) { Q.el.focus(); }
|
||||||
|
this.keyboardEnabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to activate keyboard controls (call `bindKey` and `enableKeyboard` internally)
|
||||||
|
*
|
||||||
|
* @method keyboardControls
|
||||||
|
* @for Q.InputSystem
|
||||||
|
* @param {Object} [keys] - hash of key names or codes to actions
|
||||||
|
*/
|
||||||
|
keyboardControls: function(keys) {
|
||||||
|
keys = keys || DEFAULT_KEYS;
|
||||||
|
Q._each(keys,function(name,key) {
|
||||||
|
this.bindKey(key,name);
|
||||||
|
},Q.input);
|
||||||
|
this.enableKeyboard();
|
||||||
|
},
|
||||||
|
|
||||||
|
_containerOffset: function() {
|
||||||
|
Q.input.offsetX = 0;
|
||||||
|
Q.input.offsetY = 0;
|
||||||
|
var el = Q.el;
|
||||||
|
do {
|
||||||
|
Q.input.offsetX += el.offsetLeft;
|
||||||
|
Q.input.offsetY += el.offsetTop;
|
||||||
|
} while(el = el.offsetParent);
|
||||||
|
},
|
||||||
|
|
||||||
|
touchLocation: function(touch) {
|
||||||
|
var el = Q.el,
|
||||||
|
posX = touch.offsetX,
|
||||||
|
posY = touch.offsetY,
|
||||||
|
touchX, touchY;
|
||||||
|
|
||||||
|
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||||
|
posX = touch.layerX;
|
||||||
|
posY = touch.layerY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||||
|
if(Q.input.offsetX === void 0) { Q.input._containerOffset(); }
|
||||||
|
posX = touch.pageX - Q.input.offsetX;
|
||||||
|
posY = touch.pageY - Q.input.offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
touchX = Q.width * posX / Q.cssWidth;
|
||||||
|
touchY = Q.height * posY / Q.cssHeight;
|
||||||
|
|
||||||
|
|
||||||
|
return { x: touchX, y: touchY };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate touch button controls - pass in an options hash to override
|
||||||
|
*
|
||||||
|
* Default Options:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* left: 0,
|
||||||
|
* gutter:10,
|
||||||
|
* controls: DEFAULT_TOUCH_CONTROLS,
|
||||||
|
* width: Q.width,
|
||||||
|
* bottom: Q.height
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Default controls are left and right buttons, a space, and 'a' and 'b' buttons, as defined as an Array of Arrays below:
|
||||||
|
*
|
||||||
|
* [ ['left','<' ],
|
||||||
|
* ['right','>' ],
|
||||||
|
* [], // use an empty array as a spacer
|
||||||
|
* ['action','b'],
|
||||||
|
* ['fire', 'a' ]]
|
||||||
|
*
|
||||||
|
* @method touchControls
|
||||||
|
* @for Q.InputSystem
|
||||||
|
* @param {Object} [opts] - Options hash
|
||||||
|
*/
|
||||||
|
touchControls: function(opts) {
|
||||||
|
if(this.touchEnabled) { return false; }
|
||||||
|
if(!hasTouch) { return false; }
|
||||||
|
|
||||||
|
Q.input.keypad = opts = Q._extend({
|
||||||
|
left: 0,
|
||||||
|
gutter:10,
|
||||||
|
controls: DEFAULT_TOUCH_CONTROLS,
|
||||||
|
width: Q.width,
|
||||||
|
bottom: Q.height,
|
||||||
|
fullHeight: false
|
||||||
|
},opts);
|
||||||
|
|
||||||
|
opts.unit = (opts.width / opts.controls.length);
|
||||||
|
opts.size = opts.unit - (opts.gutter * 2);
|
||||||
|
|
||||||
|
function getKey(touch) {
|
||||||
|
var pos = Q.input.touchLocation(touch),
|
||||||
|
minY = opts.bottom - opts.unit;
|
||||||
|
for(var i=0,len=opts.controls.length;i<len;i++) {
|
||||||
|
var minX = i * opts.unit + opts.gutter;
|
||||||
|
if(pos.x >= minX && pos.x <= (minX+opts.size) && (opts.fullHeight || (pos.y >= minY + opts.gutter && pos.y <= (minY+opts.unit - opts.gutter))))
|
||||||
|
{
|
||||||
|
return opts.controls[i][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchDispatch(event) {
|
||||||
|
var wasOn = {},
|
||||||
|
i, len, tch, key, actionName;
|
||||||
|
|
||||||
|
// Reset all the actions bound to controls
|
||||||
|
// but keep track of all the actions that were on
|
||||||
|
for(i=0,len = opts.controls.length;i<len;i++) {
|
||||||
|
actionName = opts.controls[i][0];
|
||||||
|
if(Q.inputs[actionName]) { wasOn[actionName] = true; }
|
||||||
|
Q.inputs[actionName] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var touches = event.touches ? event.touches : [ event ];
|
||||||
|
|
||||||
|
for(i=0,len=touches.length;i<len;i++) {
|
||||||
|
tch = touches[i];
|
||||||
|
key = getKey(tch);
|
||||||
|
|
||||||
|
if(key) {
|
||||||
|
// Mark this input as on
|
||||||
|
Q.inputs[key] = true;
|
||||||
|
|
||||||
|
// Either trigger a new action
|
||||||
|
// or remove from wasOn list
|
||||||
|
if(!wasOn[key]) {
|
||||||
|
Q.input.trigger(key);
|
||||||
|
} else {
|
||||||
|
delete wasOn[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any remaining were on the last frame
|
||||||
|
// and need to trigger an up action
|
||||||
|
for(actionName in wasOn) {
|
||||||
|
Q.input.trigger(actionName + "Up");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.touchDispatchHandler = function(e) {
|
||||||
|
touchDispatch(e);
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Q._each(["touchstart","touchend","touchmove","touchcancel"],function(evt) {
|
||||||
|
Q.el.addEventListener(evt,this.touchDispatchHandler);
|
||||||
|
},this);
|
||||||
|
|
||||||
|
this.touchEnabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn off touch (button and joypad) controls and remove event listeners
|
||||||
|
*
|
||||||
|
* @method disableTouchControls
|
||||||
|
* @for Q.InputSystem
|
||||||
|
*/
|
||||||
|
disableTouchControls: function() {
|
||||||
|
Q._each(["touchstart","touchend","touchmove","touchcancel"],function(evt) {
|
||||||
|
Q.el.removeEventListener(evt,this.touchDispatchHandler);
|
||||||
|
},this);
|
||||||
|
|
||||||
|
Q.el.removeEventListener('touchstart',this.joypadStart);
|
||||||
|
Q.el.removeEventListener('touchmove',this.joypadMove);
|
||||||
|
Q.el.removeEventListener('touchend',this.joypadEnd);
|
||||||
|
Q.el.removeEventListener('touchcancel',this.joypadEnd);
|
||||||
|
this.touchEnabled = false;
|
||||||
|
|
||||||
|
// clear existing inputs
|
||||||
|
for(var input in Q.inputs) {
|
||||||
|
Q.inputs[input] = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate joypad controls (i.e. 4-way touch controls)
|
||||||
|
*
|
||||||
|
* Lots of options, defaults are:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* size: 50,
|
||||||
|
* trigger: 20,
|
||||||
|
* center: 25,
|
||||||
|
* color: "#CCC",
|
||||||
|
* background: "#000",
|
||||||
|
* alpha: 0.5,
|
||||||
|
* zone: Q.width / 2,
|
||||||
|
* inputs: DEFAULT_JOYPAD_INPUTS
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Default joypad controls is an array that defines the inputs to bind to:
|
||||||
|
*
|
||||||
|
* // Clockwise from midnight (a la CSS)
|
||||||
|
* var DEFAULT_JOYPAD_INPUTS = [ 'up','right','down','left'];
|
||||||
|
*
|
||||||
|
* @method joypadControls
|
||||||
|
* @for Q.InputSystem
|
||||||
|
* @param {Object} [opts] - joypad options
|
||||||
|
*/
|
||||||
|
joypadControls: function(opts) {
|
||||||
|
if(this.joypadEnabled) { return false; }
|
||||||
|
if(!hasTouch) { return false; }
|
||||||
|
|
||||||
|
var joypad = Q.joypad = Q._defaults(opts || {},{
|
||||||
|
size: 50,
|
||||||
|
trigger: 20,
|
||||||
|
center: 25,
|
||||||
|
color: "#CCC",
|
||||||
|
background: "#000",
|
||||||
|
alpha: 0.5,
|
||||||
|
zone: Q.width / 2,
|
||||||
|
joypadTouch: null,
|
||||||
|
inputs: DEFAULT_JOYPAD_INPUTS,
|
||||||
|
triggers: []
|
||||||
|
});
|
||||||
|
|
||||||
|
this.joypadStart = function(evt) {
|
||||||
|
if(joypad.joypadTouch === null) {
|
||||||
|
var touch = evt.changedTouches[0],
|
||||||
|
loc = Q.input.touchLocation(touch);
|
||||||
|
|
||||||
|
if(loc.x < joypad.zone) {
|
||||||
|
joypad.joypadTouch = touch.identifier;
|
||||||
|
joypad.centerX = loc.x;
|
||||||
|
joypad.centerY = loc.y;
|
||||||
|
joypad.x = null;
|
||||||
|
joypad.y = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.joypadMove = function(e) {
|
||||||
|
if(joypad.joypadTouch !== null) {
|
||||||
|
var evt = e;
|
||||||
|
|
||||||
|
for(var i=0,len=evt.changedTouches.length;i<len;i++) {
|
||||||
|
var touch = evt.changedTouches[i];
|
||||||
|
|
||||||
|
if(touch.identifier === joypad.joypadTouch) {
|
||||||
|
var loc = Q.input.touchLocation(touch),
|
||||||
|
dx = loc.x - joypad.centerX,
|
||||||
|
dy = loc.y - joypad.centerY,
|
||||||
|
dist = Math.sqrt(dx * dx + dy * dy),
|
||||||
|
overage = Math.max(1,dist / joypad.size),
|
||||||
|
ang = Math.atan2(dx,dy);
|
||||||
|
|
||||||
|
if(overage > 1) {
|
||||||
|
dx /= overage;
|
||||||
|
dy /= overage;
|
||||||
|
dist /= overage;
|
||||||
|
}
|
||||||
|
|
||||||
|
var triggers = [
|
||||||
|
dy < -joypad.trigger,
|
||||||
|
dx > joypad.trigger,
|
||||||
|
dy > joypad.trigger,
|
||||||
|
dx < -joypad.trigger
|
||||||
|
];
|
||||||
|
|
||||||
|
for(var k=0;k<triggers.length;k++) {
|
||||||
|
var actionName = joypad.inputs[k];
|
||||||
|
if(triggers[k]) {
|
||||||
|
Q.inputs[actionName] = true;
|
||||||
|
|
||||||
|
if(!joypad.triggers[k]) {
|
||||||
|
Q.input.trigger(actionName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Q.inputs[actionName] = false;
|
||||||
|
if(joypad.triggers[k]) {
|
||||||
|
Q.input.trigger(actionName + "Up");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q._extend(joypad, {
|
||||||
|
dx: dx, dy: dy,
|
||||||
|
x: joypad.centerX + dx,
|
||||||
|
y: joypad.centerY + dy,
|
||||||
|
dist: dist,
|
||||||
|
ang: ang,
|
||||||
|
triggers: triggers
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.joypadEnd = function(e) {
|
||||||
|
var evt = e;
|
||||||
|
|
||||||
|
if(joypad.joypadTouch !== null) {
|
||||||
|
for(var i=0,len=evt.changedTouches.length;i<len;i++) {
|
||||||
|
var touch = evt.changedTouches[i];
|
||||||
|
if(touch.identifier === joypad.joypadTouch) {
|
||||||
|
for(var k=0;k<joypad.triggers.length;k++) {
|
||||||
|
var actionName = joypad.inputs[k];
|
||||||
|
Q.inputs[actionName] = false;
|
||||||
|
if(joypad.triggers[k]) {
|
||||||
|
Q.input.trigger(actionName + "Up");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
joypad.joypadTouch = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
Q.el.addEventListener("touchstart",this.joypadStart);
|
||||||
|
Q.el.addEventListener("touchmove",this.joypadMove);
|
||||||
|
Q.el.addEventListener("touchend",this.joypadEnd);
|
||||||
|
Q.el.addEventListener("touchcancel",this.joypadEnd);
|
||||||
|
|
||||||
|
this.joypadEnabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate mouse controls - mouse controls don't trigger events, but just set `Q.inputs['mouseX']` & `Q.inputs['mouseY']` on each frame.
|
||||||
|
*
|
||||||
|
* Default options:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* stageNum: 0,
|
||||||
|
* mouseX: "mouseX",
|
||||||
|
* mouseY: "mouseY",
|
||||||
|
* cursor: "off"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @method mouseControls
|
||||||
|
* @for Q.InputSystem
|
||||||
|
* @param {Object} [options] - override default options
|
||||||
|
*/
|
||||||
|
mouseControls: function(options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
var stageNum = options.stageNum || 0;
|
||||||
|
var mouseInputX = options.mouseX || "mouseX";
|
||||||
|
var mouseInputY = options.mouseY || "mouseY";
|
||||||
|
var cursor = options.cursor || "off";
|
||||||
|
|
||||||
|
var mouseMoveObj = {};
|
||||||
|
|
||||||
|
if(cursor !== "on") {
|
||||||
|
if(cursor === "off") {
|
||||||
|
Q.el.style.cursor = 'none';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q.el.style.cursor = cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q.inputs[mouseInputX] = 0;
|
||||||
|
Q.inputs[mouseInputY] = 0;
|
||||||
|
|
||||||
|
Q._mouseMove = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var touch = e.touches ? e.touches[0] : e;
|
||||||
|
var el = Q.el,
|
||||||
|
rect = el.getBoundingClientRect(),
|
||||||
|
style = window.getComputedStyle(el),
|
||||||
|
posX = touch.clientX - rect.left - parseInt(style.paddingLeft, 10),
|
||||||
|
posY = touch.clientY - rect.top - parseInt(style.paddingTop, 10);
|
||||||
|
|
||||||
|
var stage = Q.stage(stageNum);
|
||||||
|
|
||||||
|
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||||
|
posX = touch.offsetX;
|
||||||
|
posY = touch.offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||||
|
posX = touch.layerX;
|
||||||
|
posY = touch.layerY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||||
|
if(Q.input.offsetX === void 0) { Q.input._containerOffset(); }
|
||||||
|
posX = touch.pageX - Q.input.offsetX;
|
||||||
|
posY = touch.pageY - Q.input.offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stage) {
|
||||||
|
mouseMoveObj.x= Q.canvasToStageX(posX,stage);
|
||||||
|
mouseMoveObj.y= Q.canvasToStageY(posY,stage);
|
||||||
|
|
||||||
|
Q.inputs[mouseInputX] = mouseMoveObj.x;
|
||||||
|
Q.inputs[mouseInputY] = mouseMoveObj.y;
|
||||||
|
|
||||||
|
Q.input.trigger('mouseMove',mouseMoveObj);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when the user scrolls the mouse wheel up or down
|
||||||
|
* Anyone subscribing to the "mouseWheel" event will receive an event with one numeric parameter
|
||||||
|
* indicating the scroll direction. -1 for down, 1 for up.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
Q._mouseWheel = function(e) {
|
||||||
|
// http://www.sitepoint.com/html5-javascript-mouse-wheel/
|
||||||
|
// cross-browser wheel delta
|
||||||
|
e = window.event || e; // old IE support
|
||||||
|
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
|
||||||
|
Q.input.trigger('mouseWheel', delta);
|
||||||
|
};
|
||||||
|
|
||||||
|
Q.el.addEventListener('mousemove',Q._mouseMove,true);
|
||||||
|
Q.el.addEventListener('touchstart',Q._mouseMove,true);
|
||||||
|
Q.el.addEventListener('touchmove',Q._mouseMove,true);
|
||||||
|
Q.el.addEventListener('mousewheel',Q._mouseWheel,true);
|
||||||
|
Q.el.addEventListener('DOMMouseScroll',Q._mouseWheel,true);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn off mouse controls
|
||||||
|
*
|
||||||
|
* @method disableMouseControls
|
||||||
|
* @for Q.InputSystem
|
||||||
|
*/
|
||||||
|
disableMouseControls: function() {
|
||||||
|
if(Q._mouseMove) {
|
||||||
|
Q.el.removeEventListener("mousemove",Q._mouseMove, true);
|
||||||
|
Q.el.removeEventListener("mousewheel",Q._mouseWheel, true);
|
||||||
|
Q.el.removeEventListener("DOMMouseScroll",Q._mouseWheel, true);
|
||||||
|
Q.el.style.cursor = 'inherit';
|
||||||
|
Q._mouseMove = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the touch buttons on the screen
|
||||||
|
*
|
||||||
|
* overload this to change how buttons are drawn
|
||||||
|
*
|
||||||
|
* @method drawButtons
|
||||||
|
* @for Q.InputSystem
|
||||||
|
*/
|
||||||
|
drawButtons: function() {
|
||||||
|
var keypad = Q.input.keypad,
|
||||||
|
ctx = Q.ctx;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
|
||||||
|
for(var i=0;i<keypad.controls.length;i++) {
|
||||||
|
var control = keypad.controls[i];
|
||||||
|
|
||||||
|
if(control[0]) {
|
||||||
|
ctx.font = "bold " + (keypad.size/2) + "px arial";
|
||||||
|
var x = keypad.left + i * keypad.unit + keypad.gutter,
|
||||||
|
y = keypad.bottom - keypad.unit,
|
||||||
|
key = Q.inputs[control[0]];
|
||||||
|
|
||||||
|
ctx.fillStyle = keypad.color || "#FFFFFF";
|
||||||
|
ctx.globalAlpha = key ? 1.0 : 0.5;
|
||||||
|
ctx.fillRect(x,y,keypad.size,keypad.size);
|
||||||
|
|
||||||
|
ctx.fillStyle = keypad.text || "#000000";
|
||||||
|
ctx.fillText(control[1],
|
||||||
|
x+keypad.size/2,
|
||||||
|
y+keypad.size/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
},
|
||||||
|
|
||||||
|
drawCircle: function(x,y,color,size) {
|
||||||
|
var ctx = Q.ctx,
|
||||||
|
joypad = Q.joypad;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.globalAlpha=joypad.alpha;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.arc(x, y, size, 0, Math.PI*2, true);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the joypad on the screen
|
||||||
|
*
|
||||||
|
* overload this to change how joypad is drawn
|
||||||
|
*
|
||||||
|
* @method drawJoypad
|
||||||
|
* @for Q.InputSystem
|
||||||
|
*/
|
||||||
|
drawJoypad: function() {
|
||||||
|
var joypad = Q.joypad;
|
||||||
|
if(joypad.joypadTouch !== null) {
|
||||||
|
Q.input.drawCircle(joypad.centerX,
|
||||||
|
joypad.centerY,
|
||||||
|
joypad.background,
|
||||||
|
joypad.size);
|
||||||
|
|
||||||
|
if(joypad.x !== null) {
|
||||||
|
Q.input.drawCircle(joypad.x,
|
||||||
|
joypad.y,
|
||||||
|
joypad.color,
|
||||||
|
joypad.center);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called each frame by the stage game loop to render any onscreen UI
|
||||||
|
*
|
||||||
|
* calls `drawJoypad` and `drawButtons` if enabled
|
||||||
|
*
|
||||||
|
* @method drawCanvas
|
||||||
|
* @for Q.InputSystem
|
||||||
|
*/
|
||||||
|
drawCanvas: function() {
|
||||||
|
if(this.touchEnabled) {
|
||||||
|
this.drawButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.joypadEnabled) {
|
||||||
|
this.drawJoypad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance of the input subsytem that is actually used during gameplay
|
||||||
|
*
|
||||||
|
* @property Q.input
|
||||||
|
* @for Quintus.Input
|
||||||
|
* @type Q.InputSystem
|
||||||
|
*/
|
||||||
|
Q.input = new Q.InputSystem();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to activate controls with default options
|
||||||
|
*
|
||||||
|
* @for Quintus.Input
|
||||||
|
* @method Q.controls
|
||||||
|
* @param {Boolean} joypad - enable 4-way joypad (true) or just left, right controls (false, undefined)
|
||||||
|
*/
|
||||||
|
Q.controls = function(joypad) {
|
||||||
|
Q.input.keyboardControls();
|
||||||
|
|
||||||
|
if(joypad) {
|
||||||
|
Q.input.touchControls({
|
||||||
|
controls: [ [],[],[],['action','b'],['fire','a']]
|
||||||
|
});
|
||||||
|
Q.input.joypadControls();
|
||||||
|
} else {
|
||||||
|
Q.input.touchControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Q;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platformer Control Component
|
||||||
|
*
|
||||||
|
* Adds 2D platformer controls onto a Sprite
|
||||||
|
*
|
||||||
|
* Platformer controls bind to left, and right and allow the player to jump.
|
||||||
|
*
|
||||||
|
* Adds the following properties to the entity to control speed and jumping:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* speed: 200,
|
||||||
|
* jumpSpeed: -300
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @class platformerControls
|
||||||
|
* @for Quintus.Input
|
||||||
|
*/
|
||||||
|
Q.component("platformerControls", {
|
||||||
|
defaults: {
|
||||||
|
speed: 200,
|
||||||
|
jumpSpeed: -300,
|
||||||
|
collisions: []
|
||||||
|
},
|
||||||
|
|
||||||
|
added: function() {
|
||||||
|
var p = this.entity.p;
|
||||||
|
|
||||||
|
Q._defaults(p,this.defaults);
|
||||||
|
|
||||||
|
this.entity.on("step",this,"step");
|
||||||
|
this.entity.on("bump.bottom",this,"landed");
|
||||||
|
|
||||||
|
p.landed = 0;
|
||||||
|
p.direction ='right';
|
||||||
|
},
|
||||||
|
|
||||||
|
landed: function(col) {
|
||||||
|
var p = this.entity.p;
|
||||||
|
p.landed = 1/5;
|
||||||
|
},
|
||||||
|
|
||||||
|
step: function(dt) {
|
||||||
|
var p = this.entity.p;
|
||||||
|
|
||||||
|
if(p.ignoreControls === undefined || !p.ignoreControls) {
|
||||||
|
var collision = null;
|
||||||
|
|
||||||
|
// Follow along the current slope, if possible.
|
||||||
|
if(p.collisions !== undefined && p.collisions.length > 0 && (Q.inputs['left'] || Q.inputs['right'] || p.landed > 0)) {
|
||||||
|
if(p.collisions.length === 1) {
|
||||||
|
collision = p.collisions[0];
|
||||||
|
} else {
|
||||||
|
// If there's more than one possible slope, follow slope with negative Y normal
|
||||||
|
collision = null;
|
||||||
|
|
||||||
|
for(var i = 0; i < p.collisions.length; i++) {
|
||||||
|
if(p.collisions[i].normalY < 0) {
|
||||||
|
collision = p.collisions[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't climb up walls.
|
||||||
|
if(collision !== null && collision.normalY > -0.3 && collision.normalY < 0.3) {
|
||||||
|
collision = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Q.inputs['left']) {
|
||||||
|
p.direction = 'left';
|
||||||
|
if(collision && p.landed > 0) {
|
||||||
|
p.vx = p.speed * collision.normalY;
|
||||||
|
p.vy = -p.speed * collision.normalX;
|
||||||
|
} else {
|
||||||
|
p.vx = -p.speed;
|
||||||
|
}
|
||||||
|
} else if(Q.inputs['right']) {
|
||||||
|
p.direction = 'right';
|
||||||
|
if(collision && p.landed > 0) {
|
||||||
|
p.vx = -p.speed * collision.normalY;
|
||||||
|
p.vy = p.speed * collision.normalX;
|
||||||
|
} else {
|
||||||
|
p.vx = p.speed;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.vx = 0;
|
||||||
|
if(collision && p.landed > 0) {
|
||||||
|
p.vy = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(p.landed > 0 && (Q.inputs['up'] || Q.inputs['action']) && !p.jumping) {
|
||||||
|
p.vy = p.jumpSpeed;
|
||||||
|
p.landed = -dt;
|
||||||
|
p.jumping = true;
|
||||||
|
} else if(Q.inputs['up'] || Q.inputs['action']) {
|
||||||
|
this.entity.trigger('jump', this.entity);
|
||||||
|
p.jumping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(p.jumping && !(Q.inputs['up'] || Q.inputs['action'])) {
|
||||||
|
p.jumping = false;
|
||||||
|
this.entity.trigger('jumped', this.entity);
|
||||||
|
if(p.vy < p.jumpSpeed / 3) {
|
||||||
|
p.vy = p.jumpSpeed / 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.landed -= dt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step Controls component
|
||||||
|
*
|
||||||
|
* Adds Step (square grid based) 4-ways controls onto a Sprite
|
||||||
|
*
|
||||||
|
* Adds the following properties to the entity:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* stepDistance: 32, // should be tile size
|
||||||
|
* stepDelay: 0.2 // seconds to delay before next step
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @class stepControls
|
||||||
|
* @for Quintus.Input
|
||||||
|
*/
|
||||||
|
Q.component("stepControls", {
|
||||||
|
|
||||||
|
added: function() {
|
||||||
|
var p = this.entity.p;
|
||||||
|
|
||||||
|
if(!p.stepDistance) { p.stepDistance = 32; }
|
||||||
|
if(!p.stepDelay) { p.stepDelay = 0.2; }
|
||||||
|
|
||||||
|
p.stepWait = 0;
|
||||||
|
this.entity.on("step",this,"step");
|
||||||
|
this.entity.on("hit", this,"collision");
|
||||||
|
},
|
||||||
|
|
||||||
|
collision: function(col) {
|
||||||
|
var p = this.entity.p;
|
||||||
|
|
||||||
|
if(p.stepping) {
|
||||||
|
p.stepping = false;
|
||||||
|
p.x = p.origX;
|
||||||
|
p.y = p.origY;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
step: function(dt) {
|
||||||
|
var p = this.entity.p,
|
||||||
|
moved = false;
|
||||||
|
p.stepWait -= dt;
|
||||||
|
|
||||||
|
if(p.stepping) {
|
||||||
|
p.x += p.diffX * dt / p.stepDelay;
|
||||||
|
p.y += p.diffY * dt / p.stepDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(p.stepWait > 0) { return; }
|
||||||
|
if(p.stepping) {
|
||||||
|
p.x = p.destX;
|
||||||
|
p.y = p.destY;
|
||||||
|
}
|
||||||
|
p.stepping = false;
|
||||||
|
|
||||||
|
p.diffX = 0;
|
||||||
|
p.diffY = 0;
|
||||||
|
|
||||||
|
if(Q.inputs['left']) {
|
||||||
|
p.diffX = -p.stepDistance;
|
||||||
|
} else if(Q.inputs['right']) {
|
||||||
|
p.diffX = p.stepDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Q.inputs['up']) {
|
||||||
|
p.diffY = -p.stepDistance;
|
||||||
|
} else if(Q.inputs['down']) {
|
||||||
|
p.diffY = p.stepDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(p.diffY || p.diffX ) {
|
||||||
|
p.stepping = true;
|
||||||
|
p.origX = p.x;
|
||||||
|
p.origY = p.y;
|
||||||
|
p.destX = p.x + p.diffX;
|
||||||
|
p.destY = p.y + p.diffY;
|
||||||
|
p.stepWait = p.stepDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if(typeof Quintus === 'undefined') {
|
||||||
|
module.exports = quintusInput;
|
||||||
|
} else {
|
||||||
|
quintusInput(Quintus);
|
||||||
|
}
|
||||||
+1172
File diff suppressed because it is too large
Load Diff
+770
@@ -0,0 +1,770 @@
|
|||||||
|
/*global Quintus:false, module:false */
|
||||||
|
|
||||||
|
/**
|
||||||
|
Quintus HTML5 Game Engine - Sprites Module
|
||||||
|
|
||||||
|
The code in `quintus_sprites.js` defines the `Quintus.Sprites` module, which
|
||||||
|
add support for sprite sheets and the base sprite class.
|
||||||
|
|
||||||
|
Most games will include at a minimum `Quintus.Sprites` and `Quintus.Scenes`
|
||||||
|
|
||||||
|
@module Quintus.Sprites
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
var quintusSprites = function(Quintus) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quintus Sprites Module Class
|
||||||
|
*
|
||||||
|
* @class Quintus.Sprites
|
||||||
|
*/
|
||||||
|
Quintus.Sprites = function(Q) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
Sprite sheet class - generally instantiated with `Q.sheet` new `new`
|
||||||
|
|
||||||
|
|
||||||
|
@class Q.SpriteSheet
|
||||||
|
@extends Q.Class
|
||||||
|
@for Quintus.Sprites
|
||||||
|
*/
|
||||||
|
Q.Class.extend("SpriteSheet",{
|
||||||
|
|
||||||
|
/**
|
||||||
|
constructor
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
* tileW - tile width
|
||||||
|
* tileH - tile height
|
||||||
|
* w - width of the sprite block
|
||||||
|
* h - height of the sprite block
|
||||||
|
* sx - start x
|
||||||
|
* sy - start y
|
||||||
|
* spacingX - spacing between each tile x (after 1st)
|
||||||
|
* spacingY - spacing between each tile y
|
||||||
|
* marginX - margin around each tile x
|
||||||
|
* marginY - margin around each tile y
|
||||||
|
* cols - number of columns per row
|
||||||
|
|
||||||
|
@constructor
|
||||||
|
@for Q.SpriteSheet
|
||||||
|
@method init
|
||||||
|
@param {String} name
|
||||||
|
@param {String} asset
|
||||||
|
@param {Object} options
|
||||||
|
*/
|
||||||
|
init: function(name, asset,options) {
|
||||||
|
if(!Q.asset(asset)) { throw "Invalid Asset:" + asset; }
|
||||||
|
Q._extend(this,{
|
||||||
|
name: name,
|
||||||
|
asset: asset,
|
||||||
|
w: Q.asset(asset).width,
|
||||||
|
h: Q.asset(asset).height,
|
||||||
|
tileW: 64,
|
||||||
|
tileH: 64,
|
||||||
|
sx: 0,
|
||||||
|
sy: 0,
|
||||||
|
spacingX: 0,
|
||||||
|
spacingY: 0,
|
||||||
|
frameProperties: {}
|
||||||
|
});
|
||||||
|
if(options) { Q._extend(this,options); }
|
||||||
|
// fix for old tilew instead of tileW
|
||||||
|
if(this.tilew) {
|
||||||
|
this.tileW = this.tilew;
|
||||||
|
delete this['tilew'];
|
||||||
|
}
|
||||||
|
if(this.tileh) {
|
||||||
|
this.tileH = this.tileh;
|
||||||
|
delete this['tileh'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cols = this.cols ||
|
||||||
|
Math.floor((this.w + this.spacingX) / (this.tileW + this.spacingX));
|
||||||
|
|
||||||
|
this.frames = this.cols * (Math.floor(this.h/(this.tileH + this.spacingY)));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns the starting x position of a single frame
|
||||||
|
|
||||||
|
@method fx
|
||||||
|
@for Q.SpriteSheet
|
||||||
|
@param {Integer} frame
|
||||||
|
*/
|
||||||
|
fx: function(frame) {
|
||||||
|
return Math.floor((frame % this.cols) * (this.tileW + this.spacingX) + this.sx);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns the starting y position of a single frame
|
||||||
|
|
||||||
|
@method fy
|
||||||
|
@for Q.SpriteSheet
|
||||||
|
@param {Integer} frame
|
||||||
|
*/
|
||||||
|
fy: function(frame) {
|
||||||
|
return Math.floor(Math.floor(frame / this.cols) * (this.tileH + this.spacingY) + this.sy);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Draw a single frame at x,y on the provided context
|
||||||
|
|
||||||
|
@method draw
|
||||||
|
@for Q.SpriteSheet
|
||||||
|
@param {Context2D} ctx
|
||||||
|
@param {Float} x
|
||||||
|
@param {Float} y
|
||||||
|
@param {Integer} frame
|
||||||
|
*/
|
||||||
|
draw: function(ctx, x, y, frame) {
|
||||||
|
if(!ctx) { ctx = Q.ctx; }
|
||||||
|
ctx.drawImage(Q.asset(this.asset),
|
||||||
|
this.fx(frame),this.fy(frame),
|
||||||
|
this.tileW, this.tileH,
|
||||||
|
Math.floor(x),Math.floor(y),
|
||||||
|
this.tileW, this.tileH);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Q.sheets = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
Return a `Q.SpriteSheet` or create a new sprite sheet
|
||||||
|
|
||||||
|
@method Q.sheet
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@param {String} name - name of sheet to return or create
|
||||||
|
@param {String} [asset] - if provided, will create a sprite sheet using this asset
|
||||||
|
@param {Object} [options] - if provided, will be passed as options to `Q.SpriteSheet`
|
||||||
|
*/
|
||||||
|
Q.sheet = function(name,asset,options) {
|
||||||
|
if(asset) {
|
||||||
|
Q.sheets[name] = new Q.SpriteSheet(name,asset,options);
|
||||||
|
} else {
|
||||||
|
return Q.sheets[name];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a number of `Q.SpriteSheet` objects from an image asset and a sprite data JSON asset
|
||||||
|
|
||||||
|
@method Q.compileSheets
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@param {String} imageAsset
|
||||||
|
@param {String spriteDataAsset
|
||||||
|
*/
|
||||||
|
Q.compileSheets = function(imageAsset,spriteDataAsset) {
|
||||||
|
var data = Q.asset(spriteDataAsset);
|
||||||
|
Q._each(data,function(spriteData,name) {
|
||||||
|
Q.sheet(name,imageAsset,spriteData);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Bitmask 0 to indicate no sprites
|
||||||
|
|
||||||
|
@property Q.SPRITE_NONE
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_NONE = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
default sprite type 1
|
||||||
|
|
||||||
|
@property Q.SPRITE_DEFAULT
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_DEFAULT = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
particle sprite type 2
|
||||||
|
|
||||||
|
@property Q.SPRITE_PARTICLE
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_PARTICLE = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
active sprite type 4
|
||||||
|
|
||||||
|
@property Q.SPRITE_ACTIVE
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_ACTIVE = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
friendly sprite type 8
|
||||||
|
|
||||||
|
@property Q.SPRITE_FRIENDLY
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_FRIENDLY = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
enemy sprite type 16
|
||||||
|
|
||||||
|
@property Q.SPRITE_ENEMY
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_ENEMY = 16;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
powerup sprite type 32
|
||||||
|
|
||||||
|
@property Q.SPRITE_POWERUP
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_POWERUP = 32;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
UI sprite type 64
|
||||||
|
|
||||||
|
@property Q.SPRITE_UI
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_UI = 64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
all sprite type - 0xFFFF
|
||||||
|
|
||||||
|
@property Q.SPRITE_ALL
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@final
|
||||||
|
*/
|
||||||
|
Q.SPRITE_ALL = 0xFFFF;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
generate a square set of `p.points` on an object from `p.w` and `p.h`
|
||||||
|
|
||||||
|
`p.points` represent the collision points for an object in object coordinates.
|
||||||
|
|
||||||
|
|
||||||
|
@method q._generatePoints
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@param {Q.Sprite} obj - object to add points to
|
||||||
|
@param {Boolean} force - if set to true, will regenerate `p.points` even if it already exists, otherwise if p.points exist it'll be left alone
|
||||||
|
*/
|
||||||
|
Q._generatePoints = function(obj,force) {
|
||||||
|
if(obj.p.points && !force) { return; }
|
||||||
|
var p = obj.p,
|
||||||
|
halfW = p.w/2,
|
||||||
|
halfH = p.h/2;
|
||||||
|
|
||||||
|
p.points = [
|
||||||
|
[ -halfW, -halfH ],
|
||||||
|
[ halfW, -halfH ],
|
||||||
|
[ halfW, halfH ],
|
||||||
|
[ -halfW, halfH ]
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Generate a square set of `c.points` on an object from the object transform matrix and `p.points`
|
||||||
|
|
||||||
|
`c.points` represents the collision points of an sprite in world coordinates, scaled, rotate and taking into account any parent transforms.
|
||||||
|
|
||||||
|
|
||||||
|
@method Q._generateCollisionPoints
|
||||||
|
@for Quintus.Sprites
|
||||||
|
@param {q.sprite} obj - object to add collision points to
|
||||||
|
*/
|
||||||
|
Q._generateCollisionPoints = function(obj) {
|
||||||
|
if(!obj.matrix && !obj.refreshMatrix) { return; }
|
||||||
|
if(!obj.c) { obj.c = { points: [] }; }
|
||||||
|
var p = obj.p, c = obj.c;
|
||||||
|
|
||||||
|
if(!p.moved &&
|
||||||
|
c.origX === p.x &&
|
||||||
|
c.origY === p.y &&
|
||||||
|
c.origScale === p.scale &&
|
||||||
|
c.origAngle === p.angle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
c.origX = p.x;
|
||||||
|
c.origY = p.y;
|
||||||
|
c.origScale = p.scale;
|
||||||
|
c.origAngle = p.angle;
|
||||||
|
|
||||||
|
obj.refreshMatrix();
|
||||||
|
|
||||||
|
var i;
|
||||||
|
|
||||||
|
// Early out if we don't need to rotate / scale / deal with a container
|
||||||
|
if(!obj.container && (!p.scale || p.scale === 1) && p.angle === 0) {
|
||||||
|
for(i=0;i<obj.p.points.length;i++) {
|
||||||
|
obj.c.points[i] = obj.c.points[i] || [];
|
||||||
|
obj.c.points[i][0] = p.x + obj.p.points[i][0];
|
||||||
|
obj.c.points[i][1] = p.y + obj.p.points[i][1];
|
||||||
|
}
|
||||||
|
c.x = p.x; c.y = p.y;
|
||||||
|
c.cx = p.cx; c.cy = p.cy;
|
||||||
|
c.w = p.w; c.h = p.h;
|
||||||
|
} else {
|
||||||
|
var container = obj.container || Q._nullContainer;
|
||||||
|
|
||||||
|
c.x = container.matrix.transformX(p.x,p.y);
|
||||||
|
c.y = container.matrix.transformY(p.x,p.y);
|
||||||
|
c.angle = p.angle + container.c.angle;
|
||||||
|
c.scale = (container.c.scale || 1) * (p.scale || 1);
|
||||||
|
|
||||||
|
var minX = Infinity,
|
||||||
|
minY = Infinity,
|
||||||
|
maxX = -Infinity,
|
||||||
|
maxY = -Infinity;
|
||||||
|
|
||||||
|
for(i=0;i<obj.p.points.length;i++) {
|
||||||
|
if(!obj.c.points[i]) {
|
||||||
|
obj.c.points[i] = [];
|
||||||
|
}
|
||||||
|
obj.matrix.transformArr(obj.p.points[i],obj.c.points[i]);
|
||||||
|
var x = obj.c.points[i][0],
|
||||||
|
y = obj.c.points[i][1];
|
||||||
|
|
||||||
|
if(x < minX) { minX = x; }
|
||||||
|
if(x > maxX) { maxX = x; }
|
||||||
|
if(y < minY) { minY = y; }
|
||||||
|
if(y > maxY) { maxY = y; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if(minX === maxX) { maxX+=1; }
|
||||||
|
if(minY === maxY) { maxY+=1; }
|
||||||
|
|
||||||
|
c.cx = c.x - minX;
|
||||||
|
c.cy = c.y - minY;
|
||||||
|
|
||||||
|
c.w = maxX - minX;
|
||||||
|
c.h = maxY - minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.moved = false;
|
||||||
|
|
||||||
|
// TODO: Invoke moved on children
|
||||||
|
if(obj.children && obj.children.length > 0) {
|
||||||
|
Q._invoke(obj.children,"moved");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
Basic sprite class - will render either and asset or a frame from a sprite sheet.
|
||||||
|
|
||||||
|
Auto sets the width and height (`p.w` and `p.h`) from the provided image asset and
|
||||||
|
centers the sprite so 0,0 is the center of the provide image.
|
||||||
|
|
||||||
|
Most of the times you'll sub-class `Q.Sprite`
|
||||||
|
|
||||||
|
@extends Q.GameObject
|
||||||
|
@class Q.Sprite
|
||||||
|
@for Quintus.Sprites
|
||||||
|
*/
|
||||||
|
Q.GameObject.extend("Sprite",{
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
Default sprite constructor, takes in a set of properties and a set of default properties (useful when you create a subclass of sprite)
|
||||||
|
|
||||||
|
Default properties:
|
||||||
|
|
||||||
|
{
|
||||||
|
asset: null, // asset to use
|
||||||
|
sheet: null, // sprite sheet to use (overrides asset)
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0,
|
||||||
|
w: 0, // width, set from p.asset or p.sheet
|
||||||
|
h: 0, // height, set from p.asset or p.sheet
|
||||||
|
cx: w/2, // center x, defaults to center of the asset or sheet
|
||||||
|
cy: h/2, // center y, default same as cx
|
||||||
|
// points defines the collision shape, override to customer the collision shape,
|
||||||
|
// must be a convex polygon in clockwise order
|
||||||
|
points: [ [ -w/2, -h/2 ], [ w/2, -h/2 ], [ w/2, h/2 ], [ -w/2, h/2 ] ],
|
||||||
|
opacity: 1,
|
||||||
|
angle: 0,
|
||||||
|
frame: 0
|
||||||
|
type: Q.SPRITE_DEFAULT | Q.SPRITE_ACTIVE,
|
||||||
|
name: '',
|
||||||
|
sort: false, // set to true to force children to be sorted by theier p.z,
|
||||||
|
hidden: false, // set to true to hide the sprite
|
||||||
|
flip: "" // set to "x", "y", or "xy" to flip sprite over that dimension
|
||||||
|
}
|
||||||
|
|
||||||
|
@method init
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {Object} props - property has that will be turned into `p`
|
||||||
|
@param {Object} [defaultProps] - default properties that are assigned only if there's not a corresponding value in `props`
|
||||||
|
*/
|
||||||
|
init: function(props,defaultProps) {
|
||||||
|
this.p = Q._extend({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0,
|
||||||
|
opacity: 1,
|
||||||
|
angle: 0,
|
||||||
|
frame: 0,
|
||||||
|
type: Q.SPRITE_DEFAULT | Q.SPRITE_ACTIVE,
|
||||||
|
name: '',
|
||||||
|
spriteProperties: {}
|
||||||
|
},defaultProps);
|
||||||
|
|
||||||
|
this.matrix = new Q.Matrix2D();
|
||||||
|
this.children = [];
|
||||||
|
|
||||||
|
Q._extend(this.p,props);
|
||||||
|
|
||||||
|
this.size();
|
||||||
|
this.p.id = this.p.id || Q._uniqueId();
|
||||||
|
|
||||||
|
this.refreshMatrix();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Resets the width, height and center based on the
|
||||||
|
asset or sprite sheet
|
||||||
|
|
||||||
|
@method size
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {Boolean} force - force a reset (call if w or h changes)
|
||||||
|
*/
|
||||||
|
size: function(force) {
|
||||||
|
if(force || (!this.p.w || !this.p.h)) {
|
||||||
|
if(this.asset()) {
|
||||||
|
this.p.w = this.asset().width;
|
||||||
|
this.p.h = this.asset().height;
|
||||||
|
} else if(this.sheet()) {
|
||||||
|
this.p.w = this.sheet().tileW;
|
||||||
|
this.p.h = this.sheet().tileH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.p.cx = (force || this.p.cx === void 0) ? (this.p.w / 2) : this.p.cx;
|
||||||
|
this.p.cy = (force || this.p.cy === void 0) ? (this.p.h / 2) : this.p.cy;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Get or set the asset associate with this sprite
|
||||||
|
|
||||||
|
@method asset
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {String} [name] - leave empty to return the asset, add to set the asset
|
||||||
|
@param {Boolean} [resize] - force a call to `size()` and `_generatePoints`
|
||||||
|
*/
|
||||||
|
asset: function(name,resize) {
|
||||||
|
if(!name) { return Q.asset(this.p.asset); }
|
||||||
|
|
||||||
|
this.p.asset = name;
|
||||||
|
if(resize) {
|
||||||
|
this.size(true);
|
||||||
|
Q._generatePoints(this,true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
Get or set the sheet associate with this sprite
|
||||||
|
|
||||||
|
@method sheet
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {String} [name] - leave empty to return the sprite sheet, add to resize
|
||||||
|
@param {Boolean} [resize] - force a resize
|
||||||
|
*/
|
||||||
|
sheet: function(name,resize) {
|
||||||
|
if(!name) { return Q.sheet(this.p.sheet); }
|
||||||
|
|
||||||
|
this.p.sheet = name;
|
||||||
|
if(resize) {
|
||||||
|
this.size(true);
|
||||||
|
Q._generatePoints(this,true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Hide the sprite (render returns without rendering)
|
||||||
|
|
||||||
|
@method hide
|
||||||
|
@for Q.Sprite
|
||||||
|
*/
|
||||||
|
hide: function() {
|
||||||
|
this.p.hidden = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Show the sprite
|
||||||
|
|
||||||
|
@method show
|
||||||
|
@for Q.Sprite
|
||||||
|
*/
|
||||||
|
show: function() {
|
||||||
|
this.p.hidden = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Set a set of `p` properties on a Sprite
|
||||||
|
|
||||||
|
@method set
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {Object} properties - hash of properties to set
|
||||||
|
*/
|
||||||
|
set: function(properties) {
|
||||||
|
Q._extend(this.p,properties);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
_sortChild: function(a,b) {
|
||||||
|
return ((a.p && a.p.z) || -1) - ((b.p && b.p.z) || -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
_flipArgs: {
|
||||||
|
"x": [ -1, 1],
|
||||||
|
"y": [ 1, -1],
|
||||||
|
"xy": [ -1, -1]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Default render method for the sprite. Don't overload this unless you want to
|
||||||
|
handle all the transform and scale stuff yourself. Rather overload the `draw` method.
|
||||||
|
|
||||||
|
@method render
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {Context2D} ctx - context to render to
|
||||||
|
*/
|
||||||
|
render: function(ctx) {
|
||||||
|
var p = this.p;
|
||||||
|
|
||||||
|
if(p.hidden || p.opacity === 0) { return; }
|
||||||
|
if(!ctx) { ctx = Q.ctx; }
|
||||||
|
|
||||||
|
this.trigger('predraw',ctx);
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
if(this.p.opacity !== void 0 && this.p.opacity !== 1) {
|
||||||
|
ctx.globalAlpha = this.p.opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.matrix.setContextTransform(ctx);
|
||||||
|
|
||||||
|
if(this.p.flip) { ctx.scale.apply(ctx,this._flipArgs[this.p.flip]); }
|
||||||
|
|
||||||
|
this.trigger('beforedraw',ctx);
|
||||||
|
this.draw(ctx);
|
||||||
|
this.trigger('draw',ctx);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Children set up their own complete matrix
|
||||||
|
// from the base stage matrix
|
||||||
|
if(this.p.sort) { this.children.sort(this._sortChild); }
|
||||||
|
Q._invoke(this.children,"render",ctx);
|
||||||
|
|
||||||
|
this.trigger('postdraw',ctx);
|
||||||
|
|
||||||
|
if(Q.debug) { this.debugRender(ctx); }
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Center sprite inside of it's container (or the stage)
|
||||||
|
|
||||||
|
@method center
|
||||||
|
@for Q.Sprite
|
||||||
|
*/
|
||||||
|
center: function() {
|
||||||
|
if(this.container) {
|
||||||
|
this.p.x = 0;
|
||||||
|
this.p.y = 0;
|
||||||
|
} else {
|
||||||
|
this.p.x = Q.width / 2;
|
||||||
|
this.p.y = Q.height / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Draw the asset on the stage. the context passed in is alreay transformed.
|
||||||
|
|
||||||
|
All you need to do is a draw the sprite centered at 0,0
|
||||||
|
|
||||||
|
@method draw
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {Context2D} ctx
|
||||||
|
*/
|
||||||
|
draw: function(ctx) {
|
||||||
|
var p = this.p;
|
||||||
|
if(p.sheet) {
|
||||||
|
this.sheet().draw(ctx,-p.cx,-p.cy,p.frame);
|
||||||
|
} else if(p.asset) {
|
||||||
|
ctx.drawImage(Q.asset(p.asset),-p.cx,-p.cy);
|
||||||
|
} else if(p.color) {
|
||||||
|
ctx.fillStyle = p.color;
|
||||||
|
ctx.fillRect(-p.cx,-p.cy,p.w,p.h);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
debugRender: function(ctx) {
|
||||||
|
if(!this.p.points) {
|
||||||
|
Q._generatePoints(this);
|
||||||
|
}
|
||||||
|
ctx.save();
|
||||||
|
this.matrix.setContextTransform(ctx);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = this.p.hit ? "blue" : "red";
|
||||||
|
ctx.strokeStyle = "#FF0000";
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.5)";
|
||||||
|
|
||||||
|
ctx.moveTo(this.p.points[0][0],this.p.points[0][1]);
|
||||||
|
for(var i=0;i<this.p.points.length;i++) {
|
||||||
|
ctx.lineTo(this.p.points[i][0],this.p.points[i][1]);
|
||||||
|
}
|
||||||
|
ctx.lineTo(this.p.points[0][0],this.p.points[0][1]);
|
||||||
|
ctx.stroke();
|
||||||
|
if(Q.debugFill) { ctx.fill(); }
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
if(this.c) {
|
||||||
|
var c = this.c;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = "#FF00FF";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(c.x - c.cx, c.y - c.cy);
|
||||||
|
ctx.lineTo(c.x - c.cx + c.w, c.y - c.cy);
|
||||||
|
ctx.lineTo(c.x - c.cx + c.w, c.y - c.cy + c.h);
|
||||||
|
ctx.lineTo(c.x - c.cx , c.y - c.cy + c.h);
|
||||||
|
ctx.lineTo(c.x - c.cx, c.y - c.cy);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Update method is called each step with the time elapsed since the last step.
|
||||||
|
|
||||||
|
Doesn't do anything other than trigger events, call a `step` method if defined
|
||||||
|
and run update on all its children.
|
||||||
|
|
||||||
|
Generally leave this method alone and define a `step` method that will be called
|
||||||
|
|
||||||
|
@method update
|
||||||
|
@for Q.Sprite
|
||||||
|
@param {Float} dt - time elapsed since last call
|
||||||
|
*/
|
||||||
|
update: function(dt) {
|
||||||
|
this.trigger('prestep',dt);
|
||||||
|
if(this.step) { this.step(dt); }
|
||||||
|
this.trigger('step',dt);
|
||||||
|
Q._generateCollisionPoints(this);
|
||||||
|
|
||||||
|
// Ugly coupling to stage - workaround?
|
||||||
|
if(this.stage && this.children.length > 0) {
|
||||||
|
this.stage.updateSprites(this.children,dt,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset collisions if we're tracking them
|
||||||
|
if(this.p.collisions) { this.p.collisions = []; }
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
Regenerates this sprite's transformation matrix
|
||||||
|
|
||||||
|
@method refreshMatrix
|
||||||
|
@for Q.Sprite
|
||||||
|
*/
|
||||||
|
refreshMatrix: function() {
|
||||||
|
var p = this.p;
|
||||||
|
this.matrix.identity();
|
||||||
|
|
||||||
|
if(this.container) { this.matrix.multiply(this.container.matrix); }
|
||||||
|
|
||||||
|
this.matrix.translate(p.x,p.y);
|
||||||
|
|
||||||
|
if(p.scale) { this.matrix.scale(p.scale,p.scale); }
|
||||||
|
|
||||||
|
this.matrix.rotateDeg(p.angle);
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
Marks a sprite as having been moved
|
||||||
|
|
||||||
|
@method moved
|
||||||
|
@for Q.Sprite
|
||||||
|
*/
|
||||||
|
moved: function() {
|
||||||
|
this.p.moved = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
Simple sprite that adds in basic newtonian physics on each step:
|
||||||
|
|
||||||
|
p.vx += p.ax * dt;
|
||||||
|
p.vy += p.ay * dt;
|
||||||
|
|
||||||
|
p.x += p.vx * dt;
|
||||||
|
p.y += p.vy * dt;
|
||||||
|
|
||||||
|
@class Q.MovingSprite
|
||||||
|
@extends Q.Sprite
|
||||||
|
@for Quintus.Sprites
|
||||||
|
*/
|
||||||
|
Q.Sprite.extend("MovingSprite",{
|
||||||
|
init: function(props,defaultProps) {
|
||||||
|
this._super(Q._extend({
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
ax: 0,
|
||||||
|
ay: 0
|
||||||
|
},props),defaultProps);
|
||||||
|
},
|
||||||
|
|
||||||
|
step: function(dt) {
|
||||||
|
var p = this.p;
|
||||||
|
|
||||||
|
p.vx += p.ax * dt;
|
||||||
|
p.vy += p.ay * dt;
|
||||||
|
|
||||||
|
p.x += p.vx * dt;
|
||||||
|
p.y += p.vy * dt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return Q;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if(typeof Quintus === 'undefined') {
|
||||||
|
module.exports = quintusSprites;
|
||||||
|
} else {
|
||||||
|
quintusSprites(Quintus);
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ target_compile_definitions(usockets PUBLIC LIBUS_NO_SSL LIBUS_USE_EPOLL)
|
|||||||
add_executable(gameserver
|
add_executable(gameserver
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/Lobby.cpp
|
src/Lobby.cpp
|
||||||
|
src/Game.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(gameserver PRIVATE
|
target_include_directories(gameserver PRIVATE
|
||||||
${uwebsockets_SOURCE_DIR}/src
|
${uwebsockets_SOURCE_DIR}/src
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
|||||||
# Заглушки вместо src/, чтобы cmake configure не падал на несуществующих
|
# Заглушки вместо src/, чтобы cmake configure не падал на несуществующих
|
||||||
# исходниках — они не компилируются, т.к. просим собрать только usockets.
|
# исходниках — они не компилируются, т.к. просим собрать только usockets.
|
||||||
COPY CMakeLists.txt ./
|
COPY CMakeLists.txt ./
|
||||||
RUN mkdir src && touch src/main.cpp src/Lobby.cpp src/Lobby.hpp
|
RUN mkdir src && touch src/main.cpp src/Lobby.cpp src/Lobby.hpp src/Game.cpp src/Game.hpp
|
||||||
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --target usockets -j"$(nproc)"
|
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --target usockets -j"$(nproc)"
|
||||||
|
|
||||||
# Реальный код поверх уже собранных зависимостей — пересобирается за секунды.
|
# Реальный код поверх уже собранных зависимостей — пересобирается за секунды.
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
#include "Game.hpp"
|
||||||
|
|
||||||
|
#include <App.h>
|
||||||
|
#include <libusockets.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr double kTankHalf = 0.4;
|
||||||
|
constexpr double kTankSpeed = 4.0; // тайлов/сек
|
||||||
|
constexpr double kBulletSpeed = 8.0; // тайлов/сек
|
||||||
|
constexpr double kBulletHalf = 0.08;
|
||||||
|
constexpr int kFireCooldownTicks = 10; // 0.5с при 20 Гц
|
||||||
|
constexpr double kDt = Game::kTickMs / 1000.0;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Game::Game(Room &room, std::function<void()> onFinished) : onFinished_(std::move(onFinished)) {
|
||||||
|
buildMap();
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
Tank &t = tanks_[i];
|
||||||
|
t.player_id = room.players[i].player_id;
|
||||||
|
t.ws = room.players[i].ws;
|
||||||
|
if (i == 0) {
|
||||||
|
t.x = 1.5;
|
||||||
|
t.y = kMapHeight - 2.5;
|
||||||
|
t.dir = Dir::Up;
|
||||||
|
} else {
|
||||||
|
t.x = kMapWidth - 2.5;
|
||||||
|
t.y = 1.5;
|
||||||
|
t.dir = Dir::Down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Game::~Game() {
|
||||||
|
if (timer_) {
|
||||||
|
us_timer_close(timer_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::buildMap() {
|
||||||
|
map_.assign(kMapHeight, std::vector<int>(kMapWidth, kEmpty));
|
||||||
|
for (int y = 0; y < kMapHeight; y++) {
|
||||||
|
for (int x = 0; x < kMapWidth; x++) {
|
||||||
|
if (x == 0 || y == 0 || x == kMapWidth - 1 || y == kMapHeight - 1) {
|
||||||
|
map_[y][x] = kSteel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Симметричные (180°) кластеры кирпича/стали в интерьере карты.
|
||||||
|
auto placeMirrored = [this](int x, int y, int w, int h, int tile) {
|
||||||
|
for (int dy = 0; dy < h; dy++) {
|
||||||
|
for (int dx = 0; dx < w; dx++) {
|
||||||
|
map_[y + dy][x + dx] = tile;
|
||||||
|
map_[kMapHeight - 1 - (y + dy)][kMapWidth - 1 - (x + dx)] = tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
placeMirrored(3, 3, 2, 2, kBrick);
|
||||||
|
placeMirrored(6, 3, 1, 2, kBrick);
|
||||||
|
placeMirrored(3, 6, 2, 1, kSteel);
|
||||||
|
placeMirrored(6, 6, 3, 3, kBrick);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::start() {
|
||||||
|
timer_ = us_create_timer((us_loop_t *)uWS::Loop::get(), 0, sizeof(Game *));
|
||||||
|
*(Game **)us_timer_ext(timer_) = this;
|
||||||
|
us_timer_set(
|
||||||
|
timer_,
|
||||||
|
[](us_timer_t *t) {
|
||||||
|
Game *self = *(Game **)us_timer_ext(t);
|
||||||
|
self->tick();
|
||||||
|
},
|
||||||
|
kTickMs, kTickMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::handleInput(const std::string &player_id, const json &payload) {
|
||||||
|
for (auto &t : tanks_) {
|
||||||
|
if (t.player_id == player_id) {
|
||||||
|
t.up = payload.value("up", false);
|
||||||
|
t.down = payload.value("down", false);
|
||||||
|
t.left = payload.value("left", false);
|
||||||
|
t.right = payload.value("right", false);
|
||||||
|
t.fire = payload.value("fire", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::tileSolid(int tx, int ty) const {
|
||||||
|
if (tx < 0 || ty < 0 || ty >= kMapHeight || tx >= kMapWidth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return map_[ty][tx] != kEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::rectHitsSolid(double x, double y) const {
|
||||||
|
int minX = (int)std::floor(x - kTankHalf);
|
||||||
|
int maxX = (int)std::floor(x + kTankHalf - 1e-6);
|
||||||
|
int minY = (int)std::floor(y - kTankHalf);
|
||||||
|
int maxY = (int)std::floor(y + kTankHalf - 1e-6);
|
||||||
|
for (int ty = minY; ty <= maxY; ty++) {
|
||||||
|
for (int tx = minX; tx <= maxX; tx++) {
|
||||||
|
if (tileSolid(tx, ty)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::rectHitsTank(double x, double y, const Tank &self) const {
|
||||||
|
for (const auto &t : tanks_) {
|
||||||
|
if (&t == &self || !t.alive) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::abs(x - t.x) < 2 * kTankHalf && std::abs(y - t.y) < 2 * kTankHalf) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::applyInput(Tank &tank) {
|
||||||
|
if (!tank.alive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tank.fire_cooldown > 0) {
|
||||||
|
tank.fire_cooldown--;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dir moveDir = tank.dir;
|
||||||
|
bool moving = false;
|
||||||
|
if (tank.up) {
|
||||||
|
moveDir = Dir::Up;
|
||||||
|
moving = true;
|
||||||
|
} else if (tank.down) {
|
||||||
|
moveDir = Dir::Down;
|
||||||
|
moving = true;
|
||||||
|
} else if (tank.left) {
|
||||||
|
moveDir = Dir::Left;
|
||||||
|
moving = true;
|
||||||
|
} else if (tank.right) {
|
||||||
|
moveDir = Dir::Right;
|
||||||
|
moving = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tank.dir = moveDir;
|
||||||
|
|
||||||
|
if (moving) {
|
||||||
|
double nx = tank.x, ny = tank.y;
|
||||||
|
double step = kTankSpeed * kDt;
|
||||||
|
switch (moveDir) {
|
||||||
|
case Dir::Up: ny -= step; break;
|
||||||
|
case Dir::Down: ny += step; break;
|
||||||
|
case Dir::Left: nx -= step; break;
|
||||||
|
case Dir::Right: nx += step; break;
|
||||||
|
}
|
||||||
|
if (!rectHitsSolid(nx, ny) && !rectHitsTank(nx, ny, tank)) {
|
||||||
|
tank.x = nx;
|
||||||
|
tank.y = ny;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tank.fire) {
|
||||||
|
fireBullet(tank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::fireBullet(Tank &tank) {
|
||||||
|
if (tank.fire_cooldown > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool hasOwnBullet = std::any_of(bullets_.begin(), bullets_.end(),
|
||||||
|
[&](const Bullet &b) { return b.owner_id == tank.player_id; });
|
||||||
|
if (hasOwnBullet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bullet b;
|
||||||
|
b.id = next_bullet_id_++;
|
||||||
|
b.owner_id = tank.player_id;
|
||||||
|
b.dir = tank.dir;
|
||||||
|
double offset = kTankHalf + kBulletHalf + 0.01;
|
||||||
|
b.x = tank.x;
|
||||||
|
b.y = tank.y;
|
||||||
|
switch (tank.dir) {
|
||||||
|
case Dir::Up: b.y -= offset; break;
|
||||||
|
case Dir::Down: b.y += offset; break;
|
||||||
|
case Dir::Left: b.x -= offset; break;
|
||||||
|
case Dir::Right: b.x += offset; break;
|
||||||
|
}
|
||||||
|
bullets_.push_back(b);
|
||||||
|
tank.fire_cooldown = kFireCooldownTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::updateBullets() {
|
||||||
|
double step = kBulletSpeed * kDt;
|
||||||
|
|
||||||
|
for (auto &b : bullets_) {
|
||||||
|
switch (b.dir) {
|
||||||
|
case Dir::Up: b.y -= step; break;
|
||||||
|
case Dir::Down: b.y += step; break;
|
||||||
|
case Dir::Left: b.x -= step; break;
|
||||||
|
case Dir::Right: b.x += step; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пуля против пули: встречные уничтожают друг друга.
|
||||||
|
std::vector<bool> dead(bullets_.size(), false);
|
||||||
|
for (size_t i = 0; i < bullets_.size(); i++) {
|
||||||
|
if (dead[i]) continue;
|
||||||
|
for (size_t j = i + 1; j < bullets_.size(); j++) {
|
||||||
|
if (dead[j] || bullets_[i].owner_id == bullets_[j].owner_id) continue;
|
||||||
|
if (std::abs(bullets_[i].x - bullets_[j].x) < 2 * kBulletHalf &&
|
||||||
|
std::abs(bullets_[i].y - bullets_[j].y) < 2 * kBulletHalf) {
|
||||||
|
dead[i] = dead[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int loserIndex = -1;
|
||||||
|
bool anyoneHit = false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < bullets_.size(); i++) {
|
||||||
|
if (dead[i]) continue;
|
||||||
|
Bullet &b = bullets_[i];
|
||||||
|
|
||||||
|
int tx = (int)std::floor(b.x);
|
||||||
|
int ty = (int)std::floor(b.y);
|
||||||
|
if (tx < 0 || ty < 0 || tx >= kMapWidth || ty >= kMapHeight) {
|
||||||
|
dead[i] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (map_[ty][tx] == kSteel) {
|
||||||
|
dead[i] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (map_[ty][tx] == kBrick) {
|
||||||
|
map_[ty][tx] = kEmpty;
|
||||||
|
dead[i] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int ti = 0; ti < 2; ti++) {
|
||||||
|
Tank &t = tanks_[ti];
|
||||||
|
if (!t.alive || t.player_id == b.owner_id) continue;
|
||||||
|
if (std::abs(b.x - t.x) < kTankHalf + kBulletHalf && std::abs(b.y - t.y) < kTankHalf + kBulletHalf) {
|
||||||
|
dead[i] = true;
|
||||||
|
t.lives--;
|
||||||
|
anyoneHit = true;
|
||||||
|
if (t.lives <= 0) {
|
||||||
|
t.alive = false;
|
||||||
|
loserIndex = ti;
|
||||||
|
} else {
|
||||||
|
// респаун на стартовой позиции
|
||||||
|
t.x = (ti == 0) ? 1.5 : kMapWidth - 2.5;
|
||||||
|
t.y = (ti == 0) ? kMapHeight - 2.5 : 1.5;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Bullet> alive;
|
||||||
|
for (size_t i = 0; i < bullets_.size(); i++) {
|
||||||
|
if (!dead[i]) alive.push_back(bullets_[i]);
|
||||||
|
}
|
||||||
|
bullets_ = std::move(alive);
|
||||||
|
|
||||||
|
(void)anyoneHit;
|
||||||
|
if (loserIndex != -1) {
|
||||||
|
endGame(1 - loserIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::tick() {
|
||||||
|
if (finished_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tick_count_++;
|
||||||
|
|
||||||
|
for (auto &t : tanks_) {
|
||||||
|
applyInput(t);
|
||||||
|
}
|
||||||
|
updateBullets();
|
||||||
|
|
||||||
|
if (!finished_) {
|
||||||
|
broadcastState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json Game::dirToJson(Dir d) {
|
||||||
|
switch (d) {
|
||||||
|
case Dir::Up: return "up";
|
||||||
|
case Dir::Down: return "down";
|
||||||
|
case Dir::Left: return "left";
|
||||||
|
case Dir::Right: return "right";
|
||||||
|
}
|
||||||
|
return "up";
|
||||||
|
}
|
||||||
|
|
||||||
|
json Game::serializeTank(const Tank &t) const {
|
||||||
|
return {
|
||||||
|
{"id", t.player_id},
|
||||||
|
{"x", t.x},
|
||||||
|
{"y", t.y},
|
||||||
|
{"direction", dirToJson(t.dir)},
|
||||||
|
{"lives", t.lives},
|
||||||
|
{"alive", t.alive},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
json Game::serializeBullet(const Bullet &b) const {
|
||||||
|
return {
|
||||||
|
{"id", b.id},
|
||||||
|
{"owner_id", b.owner_id},
|
||||||
|
{"x", b.x},
|
||||||
|
{"y", b.y},
|
||||||
|
{"direction", dirToJson(b.dir)},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::sendTo(WS *ws, const std::string &type, json payload) {
|
||||||
|
json msg = {{"type", type}, {"payload", std::move(payload)}};
|
||||||
|
ws->send(msg.dump(), uWS::OpCode::TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::broadcastState() {
|
||||||
|
json tanks = json::array();
|
||||||
|
for (const auto &t : tanks_) tanks.push_back(serializeTank(t));
|
||||||
|
json bullets = json::array();
|
||||||
|
for (const auto &b : bullets_) bullets.push_back(serializeBullet(b));
|
||||||
|
|
||||||
|
json payload = {{"tick", tick_count_}, {"tanks", tanks}, {"bullets", bullets}};
|
||||||
|
for (const auto &t : tanks_) {
|
||||||
|
sendTo(t.ws, "game.state", payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json Game::buildStartPayload(const std::string &mode) const {
|
||||||
|
json mapJson = json::array();
|
||||||
|
for (const auto &row : map_) {
|
||||||
|
mapJson.push_back(row);
|
||||||
|
}
|
||||||
|
json players = json::array();
|
||||||
|
for (const auto &t : tanks_) {
|
||||||
|
players.push_back({{"id", t.player_id}});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
{"map", mapJson},
|
||||||
|
{"mode", mode},
|
||||||
|
{"players", players},
|
||||||
|
{"tick_rate", 1000 / kTickMs},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::handleDisconnect(WS *ws) {
|
||||||
|
if (finished_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
if (tanks_[i].ws == ws) {
|
||||||
|
int winnerIndex = 1 - i;
|
||||||
|
finished_ = true;
|
||||||
|
if (timer_) {
|
||||||
|
us_timer_close(timer_);
|
||||||
|
timer_ = nullptr;
|
||||||
|
}
|
||||||
|
sendTo(tanks_[winnerIndex].ws, "game.over", {{"result", "win"}, {"reason", "соперник отключился"}});
|
||||||
|
if (onFinished_) {
|
||||||
|
onFinished_();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::endGame(int winnerIndex) {
|
||||||
|
finished_ = true;
|
||||||
|
if (timer_) {
|
||||||
|
us_timer_close(timer_);
|
||||||
|
timer_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
std::string result = (winnerIndex == -1) ? "draw" : (i == winnerIndex ? "win" : "lose");
|
||||||
|
std::string reason = (winnerIndex == -1) ? "оба танка уничтожены одновременно"
|
||||||
|
: (i == winnerIndex) ? "противник уничтожен"
|
||||||
|
: "танк уничтожен";
|
||||||
|
sendTo(tanks_[i].ws, "game.over", {{"result", result}, {"reason", reason}});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onFinished_) {
|
||||||
|
onFinished_();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Lobby.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct us_timer_t;
|
||||||
|
|
||||||
|
// Authoritative-игра для одной комнаты в режиме pvp (этап 4 из TZ.md).
|
||||||
|
// Coop с AI — этап 5, наследует эту же тик-логику.
|
||||||
|
class Game {
|
||||||
|
public:
|
||||||
|
static constexpr int kTickMs = 50; // 20 Гц
|
||||||
|
static constexpr int kMapWidth = 15;
|
||||||
|
static constexpr int kMapHeight = 15;
|
||||||
|
static constexpr int kStartLives = 3;
|
||||||
|
|
||||||
|
enum Tile { kEmpty = 0, kBrick = 1, kSteel = 2 };
|
||||||
|
enum class Dir { Up, Right, Down, Left };
|
||||||
|
|
||||||
|
struct Tank {
|
||||||
|
std::string player_id;
|
||||||
|
WS *ws;
|
||||||
|
double x, y;
|
||||||
|
Dir dir = Dir::Up;
|
||||||
|
int lives = kStartLives;
|
||||||
|
int fire_cooldown = 0;
|
||||||
|
bool up = false, down = false, left = false, right = false, fire = false;
|
||||||
|
bool alive = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Bullet {
|
||||||
|
int id;
|
||||||
|
std::string owner_id;
|
||||||
|
double x, y;
|
||||||
|
Dir dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
// room.players.size() должен быть == 2 на момент создания.
|
||||||
|
Game(Room &room, std::function<void()> onFinished);
|
||||||
|
~Game();
|
||||||
|
|
||||||
|
void start();
|
||||||
|
void handleInput(const std::string &player_id, const json &payload);
|
||||||
|
// Технический проигрыш отключившегося — второй игрок побеждает.
|
||||||
|
void handleDisconnect(WS *ws);
|
||||||
|
json buildStartPayload(const std::string &mode) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<int>> map_;
|
||||||
|
std::array<Tank, 2> tanks_;
|
||||||
|
std::vector<Bullet> bullets_;
|
||||||
|
int next_bullet_id_ = 1;
|
||||||
|
int tick_count_ = 0;
|
||||||
|
bool finished_ = false;
|
||||||
|
us_timer_t *timer_ = nullptr;
|
||||||
|
std::function<void()> onFinished_;
|
||||||
|
|
||||||
|
void buildMap();
|
||||||
|
void tick();
|
||||||
|
void applyInput(Tank &tank);
|
||||||
|
void updateBullets();
|
||||||
|
bool tileSolid(int tx, int ty) const;
|
||||||
|
bool rectHitsSolid(double x, double y) const;
|
||||||
|
bool rectHitsTank(double x, double y, const Tank &self) const;
|
||||||
|
void fireBullet(Tank &tank);
|
||||||
|
void endGame(int winnerIndex); // -1 = ничья
|
||||||
|
void broadcastState();
|
||||||
|
void sendTo(WS *ws, const std::string &type, json payload);
|
||||||
|
|
||||||
|
static json dirToJson(Dir d);
|
||||||
|
json serializeTank(const Tank &t) const;
|
||||||
|
json serializeBullet(const Bullet &b) const;
|
||||||
|
};
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
#include "Lobby.hpp"
|
#include "Lobby.hpp"
|
||||||
|
#include "Game.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
Lobby::Lobby() = default;
|
||||||
|
Lobby::~Lobby() = default;
|
||||||
|
|
||||||
void Lobby::send(WS *ws, const std::string &type, json payload) {
|
void Lobby::send(WS *ws, const std::string &type, json payload) {
|
||||||
json msg = {{"type", type}, {"payload", std::move(payload)}};
|
json msg = {{"type", type}, {"payload", std::move(payload)}};
|
||||||
ws->send(msg.dump(), uWS::OpCode::TEXT);
|
ws->send(msg.dump(), uWS::OpCode::TEXT);
|
||||||
@@ -41,6 +45,7 @@ json Lobby::roomPlayers(const Room &room) const {
|
|||||||
{"id", room.players[i].player_id},
|
{"id", room.players[i].player_id},
|
||||||
{"nickname", room.players[i].nickname},
|
{"nickname", room.players[i].nickname},
|
||||||
{"slot", i},
|
{"slot", i},
|
||||||
|
{"ready", room.players[i].ready},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return players;
|
return players;
|
||||||
@@ -69,6 +74,7 @@ void Lobby::onOpen(WS *ws) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Lobby::onClose(WS *ws) {
|
void Lobby::onClose(WS *ws) {
|
||||||
|
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||||
removePlayerFromRoom(ws);
|
removePlayerFromRoom(ws);
|
||||||
sockets_.erase(ws);
|
sockets_.erase(ws);
|
||||||
}
|
}
|
||||||
@@ -96,6 +102,10 @@ void Lobby::onMessage(WS *ws, std::string_view message) {
|
|||||||
handleJoinRoom(ws, payload);
|
handleJoinRoom(ws, payload);
|
||||||
} else if (type == "lobby.leave_room") {
|
} else if (type == "lobby.leave_room") {
|
||||||
handleLeaveRoom(ws);
|
handleLeaveRoom(ws);
|
||||||
|
} else if (type == "game.ready") {
|
||||||
|
handleGameReady(ws);
|
||||||
|
} else if (type == "game.input") {
|
||||||
|
handleGameInput(ws, payload);
|
||||||
} else {
|
} else {
|
||||||
sendError(ws, "unknown_type", "unknown message type: " + type);
|
sendError(ws, "unknown_type", "unknown message type: " + type);
|
||||||
}
|
}
|
||||||
@@ -197,10 +207,98 @@ void Lobby::handleLeaveRoom(WS *ws) {
|
|||||||
sendError(ws, "not_in_room", "you are not in a room");
|
sendError(ws, "not_in_room", "you are not in a room");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||||
removePlayerFromRoom(ws);
|
removePlayerFromRoom(ws);
|
||||||
broadcastRoomsToLobby();
|
broadcastRoomsToLobby();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Lobby::handleGameReady(WS *ws) {
|
||||||
|
auto *data = ws->getUserData();
|
||||||
|
if (data->room_id.empty()) {
|
||||||
|
sendError(ws, "not_in_room", "you are not in a room");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = rooms_.find(data->room_id);
|
||||||
|
if (it == rooms_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (auto &p : it->second.players) {
|
||||||
|
if (p.ws == ws) {
|
||||||
|
p.ready = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcastRoomUpdated(it->second);
|
||||||
|
maybeStartGame(data->room_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::handleGameInput(WS *ws, const json &payload) {
|
||||||
|
auto *data = ws->getUserData();
|
||||||
|
if (data->room_id.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto it = games_.find(data->room_id);
|
||||||
|
if (it == games_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it->second->handleInput(data->player_id, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::maybeStartGame(const std::string &room_id) {
|
||||||
|
auto it = rooms_.find(room_id);
|
||||||
|
if (it == rooms_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Room &room = it->second;
|
||||||
|
if (room.mode != "pvp" || room.state != "waiting" || room.players.size() != kMaxPlayersPerRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool allReady = std::all_of(room.players.begin(), room.players.end(),
|
||||||
|
[](const RoomPlayer &p) { return p.ready; });
|
||||||
|
if (!allReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
room.state = "playing";
|
||||||
|
|
||||||
|
auto game = std::make_unique<Game>(room, [this, room_id]() {
|
||||||
|
auto rit = rooms_.find(room_id);
|
||||||
|
if (rit != rooms_.end()) {
|
||||||
|
rit->second.state = "waiting";
|
||||||
|
for (auto &p : rit->second.players) {
|
||||||
|
p.ready = false;
|
||||||
|
}
|
||||||
|
broadcastRoomUpdated(rit->second);
|
||||||
|
}
|
||||||
|
broadcastRoomsToLobby();
|
||||||
|
// Мы всё ещё внутри вызова метода Game (tick/handleDisconnect), который
|
||||||
|
// и вызвал этот колбэк — удалять объект прямо сейчас было бы use-after-free
|
||||||
|
// на возврате из этого вызова. Откладываем erase на следующий тик луп'а.
|
||||||
|
uWS::Loop::get()->defer([this, room_id]() { games_.erase(room_id); });
|
||||||
|
});
|
||||||
|
|
||||||
|
json startPayload = game->buildStartPayload(room.mode);
|
||||||
|
for (const auto &p : room.players) {
|
||||||
|
send(p.ws, "game.start", startPayload);
|
||||||
|
}
|
||||||
|
game->start();
|
||||||
|
games_[room_id] = std::move(game);
|
||||||
|
|
||||||
|
broadcastRoomsToLobby();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::endActiveGameIfAny(const std::string &room_id, WS *leavingWs) {
|
||||||
|
if (room_id.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto it = games_.find(room_id);
|
||||||
|
if (it == games_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it->second->handleDisconnect(leavingWs);
|
||||||
|
games_.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
void Lobby::removePlayerFromRoom(WS *ws) {
|
void Lobby::removePlayerFromRoom(WS *ws) {
|
||||||
auto *data = ws->getUserData();
|
auto *data = ws->getUserData();
|
||||||
if (data->room_id.empty()) {
|
if (data->room_id.empty()) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <App.h>
|
#include <App.h>
|
||||||
|
#include <memory>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -17,10 +18,13 @@ struct PerSocketData {
|
|||||||
|
|
||||||
using WS = uWS::WebSocket<false, true, PerSocketData>;
|
using WS = uWS::WebSocket<false, true, PerSocketData>;
|
||||||
|
|
||||||
|
class Game;
|
||||||
|
|
||||||
struct RoomPlayer {
|
struct RoomPlayer {
|
||||||
std::string player_id;
|
std::string player_id;
|
||||||
std::string nickname;
|
std::string nickname;
|
||||||
WS *ws;
|
WS *ws;
|
||||||
|
bool ready = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Room {
|
struct Room {
|
||||||
@@ -33,6 +37,9 @@ struct Room {
|
|||||||
// Лобби и реестр комнат (этап 3 из TZ.md). Игровая логика — этапы 4-5.
|
// Лобби и реестр комнат (этап 3 из TZ.md). Игровая логика — этапы 4-5.
|
||||||
class Lobby {
|
class Lobby {
|
||||||
public:
|
public:
|
||||||
|
Lobby();
|
||||||
|
~Lobby();
|
||||||
|
|
||||||
void onOpen(WS *ws);
|
void onOpen(WS *ws);
|
||||||
void onMessage(WS *ws, std::string_view message);
|
void onMessage(WS *ws, std::string_view message);
|
||||||
void onClose(WS *ws);
|
void onClose(WS *ws);
|
||||||
@@ -41,6 +48,7 @@ private:
|
|||||||
static constexpr size_t kMaxPlayersPerRoom = 2;
|
static constexpr size_t kMaxPlayersPerRoom = 2;
|
||||||
|
|
||||||
std::unordered_map<std::string, Room> rooms_;
|
std::unordered_map<std::string, Room> rooms_;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<Game>> games_;
|
||||||
std::unordered_set<WS *> sockets_;
|
std::unordered_set<WS *> sockets_;
|
||||||
int next_player_id_ = 1;
|
int next_player_id_ = 1;
|
||||||
int next_room_id_ = 1;
|
int next_room_id_ = 1;
|
||||||
@@ -50,7 +58,11 @@ private:
|
|||||||
void handleCreateRoom(WS *ws, const json &payload);
|
void handleCreateRoom(WS *ws, const json &payload);
|
||||||
void handleJoinRoom(WS *ws, const json &payload);
|
void handleJoinRoom(WS *ws, const json &payload);
|
||||||
void handleLeaveRoom(WS *ws);
|
void handleLeaveRoom(WS *ws);
|
||||||
|
void handleGameReady(WS *ws);
|
||||||
|
void handleGameInput(WS *ws, const json &payload);
|
||||||
void removePlayerFromRoom(WS *ws);
|
void removePlayerFromRoom(WS *ws);
|
||||||
|
void maybeStartGame(const std::string &room_id);
|
||||||
|
void endActiveGameIfAny(const std::string &room_id, WS *leavingWs);
|
||||||
|
|
||||||
void broadcastRoomsToLobby();
|
void broadcastRoomsToLobby();
|
||||||
void broadcastRoomUpdated(const Room &room, WS *exclude = nullptr);
|
void broadcastRoomUpdated(const Room &room, WS *exclude = nullptr);
|
||||||
|
|||||||
Reference in New Issue
Block a user