Compare commits

...
10 Commits
6 changed files with 264 additions and 30 deletions

No files matched your search

+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;