UNPKG

sc4

Version:

A command line utility for automating SimCity 4 modding tasks & modifying savegames

188 lines (187 loc) 6.54 kB
import Stream from './stream.js'; import WriteBuffer from './write-buffer.js'; import { FileType } from './enums.js'; import { kFileType } from './symbols.js'; // Width of a single tile in meters. const TILE_WIDTH = 16; // # TerrainMap // The class we use for representing the terrain in a city. Note that we // follow the convention of the game, so we use x and z coordinates. The y // coordinate represents the height! export default class TerrainMap extends Array { static [kFileType] = FileType.TerrainMap; major; xSize; zSize; raw; // ## constructor(xSize, zSize) constructor(xSize = 0, zSize = xSize) { super(); this.major = 0x0002; this.xSize = xSize + 1; this.zSize = zSize + 1; Object.defineProperty(this, 'raw', { writable: true, value: null, }); this.fill(); } // ## clone() clone() { const clone = new TerrainMap(this.xSize - 1, this.zSize - 1); clone.raw.set(this.raw); return clone; } // ## fill() fill() { const raw = this.raw = new Float32Array(this.zSize * this.xSize); for (let z = 0; z < this.zSize; z++) { this[z] = new Float32Array(raw.buffer, z * Float32Array.BYTES_PER_ELEMENT * this.xSize, this.xSize); } return this; } // ## parse(bufferOrStream) parse(bufferOrStream) { // Determine the size based on the buffer length alone. const rs = new Stream(bufferOrStream); this.xSize = this.zSize = Math.sqrt((rs.internalUint8Array.length - 2) / 4); this.fill(); // Now actually read in all the values this.major = rs.word(); for (let i = 0; i < this.raw.length; i++) { this.raw[i] = rs.float(); } } // ## toBuffer() toBuffer() { const ws = new WriteBuffer(); ws.word(this.major); for (let i = 0; i < this.raw.length; i++) { ws.float(this.raw[i]); } return ws.toUint8Array(); } // ## get(i, j) get(i, j) { return this[j][i]; } // ## set(i, j, h) set(i, j, h) { this[j][i] = h; return this; } // ## isCliff(x, z, cliff = 0.5) // Internal helper function that checks if the given tile is to be // considered as a cliff, meaning we need to flip the triangulation. isCliff(x, z, cliff = 0.5) { const P = [0, this[z][x], 0]; const Q = [TILE_WIDTH, this[z][x + 1], 0]; const R = [0, this[z + 1][x], TILE_WIDTH]; const S = [TILE_WIDTH, this[z + 1][x + 1], TILE_WIDTH]; const n1 = normal(P, Q, S); const n2 = normal(P, S, R); const yy1 = (n1[1] ** 2) / sql(n1); const yy2 = (n2[1] ** 2) / sql(n2); return Math.max(yy1, yy2) < cliff; } // ## query(x, z, cliff = 0.5) // Performs a terrain query using interpolation. This means that the // coordinates are given in *meters*, not in tiles! Note that the game // normally triangulates from north-west to south-east, but this is // changed for cliffs. The cliff threshold can be modded though, so we // allow this to be specified as a parameter which defaults to 0.5 (it's // the maxNormalYForCliff) value. query(x, z, cliff = 0.5) { // Find the tile numbers and local coordinates first. const i = Math.floor(x / TILE_WIDTH); const j = Math.floor(z / TILE_WIDTH); const t = (x - TILE_WIDTH * i) / TILE_WIDTH; const s = (z - TILE_WIDTH * j) / TILE_WIDTH; // Handle edge cases if (t === 0 && s === 0) { return this[j][i]; } else if (t === 0) { return (1 - s) * this[j][i] + s * this[j + 1][i]; } else if (s === 0) { return (1 - t) * this[j][i] + t * this[j][i + 1]; } if (!this.isCliff(i, j, cliff)) { const P = [0, 0, this[j][i]]; const Q = [1, 1, this[j + 1][i + 1]]; const R = t > s ? [1, 0, this[j][i + 1]] : [0, 1, this[j + 1][i]]; return ipol(P, Q, R, [t, s]); } else { const P = [1, 0, this[j][i + 1]]; const Q = [0, 1, this[j + 1][i]]; const R = t > 1 - s ? [1, 1, this[j + 1][i + 1]] : [0, 0, this[j][i]]; return ipol(P, Q, R, [t, s]); } } // ## contour(i, j) // Returns the array containing the height values allong the contour of // the given tile. contour(i, j) { return [ this[j][i], this[j][i + 1], this[j + 1][i + 1], this[j + 1][i], ]; } // ## flatten(i, j, h) // Flattens the tile (i, j). By default we set the *minimum* height value. flatten(i, j, h = Math.min(...this.contour(i, j))) { this[j][i] = this[j][i + 1] = this[j + 1][i] = this[j + 1][i + 1] = h; return this; } // ## egalizeX(i, j) // Egalizes the tile in the x-direction. This makes the terrain suitable // for drawing a road over it for example. egalizeX(i, j) { for (let di = 0; di < 2; di++) { const ii = i + di; const h = Math.min(this[j][ii], this[j + 1][ii]); this[j][ii] = h; this[j + 1][ii] = h; } return this; } // ## egalizeZ(i, j) // Egalizes the tile in the z direction. egalizeZ(i, j) { for (let dj = 0; dj < 2; dj++) { const jj = j + dj; const h = Math.min(this[jj][i], this[jj][i + 1]); this[jj][i] = h; this[jj][i + 1] = h; } return this; } } // # normal(P, Q, R) // Calculates the normal vector for the plane going through the given three // points. function normal(P, Q, R) { const u = [Q[0] - P[0], Q[1] - P[1], Q[2] - P[2]]; const v = [R[0] - P[0], R[1] - P[1], R[2] - P[2]]; const a = u[2] * v[1] - u[1] * v[2]; const b = u[0] * v[2] - u[2] * v[0]; const c = u[1] * v[0] - u[0] * v[1]; return [a, b, c]; } // # sql(v) // Finds the *squared* length of the given vector function sql(v) { return v[0] ** 2 + v[1] ** 2 + v[2] ** 2; } // # ipol(P, Q, R, [x, z]) // Helper function for triangular interpolation. It accepts three points and // finds the equation of the plane going to those three points. function ipol(P, Q, R, [x, z]) { const [a, b, c] = normal(P, Q, R); const d = P[0] * a + P[1] * b + P[2] * c; return (d - a * x - b * z) / c; }