Basic sitemode
This commit is contained in:
parent
76e92a2a50
commit
e3e43f0223
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
72
lib/algorithms/dijkstra.dart
Normal file
72
lib/algorithms/dijkstra.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class Edge<T> {
|
||||||
|
final double cost;
|
||||||
|
final T destination;
|
||||||
|
|
||||||
|
Edge(this.cost, this.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Result<T> {
|
||||||
|
final double cost;
|
||||||
|
final T? predecessor;
|
||||||
|
final T item;
|
||||||
|
|
||||||
|
Result(this.cost, this.predecessor, this.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Result<T>> dijkstra<T>(
|
||||||
|
T source, Iterable<Edge<T>> Function(T) neighbors) sync* {
|
||||||
|
var queue = PriorityQueue<Result<T>>((i0, i1) => i0.cost.compareTo(i1.cost));
|
||||||
|
|
||||||
|
Set<T> 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<T>? dijkstraPath<T>(
|
||||||
|
T source, T destination, Iterable<Edge<T>> Function(T) neighbors,
|
||||||
|
{double? maxCost}) {
|
||||||
|
if (source == destination) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
Map<T, T?> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,13 @@ class Size {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return "$dx x $dy";
|
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 {
|
class Offset {
|
||||||
@ -29,6 +36,13 @@ class Offset {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return "@($x, $y)";
|
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 {
|
class Rect {
|
||||||
@ -63,4 +77,15 @@ class Rect {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return "@($x0, $y0) $size";
|
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;
|
||||||
}
|
}
|
||||||
|
141
lib/algorithms/shadowcasting.dart
Normal file
141
lib/algorithms/shadowcasting.dart
Normal file
@ -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<geo.Offset> 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();
|
||||||
|
}
|
@ -2,10 +2,25 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class Palette {
|
class Palette {
|
||||||
static const defaultBg = Color(0xFF272D1B);
|
static const defaultBg = Color(0xFF272D1B);
|
||||||
static const uiBg = Color(0xFF847A4B);
|
static const uiBg = Color(0xFF232308);
|
||||||
static const defaultFg = Color(0xFFEEE9D1);
|
static const defaultFg = Color(0xFFEEE9D1);
|
||||||
static const demoPlayer = Color(0xFFFEFEF2);
|
static const demoPlayer = Color(0xFFFEFEF2);
|
||||||
|
|
||||||
static const demoDoor = Color(0xFF847A4B);
|
static const demoDoor = Color(0xFF847A4B);
|
||||||
static const demoExit = 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);
|
||||||
}
|
}
|
||||||
|
@ -260,6 +260,7 @@ final List<String> _fromCp437 = [
|
|||||||
"\u00a0"
|
"\u00a0"
|
||||||
];
|
];
|
||||||
final Map<String, Cp437> _toCp437 = {};
|
final Map<String, Cp437> _toCp437 = {};
|
||||||
|
final Map<int, Cp437> _toCp437I = {};
|
||||||
|
|
||||||
void _init() {
|
void _init() {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
@ -268,6 +269,7 @@ void _init() {
|
|||||||
|
|
||||||
for (final (i, c) in _fromCp437.indexed) {
|
for (final (i, c) in _fromCp437.indexed) {
|
||||||
_toCp437[c] = i;
|
_toCp437[c] = i;
|
||||||
|
_toCp437I[c.runes.first] = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
@ -291,6 +293,7 @@ Cp437 toCp437Char(String c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String fromCp437String(List<Cp437> s) {
|
String fromCp437String(List<Cp437> s) {
|
||||||
|
_init();
|
||||||
var out = "";
|
var out = "";
|
||||||
for (final c in s) {
|
for (final c in s) {
|
||||||
out += fromCp437Char(c);
|
out += fromCp437Char(c);
|
||||||
@ -299,9 +302,10 @@ String fromCp437String(List<Cp437> s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Cp437> toCp437String(String s) {
|
List<Cp437> toCp437String(String s) {
|
||||||
|
_init();
|
||||||
List<Cp437> out = [];
|
List<Cp437> out = [];
|
||||||
for (final c in s.runes) {
|
for (final c in s.runes) {
|
||||||
out.add(c);
|
out.add(_toCp437I[c] ?? toCp437Char("?"));
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,6 @@ Future<Level> getLevel() async {
|
|||||||
getVaultsIfAvailable("assets/images/vaults/house1.png");
|
getVaultsIfAvailable("assets/images/vaults/house1.png");
|
||||||
|
|
||||||
if (maybeVaults != null) {
|
if (maybeVaults != null) {
|
||||||
skreek("wasn't null!");
|
|
||||||
vaults = maybeVaults;
|
vaults = maybeVaults;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
127
lib/game/sitemode/camera.dart
Normal file
127
lib/game/sitemode/camera.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
lib/game/sitemode/fov.dart
Normal file
81
lib/game/sitemode/fov.dart
Normal file
@ -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<geo.Offset>(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<Edge<geo.Offset>> _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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
lib/game/sitemode/player.dart
Normal file
33
lib/game/sitemode/player.dart
Normal file
@ -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<void> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,65 @@
|
|||||||
|
import 'package:dartterm/algorithms/dijkstra.dart';
|
||||||
import 'package:dartterm/algorithms/geometry.dart' as geo;
|
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/terminal.dart';
|
||||||
import 'package:dartterm/world/level.dart';
|
import 'package:dartterm/world/level.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
part 'camera.dart';
|
||||||
|
part 'player.dart';
|
||||||
|
part 'fov.dart';
|
||||||
|
|
||||||
Future<void> sitemode(Level level) async {
|
Future<void> sitemode(Level level) async {
|
||||||
await _SiteMode(level).start();
|
await SiteMode(level).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SiteMode {
|
class SiteMode {
|
||||||
Level level;
|
Level level;
|
||||||
late geo.Offset position;
|
late geo.Offset playerPosition;
|
||||||
|
bool playerTookAutomatedAction = false;
|
||||||
|
List<geo.Offset> playerIntendedPath = [];
|
||||||
|
|
||||||
_SiteMode(this.level) {
|
late geo.Offset camera;
|
||||||
position = level.spawn;
|
|
||||||
|
late Set<geo.Offset> fovVisible;
|
||||||
|
late Set<geo.Offset> fovMovable;
|
||||||
|
Set<geo.Offset> fovMemory = {};
|
||||||
|
|
||||||
|
SiteMode(this.level) {
|
||||||
|
playerPosition = level.spawn;
|
||||||
|
|
||||||
|
init();
|
||||||
|
maintain();
|
||||||
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
cameraInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void maintain() {
|
||||||
|
playerMaintain();
|
||||||
|
fovMaintain();
|
||||||
|
cameraMaintain();
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() {
|
||||||
|
clear();
|
||||||
|
cameraDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
while (true) {
|
while (true) {
|
||||||
at(0, 0).puts("Site mode!");
|
maintain();
|
||||||
|
draw();
|
||||||
|
|
||||||
|
// take automated actions, otherwise receive input
|
||||||
|
if (playerTookAutomatedAction) {
|
||||||
await zzz(0.1);
|
await zzz(0.1);
|
||||||
|
} else {
|
||||||
|
await waitMenu();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ class Vaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Vault loadVault(Region r, Bitmap<VaultTile?> b) {
|
static Vault loadVault(Region r, Bitmap<VaultTile?> b) {
|
||||||
skreek("Loading vault: $r");
|
|
||||||
var tiles = [
|
var tiles = [
|
||||||
for (var y = r.rect.y0; y < r.rect.y1; y++)
|
for (var y = r.rect.y0; y < r.rect.y1; y++)
|
||||||
for (var x = r.rect.x0; x < r.rect.x1; x++)
|
for (var x = r.rect.x0; x < r.rect.x1; x++)
|
||||||
|
@ -17,6 +17,7 @@ class App extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'DARTTERM',
|
title: 'DARTTERM',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true, scaffoldBackgroundColor: Palette.defaultBg),
|
useMaterial3: true, scaffoldBackgroundColor: Palette.defaultBg),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
@ -72,7 +71,6 @@ class Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _notifyInput(Input i) {
|
void _notifyInput(Input i) {
|
||||||
skreek("Input: $i $_lastSeenMouse");
|
|
||||||
_inputSink.add(i);
|
_inputSink.add(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,12 +18,20 @@ class Cursor {
|
|||||||
t._tiles = [for (var i = 0; i < nTiles; i += 1) Tile()];
|
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 dx = 0; dx < font.cellWidth; dx += 1) {
|
||||||
for (var dy = 0; dy < font.cellHeight; dy += 1) {
|
for (var dy = 0; dy < font.cellHeight; dy += 1) {
|
||||||
var i = t._fromXY(x + dx, y + dy);
|
var i = t._fromXY(x + dx, y + dy);
|
||||||
|
|
||||||
if (i != null) {
|
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) = (
|
final (sourceCx, sourceCy) = (
|
||||||
(c % font.nCellsW) * font.cellWidth + dx,
|
(c % font.nCellsW) * font.cellWidth + dx,
|
||||||
(c ~/ font.nCellsW) * font.cellHeight + dy
|
(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) {
|
void puts(String s) {
|
||||||
var startX = x;
|
var startX = x;
|
||||||
for (final c in toCp437String(s)) {
|
for (final c in toCp437String(s)) {
|
||||||
|
@ -34,7 +34,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||||
|
@ -28,6 +28,7 @@ environment:
|
|||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
# versions available, run `flutter pub outdated`.
|
# versions available, run `flutter pub outdated`.
|
||||||
dependencies:
|
dependencies:
|
||||||
|
collection: ^1.17.2
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user