mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
245 lines
9.6 KiB
TypeScript
245 lines
9.6 KiB
TypeScript
import { FloorplanAction, FloorplanState, Tile } from './types';
|
|
import { MAX_NUM_TILE_PER_AXIS, EMPTY_DOOR, MIN_WALL_HEIGHT, MAX_WALL_HEIGHT } from './constants';
|
|
import { parseTilemap } from './encoding';
|
|
|
|
export const initialState: FloorplanState = {
|
|
tiles: [],
|
|
door: { ...EMPTY_DOOR },
|
|
thickness: { wall: 1, floor: 1 },
|
|
wallHeight: -1,
|
|
brush: { h: 0, action: 'SET' },
|
|
selection: new Set<`${number},${number}`>(),
|
|
squareSelect: false,
|
|
lease: { holder: null, me: false, expiresAt: null },
|
|
seq: 0
|
|
};
|
|
|
|
const clampHeight = (h: number): number => Math.max(0, Math.min(26, h | 0));
|
|
const clamp64 = (n: number): number => Math.max(0, Math.min(MAX_NUM_TILE_PER_AXIS - 1, n | 0));
|
|
|
|
const ensureRect = (tiles: Tile[][], rows: number, cols: number): Tile[][] =>
|
|
{
|
|
const tRows = Math.min(MAX_NUM_TILE_PER_AXIS, Math.max(rows, tiles.length));
|
|
const tCols = Math.min(MAX_NUM_TILE_PER_AXIS, Math.max(cols, tiles[0]?.length ?? 0));
|
|
if(tRows === tiles.length && (tiles[0]?.length ?? 0) === tCols) return tiles;
|
|
const next: Tile[][] = [];
|
|
for(let r = 0; r < tRows; r++)
|
|
{
|
|
const src = tiles[r] ?? [];
|
|
const row: Tile[] = [];
|
|
for(let c = 0; c < tCols; c++)
|
|
{
|
|
row.push(src[c] ?? { h: 0, blocked: true });
|
|
}
|
|
next.push(row);
|
|
}
|
|
return next;
|
|
};
|
|
|
|
const setTile = (tiles: Tile[][], row: number, col: number, tile: Tile): Tile[][] =>
|
|
{
|
|
const current = tiles[row]?.[col];
|
|
if(current && current.h === tile.h && current.blocked === tile.blocked) return tiles;
|
|
const next = tiles.map((r, ri) => ri === row ? r.map((t, ci) => ci === col ? tile : t) : r);
|
|
return next;
|
|
};
|
|
|
|
export const reducer = (state: FloorplanState, action: FloorplanAction): FloorplanState =>
|
|
{
|
|
switch(action.type)
|
|
{
|
|
case 'PAINT_TILE':
|
|
{
|
|
const row = clamp64(action.row);
|
|
const col = clamp64(action.col);
|
|
const tiles = ensureRect(state.tiles, row + 1, col + 1);
|
|
const target = { h: clampHeight(action.h), blocked: false };
|
|
const next = setTile(tiles, row, col, target);
|
|
if(next === tiles && tiles === state.tiles) return state;
|
|
return { ...state, tiles: next };
|
|
}
|
|
case 'ERASE_TILE':
|
|
{
|
|
const row = action.row | 0;
|
|
const col = action.col | 0;
|
|
if(row < 0 || col < 0 || row >= state.tiles.length || col >= (state.tiles[0]?.length ?? 0)) return state;
|
|
const current = state.tiles[row][col];
|
|
const target = { h: current.h, blocked: true };
|
|
const next = setTile(state.tiles, row, col, target);
|
|
if(next === state.tiles) return state;
|
|
return { ...state, tiles: next };
|
|
}
|
|
case 'ADJUST_HEIGHT':
|
|
{
|
|
const row = action.row | 0;
|
|
const col = action.col | 0;
|
|
if(row < 0 || col < 0 || row >= state.tiles.length || col >= (state.tiles[0]?.length ?? 0)) return state;
|
|
const current = state.tiles[row][col];
|
|
if(current.blocked) return state;
|
|
const newH = clampHeight(current.h + action.delta);
|
|
if(newH === current.h) return state;
|
|
const next = setTile(state.tiles, row, col, { h: newH, blocked: false });
|
|
return { ...state, tiles: next };
|
|
}
|
|
case 'SET_DOOR':
|
|
{
|
|
const x = clamp64(action.x);
|
|
const y = clamp64(action.y);
|
|
if(state.door.x === x && state.door.y === y) return state;
|
|
return { ...state, door: { ...state.door, x, y } };
|
|
}
|
|
case 'SET_DOOR_DIR':
|
|
{
|
|
if(state.door.dir === action.dir) return state;
|
|
return { ...state, door: { ...state.door, dir: action.dir } };
|
|
}
|
|
case 'SET_THICKNESS':
|
|
{
|
|
const wall = action.wall ?? state.thickness.wall;
|
|
const floor = action.floor ?? state.thickness.floor;
|
|
if(wall === state.thickness.wall && floor === state.thickness.floor) return state;
|
|
return { ...state, thickness: { wall, floor } };
|
|
}
|
|
case 'SET_WALL_HEIGHT':
|
|
{
|
|
const value = Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.value | 0));
|
|
if(value === state.wallHeight) return state;
|
|
return { ...state, wallHeight: value };
|
|
}
|
|
case 'BRUSH_SET':
|
|
{
|
|
const h = action.h ?? state.brush.h;
|
|
const act = action.action ?? state.brush.action;
|
|
if(h === state.brush.h && act === state.brush.action) return state;
|
|
return { ...state, brush: { h: clampHeight(h), action: act } };
|
|
}
|
|
case 'SELECT_ALL':
|
|
{
|
|
const sel = new Set<`${number},${number}`>();
|
|
for(let r = 0; r < MAX_NUM_TILE_PER_AXIS; r++)
|
|
{
|
|
for(let c = 0; c < MAX_NUM_TILE_PER_AXIS; c++)
|
|
{
|
|
sel.add(`${r},${c}`);
|
|
}
|
|
}
|
|
return { ...state, selection: sel };
|
|
}
|
|
case 'CLEAR_SELECTION':
|
|
return state.selection.size === 0 ? state : { ...state, selection: new Set() };
|
|
case 'SELECT_RECT':
|
|
{
|
|
const [ r0, c0 ] = action.from;
|
|
const [ r1, c1 ] = action.to;
|
|
const rMin = Math.max(0, Math.min(r0, r1));
|
|
const rMax = Math.min(MAX_NUM_TILE_PER_AXIS - 1, Math.max(r0, r1));
|
|
const cMin = Math.max(0, Math.min(c0, c1));
|
|
const cMax = Math.min(MAX_NUM_TILE_PER_AXIS - 1, Math.max(c0, c1));
|
|
const sel = new Set<`${number},${number}`>();
|
|
for(let r = rMin; r <= rMax; r++)
|
|
{
|
|
for(let c = cMin; c <= cMax; c++)
|
|
{
|
|
sel.add(`${r},${c}`);
|
|
}
|
|
}
|
|
return { ...state, selection: sel };
|
|
}
|
|
case 'APPLY_BRUSH_TO_SELECTION':
|
|
{
|
|
if(state.selection.size === 0) return state;
|
|
if(state.brush.action === 'DOOR') return { ...state, selection: new Set() };
|
|
|
|
let maxR = -1;
|
|
let maxC = -1;
|
|
for(const key of state.selection)
|
|
{
|
|
const [ rStr, cStr ] = key.split(',');
|
|
const r = parseInt(rStr, 10);
|
|
const c = parseInt(cStr, 10);
|
|
if(r > maxR) maxR = r;
|
|
if(c > maxC) maxC = c;
|
|
}
|
|
|
|
let tiles = state.tiles;
|
|
if(state.brush.action === 'SET' && maxR >= 0 && maxC >= 0)
|
|
{
|
|
tiles = ensureRect(tiles, maxR + 1, maxC + 1);
|
|
}
|
|
|
|
for(const key of state.selection)
|
|
{
|
|
const [ rStr, cStr ] = key.split(',');
|
|
const row = parseInt(rStr, 10);
|
|
const col = parseInt(cStr, 10);
|
|
const current = tiles[row]?.[col];
|
|
if(!current) continue;
|
|
|
|
switch(state.brush.action)
|
|
{
|
|
case 'SET':
|
|
tiles = setTile(tiles, row, col, { h: clampHeight(state.brush.h), blocked: false });
|
|
break;
|
|
case 'UNSET':
|
|
if(current.blocked) continue;
|
|
tiles = setTile(tiles, row, col, { h: current.h, blocked: true });
|
|
break;
|
|
case 'UP':
|
|
if(current.blocked) continue;
|
|
tiles = setTile(tiles, row, col, { h: clampHeight(current.h + 1), blocked: false });
|
|
break;
|
|
case 'DOWN':
|
|
if(current.blocked) continue;
|
|
tiles = setTile(tiles, row, col, { h: clampHeight(current.h - 1), blocked: false });
|
|
break;
|
|
}
|
|
}
|
|
|
|
return { ...state, tiles, selection: new Set() };
|
|
}
|
|
case 'SQUARE_SELECT_TOGGLE':
|
|
return { ...state, squareSelect: !state.squareSelect };
|
|
case 'IMPORT_STRING':
|
|
{
|
|
const tiles = parseTilemap(action.raw);
|
|
const next: FloorplanState = { ...state, tiles };
|
|
if(action.door) next.door = action.door;
|
|
if(action.thickness) next.thickness = action.thickness;
|
|
if(action.wallHeight !== undefined) next.wallHeight = Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.wallHeight | 0));
|
|
return next;
|
|
}
|
|
case 'APPLY_REMOTE_DIFF':
|
|
{
|
|
let next: FloorplanState = { ...state, seq: action.seq };
|
|
if(action.diff.tiles)
|
|
{
|
|
let tiles = next.tiles;
|
|
for(const e of action.diff.tiles)
|
|
{
|
|
tiles = ensureRect(tiles, e.row + 1, e.col + 1);
|
|
tiles = setTile(tiles, e.row, e.col, { h: clampHeight(e.h), blocked: e.blocked });
|
|
}
|
|
next.tiles = tiles;
|
|
}
|
|
if(action.diff.door) next.door = action.diff.door;
|
|
if(action.diff.thickness) next.thickness = action.diff.thickness;
|
|
if(action.diff.wallHeight !== undefined) next.wallHeight = Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.diff.wallHeight | 0));
|
|
return next;
|
|
}
|
|
case 'APPLY_REMOTE_SNAPSHOT':
|
|
{
|
|
return {
|
|
...state,
|
|
tiles: parseTilemap(action.raw),
|
|
door: action.door,
|
|
thickness: action.thickness,
|
|
wallHeight: Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.wallHeight | 0)),
|
|
selection: new Set(),
|
|
seq: action.seq
|
|
};
|
|
}
|
|
default:
|
|
return state;
|
|
}
|
|
};
|