diff --git a/src/Myrian/Myrian.ts b/src/Myrian/Myrian.ts index c828ea1a8..7639a5633 100644 --- a/src/Myrian/Myrian.ts +++ b/src/Myrian/Myrian.ts @@ -2,6 +2,7 @@ import { SleeveMyrianWork } from "../PersonObjects/Sleeve/Work/SleeveMyrianWork" import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; import { Player } from "@player"; import { DefaultWorld } from "./World"; +import { MyrianTile } from "@nsdefs"; export interface MyrianSleeve { index: number; @@ -56,6 +57,13 @@ export class Myrian { } } + getTile(x: number, y: number): MyrianTile { + if (x < 0 || y < 0 || y > this.world.length || x > this.world[y].length) return { Content: "?" }; + return { + Content: this.world[y][x], + }; + } + /** Serialize the current object to a JSON save state. */ toJSON(): IReviverValue { return Generic_toJSON("Myrian", this); diff --git a/src/Myrian/aStar.ts b/src/Myrian/aStar.ts new file mode 100644 index 000000000..c5abf9713 --- /dev/null +++ b/src/Myrian/aStar.ts @@ -0,0 +1,78 @@ +import { Myrian } from "./Myrian"; + +export const aStar = (myrian: Myrian, start: [number, number], goal: [number, number]): [number, number][] => { + if (myrian.getTile(goal[0], goal[1]).Content !== " ") return []; + + const h = (n: [number, number], m: [number, number]) => Math.abs(n[0] - m[0]) + Math.abs(n[1] - m[1]); + + const d = (n: [number, number], m: [number, number]) => { + const tile = myrian.getTile(m[0], m[1]); + if (tile.Content !== " ") return Infinity; + return Math.abs(n[0] - m[0]) + Math.abs(n[1] - m[1]); + }; + + const openSet = new Set([start]); + + const cameFrom = new Map<[number, number], [number, number]>(); + + const gScore = new Map<[number, number], number>(); + gScore.set(start, 0); + + const fScore = new Map<[number, number], number>(); + fScore.set(start, h(start, goal)); + + const bestCurrent = () => { + let best; + for (const n of openSet.values()) { + if (best !== undefined && (fScore.get(n) ?? Infinity) >= (fScore.get(best) ?? Infinity)) continue; + best = n; + } + return best ?? [-1, -1]; + }; + + const eq = (n: [number, number], m: [number, number]) => n[0] === m[0] && n[1] === m[1]; + + const neighbors = (n: [number, number]): Iterable<[number, number]> => { + const diffs = [ + [-1, 0], + [+1, 0], + [0, -1], + [0, +1], + ]; + let i = -1; + return { + [Symbol.iterator]: () => ({ + next: () => + ++i === diffs.length ? { value: [-1, -1], done: true } : { value: [n[0] + diffs[i][0], n[1] + diffs[i][1]] }, + }), + }; + }; + + /** + * @param {Map<[number, number], [number, number]>} cameFrom + * @param {[number, number]} current + */ + const reconstructPath = (cameFrom: Map<[number, number], [number, number]>, current: [number, number]) => { + const totalPath = [current]; + while (cameFrom.has(current)) { + current = cameFrom.get(current) ?? [-1, -1]; + totalPath.unshift(current); + } + return totalPath; + }; + + while (openSet.size) { + const current = bestCurrent(); + if (eq(current, goal)) return reconstructPath(cameFrom, current); + openSet.delete(current); + for (const neighbor of neighbors(current)) { + const tentativegScore = (gScore.get(current) ?? Infinity) + d(current, neighbor); + if (tentativegScore >= (gScore.get(neighbor) ?? Infinity)) continue; + cameFrom.set(neighbor, current); + gScore.set(neighbor, tentativegScore); + fScore.set(neighbor, tentativegScore + h(neighbor, goal)); + if (!openSet.has(neighbor)) openSet.add(neighbor); + } + } + return []; +}; diff --git a/src/Myrian/ui/MyrianRoot.tsx b/src/Myrian/ui/MyrianRoot.tsx index 665593254..b59231ba8 100644 --- a/src/Myrian/ui/MyrianRoot.tsx +++ b/src/Myrian/ui/MyrianRoot.tsx @@ -25,10 +25,10 @@ const iterator = (i: number, offset = 0): number[] => { interface ICellProps { tile: string; + tileSize: number; } -const Cell = ({ tile }: ICellProps): React.ReactElement => { - const x = 50; +const Cell = ({ tile, tileSize: x }: ICellProps): React.ReactElement => { const sx = { display: "block", color: "white", @@ -59,9 +59,37 @@ interface IProps { myrian: Myrian; } +const calcTiles = (v: number) => Math.floor((50 * 11) / v); + export function MyrianRoot({ myrian }: IProps): React.ReactElement { - const [center, setCenter] = useState([myrian.world[0].length / 2, myrian.world.length / 2]); - const [size, setSize] = useState(11); + const [center, rawSetCenter] = useState<[number, number]>([14, 8]); + const [size, rawSetSize] = useState(60); + const tiles = calcTiles(size); + const [dragging, setDragging] = useState(false); + const [dragScreenPos, setDragScreenPos] = useState<[number, number]>([0, 0]); + const [dragCenter, setDragCenter] = useState<[number, number]>([0, 0]); + + const setCenter = (v: [number, number], tiles: number) => { + rawSetCenter(() => { + v[0] = Math.max(Math.floor(tiles / 2), v[0]); + v[1] = Math.max(Math.floor(tiles / 2), v[1]); + v[0] = Math.min(myrian.world[0].length - Math.floor(tiles / 2), v[0]); + v[1] = Math.min(myrian.world.length - Math.floor(tiles / 2), v[1]); + return v; + }); + }; + + const setSize = (px: number) => { + const newTiles = calcTiles(px); + if (newTiles > 25) { + px = calcTiles(25); + } + if (newTiles < 5) { + px = calcTiles(5); + } + rawSetSize(px); + return px; + }; const [, setRerender] = useState(false); const rerender = () => setRerender((old) => !old); useEffect(() => { @@ -72,37 +100,68 @@ export function MyrianRoot({ myrian }: IProps): React.ReactElement { const sleeves = Object.fromEntries(myrian.sleeves.map((s) => [`${s.x}_${s.y}`, s])); const move = (x: number, y: number) => () => { - setCenter((c) => [Math.max(Math.floor(size / 2), c[0] + x), Math.max(Math.floor(size / 2), c[1] + y)]); + setCenter([center[0] + x, center[1] + y], tiles); }; const zoom = (size: number) => () => { - setSize((s) => s + size); + // setSize((s) => s + size); }; + + const onMouseDown = (event: React.MouseEvent) => { + setDragging(true); + setDragScreenPos([event.screenX, event.screenY]); + setDragCenter(center); + }; + const onStopDragging = () => setDragging(false); + + const onMouseMove = (event: React.MouseEvent) => { + if (!dragging) return; + const dx = event.screenX - dragScreenPos[0] + size / 2; + const dy = event.screenY - dragScreenPos[1] + size / 2; + setCenter([dragCenter[0] - Math.floor(dx / size), dragCenter[1] - Math.floor(dy / size)], tiles); + }; + + const onWheel = (event: React.WheelEvent) => { + const newSize = setSize(size * (1 + event.deltaY / 500)); + const newTiles = calcTiles(newSize); + setCenter(center, newTiles); + }; + return ( - - {iterator(size, center[1] - Math.floor(size / 2)).map((j) => ( - - {iterator(size, center[0] - Math.floor(size / 2)).map((i) => ( - - ))} - - ))} - + + {iterator(tiles, center[1] - Math.floor(tiles / 2)).map((j) => ( + + {iterator(tiles, center[0] - Math.floor(tiles / 2)).map((i) => ( + + ))} + + ))} + + diff --git a/src/NetscriptFunctions/Myrian.ts b/src/NetscriptFunctions/Myrian.ts index 9a6b82dea..a14131027 100644 --- a/src/NetscriptFunctions/Myrian.ts +++ b/src/NetscriptFunctions/Myrian.ts @@ -38,9 +38,7 @@ export function NetscriptMyrian(): InternalAPI { ianGetTile: (ctx) => (_x, _y) => { const x = helpers.number(ctx, "x", _x); const y = helpers.number(ctx, "y", _y); - return { - Content: myrian.world[y][x], - }; + return myrian.getTile(x, y); }, ianGetTask: (ctx) => (_sleeveId) => { throw new Error("Unimplemented");