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_BORDER_COLOR = '#333';
|
||||
|
||||
export const NETWORK_DISCONNECT_TIMEOUT = 15000;
|
||||
export const STARTING_SCORE = 100;
|
||||
export const SCORE_PER_KILL = 20;
|
||||
export const SCORE_FOR_WIN = 100;
|
||||
|
||||
+5
-2
@@ -6,6 +6,9 @@ let clickQueue = [];
|
||||
let canvasEl = null;
|
||||
let pointerX = 0, pointerY = 0;
|
||||
let isPointerDown = false;
|
||||
let inputScale = Z;
|
||||
|
||||
export function setInputScale(s) { inputScale = s; }
|
||||
|
||||
document.addEventListener('keydown', (e) => {input[e.key] = true; e.preventDefault()});
|
||||
document.addEventListener('keyup', (e) => {input[e.key] = false; e.preventDefault()});
|
||||
@@ -48,8 +51,8 @@ export function initClickCanvas(canvas) {
|
||||
function getCanvasPos(clientX, clientY) {
|
||||
const rect = canvasEl.getBoundingClientRect();
|
||||
return {
|
||||
x: (clientX - rect.left) / Z,
|
||||
y: (clientY - rect.top) / Z,
|
||||
x: (clientX - rect.left) / inputScale,
|
||||
y: (clientY - rect.top) / inputScale,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+164
-24
@@ -1,12 +1,12 @@
|
||||
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 {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 {PROJECTILE_TYPES} from './projectiles.js?v=26';
|
||||
import {generateSky} from './sky.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 {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';
|
||||
@@ -38,6 +38,11 @@ let netSnapCurAt = 0;
|
||||
const reservedPositions = new Set();
|
||||
let winner;
|
||||
|
||||
const seenClientExplosions = new Map();
|
||||
const carveQueue = [];
|
||||
let clientCollapseState = null;
|
||||
let nextTraceId = 1;
|
||||
|
||||
let score = 0;
|
||||
let round = 0;
|
||||
let totalRounds = 1;
|
||||
@@ -77,8 +82,14 @@ const foreground = createCanvas(W, H);
|
||||
|
||||
// Composited layer
|
||||
const framebuffer = createCanvas(W, H);
|
||||
framebuffer.canvas.style.width = `${W * Z}px`;
|
||||
framebuffer.canvas.style.height = `${H * Z}px`;
|
||||
function applyDisplayScale() {
|
||||
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);
|
||||
initClickCanvas(framebuffer.canvas);
|
||||
|
||||
@@ -145,6 +156,7 @@ function initNewGame() {
|
||||
}
|
||||
|
||||
function commitStartMenu() {
|
||||
framebuffer.canvas.style.cursor = 'default';
|
||||
const playerCounts = [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'];
|
||||
@@ -166,6 +178,10 @@ function commitStartMenu() {
|
||||
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() {
|
||||
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']];
|
||||
@@ -234,6 +250,9 @@ function updateStartMenu() {
|
||||
if (checkHit(click.x, click.y, PANEL_X+100, 318, 200, 22)) {
|
||||
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;
|
||||
player.shotsFired++;
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -647,6 +669,9 @@ const NOOP_OSC = {frequency: {setValueAtTime() {}}, stop() {}};
|
||||
let lastBroadcastState = null;
|
||||
let lastRemoteCommandAt = 0;
|
||||
let pendingRoster = null;
|
||||
let connectedClientIds = new Set();
|
||||
let disconnectedPlayerTimers = {};
|
||||
let disconnectedPlayerIds = new Set();
|
||||
|
||||
function applyRoster(msg) {
|
||||
const current = new Map(netLobby.players.map(p => [p.id, p]));
|
||||
@@ -658,12 +683,40 @@ function applyRoster(msg) {
|
||||
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() {
|
||||
if (!networkMode) return players.find(p => !p.ai);
|
||||
return players.find(p => p.id === netPlayerId);
|
||||
}
|
||||
|
||||
function endNetworkSession() {
|
||||
for (const id of Object.keys(disconnectedPlayerTimers)) {
|
||||
clearTimeout(disconnectedPlayerTimers[id]);
|
||||
}
|
||||
disconnectedPlayerTimers = {};
|
||||
connectedClientIds.clear();
|
||||
disconnectedPlayerIds.clear();
|
||||
netDisconnect();
|
||||
networkMode = false;
|
||||
netPlayerId = null;
|
||||
@@ -676,6 +729,28 @@ function endNetworkSession() {
|
||||
netOnMessage((msg) => {
|
||||
if (msg.type === MSG.ROSTER) {
|
||||
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);
|
||||
else pendingRoster = msg;
|
||||
}
|
||||
@@ -699,7 +774,7 @@ netOnMessage((msg) => {
|
||||
return;
|
||||
}
|
||||
if (msg.type === HOST_MSG.TERRAIN) {
|
||||
applyTerrain(msg.png);
|
||||
applyTerrain(msg.terrain);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
projectiles = snap.projectiles || [];
|
||||
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 || [];
|
||||
fireCells = snap.fireCells || [];
|
||||
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() {
|
||||
const cur = netSnapCur;
|
||||
if (!cur) return;
|
||||
@@ -803,13 +931,11 @@ function interpolateWorld() {
|
||||
}
|
||||
}
|
||||
|
||||
function applyTerrain(png) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
terrain.clearRect(0, 0, W, H);
|
||||
terrain.drawImage(img, 0, 0);
|
||||
};
|
||||
img.src = png;
|
||||
function applyTerrain(grid) {
|
||||
carveQueue.length = 0;
|
||||
seenClientExplosions.clear();
|
||||
clientCollapseState = null;
|
||||
decodeTerrain(terrain, grid);
|
||||
}
|
||||
|
||||
function broadcastWorld() {
|
||||
@@ -849,7 +975,7 @@ function broadcastWorld() {
|
||||
napalmParticles: napalmParticles.map(p => ({x: p.x, y: p.y})),
|
||||
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})),
|
||||
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});
|
||||
}
|
||||
@@ -861,7 +987,7 @@ function broadcastTerrain(force) {
|
||||
if (!force && now - lastTerrainAt < 500) return;
|
||||
terrainDirty = false;
|
||||
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) {
|
||||
@@ -899,6 +1025,7 @@ function initNetworkPlayers() {
|
||||
for (let i = 0; i < total; i++) {
|
||||
const [color, borderColor] = PLAYER_COLORS[i];
|
||||
const human = i < roster.length ? roster[i] : null;
|
||||
const wasDisconnected = human && disconnectedPlayerIds.has(human.id);
|
||||
players.push({
|
||||
name: human ? human.name : `AI ${i+1}`,
|
||||
dead: false,
|
||||
@@ -911,7 +1038,7 @@ function initNetworkPlayers() {
|
||||
currentWeapon: 0,
|
||||
energy: PLAYER_MAX_ENERGY,
|
||||
shield: null,
|
||||
ai: human ? undefined : sample(Object.keys(AI_TYPES)),
|
||||
ai: (human && !wasDisconnected) ? undefined : sample(Object.keys(AI_TYPES)),
|
||||
parachute: null,
|
||||
fallHeight: 0,
|
||||
score: STARTING_SCORE,
|
||||
@@ -937,6 +1064,7 @@ function startNetworkGame() {
|
||||
totalRounds = netLobby.config.rounds;
|
||||
selectedTerrain = netLobby.config.terrain;
|
||||
menuState = null;
|
||||
disconnectedPlayerIds.clear();
|
||||
initNetworkPlayers();
|
||||
broadcastLobby();
|
||||
broadcastWorld();
|
||||
@@ -1013,6 +1141,10 @@ function updateNetMenu() {
|
||||
state = 'start-menu';
|
||||
netMenuState = 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 = () => {
|
||||
@@ -1295,6 +1427,7 @@ function update() {
|
||||
return;
|
||||
}
|
||||
interpolateWorld();
|
||||
simulateClientTerrain();
|
||||
if (state !== 'aim') return;
|
||||
}
|
||||
|
||||
@@ -1327,6 +1460,7 @@ function update() {
|
||||
|
||||
else if (state === 'start-turn') {
|
||||
if (networkMode && netIsHost()) lastRemoteCommandAt = performance.now();
|
||||
if (networkMode && netIsHost()) broadcastTerrain(true);
|
||||
state = 'aim';
|
||||
}
|
||||
|
||||
@@ -1464,6 +1598,7 @@ function update() {
|
||||
}
|
||||
|
||||
else if (state === 'shoot') {
|
||||
for (const p of projectiles) if (!p.traceId) p.traceId = nextTraceId++;
|
||||
for (let i=projectiles.length-1; i>=0; i--) {
|
||||
const projectile = projectiles[i];
|
||||
const projectileType = PROJECTILE_TYPES[projectile.type];
|
||||
@@ -1685,7 +1820,6 @@ function update() {
|
||||
lastBroadcastAt = now;
|
||||
broadcastWorld();
|
||||
}
|
||||
broadcastTerrain(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2300,11 +2434,13 @@ function drawPlayers() {
|
||||
|
||||
function drawTrajectories() {
|
||||
traces.clearRect(0, 0, W, H);
|
||||
for (let i=trajectories.length-1; i>=0; i--) {
|
||||
const trajectory = trajectories[i];
|
||||
const {x, y, c} = trajectory;
|
||||
traces.globalAlpha = trajectory.a / 255;
|
||||
plot(traces, x, y, c);
|
||||
for (let i=1; i<trajectories.length; i++) {
|
||||
const prev = trajectories[i-1];
|
||||
const curr = trajectories[i];
|
||||
if (prev.c === curr.c && prev.traceId === curr.traceId) {
|
||||
traces.globalAlpha = curr.a / 255;
|
||||
drawLine(traces, prev.x, prev.y, curr.x, curr.y, curr.c);
|
||||
}
|
||||
}
|
||||
traces.globalAlpha = 1;
|
||||
}
|
||||
@@ -2481,6 +2617,10 @@ function drawStartMenuPanel() {
|
||||
|
||||
drawPanelDivider(framebuffer, 308);
|
||||
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() {
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export const CMD = {
|
||||
export const HOST_MSG = {
|
||||
LOBBY: 'lobby', // {players: [{id, name, ready}], config: {players, rounds, terrain}}
|
||||
WORLD: 'world', // {snap}
|
||||
TERRAIN: 'terrain', // {png: base64}
|
||||
TERRAIN: 'terrain', // {terrain: {palette: [hex], tops: b64(Uint16), colors: b64(Uint8)}} column-heights
|
||||
EVENT: 'event', // {kind, ...}
|
||||
START_ROUND: 'start-round', // {} round N begins
|
||||
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 {EXPLOSION_TYPES} from './weapons.js?v=26';
|
||||
|
||||
let trajectorySeq = 1;
|
||||
|
||||
export const PROJECTILE_TYPES = {
|
||||
normal: {
|
||||
@@ -109,7 +110,7 @@ export const PROJECTILE_TYPES = {
|
||||
|
||||
trajectory
|
||||
.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));
|
||||
}
|
||||
|
||||
@@ -269,7 +270,7 @@ export const PROJECTILE_TYPES = {
|
||||
|
||||
trajectory
|
||||
.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));
|
||||
}
|
||||
|
||||
|
||||
+90
-1
@@ -4,11 +4,100 @@ import {sample} from './utils.js?v=26';
|
||||
|
||||
let cachedImageData;
|
||||
|
||||
function cacheImageData(ctx) {
|
||||
export function cacheImageData(ctx) {
|
||||
const {width, height} = ctx.canvas;
|
||||
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) {
|
||||
const generator = type? TERRAIN_TYPES[type] : sample(Object.values(TERRAIN_TYPES));
|
||||
const {width, height} = ctx.canvas;
|
||||
|
||||
Reference in new issue
Block a user