468 lines
14 KiB
Dart
468 lines
14 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:dartterm/algorithms/geometry.dart' as geo;
|
|
import 'package:dartterm/algorithms/regionalize.dart';
|
|
import 'package:dartterm/algorithms/kruskal.dart';
|
|
import 'package:dartterm/bitmap.dart';
|
|
import 'package:dartterm/skreek.dart';
|
|
import 'package:dartterm/world/level.dart';
|
|
|
|
part 'direction.dart';
|
|
part 'direction_set.dart';
|
|
part 'orientation.dart';
|
|
part 'requirement.dart';
|
|
part 'vault.dart';
|
|
part 'vault_tile.dart';
|
|
part 'vaults.dart';
|
|
|
|
const vaultTries = 30;
|
|
|
|
class Generator {
|
|
final math.Random _random;
|
|
final Vaults _vaults;
|
|
List<Vault> _queue = [];
|
|
|
|
Generator(this._random, this._vaults);
|
|
|
|
Level generateLevel(Requirement requirement) {
|
|
var out = _generateOriented(requirement, false);
|
|
return _finalize(out);
|
|
}
|
|
|
|
/*
|
|
Vault generateVault(Requirement requirement) {
|
|
var out = _generateOriented(requirement, false);
|
|
var (vault, (_, _)) = _finalize(out);
|
|
return vault;
|
|
}
|
|
*/
|
|
|
|
Vault _generateOriented(Requirement requirement, bool canBeVault) {
|
|
if (canBeVault) {
|
|
Vault? suggested = _suggest(vaultTries, requirement);
|
|
if (suggested != null) {
|
|
return _fillMetaRegions(requirement, suggested);
|
|
}
|
|
}
|
|
|
|
// First of all: randomize orientation
|
|
// This way we only have to consider one kind of spilt
|
|
var orientation = randomOrientation(_random);
|
|
|
|
// Try to make vx the long axis if possible
|
|
var req2 = unReorientRequirement(requirement, orientation);
|
|
if (req2.vyMax > (req2.vxMax - 2) * 3 / 2) {
|
|
orientation = (orientation + 2) % 8; // rotate once more
|
|
}
|
|
// if only one of "left" and "right" needs to be smooth, prioritize right
|
|
// as left is generated first
|
|
req2 = unReorientRequirement(requirement, orientation);
|
|
if (req2.smooth.directions.contains(Direction.left) &&
|
|
req2.smooth.directions.contains(Direction.right)) {
|
|
orientation = (orientation + 4) % 8;
|
|
}
|
|
|
|
req2 = unReorientRequirement(requirement, orientation);
|
|
var out2 = _generateBsp(req2);
|
|
var out1 = reorientVault(out2, orientation);
|
|
|
|
// log("$orientation ${requirement.vx} ${requirement.vy} ${req2.vx} ${req2.vy} ${out2.vx} ${out2.vy} ${out1.vx} ${out1.vy}");
|
|
var geo.Size(:dx, :dy) = out1.size;
|
|
assert(dx >= requirement.vxMin && dx <= requirement.vxMax);
|
|
assert(dy >= requirement.vyMin && dy <= requirement.vyMax);
|
|
assert(out1.smooth.directions.containsAll(requirement.smooth.directions));
|
|
return out1;
|
|
}
|
|
|
|
Vault _generateBsp(Requirement req) {
|
|
var vxMin = req.vxMin;
|
|
var vyMin = req.vyMin;
|
|
var vxMax = req.vxMax;
|
|
var vyMax = req.vyMax;
|
|
|
|
var smoothUp = req.smooth.directions.contains(Direction.up);
|
|
var smoothDown = req.smooth.directions.contains(Direction.down);
|
|
var smoothUpDown = smoothUp && smoothDown;
|
|
|
|
// var vxRand = _random.nextInt(vxMax - vxMin) + vxMin;
|
|
var vyRand = _random.nextInt(vyMax + 1 - vyMin) + vyMin;
|
|
|
|
if (vxMax < 2 || vyMax < 2) {
|
|
return Vault.blank(vxMax, vyRand, VaultTile.defaultwall, req.smooth);
|
|
} else if (vxMax < 9 || (vxMax - 2) * (vyMax - 2) < 12) {
|
|
var v2 = Vault.blank(
|
|
vxMax - 2, vyMax - 2, VaultTile.bspfloor, req.smooth.clone());
|
|
var v = Vault.blank(vxMax, vyMax, VaultTile.wall, req.smooth.clone());
|
|
v.blitFrom(v2, 1, 1);
|
|
return v;
|
|
} else {
|
|
var leftReq = Requirement(
|
|
math.max(vxMin - 4, 2), vxMax - 4, vyMin, vyMax, req.smooth.clone());
|
|
leftReq.smooth.directions.add(Direction.right);
|
|
var leftChild = _generateOriented(leftReq, true);
|
|
|
|
var vyMinRight = vyMin;
|
|
var vyMaxRight = vyMax;
|
|
|
|
if (smoothUpDown) {
|
|
vyMaxRight = vyMinRight = leftChild.size.dy;
|
|
}
|
|
|
|
var rightReq = Requirement(
|
|
vxMin - (leftChild.vx - 1),
|
|
vxMax - (leftChild.vx - 1),
|
|
vyMinRight,
|
|
vyMaxRight,
|
|
req.smooth.clone(),
|
|
);
|
|
rightReq.smooth.directions.add(Direction.left);
|
|
var rightChild = _generateOriented(rightReq, true);
|
|
|
|
var vxTotal = leftChild.vx + rightChild.vx - 1;
|
|
var vyTotal = math.max(leftChild.vy, rightChild.vy);
|
|
|
|
if (smoothUp) {
|
|
var v = Vault.blank(
|
|
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
|
|
v.blitFrom(leftChild, 0, 0);
|
|
v.blitFrom(rightChild, leftChild.vx - 1, 0);
|
|
return v;
|
|
}
|
|
if (smoothDown) {
|
|
var v = Vault.blank(
|
|
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
|
|
v.blitFrom(leftChild, 0, vyTotal - leftChild.vy);
|
|
v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy);
|
|
return v;
|
|
}
|
|
|
|
// no smoothing reqs
|
|
// min: ensure some overlap
|
|
var vyTMax = math.min(vyMax, leftChild.vy + rightChild.vy - 3);
|
|
if (vyTMax > vyTotal) {
|
|
vyTotal += _random.nextInt(vyTMax - vyTotal);
|
|
}
|
|
var v = Vault.blank(
|
|
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
|
|
if (_random.nextBool()) {
|
|
v.blitFrom(leftChild, 0, 0);
|
|
v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy);
|
|
} else {
|
|
v.blitFrom(leftChild, 0, vyTotal - leftChild.vy);
|
|
v.blitFrom(rightChild, leftChild.vx - 1, 0);
|
|
}
|
|
return v;
|
|
}
|
|
}
|
|
|
|
Vault? _suggest(int tries, Requirement req) {
|
|
for (var i = 0; i < tries; i++) {
|
|
var sugg = _popSuggestion();
|
|
if (sugg == null) {
|
|
return null;
|
|
}
|
|
|
|
sugg = reorientVault(sugg, randomOrientation(_random));
|
|
sugg = _tidy(sugg, req);
|
|
if (sugg != null) {
|
|
return sugg;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Vault? _popSuggestion() {
|
|
if (_queue.isEmpty) {
|
|
_queue = _vaults.randomFlight(_random);
|
|
}
|
|
if (_queue.isEmpty) {
|
|
return null;
|
|
}
|
|
return _queue.removeLast();
|
|
}
|
|
|
|
Vault? _tidy(Vault vault, Requirement req) {
|
|
if (vault.vx > req.vxMax || vault.vy > req.vyMax) {
|
|
return null;
|
|
}
|
|
if (vault.vx < req.vxMin || vault.vy < req.vyMin) {
|
|
return null;
|
|
}
|
|
if (!vault.smooth.directions.containsAll(req.smooth.directions)) {
|
|
return null;
|
|
}
|
|
// NOTE: If the vault has metaBSP regions, and they touch the outer edge, then it should be possible
|
|
// to extend those regions
|
|
// Extending a metaBSP region results in _two_ metaBSP regions:
|
|
// a big version of the original, and a second one covering all the space left over
|
|
// in the set of rows or columns the metabsp region did not touch
|
|
//
|
|
// Ex:
|
|
// XXXX##
|
|
// XXXX #
|
|
// XXXX #
|
|
// # #
|
|
// ######
|
|
//
|
|
// becomes
|
|
//
|
|
// XXXZYY
|
|
// XXXZYY
|
|
// XXXZYY
|
|
// XXXX##
|
|
// XXXX #
|
|
// XXXX #
|
|
// # #
|
|
// ######
|
|
//
|
|
// (where the Zs are spaces that Xs and Ys both touch)
|
|
//
|
|
// Extension can happen more than once, on each axis:
|
|
//
|
|
// XXXXXZYY
|
|
// XXXXXZYY
|
|
// XXXXXZYY
|
|
// XXXXXX##
|
|
// XXXXXX #
|
|
// BBXXXX #
|
|
// AA# #
|
|
// AA######
|
|
//
|
|
return vault;
|
|
}
|
|
|
|
Vault _fillMetaRegions(Requirement requirement, Vault vault) {
|
|
var geo.Size(:dx, :dy) = vault.size;
|
|
var (metaregions, _) = regionalize(geo.Rect(0, 0, dx, dy),
|
|
(x, y) => vault.tiles.get(x, y) == VaultTile.meta0);
|
|
|
|
for (var i in metaregions) {
|
|
assert(i.isRectangle);
|
|
var sz = i.rect.size;
|
|
// TODO: Relax these based on our environs -- for instance, if one of our sides doesn't need to be smooth, that metaregion doesn't either
|
|
var metaRequirement = Requirement(
|
|
sz.dx,
|
|
sz.dx,
|
|
sz.dy,
|
|
sz.dy,
|
|
DirectionSet(
|
|
{Direction.up, Direction.left, Direction.down, Direction.right}));
|
|
var inner = _generateOriented(metaRequirement, true);
|
|
var dest = Vault(Bitmap.blank(vault.vx, vault.vy, VaultTile.defaultwall),
|
|
vault.smooth.clone());
|
|
dest.blitFrom(vault, 0, 0);
|
|
dest.blitFrom(inner, i.rect.x0, i.rect.y0);
|
|
vault = dest;
|
|
}
|
|
|
|
return vault;
|
|
}
|
|
|
|
Level _finalize(Vault subj) {
|
|
var vx = subj.vx, vy = subj.vy;
|
|
|
|
var orthoOffsets = [(0, -1), (0, 1), (-1, 0), (1, 0)];
|
|
|
|
// == build arches ==
|
|
bool floorlike(VaultTile? tile) {
|
|
return tile == VaultTile.bspfloor ||
|
|
tile == VaultTile.floor ||
|
|
tile == VaultTile.doorpronefloor ||
|
|
tile == VaultTile.exit;
|
|
}
|
|
|
|
bool walkable(VaultTile? tile) {
|
|
return tile == VaultTile.bspfloor ||
|
|
tile == VaultTile.floor ||
|
|
tile == VaultTile.doorpronefloor ||
|
|
tile == VaultTile.exit ||
|
|
tile == VaultTile.door;
|
|
}
|
|
|
|
List<(int, int)> newArches = [];
|
|
for (int x = 0; x < vx; x++) {
|
|
for (int y = 0; y < vy; y++) {
|
|
var t = subj.tiles.get(x, y);
|
|
if (t == VaultTile.archwall) {
|
|
var supporters = 0;
|
|
for (var (dx, dy) in orthoOffsets) {
|
|
VaultTile? neighbor = subj.tiles.get(x + dx, y + dy);
|
|
if (floorlike(neighbor)) {
|
|
supporters++;
|
|
}
|
|
}
|
|
|
|
if (supporters == 2) {
|
|
newArches.add((x, y));
|
|
}
|
|
subj.tiles.set(x, y, VaultTile.wall);
|
|
}
|
|
if (t == VaultTile.archpronewall || t == VaultTile.defaultwall) {
|
|
subj.tiles.set(x, y, VaultTile.wall);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var (ax, ay) in newArches) {
|
|
subj.tiles.set(ax, ay, VaultTile.floor);
|
|
}
|
|
|
|
// == build doors ==
|
|
var (regions, toRegion) =
|
|
regionalize(geo.Rect(0, 0, subj.vx, subj.vy), (x, y) {
|
|
return walkable(subj.tiles.get(x, y));
|
|
});
|
|
|
|
// generate one fake region for the exit doors to be in
|
|
Set<(int, int)> exitRegion = {};
|
|
for (var x = -2; x < subj.vx + 2; x++) {
|
|
exitRegion.add((x, -1));
|
|
exitRegion.add((x, subj.vy));
|
|
}
|
|
for (var y = -2; y < subj.vy + 2; y++) {
|
|
exitRegion.add((-1, y));
|
|
exitRegion.add((subj.vx, y));
|
|
}
|
|
|
|
int exitRegionId = regions.length;
|
|
for (var (x, y) in exitRegion) {
|
|
toRegion[(x, y)] = exitRegionId;
|
|
}
|
|
regions.add(Region.fromNonEmptySet(exitRegion));
|
|
|
|
// OK: now build the doors
|
|
double doorPoints(int x, int y) {
|
|
return subj.tiles.get(x, y) == VaultTile.doorpronefloor ? 0.5 : 0.0;
|
|
}
|
|
|
|
List<Edge<(int, int)>> possibleDoors = [];
|
|
for (var x = 0; x < subj.vx; x++) {
|
|
for (var y = 0; y < subj.vy; y++) {
|
|
double points;
|
|
int region0, region1;
|
|
|
|
if (subj.tiles.get(x, y) != VaultTile.wall) {
|
|
continue;
|
|
}
|
|
|
|
var regionL = toRegion[(x - 1, y)];
|
|
var regionR = toRegion[(x + 1, y)];
|
|
var regionU = toRegion[(x, y - 1)];
|
|
var regionD = toRegion[(x, y + 1)];
|
|
|
|
if (regionL != null &&
|
|
regionR != null &&
|
|
regionU == null &&
|
|
regionD == null) {
|
|
(region0, region1) = (regionL, regionR);
|
|
points = doorPoints(x - 1, y) + doorPoints(x + 1, y);
|
|
} else if (regionL == null &&
|
|
regionR == null &&
|
|
regionU != null &&
|
|
regionD != null) {
|
|
(region0, region1) = (regionU, regionD);
|
|
points = doorPoints(x, y - 1) + doorPoints(x, y + 1);
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
if (region0 == region1) {
|
|
continue;
|
|
}
|
|
|
|
int roomSize = math.min(
|
|
regions[region0].points.length,
|
|
regions[region1].points.length,
|
|
);
|
|
|
|
possibleDoors.add(Edge(
|
|
region0,
|
|
region1,
|
|
(x, y),
|
|
doorScore(region0 != exitRegionId && region1 != exitRegionId,
|
|
points, roomSize, _random.nextDouble())));
|
|
}
|
|
}
|
|
List<Edge<(int, int)>> exitDoors = [];
|
|
var minimalDoors = kruskal(regions.length, possibleDoors);
|
|
for (var d in minimalDoors) {
|
|
var (x, y) = d.value;
|
|
subj.tiles.set(x, y, VaultTile.door);
|
|
if (d.dst == exitRegionId || d.src == exitRegionId) {
|
|
exitDoors.add(d);
|
|
}
|
|
}
|
|
|
|
for (var x = 0; x < subj.vx; x++) {
|
|
for (var y = 0; y < subj.vy; y++) {
|
|
if (subj.tiles.get(x, y) == VaultTile.doorpronefloor) {
|
|
subj.tiles.set(x, y, VaultTile.floor);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (exitDoors.length != 1) {
|
|
throw Exception("should be exactly one exit door");
|
|
}
|
|
|
|
// == Build the exit area ==
|
|
var (exitX, exitY) = exitDoors[0].value;
|
|
int exitVaultX, exitVaultY;
|
|
Vault finalVault;
|
|
int vaultBlitX, vaultBlitY;
|
|
if (exitX == 0 || exitX == vx - 1) {
|
|
finalVault =
|
|
Vault.blank(vx + 3, vy, VaultTile.defaultwall, DirectionSet({}));
|
|
vaultBlitX = exitX == 0 ? 3 : 0;
|
|
vaultBlitY = 0;
|
|
exitVaultX = exitX == 0 ? 1 : vx + 1;
|
|
exitVaultY = exitY;
|
|
} else if (exitY == 0 || exitY == vy - 1) {
|
|
finalVault =
|
|
Vault.blank(vx, vy + 3, VaultTile.defaultwall, DirectionSet({}));
|
|
vaultBlitX = 0;
|
|
vaultBlitY = exitY == 0 ? 3 : 0;
|
|
exitVaultX = exitX;
|
|
exitVaultY = exitY == 0 ? 1 : vy + 1;
|
|
} else {
|
|
throw Exception("exit door in invalid position $exitX $exitY $vx $vy");
|
|
}
|
|
|
|
for (var x = exitVaultX - 1; x <= exitVaultX + 1; x++) {
|
|
for (var y = exitVaultY - 1; y <= exitVaultY + 1; y++) {
|
|
finalVault.tiles.set(x, y, VaultTile.exit);
|
|
if (x == exitVaultX && y == exitVaultY ||
|
|
_manhattan(x, y, vaultBlitX + exitX, vaultBlitY + exitY) == 1) {
|
|
finalVault.tiles.set(x, y, VaultTile.floor);
|
|
}
|
|
}
|
|
}
|
|
finalVault.blitFrom(subj, vaultBlitX, vaultBlitY);
|
|
|
|
return Level(
|
|
Bitmap.blankWith(finalVault.vx, finalVault.vy,
|
|
(x, y) => flattenVaultTile(finalVault.tiles.get(x, y)!)),
|
|
geo.Offset(exitVaultX, exitVaultY));
|
|
}
|
|
}
|
|
|
|
// components:
|
|
// - is not exit (exit should be placed last so it doesn't get more than one door)
|
|
// - points for placement
|
|
// - size of the underlying room
|
|
// - random factor
|
|
double doorScore(bool isNotExit, double pointsForPlacement, int roomSize,
|
|
double randomFactor) {
|
|
assert(pointsForPlacement >= 0.0 && pointsForPlacement <= 1.0);
|
|
assert(roomSize >= 0 && roomSize < 100000);
|
|
assert(randomFactor >= 0.0 && randomFactor < 1.0);
|
|
return (isNotExit ? 1.0 : 0.0) * 1000000 +
|
|
pointsForPlacement * 100000 +
|
|
(100000 - roomSize).toDouble() +
|
|
randomFactor;
|
|
}
|
|
|
|
int _manhattan(int x0, int y0, int x1, int y1) {
|
|
return (x1 - x0).abs() + (y1 - y0).abs();
|
|
}
|