Compare commits
10
Commits
0995578092
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be2a04462a | ||
|
|
69b494d054 | ||
|
|
21b383edd8 | ||
|
|
828b1efc9c | ||
|
|
2a22cf544f | ||
|
|
393998b48a | ||
|
|
995fcc1ef7 | ||
|
|
9422b195f6 | ||
|
|
9b8fb83b36 | ||
|
|
6fad11469d |
No files matched your search
@@ -135,6 +135,7 @@ export const PANEL_TITLE_FONT_SIZE = 16;
|
|||||||
export const PANEL_BG_COLOR = '#666';
|
export const PANEL_BG_COLOR = '#666';
|
||||||
export const PANEL_BORDER_COLOR = '#333';
|
export const PANEL_BORDER_COLOR = '#333';
|
||||||
|
|
||||||
|
export const NETWORK_DISCONNECT_TIMEOUT = 15000;
|
||||||
export const STARTING_SCORE = 100;
|
export const STARTING_SCORE = 100;
|
||||||
export const SCORE_PER_KILL = 20;
|
export const SCORE_PER_KILL = 20;
|
||||||
export const SCORE_FOR_WIN = 100;
|
export const SCORE_FOR_WIN = 100;
|
||||||
|
|||||||
+5
-2
@@ -6,6 +6,9 @@ let clickQueue = [];
|
|||||||
let canvasEl = null;
|
let canvasEl = null;
|
||||||
let pointerX = 0, pointerY = 0;
|
let pointerX = 0, pointerY = 0;
|
||||||
let isPointerDown = false;
|
let isPointerDown = false;
|
||||||
|
let inputScale = Z;
|
||||||
|
|
||||||
|
export function setInputScale(s) { inputScale = s; }
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {input[e.key] = true; e.preventDefault()});
|
document.addEventListener('keydown', (e) => {input[e.key] = true; e.preventDefault()});
|
||||||
document.addEventListener('keyup', (e) => {input[e.key] = false; e.preventDefault()});
|
document.addEventListener('keyup', (e) => {input[e.key] = false; e.preventDefault()});
|
||||||
@@ -48,8 +51,8 @@ export function initClickCanvas(canvas) {
|
|||||||
function getCanvasPos(clientX, clientY) {
|
function getCanvasPos(clientX, clientY) {
|
||||||
const rect = canvasEl.getBoundingClientRect();
|
const rect = canvasEl.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x: (clientX - rect.left) / Z,
|
x: (clientX - rect.left) / inputScale,
|
||||||
y: (clientY - rect.top) / Z,
|
y: (clientY - rect.top) / inputScale,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+164
-24
@@ -1,12 +1,12 @@
|
|||||||
import {AI_TYPES} from './ai.js?v=26';
|
import {AI_TYPES} from './ai.js?v=26';
|
||||||
import {DEATH_SPECS, EXPLOSION_SHAKE_REDUCTION_FACTOR, H, MAX_EXPLOSION_SHAKE_FACTOR, MAX_WIND, PARTICLE_AMOUNT, PARTICLE_FADE_AMOUNT, PARTICLE_MAX_POWER_FACTOR, PARTICLE_MIN_LIFETIME, PARTICLE_MIN_POWER_FACTOR, PARTICLE_POWER_REDUCTION_FACTOR, PARTICLE_TIME_FACTOR, PARTICLE_WIND_REDUCTION_FACTOR, PLAYER_ANGLE_FAST_INCREMENT, PLAYER_ANGLE_INCREMENT, PLAYER_ANGLE_TICK_SOUND_INTERVAL, PLAYER_COLORS, PLAYER_ENERGY_POWER_MULTIPLIER, PLAYER_EXPLOSION_PARTICLE_POWER, PLAYER_FALL_DAMAGE_FACTOR, PLAYER_FALL_DAMAGE_HEIGHT, PLAYER_INITIAL_POWER, PLAYER_MAX_ENERGY, PLAYER_POWER_FAST_INCREMENT, PLAYER_POWER_INCREMENT, PLAYER_POWER_TICK_SOUND_INTERVAL, PLAYER_STARTING_TOOLS, PLAYER_STARTING_WEAPONS, PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, PROJECTILE_POWER_REDUCTION_FACTOR, PROJECTILE_WIND_REDUCTION_FACTOR, SHIELD_TYPES, TRAJECTORY_FADE_SPEED, TRAJECTORY_FLOAT_SPEED, W, WEAPON_TYPES, Z, STARTING_SCORE, SCORE_PER_KILL, SCORE_FOR_WIN, MARKET_ITEMS, NAPALM_SPAWN_RATE, FIRE_DURATION, FIRE_DAMAGE, MAX_PLAYERS} from './constants.js?v=26';
|
import {DEATH_SPECS, EXPLOSION_SHAKE_REDUCTION_FACTOR, H, MAX_EXPLOSION_SHAKE_FACTOR, MAX_WIND, NETWORK_DISCONNECT_TIMEOUT, PARTICLE_AMOUNT, PARTICLE_FADE_AMOUNT, PARTICLE_MAX_POWER_FACTOR, PARTICLE_MIN_LIFETIME, PARTICLE_MIN_POWER_FACTOR, PARTICLE_POWER_REDUCTION_FACTOR, PARTICLE_TIME_FACTOR, PARTICLE_WIND_REDUCTION_FACTOR, PLAYER_ANGLE_FAST_INCREMENT, PLAYER_ANGLE_INCREMENT, PLAYER_ANGLE_TICK_SOUND_INTERVAL, PLAYER_COLORS, PLAYER_ENERGY_POWER_MULTIPLIER, PLAYER_EXPLOSION_PARTICLE_POWER, PLAYER_FALL_DAMAGE_FACTOR, PLAYER_FALL_DAMAGE_HEIGHT, PLAYER_INITIAL_POWER, PLAYER_MAX_ENERGY, PLAYER_POWER_FAST_INCREMENT, PLAYER_POWER_INCREMENT, PLAYER_POWER_TICK_SOUND_INTERVAL, PLAYER_STARTING_TOOLS, PLAYER_STARTING_WEAPONS, PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, PROJECTILE_POWER_REDUCTION_FACTOR, PROJECTILE_WIND_REDUCTION_FACTOR, SHIELD_TYPES, TRAJECTORY_FADE_SPEED, TRAJECTORY_FLOAT_SPEED, W, WEAPON_TYPES, Z, STARTING_SCORE, SCORE_PER_KILL, SCORE_FOR_WIN, MARKET_ITEMS, NAPALM_SPAWN_RATE, FIRE_DURATION, FIRE_DAMAGE, MAX_PLAYERS} from './constants.js?v=26';
|
||||||
import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot, strokeCircle} from './gfx.js?v=26';
|
import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot, strokeCircle} from './gfx.js?v=26';
|
||||||
import {afterKeyDelay, key, initClickCanvas, popClick, getPointer, clearKeys} from './input.js?v=26';
|
import {afterKeyDelay, clearKeys, getPointer, initClickCanvas, key, popClick, setInputScale} from './input.js?v=26';
|
||||||
import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from './math.js?v=26';
|
import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from './math.js?v=26';
|
||||||
import {PROJECTILE_TYPES} from './projectiles.js?v=26';
|
import {PROJECTILE_TYPES} from './projectiles.js?v=26';
|
||||||
import {generateSky} from './sky.js?v=26';
|
import {generateSky} from './sky.js?v=26';
|
||||||
import {playTickSound} from './sound.js?v=26';
|
import {playTickSound} from './sound.js?v=26';
|
||||||
import {clipTerrain, closestLand, collapseTerrain, generateTerrain, isTerrain, landHeight, startCollapseTerrain, collapseTerrainStep} from './terrain.js?v=26';
|
import {cacheImageData, clipTerrain, closestLand, collapseTerrain, decodeTerrain, encodeTerrain, generateTerrain, isTerrain, landHeight, startCollapseTerrain, collapseTerrainStep} from './terrain.js?v=26';
|
||||||
import {sample, shuffle, newPlayerId} from './utils.js?v=26';
|
import {sample, shuffle, newPlayerId} from './utils.js?v=26';
|
||||||
import {EXPLOSION_TYPES} from './weapons.js?v=26';
|
import {EXPLOSION_TYPES} from './weapons.js?v=26';
|
||||||
import {drawPanelBg, drawPanelTitle, drawPanelTitleFancy, drawPanelText, drawPanelDivider, drawPanelMenu, getPanelBounds, drawButton, checkHit, PANEL_X, PANEL_WIDTH} from './panel.js?v=26';
|
import {drawPanelBg, drawPanelTitle, drawPanelTitleFancy, drawPanelText, drawPanelDivider, drawPanelMenu, getPanelBounds, drawButton, checkHit, PANEL_X, PANEL_WIDTH} from './panel.js?v=26';
|
||||||
@@ -38,6 +38,11 @@ let netSnapCurAt = 0;
|
|||||||
const reservedPositions = new Set();
|
const reservedPositions = new Set();
|
||||||
let winner;
|
let winner;
|
||||||
|
|
||||||
|
const seenClientExplosions = new Map();
|
||||||
|
const carveQueue = [];
|
||||||
|
let clientCollapseState = null;
|
||||||
|
let nextTraceId = 1;
|
||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let round = 0;
|
let round = 0;
|
||||||
let totalRounds = 1;
|
let totalRounds = 1;
|
||||||
@@ -77,8 +82,14 @@ const foreground = createCanvas(W, H);
|
|||||||
|
|
||||||
// Composited layer
|
// Composited layer
|
||||||
const framebuffer = createCanvas(W, H);
|
const framebuffer = createCanvas(W, H);
|
||||||
framebuffer.canvas.style.width = `${W * Z}px`;
|
function applyDisplayScale() {
|
||||||
framebuffer.canvas.style.height = `${H * Z}px`;
|
const s = Math.max(1, Math.min(Z, Math.floor(window.innerWidth / W), Math.floor(window.innerHeight / H)));
|
||||||
|
framebuffer.canvas.style.width = `${W * s}px`;
|
||||||
|
framebuffer.canvas.style.height = `${H * s}px`;
|
||||||
|
setInputScale(s);
|
||||||
|
}
|
||||||
|
applyDisplayScale();
|
||||||
|
window.addEventListener('resize', applyDisplayScale);
|
||||||
document.body.appendChild(framebuffer.canvas);
|
document.body.appendChild(framebuffer.canvas);
|
||||||
initClickCanvas(framebuffer.canvas);
|
initClickCanvas(framebuffer.canvas);
|
||||||
|
|
||||||
@@ -145,6 +156,7 @@ function initNewGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function commitStartMenu() {
|
function commitStartMenu() {
|
||||||
|
framebuffer.canvas.style.cursor = 'default';
|
||||||
const playerCounts = [3, 4, 5, 6, 7, 8, 9, 10];
|
const playerCounts = [3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
const roundCounts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
const roundCounts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
const terrains = ['Random', 'Mountain', 'Sand'];
|
const terrains = ['Random', 'Mountain', 'Sand'];
|
||||||
@@ -166,6 +178,10 @@ function commitStartMenu() {
|
|||||||
state = 'market';
|
state = 'market';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GITHUB_URL = 'https://github.com/anttro/ravaged-planet';
|
||||||
|
const GITHUB_HIT = {x: PANEL_X + PANEL_WIDTH - 50, y: 320, w: 34, h: 16};
|
||||||
|
const GITHUB_POS = {x: PANEL_X + PANEL_WIDTH - 46, y: 326};
|
||||||
|
|
||||||
function updateStartMenu() {
|
function updateStartMenu() {
|
||||||
const options = ['Players', 'Rounds', 'Terrain', 'Permanent tracer mode', 'Mode'];
|
const options = ['Players', 'Rounds', 'Terrain', 'Permanent tracer mode', 'Mode'];
|
||||||
const optionValues = [[3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10], ['Random','Mountain','Sand'], ['On','Off'], ['Single','Multi']];
|
const optionValues = [[3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10], ['Random','Mountain','Sand'], ['On','Off'], ['Single','Multi']];
|
||||||
@@ -234,6 +250,9 @@ function updateStartMenu() {
|
|||||||
if (checkHit(click.x, click.y, PANEL_X+100, 318, 200, 22)) {
|
if (checkHit(click.x, click.y, PANEL_X+100, 318, 200, 22)) {
|
||||||
commitStartMenu();
|
commitStartMenu();
|
||||||
}
|
}
|
||||||
|
if (checkHit(click.x, click.y, GITHUB_HIT.x, GITHUB_HIT.y, GITHUB_HIT.w, GITHUB_HIT.h)) {
|
||||||
|
window.open(GITHUB_URL, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +407,10 @@ function executeFire(player, a, p, weaponIndex) {
|
|||||||
weapon.ammo -= 1;
|
weapon.ammo -= 1;
|
||||||
player.shotsFired++;
|
player.shotsFired++;
|
||||||
projectileType.create(projectile, player, weapon, px, py, a, p, wind)
|
projectileType.create(projectile, player, weapon, px, py, a, p, wind)
|
||||||
.forEach(x => projectiles.push(x));
|
.forEach(x => {
|
||||||
|
x.traceId = nextTraceId++;
|
||||||
|
projectiles.push(x);
|
||||||
|
});
|
||||||
state = 'shoot';
|
state = 'shoot';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,6 +669,9 @@ const NOOP_OSC = {frequency: {setValueAtTime() {}}, stop() {}};
|
|||||||
let lastBroadcastState = null;
|
let lastBroadcastState = null;
|
||||||
let lastRemoteCommandAt = 0;
|
let lastRemoteCommandAt = 0;
|
||||||
let pendingRoster = null;
|
let pendingRoster = null;
|
||||||
|
let connectedClientIds = new Set();
|
||||||
|
let disconnectedPlayerTimers = {};
|
||||||
|
let disconnectedPlayerIds = new Set();
|
||||||
|
|
||||||
function applyRoster(msg) {
|
function applyRoster(msg) {
|
||||||
const current = new Map(netLobby.players.map(p => [p.id, p]));
|
const current = new Map(netLobby.players.map(p => [p.id, p]));
|
||||||
@@ -658,12 +683,40 @@ function applyRoster(msg) {
|
|||||||
broadcastLobby();
|
broadcastLobby();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePlayerDisconnect(playerId) {
|
||||||
|
const player = players.find(p => p.id === playerId && !p.dead);
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
const {x, y, c} = player;
|
||||||
|
const explosionSpec = sample(DEATH_SPECS);
|
||||||
|
const explosionType = EXPLOSION_TYPES[explosionSpec.type];
|
||||||
|
explosions.push(explosionType.create(explosionSpec, x, y));
|
||||||
|
createParticles(x, y, PLAYER_EXPLOSION_PARTICLE_POWER, c);
|
||||||
|
player.dead = true;
|
||||||
|
player.deaths++;
|
||||||
|
player.deathOrder = typeof deathOrderCounter !== 'undefined' ? deathOrderCounter++ : 0;
|
||||||
|
disconnectedPlayerIds.add(playerId);
|
||||||
|
|
||||||
|
if (state === 'aim' && players[currentPlayer].id === playerId) {
|
||||||
|
state = 'explosions';
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastWorld();
|
||||||
|
delete disconnectedPlayerTimers[playerId];
|
||||||
|
}
|
||||||
|
|
||||||
function myPlayer() {
|
function myPlayer() {
|
||||||
if (!networkMode) return players.find(p => !p.ai);
|
if (!networkMode) return players.find(p => !p.ai);
|
||||||
return players.find(p => p.id === netPlayerId);
|
return players.find(p => p.id === netPlayerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function endNetworkSession() {
|
function endNetworkSession() {
|
||||||
|
for (const id of Object.keys(disconnectedPlayerTimers)) {
|
||||||
|
clearTimeout(disconnectedPlayerTimers[id]);
|
||||||
|
}
|
||||||
|
disconnectedPlayerTimers = {};
|
||||||
|
connectedClientIds.clear();
|
||||||
|
disconnectedPlayerIds.clear();
|
||||||
netDisconnect();
|
netDisconnect();
|
||||||
networkMode = false;
|
networkMode = false;
|
||||||
netPlayerId = null;
|
netPlayerId = null;
|
||||||
@@ -676,6 +729,28 @@ function endNetworkSession() {
|
|||||||
netOnMessage((msg) => {
|
netOnMessage((msg) => {
|
||||||
if (msg.type === MSG.ROSTER) {
|
if (msg.type === MSG.ROSTER) {
|
||||||
if (netIsHost()) {
|
if (netIsHost()) {
|
||||||
|
const newIds = new Set(msg.players.map(p => p.id));
|
||||||
|
|
||||||
|
for (const id of connectedClientIds) {
|
||||||
|
if (!newIds.has(id) && !disconnectedPlayerTimers[id]) {
|
||||||
|
const player = players.find(p => p.id === id && !p.ai && !p.dead);
|
||||||
|
if (player && state !== 'net-lobby' && state !== 'market') {
|
||||||
|
disconnectedPlayerTimers[id] = setTimeout(() => {
|
||||||
|
handlePlayerDisconnect(id);
|
||||||
|
}, NETWORK_DISCONNECT_TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of Object.keys(disconnectedPlayerTimers)) {
|
||||||
|
if (newIds.has(id)) {
|
||||||
|
clearTimeout(disconnectedPlayerTimers[id]);
|
||||||
|
delete disconnectedPlayerTimers[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedClientIds = newIds;
|
||||||
|
|
||||||
if (netLobby) applyRoster(msg);
|
if (netLobby) applyRoster(msg);
|
||||||
else pendingRoster = msg;
|
else pendingRoster = msg;
|
||||||
}
|
}
|
||||||
@@ -699,7 +774,7 @@ netOnMessage((msg) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.type === HOST_MSG.TERRAIN) {
|
if (msg.type === HOST_MSG.TERRAIN) {
|
||||||
applyTerrain(msg.png);
|
applyTerrain(msg.terrain);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.type === HOST_MSG.END_GAME) {
|
if (msg.type === HOST_MSG.END_GAME) {
|
||||||
@@ -736,7 +811,14 @@ function applyWorld(snap) {
|
|||||||
winner = snap.winnerId ? players.find(p => p.id === snap.winnerId) || null : null;
|
winner = snap.winnerId ? players.find(p => p.id === snap.winnerId) || null : null;
|
||||||
projectiles = snap.projectiles || [];
|
projectiles = snap.projectiles || [];
|
||||||
explosions = (snap.explosions || []).map(e => ({...e, osc: NOOP_OSC}));
|
explosions = (snap.explosions || []).map(e => ({...e, osc: NOOP_OSC}));
|
||||||
trajectories = snap.trajectories || [];
|
if (networkMode && !netIsHost()) {
|
||||||
|
let lastSeq = 0;
|
||||||
|
for (const t of trajectories) if (t.seq > lastSeq) lastSeq = t.seq;
|
||||||
|
const incoming = (snap.trajectories || []).filter(t => t.seq > lastSeq);
|
||||||
|
trajectories = [...trajectories, ...incoming].slice(-600);
|
||||||
|
} else {
|
||||||
|
trajectories = snap.trajectories || [];
|
||||||
|
}
|
||||||
napalmParticles = snap.napalmParticles || [];
|
napalmParticles = snap.napalmParticles || [];
|
||||||
fireCells = snap.fireCells || [];
|
fireCells = snap.fireCells || [];
|
||||||
smokeParticles = snap.smokeParticles || [];
|
smokeParticles = snap.smokeParticles || [];
|
||||||
@@ -748,6 +830,52 @@ function applyWorld(snap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function simulateClientTerrain() {
|
||||||
|
const cur = netSnapCur;
|
||||||
|
if (!cur) return;
|
||||||
|
|
||||||
|
if (cur.explosions) {
|
||||||
|
const curKeys = new Set();
|
||||||
|
for (const e of cur.explosions) {
|
||||||
|
const k = `${e.type}:${e.x}:${e.y}`;
|
||||||
|
curKeys.add(k);
|
||||||
|
if (e.type !== 'tracer' && e.type !== 'napalm') {
|
||||||
|
seenClientExplosions.set(k, {...e});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [k, e] of seenClientExplosions) {
|
||||||
|
if (!curKeys.has(k)) {
|
||||||
|
carveQueue.push(e);
|
||||||
|
seenClientExplosions.delete(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = carveQueue.length - 1; i >= 0; i--) {
|
||||||
|
const job = carveQueue[i];
|
||||||
|
const rate = job.type === 'dirtCone' ? 60 : 30;
|
||||||
|
job.cr += dt * rate;
|
||||||
|
const done = job.type === 'dirtCone'
|
||||||
|
? job.cr >= Math.min(job.r, job.y)
|
||||||
|
: job.cr >= job.r;
|
||||||
|
if (done) {
|
||||||
|
EXPLOSION_TYPES[job.type].clip(job, terrain);
|
||||||
|
cacheImageData(terrain);
|
||||||
|
carveQueue.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'land-collapse' && carveQueue.length === 0) {
|
||||||
|
if (!clientCollapseState) clientCollapseState = startCollapseTerrain(terrain);
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
if (collapseTerrainStep(terrain, clientCollapseState)) {
|
||||||
|
clientCollapseState = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function interpolateWorld() {
|
function interpolateWorld() {
|
||||||
const cur = netSnapCur;
|
const cur = netSnapCur;
|
||||||
if (!cur) return;
|
if (!cur) return;
|
||||||
@@ -803,13 +931,11 @@ function interpolateWorld() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTerrain(png) {
|
function applyTerrain(grid) {
|
||||||
const img = new Image();
|
carveQueue.length = 0;
|
||||||
img.onload = () => {
|
seenClientExplosions.clear();
|
||||||
terrain.clearRect(0, 0, W, H);
|
clientCollapseState = null;
|
||||||
terrain.drawImage(img, 0, 0);
|
decodeTerrain(terrain, grid);
|
||||||
};
|
|
||||||
img.src = png;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastWorld() {
|
function broadcastWorld() {
|
||||||
@@ -849,7 +975,7 @@ function broadcastWorld() {
|
|||||||
napalmParticles: napalmParticles.map(p => ({x: p.x, y: p.y})),
|
napalmParticles: napalmParticles.map(p => ({x: p.x, y: p.y})),
|
||||||
fireCells: fireCells.map(f => ({x: f.x, y: f.y, timeLeft: f.timeLeft})),
|
fireCells: fireCells.map(f => ({x: f.x, y: f.y, timeLeft: f.timeLeft})),
|
||||||
smokeParticles: smokeParticles.map(s => ({x: s.x, y: s.y, vx: s.vx, vy: s.vy, alpha: s.alpha, lifetime: s.lifetime})),
|
smokeParticles: smokeParticles.map(s => ({x: s.x, y: s.y, vx: s.vx, vy: s.vy, alpha: s.alpha, lifetime: s.lifetime})),
|
||||||
trajectories: trajectories.slice(-300).filter((_, i) => i % 3 === 0).map(t => ({x: t.x, y: t.y, a: t.a, c: t.c})),
|
trajectories: trajectories.slice(-300).filter((_, i) => i % 3 === 0).map(t => ({x: t.x, y: t.y, a: t.a, c: t.c, traceId: t.traceId, seq: t.seq})),
|
||||||
};
|
};
|
||||||
netBroadcast({type: HOST_MSG.WORLD, snap});
|
netBroadcast({type: HOST_MSG.WORLD, snap});
|
||||||
}
|
}
|
||||||
@@ -861,7 +987,7 @@ function broadcastTerrain(force) {
|
|||||||
if (!force && now - lastTerrainAt < 500) return;
|
if (!force && now - lastTerrainAt < 500) return;
|
||||||
terrainDirty = false;
|
terrainDirty = false;
|
||||||
lastTerrainAt = now;
|
lastTerrainAt = now;
|
||||||
netBroadcast({type: HOST_MSG.TERRAIN, png: terrain.canvas.toDataURL('image/png')});
|
netBroadcast({type: HOST_MSG.TERRAIN, terrain: encodeTerrain(terrain)});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCommand(playerId, cmd) {
|
function applyCommand(playerId, cmd) {
|
||||||
@@ -899,6 +1025,7 @@ function initNetworkPlayers() {
|
|||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const [color, borderColor] = PLAYER_COLORS[i];
|
const [color, borderColor] = PLAYER_COLORS[i];
|
||||||
const human = i < roster.length ? roster[i] : null;
|
const human = i < roster.length ? roster[i] : null;
|
||||||
|
const wasDisconnected = human && disconnectedPlayerIds.has(human.id);
|
||||||
players.push({
|
players.push({
|
||||||
name: human ? human.name : `AI ${i+1}`,
|
name: human ? human.name : `AI ${i+1}`,
|
||||||
dead: false,
|
dead: false,
|
||||||
@@ -911,7 +1038,7 @@ function initNetworkPlayers() {
|
|||||||
currentWeapon: 0,
|
currentWeapon: 0,
|
||||||
energy: PLAYER_MAX_ENERGY,
|
energy: PLAYER_MAX_ENERGY,
|
||||||
shield: null,
|
shield: null,
|
||||||
ai: human ? undefined : sample(Object.keys(AI_TYPES)),
|
ai: (human && !wasDisconnected) ? undefined : sample(Object.keys(AI_TYPES)),
|
||||||
parachute: null,
|
parachute: null,
|
||||||
fallHeight: 0,
|
fallHeight: 0,
|
||||||
score: STARTING_SCORE,
|
score: STARTING_SCORE,
|
||||||
@@ -937,6 +1064,7 @@ function startNetworkGame() {
|
|||||||
totalRounds = netLobby.config.rounds;
|
totalRounds = netLobby.config.rounds;
|
||||||
selectedTerrain = netLobby.config.terrain;
|
selectedTerrain = netLobby.config.terrain;
|
||||||
menuState = null;
|
menuState = null;
|
||||||
|
disconnectedPlayerIds.clear();
|
||||||
initNetworkPlayers();
|
initNetworkPlayers();
|
||||||
broadcastLobby();
|
broadcastLobby();
|
||||||
broadcastWorld();
|
broadcastWorld();
|
||||||
@@ -1013,6 +1141,10 @@ function updateNetMenu() {
|
|||||||
state = 'start-menu';
|
state = 'start-menu';
|
||||||
netMenuState = null;
|
netMenuState = null;
|
||||||
netError = null;
|
netError = null;
|
||||||
|
menuState = {
|
||||||
|
selected: 0,
|
||||||
|
values: [Math.max(0, selectedPlayers - 3), Math.max(0, totalRounds - 1), selectedTerrain === null ? 0 : selectedTerrain === 'mountain' ? 1 : selectedTerrain === 'sand' ? 2 : 0, tracerMode ? 0 : 1, 0],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const enterBrowse = () => {
|
const enterBrowse = () => {
|
||||||
@@ -1295,6 +1427,7 @@ function update() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
interpolateWorld();
|
interpolateWorld();
|
||||||
|
simulateClientTerrain();
|
||||||
if (state !== 'aim') return;
|
if (state !== 'aim') return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1327,6 +1460,7 @@ function update() {
|
|||||||
|
|
||||||
else if (state === 'start-turn') {
|
else if (state === 'start-turn') {
|
||||||
if (networkMode && netIsHost()) lastRemoteCommandAt = performance.now();
|
if (networkMode && netIsHost()) lastRemoteCommandAt = performance.now();
|
||||||
|
if (networkMode && netIsHost()) broadcastTerrain(true);
|
||||||
state = 'aim';
|
state = 'aim';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1464,6 +1598,7 @@ function update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else if (state === 'shoot') {
|
else if (state === 'shoot') {
|
||||||
|
for (const p of projectiles) if (!p.traceId) p.traceId = nextTraceId++;
|
||||||
for (let i=projectiles.length-1; i>=0; i--) {
|
for (let i=projectiles.length-1; i>=0; i--) {
|
||||||
const projectile = projectiles[i];
|
const projectile = projectiles[i];
|
||||||
const projectileType = PROJECTILE_TYPES[projectile.type];
|
const projectileType = PROJECTILE_TYPES[projectile.type];
|
||||||
@@ -1685,7 +1820,6 @@ function update() {
|
|||||||
lastBroadcastAt = now;
|
lastBroadcastAt = now;
|
||||||
broadcastWorld();
|
broadcastWorld();
|
||||||
}
|
}
|
||||||
broadcastTerrain(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2300,11 +2434,13 @@ function drawPlayers() {
|
|||||||
|
|
||||||
function drawTrajectories() {
|
function drawTrajectories() {
|
||||||
traces.clearRect(0, 0, W, H);
|
traces.clearRect(0, 0, W, H);
|
||||||
for (let i=trajectories.length-1; i>=0; i--) {
|
for (let i=1; i<trajectories.length; i++) {
|
||||||
const trajectory = trajectories[i];
|
const prev = trajectories[i-1];
|
||||||
const {x, y, c} = trajectory;
|
const curr = trajectories[i];
|
||||||
traces.globalAlpha = trajectory.a / 255;
|
if (prev.c === curr.c && prev.traceId === curr.traceId) {
|
||||||
plot(traces, x, y, c);
|
traces.globalAlpha = curr.a / 255;
|
||||||
|
drawLine(traces, prev.x, prev.y, curr.x, curr.y, curr.c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
traces.globalAlpha = 1;
|
traces.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
@@ -2481,6 +2617,10 @@ function drawStartMenuPanel() {
|
|||||||
|
|
||||||
drawPanelDivider(framebuffer, 308);
|
drawPanelDivider(framebuffer, 308);
|
||||||
drawButton(framebuffer, PANEL_X+100, 318, 200, 22, 'START GAME', false);
|
drawButton(framebuffer, PANEL_X+100, 318, 200, 22, 'START GAME', false);
|
||||||
|
|
||||||
|
const ghHover = checkHit(getPointer().x, getPointer().y, GITHUB_HIT.x, GITHUB_HIT.y, GITHUB_HIT.w, GITHUB_HIT.h);
|
||||||
|
drawText(framebuffer, 'GITHUB', GITHUB_POS.x, GITHUB_POS.y, ghHover ? 'yellow' : '#899');
|
||||||
|
framebuffer.canvas.style.cursor = ghHover ? 'pointer' : 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawMarketPanel() {
|
function drawMarketPanel() {
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ export const CMD = {
|
|||||||
export const HOST_MSG = {
|
export const HOST_MSG = {
|
||||||
LOBBY: 'lobby', // {players: [{id, name, ready}], config: {players, rounds, terrain}}
|
LOBBY: 'lobby', // {players: [{id, name, ready}], config: {players, rounds, terrain}}
|
||||||
WORLD: 'world', // {snap}
|
WORLD: 'world', // {snap}
|
||||||
TERRAIN: 'terrain', // {png: base64}
|
TERRAIN: 'terrain', // {terrain: {palette: [hex], tops: b64(Uint16), colors: b64(Uint8)}} column-heights
|
||||||
EVENT: 'event', // {kind, ...}
|
EVENT: 'event', // {kind, ...}
|
||||||
START_ROUND: 'start-round', // {} round N begins
|
START_ROUND: 'start-round', // {} round N begins
|
||||||
END_GAME: 'end-game', // {reason}
|
END_GAME: 'end-game', // {reason}
|
||||||
|
|||||||
+3
-2
@@ -6,6 +6,7 @@ import {audio, createOsc} from './sound.js?v=26';
|
|||||||
import {isTerrain, landHeight} from './terrain.js?v=26';
|
import {isTerrain, landHeight} from './terrain.js?v=26';
|
||||||
import {EXPLOSION_TYPES} from './weapons.js?v=26';
|
import {EXPLOSION_TYPES} from './weapons.js?v=26';
|
||||||
|
|
||||||
|
let trajectorySeq = 1;
|
||||||
|
|
||||||
export const PROJECTILE_TYPES = {
|
export const PROJECTILE_TYPES = {
|
||||||
normal: {
|
normal: {
|
||||||
@@ -109,7 +110,7 @@ export const PROJECTILE_TYPES = {
|
|||||||
|
|
||||||
trajectory
|
trajectory
|
||||||
.slice(0, trajectory.length-1)
|
.slice(0, trajectory.length-1)
|
||||||
.map(x => ({...x, a:255}))
|
.map(x => ({...x, a:255, traceId: projectile.traceId, seq: trajectorySeq++}))
|
||||||
.forEach(x => trajectories.push(x));
|
.forEach(x => trajectories.push(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +270,7 @@ export const PROJECTILE_TYPES = {
|
|||||||
|
|
||||||
trajectory
|
trajectory
|
||||||
.slice(0, trajectory.length-1)
|
.slice(0, trajectory.length-1)
|
||||||
.map(x => ({...x, a:255}))
|
.map(x => ({...x, a:255, traceId: projectile.traceId, seq: trajectorySeq++}))
|
||||||
.forEach(x => trajectories.push(x));
|
.forEach(x => trajectories.push(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+90
-1
@@ -4,11 +4,100 @@ import {sample} from './utils.js?v=26';
|
|||||||
|
|
||||||
let cachedImageData;
|
let cachedImageData;
|
||||||
|
|
||||||
function cacheImageData(ctx) {
|
export function cacheImageData(ctx) {
|
||||||
const {width, height} = ctx.canvas;
|
const {width, height} = ctx.canvas;
|
||||||
cachedImageData = ctx.getImageData(0, 0, width, height);
|
cachedImageData = ctx.getImageData(0, 0, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rgbaToHex = (r, g, b) => '#' +
|
||||||
|
[r, g, b].map(v => Math.max(0, Math.min(255, v)).toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
const hexToRgb = (hex) => [
|
||||||
|
parseInt(hex.slice(1, 3), 16),
|
||||||
|
parseInt(hex.slice(3, 5), 16),
|
||||||
|
parseInt(hex.slice(5, 7), 16),
|
||||||
|
];
|
||||||
|
|
||||||
|
function toBase64(bytes) {
|
||||||
|
let binary = '';
|
||||||
|
const chunk = 0x8000;
|
||||||
|
for (let i = 0; i < bytes.length; i += chunk) {
|
||||||
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBase64(b64) {
|
||||||
|
const binary = atob(b64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeTerrain(ctx) {
|
||||||
|
const {width, height} = ctx.canvas;
|
||||||
|
const data = ctx.getImageData(0, 0, width, height).data;
|
||||||
|
const tops = new Uint16Array(width);
|
||||||
|
const colorIdx = new Uint8Array(width);
|
||||||
|
const palette = [];
|
||||||
|
const paletteIndex = new Map();
|
||||||
|
const indexColor = (hex) => {
|
||||||
|
let idx = paletteIndex.get(hex);
|
||||||
|
if (idx === undefined) {
|
||||||
|
idx = palette.length;
|
||||||
|
paletteIndex.set(hex, idx);
|
||||||
|
palette.push(hex);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
let top = height;
|
||||||
|
let idx = 0;
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
const px = (y * width + x) * 4;
|
||||||
|
if (data[px + 3] > 0) {
|
||||||
|
top = y;
|
||||||
|
idx = indexColor(rgbaToHex(data[px], data[px + 1], data[px + 2]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tops[x] = top;
|
||||||
|
colorIdx[x] = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
palette,
|
||||||
|
tops: toBase64(new Uint8Array(tops.buffer)),
|
||||||
|
colors: toBase64(colorIdx),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeTerrain(ctx, grid) {
|
||||||
|
if (!grid || !grid.tops || !grid.colors || !Array.isArray(grid.palette)) return;
|
||||||
|
const {width, height} = ctx.canvas;
|
||||||
|
const paletteRgb = grid.palette.map(hexToRgb);
|
||||||
|
const tops = new Uint16Array(fromBase64(grid.tops).buffer);
|
||||||
|
const colorIdx = new Uint8Array(fromBase64(grid.colors));
|
||||||
|
const imageData = ctx.createImageData(width, height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const top = Math.min(tops[x], height);
|
||||||
|
const [r, g, b] = paletteRgb[colorIdx[x]] || paletteRgb[0] || [0, 0, 0];
|
||||||
|
for (let y = top; y < height; y++) {
|
||||||
|
const px = (y * width + x) * 4;
|
||||||
|
data[px] = r;
|
||||||
|
data[px + 1] = g;
|
||||||
|
data[px + 2] = b;
|
||||||
|
data[px + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
cacheImageData(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
export function generateTerrain(ctx, type) {
|
export function generateTerrain(ctx, type) {
|
||||||
const generator = type? TERRAIN_TYPES[type] : sample(Object.values(TERRAIN_TYPES));
|
const generator = type? TERRAIN_TYPES[type] : sample(Object.values(TERRAIN_TYPES));
|
||||||
const {width, height} = ctx.canvas;
|
const {width, height} = ctx.canvas;
|
||||||
|
|||||||
Reference in new issue
Block a user