Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01cefbbe77 | ||
|
|
1fb3bebbb9 | ||
|
|
2d0be9c287 |
@@ -88,3 +88,34 @@
|
||||
content: '- ';
|
||||
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>
|
||||
|
||||
<main id="game-root">
|
||||
<!-- TODO: лобби (этап 3) и игровое поле на Quintus.js (этапы 4-5) -->
|
||||
<!-- TODO: coop-режим с AI и защитой базы — этап 5 -->
|
||||
<h2 class="prompt">./battlecity</h2>
|
||||
<div id="lobby"></div>
|
||||
<div id="battlecity-canvas-container"></div>
|
||||
<div id="battlecity-canvas-container" style="display: none"></div>
|
||||
</main>
|
||||
</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/game/main.js"></script>
|
||||
</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 { ClientMessage } from './protocol.js';
|
||||
import { startBattle } from './battle.js';
|
||||
|
||||
const container = document.getElementById('lobby');
|
||||
|
||||
let net;
|
||||
let myPlayerId = null;
|
||||
let rooms = [];
|
||||
let currentRoom = null;
|
||||
let errorMessage = '';
|
||||
@@ -47,14 +50,19 @@ function renderLobbyView() {
|
||||
}
|
||||
|
||||
function renderRoomView() {
|
||||
const readyBtn =
|
||||
currentRoom.mode === 'pvp'
|
||||
? '<button data-action="ready">готов</button>'
|
||||
: '<p class="lobby-error">coop ещё не реализован (этап 5)</p>';
|
||||
container.innerHTML = `
|
||||
<div class="lobby-panel">
|
||||
${errorBanner()}
|
||||
<h3 class="prompt">room ${currentRoom.name}</h3>
|
||||
<p>режим: <span class="tag">${currentRoom.mode}</span></p>
|
||||
<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>
|
||||
${readyBtn}
|
||||
<button data-action="leave">выйти в лобби</button>
|
||||
</div>`;
|
||||
}
|
||||
@@ -78,6 +86,9 @@ container?.addEventListener('click', (e) => {
|
||||
case 'join':
|
||||
net.joinRoom(btn.dataset.roomId);
|
||||
break;
|
||||
case 'ready':
|
||||
net.send(ClientMessage.GAME_READY);
|
||||
break;
|
||||
case 'leave':
|
||||
currentRoom = null;
|
||||
net.leaveRoom();
|
||||
@@ -99,8 +110,9 @@ export function initLobby() {
|
||||
net = new Network();
|
||||
|
||||
net.on('_open', () => net.hello(nickname));
|
||||
net.on(ServerMessage.HELLO_ACK, () => {
|
||||
net.on(ServerMessage.HELLO_ACK, (payload) => {
|
||||
connected = true;
|
||||
myPlayerId = payload.player_id;
|
||||
net.listRooms();
|
||||
});
|
||||
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) => {
|
||||
console.error('[gameserver] error:', payload);
|
||||
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
|
||||
src/main.cpp
|
||||
src/Lobby.cpp
|
||||
src/Game.cpp
|
||||
)
|
||||
target_include_directories(gameserver PRIVATE
|
||||
${uwebsockets_SOURCE_DIR}/src
|
||||
|
||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
# Заглушки вместо src/, чтобы cmake configure не падал на несуществующих
|
||||
# исходниках — они не компилируются, т.к. просим собрать только usockets.
|
||||
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)"
|
||||
|
||||
# Реальный код поверх уже собранных зависимостей — пересобирается за секунды.
|
||||
|
||||
@@ -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 "Game.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
Lobby::Lobby() = default;
|
||||
Lobby::~Lobby() = default;
|
||||
|
||||
void Lobby::send(WS *ws, const std::string &type, json payload) {
|
||||
json msg = {{"type", type}, {"payload", std::move(payload)}};
|
||||
ws->send(msg.dump(), uWS::OpCode::TEXT);
|
||||
@@ -41,6 +45,7 @@ json Lobby::roomPlayers(const Room &room) const {
|
||||
{"id", room.players[i].player_id},
|
||||
{"nickname", room.players[i].nickname},
|
||||
{"slot", i},
|
||||
{"ready", room.players[i].ready},
|
||||
});
|
||||
}
|
||||
return players;
|
||||
@@ -69,6 +74,7 @@ void Lobby::onOpen(WS *ws) {
|
||||
}
|
||||
|
||||
void Lobby::onClose(WS *ws) {
|
||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||
removePlayerFromRoom(ws);
|
||||
sockets_.erase(ws);
|
||||
}
|
||||
@@ -96,6 +102,10 @@ void Lobby::onMessage(WS *ws, std::string_view message) {
|
||||
handleJoinRoom(ws, payload);
|
||||
} else if (type == "lobby.leave_room") {
|
||||
handleLeaveRoom(ws);
|
||||
} else if (type == "game.ready") {
|
||||
handleGameReady(ws);
|
||||
} else if (type == "game.input") {
|
||||
handleGameInput(ws, payload);
|
||||
} else {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||
removePlayerFromRoom(ws);
|
||||
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) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->room_id.empty()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <App.h>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -17,10 +18,13 @@ struct PerSocketData {
|
||||
|
||||
using WS = uWS::WebSocket<false, true, PerSocketData>;
|
||||
|
||||
class Game;
|
||||
|
||||
struct RoomPlayer {
|
||||
std::string player_id;
|
||||
std::string nickname;
|
||||
WS *ws;
|
||||
bool ready = false;
|
||||
};
|
||||
|
||||
struct Room {
|
||||
@@ -33,6 +37,9 @@ struct Room {
|
||||
// Лобби и реестр комнат (этап 3 из TZ.md). Игровая логика — этапы 4-5.
|
||||
class Lobby {
|
||||
public:
|
||||
Lobby();
|
||||
~Lobby();
|
||||
|
||||
void onOpen(WS *ws);
|
||||
void onMessage(WS *ws, std::string_view message);
|
||||
void onClose(WS *ws);
|
||||
@@ -41,6 +48,7 @@ private:
|
||||
static constexpr size_t kMaxPlayersPerRoom = 2;
|
||||
|
||||
std::unordered_map<std::string, Room> rooms_;
|
||||
std::unordered_map<std::string, std::unique_ptr<Game>> games_;
|
||||
std::unordered_set<WS *> sockets_;
|
||||
int next_player_id_ = 1;
|
||||
int next_room_id_ = 1;
|
||||
@@ -50,7 +58,11 @@ private:
|
||||
void handleCreateRoom(WS *ws, const json &payload);
|
||||
void handleJoinRoom(WS *ws, const json &payload);
|
||||
void handleLeaveRoom(WS *ws);
|
||||
void handleGameReady(WS *ws);
|
||||
void handleGameInput(WS *ws, const json &payload);
|
||||
void removePlayerFromRoom(WS *ws);
|
||||
void maybeStartGame(const std::string &room_id);
|
||||
void endActiveGameIfAny(const std::string &room_id, WS *leavingWs);
|
||||
|
||||
void broadcastRoomsToLobby();
|
||||
void broadcastRoomUpdated(const Room &room, WS *exclude = nullptr);
|
||||
|
||||
Reference in New Issue
Block a user