sync terrain as column heights instead of png
This commit is contained in:
1 parent
2a22cf544f
commit
828b1efc9c
3 files changed
+94
-11
No files matched your search
+5
-10
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user