sync terrain as column heights instead of png

This commit is contained in:
catarrh committed 2026-08-02 23:34:16 +03:00
1 parent 2a22cf544f
commit 828b1efc9c
3 files changed
+94 -11

No files matched your search

+5 -10
View File
@@ -6,7 +6,7 @@ import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from '.
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, cacheImageData} 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';
@@ -760,7 +760,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) {
@@ -915,13 +915,8 @@ function interpolateWorld() {
} }
} }
function applyTerrain(png) { function applyTerrain(grid) {
const img = new Image(); decodeTerrain(terrain, grid);
img.onload = () => {
terrain.clearRect(0, 0, W, H);
terrain.drawImage(img, 0, 0);
};
img.src = png;
} }
function broadcastWorld() { function broadcastWorld() {
@@ -973,7 +968,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) {
+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}
+88
View File
@@ -9,6 +9,94 @@ export function cacheImageData(ctx) {
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) {
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;