UNPKG

sc4

Version:

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

278 lines (277 loc) 10.9 kB
// # pipe-manager.js import { Pipe, Vertex, Color, Pointer, FileType, Box3, Vector3, SavegameContext, } from 'sc4/core'; // Bit flags of what connections are enabled. Based on those sides we'll also // create a map of *lines* corresponding to those sides. const WEST = 0b0001; const NORTH = 0b0010; const EAST = 0b0100; const SOUTH = 0b1000; const SIDE_MAP = new Map([ [WEST, [0, 1, 0, 0]], [NORTH, [0, 0, 1, 0]], [EAST, [1, 0, 1, 1]], [SOUTH, [1, 1, 0, 1]], ]); export default class PipeManager { dbpf; ctx; // ## constructor(dbpf) constructor(dbpf) { this.dbpf = dbpf; this.ctx = dbpf.createContext(); } // Shortcuts. get sim() { return this.dbpf.plumbingSimulator; } get pipes() { return this.dbpf.pipes; } get terrain() { return this.dbpf.terrain; } get index() { return this.dbpf.itemIndex; } get serializer() { return this.dbpf.COMSerializer; } // ## applyOptimalLayout() // Applies the optimal pipe layout to the city. applyOptimalLayout() { // First of all clear the existing plumbing situation. const { sim, pipes } = this; sim.clear(); pipes.length = 0; // Generate the ideal layout as a sparse array of one byte per tile // and then actually create the tiles. let layout = this.generateOptimalLayout(); // Next comes the hardest part. Before we can create the tiles, we // need to create an *updated* terrain map where we egalize the tiles // that require to be flat. note that for drawing roads, we should try // to egalize the terrain for every drag operation, but for the pipes // this isn't required and we can handle the layout at once. let flat = this.egalizeTerrain(this.terrain, layout); for (let [i, j, id] of layout) { let pipe = this.createTile(i, j, id, flat); pipes.push(pipe); sim.cells[j][i] = id; sim.pipes.push(new Pointer(pipe)); } // Now rebuild the item index and update the com serializer and we're // done! sim.revision++; this.index.rebuild(FileType.Pipe, this.pipes); this.serializer.update(FileType.Pipe, this.pipes); return this; } // ## generateOptimalLayout() // Generates the coordinates of all pipe tiles along with their connection // identifier. Note that we return a sparse array, not a full map! generateOptimalLayout() { // 1. Determine the rows where we have to draw the pipes. If the last // row is too far from the edge, we'll insert a new one manually where // the intermediate distance will hence be less than optimal - but // required anyway to cover the full city! const size = this.sim.xSize; const range = 6; let rows = []; let last = range; for (let j = range, dj = 2 * range + 1; j < size; j += dj) { rows.push(last = j); } if (size - last > range) rows.push(size - 2); // 2. Insert the horizontal tiles. This has id 0x15, but we'll need to // take into account the end pieces as well of course. const vertical = size / 2 - 1; let out = []; for (let j of rows) { const a = Math.floor(range / 2); const b = size - a - 1; for (let i = a; i <= b; i++) { // Skip the crossing tiles for now, we'll handle those // later on. if (i === vertical) continue; let id = i === a ? 0x14 : (i === b ? 0x11 : 0x15); out.push([i, j, id]); } } // 3. Draw the vertical lines, *including* the crossings. const [first] = rows; for (let j = first; j <= last; j++) { let id = 0b10000; if (j !== first) id ^= NORTH; if (j !== last) id ^= SOUTH; if (rows.includes(j)) { id ^= EAST; id ^= WEST; } out.push([vertical, j, id]); } // 4. We're done, return all the tiles. return out; } // ## egalizeTerrain(terrain, layout) // This returns a *cloned* of the terrain map where we egalized the parts // that need it. For example, T and + pieces need to be completely flat, // straight pieces need to be a tile. egalizeTerrain(terrain, layout) { // Filter out all pipe tiles that require a flat terrain tile and then // egalize the terrain for that tile. let map = terrain.clone(); let flats = new Set([ 0b1100, 0b0110, 0b0011, 0b1001, 0b1110, 0b1101, 0b1011, 0b1110, 0b1111, ]); let straights = layout.filter(([i, j, id]) => { // We'll need the non-flat pieces later on as well so we'll filter // them out in the same loop, hence return true early. if (!flats.has(id & 0b1111)) return true; // Cool, we now know that the tile has to be flattened, so we'll // request the contours and set the *minimum* value as new value. map.flatten(i, j); return false; }); // Flat tiles have been inserted. Now handle the straight tiles as // well. for (let [i, j, id] of straights) { // Figure out the points that need to have the same height, which // depends on the orientation obviously. if (id & NORTH || id & SOUTH) { map.egalizeZ(i, j); } else { map.egalizeX(i, j); } } // We're done, return the egalized terrain map now. return map; } // ## createTile(i, j, id, map) // Actually creates a new pipe occupant tile and properly positions it. createTile(i, j, id, map) { // Calculate the metric x and z positions of the ne corner of the // tile. const { terrain } = this; let x = 16 * i; let z = 16 * j; // Create the pipe tile and position it correctly first. let pipe = new Pipe({ mem: this.ctx.mem(), position: new Vector3(x + 8, 0, z + 8), bbox: new Box3([x, 0, z], [x + 16, 0, z + 16]), xTile: i, zTile: j, }); let pos = pipe.position; pipe.yModel = pos.y = map.query(pos.x, pos.z) - 1.4; let corners = [['NW', 'NE'], ['SW', 'SE']]; let cornerValues = []; for (let j = 0; j < 2; j++) { for (let i = 0; i < 2; i++) { let xx = x + 16 * i; let zz = z + 16 * i; let h = pipe['y' + corners[j][i]] = terrain.query(xx, zz); cornerValues.push(h); } } pipe.bbox.max.y = Math.max(...cornerValues); pipe.bbox.min.y = Math.min(...map.contour(i, j)); pipe.tract.update(pipe); // Set the bottom vertices & bottom texture. for (let i = 0; i < 2; i++) { for (let j = 0; j < 2; j++) { let index = 2 * i + j; let v = pipe.vertices[index]; v.x = x + 16 * i; v.z = z + 16 * (+(i !== j)); v.u = i; v.v = +(i !== j); v.y = map.query(v.x, v.z) - 10.2; } } pipe.sideTextures.bottom = pipe.vertices.map(vertex => { let fresh = Object.assign(new Vertex(), vertex); fresh.color = new Color(0xff, 0xff, 0xff, 0x80); return fresh; }); // Find out the connections for this tile based on the id that was // specified. const west = !!(id & WEST); const north = !!(id & NORTH); const east = !!(id & EAST); const south = !!(id & SOUTH); // Find the sides where there are no connections and hence the // lines that we need to draw. let lines = []; for (let [side, [di, dj, dii, djj]] of SIDE_MAP.entries()) { if (!(id & side)) { lines.push([ [i + di, j + dj], [i + dii, j + djj], ]); } } // Create the sides of the hole now based on the lines we've // selected. for (let line of lines) { for (let i = 0; i < 2; i++) { for (let j = 0; j < 2; j++) { let vertex = new Vertex(); let point = line[i]; vertex.x = 16 * point[0]; vertex.z = 16 * point[1]; vertex.u = i; vertex.v = i !== j ? 0.6375007629394531 : 0; vertex.color = new Color(0xff, 0xff, 0xff, 0x80); let src = i !== j ? map : terrain; let h = src.query(vertex.x, vertex.z); vertex.y = h - Number(i !== j) * 10.2; pipe.sideTextures[0].push(vertex); } } pipe.blocks++; } // Count how many connections we have now. This determines what // model we have to insert as well as how to orient it. let sum = +west + +north + +east + +south; if (sum === 1) { pipe.textureId = 0x00000300; pipe.orientation = [south, west, north, east].indexOf(true); } else if (sum === 2) { pipe.textureId = 0x00004b00; pipe.orientation = west ? 1 : 0; } else if (sum === 3) { pipe.textureId = 0x00005700; pipe.orientation = [west, north, east, south].indexOf(false); } else if (sum === 4) { pipe.textureId = 0x00020700; } // Insert the prop model at the correct position and then rotate // into place based on the orientation we've set on the tile. pipe.matrix.position = pipe.position; if (pipe.orientation === 1) { pipe.matrix.ex = [0, 0, 1]; pipe.matrix.ez = [-1, 0, 0]; } else if (pipe.orientation === 2) { pipe.matrix.ex = [-1, 0, 0]; pipe.matrix.ez = [0, 0, -1]; } else if (pipe.orientation === 3) { pipe.matrix.ex = [0, 0, -1]; pipe.matrix.ez = [1, 0, 0]; } // Manually set our connection values as well. pipe.westConnection = west ? 0x02 : 0; pipe.northConnection = north ? 0x02 : 0; pipe.eastConnection = east ? 0x02 : 0; pipe.southConnection = south ? 0x02 : 0; return pipe; } }