Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
7fc65b49c2 | |||
e3e43f0223 | |||
76e92a2a50 |
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() {
|
||||
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;
|
||||
}
|
||||
|
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();
|
||||
}
|
@ -1,11 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Palette {
|
||||
static const defaultBg = Colors.black;
|
||||
static const defaultFg = Colors.white;
|
||||
static const defaultBg = Color(0xFF272D1B);
|
||||
static const uiBg = Color(0xFF232308);
|
||||
static const defaultFg = Color(0xFFEEE9D1);
|
||||
static const demoPlayer = Color(0xFFFEFEF2);
|
||||
|
||||
static const subtitle = Colors.red;
|
||||
static const demoDoor = Color(0xFF847A4B);
|
||||
static const demoExit = Color(0xFF847A4B);
|
||||
|
||||
static const demoDoor = Colors.red;
|
||||
static const demoExit = Colors.red;
|
||||
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"
|
||||
];
|
||||
final Map<String, Cp437> _toCp437 = {};
|
||||
final Map<int, Cp437> _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<Cp437> s) {
|
||||
_init();
|
||||
var out = "";
|
||||
for (final c in s) {
|
||||
out += fromCp437Char(c);
|
||||
@ -299,9 +302,10 @@ String fromCp437String(List<Cp437> s) {
|
||||
}
|
||||
|
||||
List<Cp437> toCp437String(String s) {
|
||||
_init();
|
||||
List<Cp437> out = [];
|
||||
for (final c in s.runes) {
|
||||
out.add(c);
|
||||
out.add(_toCp437I[c] ?? toCp437Char("?"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
72
lib/game/game.dart
Normal file
72
lib/game/game.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:dartterm/assets.dart';
|
||||
import 'package:dartterm/game/sitemode/sitemode.dart';
|
||||
import 'package:dartterm/gen/generator.dart';
|
||||
import 'package:dartterm/skreek.dart';
|
||||
import 'package:dartterm/terminal.dart';
|
||||
import 'package:dartterm/world/level.dart';
|
||||
|
||||
void main() async {
|
||||
var level = await getLevel();
|
||||
|
||||
await sitemode(level);
|
||||
}
|
||||
/*
|
||||
void main() async {
|
||||
var descriptor = "generic";
|
||||
while (true) {
|
||||
at(0, 0).clear();
|
||||
at(0, 0).puts("Hello, bats!");
|
||||
at(0, 2).fg(Palette.subtitle).small().puts("Beware of the bat!");
|
||||
at(4, 4)
|
||||
.bg(Palette.subtitle)
|
||||
.fg(Palette.defaultBg)
|
||||
.big()
|
||||
.highlight()
|
||||
.act(Act(
|
||||
isDefault: true,
|
||||
label: "Strong!",
|
||||
callback: () async {
|
||||
log("strong!");
|
||||
descriptor = "strong";
|
||||
}))
|
||||
.act(Act(
|
||||
label: "Nocturnal!",
|
||||
callback: () async {
|
||||
log("nocturnal!");
|
||||
descriptor = "nocturnal";
|
||||
}))
|
||||
.puts("ALTER BAT");
|
||||
|
||||
at(4, 8).normal().puts("A $descriptor bat!");
|
||||
|
||||
// await zzz(1.0);
|
||||
await waitMenu();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
Future<Level> getLevel() async {
|
||||
Vaults vaults;
|
||||
while (true) {
|
||||
Vaults? maybeVaults =
|
||||
getVaultsIfAvailable("assets/images/vaults/house1.png");
|
||||
|
||||
if (maybeVaults != null) {
|
||||
vaults = maybeVaults;
|
||||
break;
|
||||
}
|
||||
await zzz(0.1);
|
||||
}
|
||||
return Generator(math.Random(0), vaults).generateLevel(Requirement(
|
||||
16,
|
||||
32,
|
||||
16,
|
||||
18,
|
||||
DirectionSet({
|
||||
Direction.up,
|
||||
Direction.down,
|
||||
Direction.left,
|
||||
Direction.right,
|
||||
})));
|
||||
}
|
@ -8,7 +8,7 @@ import 'package:dartterm/skreek.dart';
|
||||
import 'package:dartterm/terminal.dart';
|
||||
import 'package:dartterm/world/level.dart';
|
||||
|
||||
void main() async {
|
||||
void generator_test_program() async {
|
||||
Vaults vaults;
|
||||
while (true) {
|
||||
skreek("about to load template");
|
||||
@ -63,6 +63,10 @@ void main() async {
|
||||
}
|
||||
}
|
||||
}
|
||||
at(output.spawn.x * 2, output.spawn.y * 2)
|
||||
.fg(Palette.demoPlayer)
|
||||
.big()
|
||||
.puts("\u00ff");
|
||||
inpLoop:
|
||||
await for (var inp in rawInput()) {
|
||||
skreek("$inp $seed");
|
||||
@ -78,37 +82,3 @@ void main() async {
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
void main() async {
|
||||
var descriptor = "generic";
|
||||
while (true) {
|
||||
at(0, 0).clear();
|
||||
at(0, 0).puts("Hello, bats!");
|
||||
at(0, 2).fg(Palette.subtitle).small().puts("Beware of the bat!");
|
||||
at(4, 4)
|
||||
.bg(Palette.subtitle)
|
||||
.fg(Palette.defaultBg)
|
||||
.big()
|
||||
.highlight()
|
||||
.act(Act(
|
||||
isDefault: true,
|
||||
label: "Strong!",
|
||||
callback: () async {
|
||||
log("strong!");
|
||||
descriptor = "strong";
|
||||
}))
|
||||
.act(Act(
|
||||
label: "Nocturnal!",
|
||||
callback: () async {
|
||||
log("nocturnal!");
|
||||
descriptor = "nocturnal";
|
||||
}))
|
||||
.puts("ALTER BAT");
|
||||
|
||||
at(4, 8).normal().puts("A $descriptor bat!");
|
||||
|
||||
// await zzz(1.0);
|
||||
await waitMenu();
|
||||
}
|
||||
}
|
||||
*/
|
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.small().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);
|
||||
}
|
||||
}
|
65
lib/game/sitemode/sitemode.dart
Normal file
65
lib/game/sitemode/sitemode.dart
Normal file
@ -0,0 +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<void> sitemode(Level level) async {
|
||||
await SiteMode(level).start();
|
||||
}
|
||||
|
||||
class SiteMode {
|
||||
Level level;
|
||||
late geo.Offset playerPosition;
|
||||
bool playerTookAutomatedAction = false;
|
||||
List<geo.Offset> playerIntendedPath = [];
|
||||
|
||||
late geo.Offset camera;
|
||||
|
||||
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 {
|
||||
while (true) {
|
||||
maintain();
|
||||
draw();
|
||||
|
||||
// take automated actions, otherwise receive input
|
||||
if (playerTookAutomatedAction) {
|
||||
await zzz(0.1);
|
||||
} else {
|
||||
await waitMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
lib/game/ui_test_program.dart
Normal file
34
lib/game/ui_test_program.dart
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
void main() async {
|
||||
var descriptor = "generic";
|
||||
while (true) {
|
||||
at(0, 0).clear();
|
||||
at(0, 0).puts("Hello, bats!");
|
||||
at(0, 2).fg(Palette.subtitle).small().puts("Beware of the bat!");
|
||||
at(4, 4)
|
||||
.bg(Palette.subtitle)
|
||||
.fg(Palette.defaultBg)
|
||||
.big()
|
||||
.highlight()
|
||||
.act(Act(
|
||||
isDefault: true,
|
||||
label: "Strong!",
|
||||
callback: () async {
|
||||
log("strong!");
|
||||
descriptor = "strong";
|
||||
}))
|
||||
.act(Act(
|
||||
label: "Nocturnal!",
|
||||
callback: () async {
|
||||
log("nocturnal!");
|
||||
descriptor = "nocturnal";
|
||||
}))
|
||||
.puts("ALTER BAT");
|
||||
|
||||
at(4, 8).normal().puts("A $descriptor bat!");
|
||||
|
||||
// await zzz(1.0);
|
||||
await waitMenu();
|
||||
}
|
||||
}
|
||||
*/
|
@ -12,6 +12,7 @@ part 'direction_set.dart';
|
||||
part 'orientation.dart';
|
||||
part 'requirement.dart';
|
||||
part 'vault.dart';
|
||||
part 'vault_tile.dart';
|
||||
part 'vaults.dart';
|
||||
|
||||
const vaultTries = 30;
|
||||
|
@ -39,37 +39,3 @@ class Vault {
|
||||
return Vault(tiles.rotateLeft(), smooth.rotateLeft());
|
||||
}
|
||||
}
|
||||
|
||||
VaultTile mergeVaultTile(VaultTile bottom, VaultTile top) {
|
||||
if (bottom == VaultTile.wall && top == VaultTile.archpronewall) {
|
||||
return VaultTile.wall;
|
||||
}
|
||||
if (bottom == VaultTile.wall && top == VaultTile.archwall) {
|
||||
return VaultTile.wall;
|
||||
}
|
||||
if (bottom == VaultTile.archwall && top == VaultTile.archpronewall) {
|
||||
return VaultTile.archwall;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
LevelTile flattenVaultTile(VaultTile vt) {
|
||||
switch (vt) {
|
||||
case VaultTile.meta0:
|
||||
case VaultTile.defaultwall:
|
||||
case VaultTile.archpronewall:
|
||||
case VaultTile.archwall:
|
||||
case VaultTile.wall:
|
||||
return LevelTile.wall;
|
||||
|
||||
case VaultTile.exit:
|
||||
return LevelTile.exit;
|
||||
case VaultTile.door:
|
||||
return LevelTile.closedDoor;
|
||||
|
||||
case VaultTile.doorpronefloor:
|
||||
case VaultTile.bspfloor:
|
||||
case VaultTile.floor:
|
||||
return LevelTile.floor;
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ class Vaults {
|
||||
}
|
||||
|
||||
static Vault loadVault(Region r, Bitmap<VaultTile?> 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++)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:dartterm/colors.dart';
|
||||
import 'package:dartterm/input.dart' as input;
|
||||
import 'package:dartterm/game.dart' as game;
|
||||
import 'package:dartterm/game/game.dart' as game;
|
||||
import 'package:dartterm/terminal.dart' as terminal;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.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),
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -34,7 +34,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user