From e3e43f0223f35ebbea8334f862df48d50cd55d0d Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Fri, 22 Sep 2023 21:08:03 -0700 Subject: [PATCH] Basic sitemode --- assets/images/fonts/font_big.png | Bin 18745 -> 18817 bytes lib/algorithms/dijkstra.dart | 72 +++++++++++++++ lib/algorithms/geometry.dart | 25 ++++++ lib/algorithms/shadowcasting.dart | 141 ++++++++++++++++++++++++++++++ lib/colors.dart | 17 +++- lib/cp437.dart | 6 +- lib/game/game.dart | 1 - lib/game/sitemode/camera.dart | 127 +++++++++++++++++++++++++++ lib/game/sitemode/fov.dart | 81 +++++++++++++++++ lib/game/sitemode/player.dart | 33 +++++++ lib/game/sitemode/sitemode.dart | 56 ++++++++++-- lib/gen/vaults.dart | 1 - lib/main.dart | 1 + lib/terminal.dart | 2 - lib/terminal/cursor.dart | 17 +++- pubspec.lock | 2 +- pubspec.yaml | 1 + 17 files changed, 568 insertions(+), 15 deletions(-) create mode 100644 lib/algorithms/dijkstra.dart create mode 100644 lib/algorithms/shadowcasting.dart create mode 100644 lib/game/sitemode/camera.dart create mode 100644 lib/game/sitemode/fov.dart create mode 100644 lib/game/sitemode/player.dart diff --git a/assets/images/fonts/font_big.png b/assets/images/fonts/font_big.png index 37072e91cce8fd0fa2c4600b5eaa9afd99356adb..5b749f98b7df0b266529615ef3249ea8be761873 100644 GIT binary patch delta 14733 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 z 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 { + final double cost; + final T destination; + + Edge(this.cost, this.destination); +} + +class Result { + final double cost; + final T? predecessor; + final T item; + + Result(this.cost, this.predecessor, this.item); +} + +Iterable> dijkstra( + T source, Iterable> Function(T) neighbors) sync* { + var queue = PriorityQueue>((i0, i1) => i0.cost.compareTo(i1.cost)); + + Set seen = {source}; + queue.add(Result(0.0, null, source)); + + while (queue.isNotEmpty) { + var u = queue.removeFirst(); + yield u; + + for (var v in neighbors(u.item)) { + if (seen.contains(v.destination)) { + continue; + } + + seen.add(v.destination); + queue.add(Result(u.cost + v.cost, u.item, v.destination)); + } + } +} + +List? dijkstraPath( + T source, T destination, Iterable> Function(T) neighbors, + {double? maxCost}) { + if (source == destination) { + return []; + } + Map predecessor = {}; + + for (var r in dijkstra(source, neighbors)) { + predecessor[r.item] = r.predecessor; + if (maxCost != null && r.cost >= maxCost) { + return null; + } + if (r.item == destination) { + break; + } + } + + var revPath = [destination]; + while (true) { + var pred = predecessor[revPath.last]; + + if (pred == source) { + revPath.reverseRange(0, revPath.length); + return revPath; + } else if (pred == null) { + throw Exception( + "predecessor should not be null -- that would mean we missed source"); + } else { + revPath.add(pred); + } + } +} diff --git a/lib/algorithms/geometry.dart b/lib/algorithms/geometry.dart index e1f705a..9e8b656 100644 --- a/lib/algorithms/geometry.dart +++ b/lib/algorithms/geometry.dart @@ -17,6 +17,13 @@ class Size { String toString() { return "$dx x $dy"; } + + @override + bool operator ==(Object other) => + other is Size && other.dx == dx && other.dy == dy; + + @override + int get hashCode => (dx, dy).hashCode; } class Offset { @@ -29,6 +36,13 @@ class Offset { String toString() { return "@($x, $y)"; } + + @override + bool operator ==(Object other) => + other is Offset && other.x == x && other.y == y; + + @override + int get hashCode => (x, y).hashCode; } class Rect { @@ -63,4 +77,15 @@ class Rect { String toString() { return "@($x0, $y0) $size"; } + + @override + bool operator ==(Object other) => + other is Rect && + other.x0 == x0 && + other.y0 == y0 && + other.dx == dx && + other.dy == dy; + + @override + int get hashCode => (x0, y0, dx, dy).hashCode; } diff --git a/lib/algorithms/shadowcasting.dart b/lib/algorithms/shadowcasting.dart new file mode 100644 index 0000000..f0bf0e1 --- /dev/null +++ b/lib/algorithms/shadowcasting.dart @@ -0,0 +1,141 @@ +// Port of https://www.albertford.com/shadowcasting/ +import 'package:dartterm/algorithms/geometry.dart' as geo; + +void shadowcast(geo.Offset origin, bool Function(geo.Offset) isBlocking, + Function(geo.Offset) markVisible) { + markVisible(origin); + + for (var i = 0; i < 4; i++) { + var quadrant = Quadrant(i, origin.x, origin.y); + + void reveal(geo.Offset tile) { + markVisible(quadrant.transform(tile)); + } + + bool isWall(geo.Offset? tile) { + if (tile == null) { + return false; + } + + return isBlocking(quadrant.transform(tile)); + } + + bool isFloor(geo.Offset? tile) { + if (tile == null) { + return false; + } + + return !isBlocking(quadrant.transform(tile)); + } + + void scan(Row row) { + geo.Offset? prevTile; + for (var tile in row.tiles()) { + if (isWall(tile) || isSymmetric(row, tile)) { + reveal(tile); + } + if (isWall(prevTile) && isFloor(tile)) { + row.startSlope = slope(tile); + } + if (isFloor(prevTile) && isWall(tile)) { + Row nextRow = row.next(); + nextRow.endSlope = slope(tile); + scan(nextRow); + } + prevTile = tile; + } + + if (isFloor(prevTile)) { + scan(row.next()); + } + } + + Row firstRow = Row(1, Fraction(-1, 1), Fraction(1, 1)); + scan(firstRow); + } +} + +class Quadrant { + static const int north = 0; + static const int east = 1; + static const int south = 2; + static const int west = 3; + + final int cardinal; + final int ox, oy; + + Quadrant(this.cardinal, this.ox, this.oy); + + geo.Offset transform(geo.Offset tile) { + var geo.Offset(x: row, y: col) = tile; + + switch (cardinal) { + case north: + return geo.Offset(ox + col, oy - row); + case south: + return geo.Offset(ox + col, oy + row); + case east: + return geo.Offset(ox + row, oy + col); + case west: + return geo.Offset(ox - row, oy + col); + default: + throw Exception("Quadrant must be initialized with a real cardinal"); + } + } +} + +class Row { + int depth; + Fraction startSlope; + Fraction endSlope; + + Row(this.depth, this.startSlope, this.endSlope); + + Iterable tiles() sync* { + var minCol = roundTiesUp(startSlope.scale(depth)); + var maxCol = roundTiesDown(endSlope.scale(depth)); + for (int col = minCol; col <= maxCol; col++) { + yield geo.Offset(depth, col); + } + } + + Row next() { + return Row(depth + 1, startSlope, endSlope); + } +} + +class Fraction { + final int numerator; + final int denominator; + + Fraction(this.numerator, this.denominator); + + Fraction scale(int n) { + return Fraction(numerator * n, denominator); + } + + // We're often comparing this to an int or a double, so it's OK + // to have precision loss _so long as we do all divides after all multiplies_ + double toDouble() { + return numerator.toDouble() / denominator.toDouble(); + } +} + +Fraction slope(geo.Offset tile) { + var geo.Offset(x: rowDepth, y: col) = tile; + return Fraction(2 * col - 1, 2 * rowDepth); +} + +bool isSymmetric(Row row, geo.Offset tile) { + var geo.Offset(x: rowDepth, y: col) = tile; + return (col >= row.startSlope.scale(rowDepth).toDouble() && + col <= (row.endSlope.scale(row.depth)).toDouble()); +} + +int roundTiesUp(Fraction n) { + return (n.toDouble() + 0.5).floor(); +} + +int roundTiesDown(Fraction n) { + return (n.toDouble() - 0.5).ceil(); +} diff --git a/lib/colors.dart b/lib/colors.dart index 17610a1..f8897b1 100644 --- a/lib/colors.dart +++ b/lib/colors.dart @@ -2,10 +2,25 @@ import 'package:flutter/material.dart'; class Palette { static const defaultBg = Color(0xFF272D1B); - static const uiBg = Color(0xFF847A4B); + static const uiBg = Color(0xFF232308); static const defaultFg = Color(0xFFEEE9D1); static const demoPlayer = Color(0xFFFEFEF2); static const demoDoor = Color(0xFF847A4B); static const demoExit = Color(0xFF847A4B); + + static const sitemodePlayer = demoPlayer; + + static const sitemodeSeenDoor = Color(0xFFFEFD4B); + static const sitemodeUnseenDoor = demoExit; + + static const sitemodeSeenWall = defaultFg; + static const sitemodeUnseenWall = demoExit; + + static const sitemodeSeenExit = defaultFg; + static const sitemodeUnseenExit = demoExit; + + static const sitemodeSeenFloor = uiBg; + + static const demoFloorHighlight = Color(0xFF863B6F); } diff --git a/lib/cp437.dart b/lib/cp437.dart index cb77f5f..8b9426d 100644 --- a/lib/cp437.dart +++ b/lib/cp437.dart @@ -260,6 +260,7 @@ final List _fromCp437 = [ "\u00a0" ]; final Map _toCp437 = {}; +final Map _toCp437I = {}; void _init() { if (initialized) { @@ -268,6 +269,7 @@ void _init() { for (final (i, c) in _fromCp437.indexed) { _toCp437[c] = i; + _toCp437I[c.runes.first] = i; } initialized = true; @@ -291,6 +293,7 @@ Cp437 toCp437Char(String c) { } String fromCp437String(List s) { + _init(); var out = ""; for (final c in s) { out += fromCp437Char(c); @@ -299,9 +302,10 @@ String fromCp437String(List s) { } List toCp437String(String s) { + _init(); List out = []; for (final c in s.runes) { - out.add(c); + out.add(_toCp437I[c] ?? toCp437Char("?")); } return out; } diff --git a/lib/game/game.dart b/lib/game/game.dart index bd8ed82..fe9873b 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -53,7 +53,6 @@ Future getLevel() async { getVaultsIfAvailable("assets/images/vaults/house1.png"); if (maybeVaults != null) { - skreek("wasn't null!"); vaults = maybeVaults; break; } diff --git a/lib/game/sitemode/camera.dart b/lib/game/sitemode/camera.dart new file mode 100644 index 0000000..680b2b5 --- /dev/null +++ b/lib/game/sitemode/camera.dart @@ -0,0 +1,127 @@ +part of 'sitemode.dart'; + +// We render each thing as a 2x2 block. +// We want the player's cell to be +// _actually centered_, and the terminal is an even number of cells across +// So, shifting the camera an extra cell to the north and then +// drawing one extra tile offscreen? That should accomplish it! +const cameraViewWidth = Terminal.width ~/ 2 + 1; +const cameraViewHeight = Terminal.height ~/ 2 + 1; +const cameraMargin = + 4; // how far the camera is from the ideal position before it pans +const cameraTileOffset = geo.Offset(-1, -1); + +extension CameraParts on SiteMode { + void cameraInit() { + camera = _cameraIdeal(); + } + + void cameraMaintain() { + var ideal = _cameraIdeal(); + while (camera.x < ideal.x - cameraMargin) { + camera = geo.Offset(camera.x + 1, camera.y); + } + while (camera.x > ideal.x + cameraMargin) { + camera = geo.Offset(camera.x - 1, camera.y); + } + while (camera.y < ideal.y - cameraMargin) { + camera = geo.Offset(camera.x, camera.y + 1); + } + while (camera.y > ideal.y + cameraMargin) { + camera = geo.Offset(camera.x, camera.y - 1); + } + } + + geo.Offset _cameraIdeal() { + return geo.Offset(playerPosition.x - cameraViewWidth ~/ 2 - 1, + playerPosition.y - cameraViewHeight ~/ 2 - 1); + } + + void cameraDraw() { + // Draw the world! + // Work in columns, top to bottom, which should facilitate our fake 3D effects + for (var cx = 0; cx < cameraViewWidth; cx++) { + for (var cy = 0; cy < cameraViewHeight; cy++) { + var tx = cameraTileOffset.x + cx * 2; + var ty = cameraTileOffset.y + cy * 2; + cameraDrawFloor(tx, ty, camera.x + cx, camera.y + cy); + } + } + for (var cx = 0; cx < cameraViewWidth; cx++) { + for (var cy = 0; cy < cameraViewHeight; cy++) { + var tx = cameraTileOffset.x + cx * 2; + var ty = cameraTileOffset.y + cy * 2; + cameraDrawCell(tx, ty, camera.x + cx, camera.y + cy); + } + } + } + + void cameraDrawFloor(int tx, int ty, int cx, int cy) { + var cxy = geo.Offset(cx, cy); + if (!fovVisible.contains(cxy)) { + return; + } + + var tile = level.tiles.get(cx, cy); + at(tx, ty).bg(Palette.sitemodeSeenFloor).puts(" "); + } + + void cameraDrawCell(int tx, int ty, int cx, int cy) { + var cursorAbove = at(tx, ty); + var cursorBelow = at(tx, ty + 2); + + LevelTile? tile; + // TODO: Fade tiles when loaded from memory + // TODO: Darken live floors + bool seenLive; + + var cxy = geo.Offset(cx, cy); + if (fovVisible.contains(cxy)) { + tile = level.tiles.get(cx, cy); + seenLive = true; + } else if (fovMemory.contains(cxy)) { + tile = level.tiles.get(cx, cy); + seenLive = false; + } else { + return; + } + + if (fovMovable.contains(geo.Offset(cx, cy))) { + cursorAbove + .highlight() + .act(Act(label: "Move", callback: () => playerMoveTo(cx, cy))); + } + + var colorWall = Palette.sitemodeSeenWall; + var colorWallInner = Palette.sitemodeSeenWall; + var colorExit = Palette.sitemodeSeenExit; + var colorDoor = Palette.sitemodeSeenDoor; + if (cy >= playerPosition.y) { + colorWallInner = Palette.sitemodeUnseenWall; // we wouldn't see it anyway + } + if (!seenLive) { + colorWallInner = colorWall = Palette.sitemodeUnseenWall; + colorExit = Palette.sitemodeUnseenExit; + colorDoor = Palette.sitemodeUnseenDoor; + } + + switch (tile) { + case null: + case LevelTile.floor: + case LevelTile.openDoor: + cursorAbove.touch(2); + case LevelTile.exit: + cursorAbove.big().fg(colorExit).puts("X"); + case LevelTile.wall: + cursorAbove.fg(colorWall).puts("██"); + cursorBelow.fg(colorWallInner).puts("░░"); + case LevelTile.closedDoor: + cursorAbove.big().fg(colorDoor).puts("+"); + } + + var cursorContent = at(tx, ty); + if (geo.Offset(cx, cy) == playerPosition) { + cursorContent.fg(Palette.sitemodePlayer).big().putc(0xff); + } + } +} diff --git a/lib/game/sitemode/fov.dart b/lib/game/sitemode/fov.dart new file mode 100644 index 0000000..727a815 --- /dev/null +++ b/lib/game/sitemode/fov.dart @@ -0,0 +1,81 @@ +part of 'sitemode.dart'; + +const double fovMaxMovementCost = 10.0; +const double fovMaxMemoryDistance = 80.0; + +extension FOVParts on SiteMode { + void fovMaintain() { + fovVisible = {}; + shadowcast(playerPosition, _fovIsBlocking, (xy) => fovVisible.add(xy)); + + var oldFovMemory = fovMemory; + fovMemory = {}; + oldFovMemory.addAll(fovVisible); + for (var xy in oldFovMemory) { + if (_fovMemorablyClose(xy)) { + fovMemory.add(xy); + } + } + + fovMovable = {}; + for (var r in dijkstra(playerPosition, _fovDijkstraNeighbors)) { + if (r.cost > fovMaxMovementCost) { + break; + } + fovMovable.add(r.item); + } + } + + bool _fovMemorablyClose(geo.Offset other) { + var dx = (other.x - playerPosition.x).toDouble(); + var dy = (other.y - playerPosition.y).toDouble(); + return math.sqrt(dx * dx + dy * dy) < fovMaxMemoryDistance; + } + + bool _fovKnown(geo.Offset other) { + return fovVisible.contains(other) || fovMemory.contains(other); + } + + bool _fovIsBlocking(geo.Offset xy) { + switch (level.tiles.get(xy.x, xy.y)) { + case null: + return true; + case LevelTile.exit: + return true; + case LevelTile.openDoor: + case LevelTile.floor: + return false; + case LevelTile.wall: + return true; + case LevelTile.closedDoor: + return true; + } + } + + Iterable> _fovDijkstraNeighbors(geo.Offset xy) sync* { + var tile = level.tiles.get(xy.x, xy.y); + if (tile == LevelTile.exit || tile == LevelTile.closedDoor) { + // can't go anywhere after this + return; + } + + for (var (dx, dy) in [(-1, 0), (1, 0), (0, -1), (0, 1)]) { + var xy2 = geo.Offset(xy.x + dx, xy.y + dy); + + // Only return visible or remembered tiles + if (!_fovKnown(xy2)) { + continue; + } + + var tile = level.tiles.get(xy2.x, xy2.y); + if (tile == LevelTile.wall) { + continue; + } + // for now, because exiting isn't implemented + if (tile == LevelTile.exit) { + continue; + } + yield Edge(1.0, xy2); + } + } +} diff --git a/lib/game/sitemode/player.dart b/lib/game/sitemode/player.dart new file mode 100644 index 0000000..21424e3 --- /dev/null +++ b/lib/game/sitemode/player.dart @@ -0,0 +1,33 @@ +part of 'sitemode.dart'; + +extension PlayerParts on SiteMode { + void playerMaintain() { + playerTookAutomatedAction = false; + var geo.Offset(:x, :y) = playerPosition; + + var tile = level.tiles.get(x, y); + if (tile == LevelTile.closedDoor) { + level.tiles.set(x, y, LevelTile.openDoor); + } + + if (playerIntendedPath.isNotEmpty) { + var nextPosition = playerIntendedPath.removeAt(0); + playerPosition = nextPosition; + playerTookAutomatedAction = true; + } + } + + // support the UI command here + Future playerMoveTo(int cx, int cy) async { + var ipath = dijkstraPath( + geo.Offset(playerPosition.x, playerPosition.y), + geo.Offset(cx, cy), + _fovDijkstraNeighbors, + ); + + if (ipath == null) { + return; // don't move, I guess + } + playerIntendedPath.addAll(ipath); + } +} diff --git a/lib/game/sitemode/sitemode.dart b/lib/game/sitemode/sitemode.dart index 06a5b35..0176d47 100644 --- a/lib/game/sitemode/sitemode.dart +++ b/lib/game/sitemode/sitemode.dart @@ -1,23 +1,65 @@ +import 'package:dartterm/algorithms/dijkstra.dart'; import 'package:dartterm/algorithms/geometry.dart' as geo; +import 'package:dartterm/algorithms/shadowcasting.dart'; +import 'package:dartterm/colors.dart'; +import 'package:dartterm/skreek.dart'; import 'package:dartterm/terminal.dart'; import 'package:dartterm/world/level.dart'; +import 'dart:math' as math; + +part 'camera.dart'; +part 'player.dart'; +part 'fov.dart'; Future sitemode(Level level) async { - await _SiteMode(level).start(); + await SiteMode(level).start(); } -class _SiteMode { +class SiteMode { Level level; - late geo.Offset position; + late geo.Offset playerPosition; + bool playerTookAutomatedAction = false; + List playerIntendedPath = []; - _SiteMode(this.level) { - position = level.spawn; + late geo.Offset camera; + + late Set fovVisible; + late Set fovMovable; + Set fovMemory = {}; + + SiteMode(this.level) { + playerPosition = level.spawn; + + init(); + maintain(); + } + + void init() { + cameraInit(); + } + + void maintain() { + playerMaintain(); + fovMaintain(); + cameraMaintain(); + } + + void draw() { + clear(); + cameraDraw(); } Future start() async { while (true) { - at(0, 0).puts("Site mode!"); - await zzz(0.1); + maintain(); + draw(); + + // take automated actions, otherwise receive input + if (playerTookAutomatedAction) { + await zzz(0.1); + } else { + await waitMenu(); + } } } } diff --git a/lib/gen/vaults.dart b/lib/gen/vaults.dart index b992cfd..a4a4986 100644 --- a/lib/gen/vaults.dart +++ b/lib/gen/vaults.dart @@ -28,7 +28,6 @@ class Vaults { } static Vault loadVault(Region r, Bitmap b) { - skreek("Loading vault: $r"); var tiles = [ for (var y = r.rect.y0; y < r.rect.y1; y++) for (var x = r.rect.x0; x < r.rect.x1; x++) diff --git a/lib/main.dart b/lib/main.dart index 365147a..a726fcd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, title: 'DARTTERM', theme: ThemeData( useMaterial3: true, scaffoldBackgroundColor: Palette.defaultBg), diff --git a/lib/terminal.dart b/lib/terminal.dart index 4ce09bf..da65be8 100644 --- a/lib/terminal.dart +++ b/lib/terminal.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:math' as math; import 'dart:ui' as ui; @@ -72,7 +71,6 @@ class Terminal { } void _notifyInput(Input i) { - skreek("Input: $i $_lastSeenMouse"); _inputSink.add(i); } diff --git a/lib/terminal/cursor.dart b/lib/terminal/cursor.dart index 7d34fdc..5360a5a 100644 --- a/lib/terminal/cursor.dart +++ b/lib/terminal/cursor.dart @@ -18,12 +18,20 @@ class Cursor { t._tiles = [for (var i = 0; i < nTiles; i += 1) Tile()]; } - void putc(int c) { + void putc(int? c) { for (var dx = 0; dx < font.cellWidth; dx += 1) { for (var dy = 0; dy < font.cellHeight; dy += 1) { var i = t._fromXY(x + dx, y + dy); if (i != null) { + // you can pass null and in that case a character-sized block of + // screen is automatically UI-affected, but not drawn + if (c == null) { + t._tiles[i].highlightGroup = highlightGroup; + t._tiles[i].actions.addAll(_acts); + continue; + } + final (sourceCx, sourceCy) = ( (c % font.nCellsW) * font.cellWidth + dx, (c ~/ font.nCellsW) * font.cellHeight + dy @@ -48,6 +56,13 @@ class Cursor { } } + void touch(int n) { + for (var i = 0; i < n; i++) { + putc(null); + x += font.cellWidth; + } + } + void puts(String s) { var startX = x; for (final c in toCp437String(s)) { diff --git a/pubspec.lock b/pubspec.lock index b898505..4d14e43 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,7 +34,7 @@ packages: source: hosted version: "1.1.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 diff --git a/pubspec.yaml b/pubspec.yaml index 5fdb915..755f714 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + collection: ^1.17.2 flutter: sdk: flutter