mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
48ed3ad7ba
The editor never requested occupied tiles, so tiles holding furniture were indistinguishable from empty floor and could be edited/voided. - request GetOccupiedTilesMessageComposer when the editor opens - handle RoomOccupiedTilesMessageEvent -> SET_OCCUPIED_TILES - new Tile.occupied flag (kept separate from `blocked`/void): occupied tiles render with a distinct marker and are protected from PAINT/ ERASE/ADJUST and brush-to-selection edits - occupied is purely informational and never changes the saved tilemap (no accidental voiding of floor under furni) Tests: reducer cases for SET_OCCUPIED_TILES + edit protection; container test asserts the occupied event is non-destructive on save; route the canvas pointer test through elementFromPoint (jsdom has no getScreenCTM).
146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import { FC, memo } from 'react';
|
|
import { Tile } from '../state/types';
|
|
import { tileFill } from '../state/selectors';
|
|
import { TILE_SIZE } from '../state/constants';
|
|
import { tileToScreen } from '../hooks/usePointerToTile';
|
|
|
|
type Props = {
|
|
row: number;
|
|
col: number;
|
|
tile: Tile;
|
|
selected: boolean;
|
|
isDoor: boolean;
|
|
southH?: number;
|
|
westH?: number;
|
|
};
|
|
|
|
const HEIGHT_LIFT = TILE_SIZE / 8;
|
|
|
|
const diamondPoints = (row: number, col: number, h: number): string =>
|
|
{
|
|
const [ cx, cyBase ] = tileToScreen(row, col);
|
|
const cy = cyBase - h * HEIGHT_LIFT;
|
|
const half = TILE_SIZE / 2;
|
|
const quarter = TILE_SIZE / 4;
|
|
return `${ cx },${ cy - quarter } ${ cx + half },${ cy } ${ cx },${ cy + quarter } ${ cx - half },${ cy }`;
|
|
};
|
|
|
|
const darkenHex = (hex: string, factor: number): string =>
|
|
{
|
|
const h = hex.replace('#', '');
|
|
if(h.length !== 6) return hex;
|
|
const r = Math.max(0, Math.floor(parseInt(h.slice(0, 2), 16) * factor));
|
|
const g = Math.max(0, Math.floor(parseInt(h.slice(2, 4), 16) * factor));
|
|
const b = Math.max(0, Math.floor(parseInt(h.slice(4, 6), 16) * factor));
|
|
return `#${ [ r, g, b ].map(v => v.toString(16).padStart(2, '0')).join('') }`;
|
|
};
|
|
|
|
const southWallPoints = (cx: number, cy: number, drop: number): string =>
|
|
{
|
|
const half = TILE_SIZE / 2;
|
|
const quarter = TILE_SIZE / 4;
|
|
return `${ cx + half },${ cy } ${ cx + half },${ cy + drop } ${ cx },${ cy + quarter + drop } ${ cx },${ cy + quarter }`;
|
|
};
|
|
|
|
const westWallPoints = (cx: number, cy: number, drop: number): string =>
|
|
{
|
|
const half = TILE_SIZE / 2;
|
|
const quarter = TILE_SIZE / 4;
|
|
return `${ cx - half },${ cy } ${ cx },${ cy + quarter } ${ cx },${ cy + quarter + drop } ${ cx - half },${ cy + drop }`;
|
|
};
|
|
|
|
const FloorplanTileImpl: FC<Props> = ({ row, col, tile, selected, isDoor, southH = 0, westH = 0 }) =>
|
|
{
|
|
if(tile.blocked)
|
|
{
|
|
if(!selected) return null;
|
|
const points = diamondPoints(row, col, tile.h);
|
|
return (
|
|
<polygon
|
|
data-testid="selection-ring"
|
|
points={ points }
|
|
fill="rgba(250, 204, 21, 0.45)"
|
|
stroke="#facc15"
|
|
strokeWidth={ 1.5 }
|
|
strokeDasharray="3 2"
|
|
/>
|
|
);
|
|
}
|
|
const points = diamondPoints(row, col, tile.h);
|
|
const fill = tileFill(tile);
|
|
|
|
const [ cx, cyBase ] = tileToScreen(row, col);
|
|
const cy = cyBase - tile.h * HEIGHT_LIFT;
|
|
const southDrop = Math.max(0, tile.h - southH) * HEIGHT_LIFT;
|
|
const westDrop = Math.max(0, tile.h - westH) * HEIGHT_LIFT;
|
|
const southFill = (southDrop > 0) ? darkenHex(fill, 0.70) : null;
|
|
const westFill = (westDrop > 0) ? darkenHex(fill, 0.55) : null;
|
|
|
|
return (
|
|
<g>
|
|
{ southFill && (
|
|
<polygon
|
|
data-testid="tile-south-wall"
|
|
points={ southWallPoints(cx, cy, southDrop) }
|
|
fill={ southFill }
|
|
stroke="#222"
|
|
strokeWidth={ 0.5 }
|
|
/>
|
|
) }
|
|
{ westFill && (
|
|
<polygon
|
|
data-testid="tile-west-wall"
|
|
points={ westWallPoints(cx, cy, westDrop) }
|
|
fill={ westFill }
|
|
stroke="#222"
|
|
strokeWidth={ 0.5 }
|
|
/>
|
|
) }
|
|
<polygon
|
|
data-row={ row }
|
|
data-col={ col }
|
|
points={ points }
|
|
fill={ fill }
|
|
stroke="#222"
|
|
strokeWidth={ 0.5 }
|
|
/>
|
|
{ tile.occupied && (
|
|
<polygon
|
|
data-testid="occupied-marker"
|
|
points={ points }
|
|
fill="rgba(249, 115, 22, 0.40)"
|
|
stroke="#f97316"
|
|
strokeWidth={ 1 }
|
|
strokeDasharray="2 2"
|
|
pointerEvents="none"
|
|
/>
|
|
) }
|
|
{ selected && (
|
|
<polygon
|
|
data-testid="selection-ring"
|
|
data-row={ row }
|
|
data-col={ col }
|
|
points={ points }
|
|
fill="none"
|
|
stroke="#fff"
|
|
strokeWidth={ 2 }
|
|
strokeDasharray="3 2"
|
|
/>
|
|
) }
|
|
{ isDoor && (
|
|
<polygon
|
|
data-testid="door-marker"
|
|
data-row={ row }
|
|
data-col={ col }
|
|
points={ points }
|
|
fill="rgba(255,255,255,0.85)"
|
|
stroke="#000"
|
|
strokeWidth={ 1 }
|
|
/>
|
|
) }
|
|
</g>
|
|
);
|
|
};
|
|
|
|
export const FloorplanTile = memo(FloorplanTileImpl);
|