Basic sitemode

This commit is contained in:
2023-09-22 21:08:03 -07:00
parent 76e92a2a50
commit e3e43f0223
17 changed files with 568 additions and 15 deletions

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}

View File

@ -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<void> 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<geo.Offset> playerIntendedPath = [];
_SiteMode(this.level) {
position = level.spawn;
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) {
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();
}
}
}
}