v0.7.5: overhauled Goo's level layout, including new level builder

This commit is contained in:
Evan Debenham 2019-09-02 13:06:56 -04:00
parent 52ed3dda2c
commit c741b82f10
6 changed files with 376 additions and 79 deletions

View File

@ -139,22 +139,6 @@ public abstract class RegularLevel extends Level {
protected abstract Painter painter(); protected abstract Painter painter();
protected float waterFill(){
return 0;
}
protected int waterSmoothing(){
return 0;
}
protected float grassFill(){
return 0;
}
protected int grassSmoothing(){
return 0;
}
protected int nTraps() { protected int nTraps() {
return Random.NormalIntRange( 1, 3+(Dungeon.depth/3) ); return Random.NormalIntRange( 1, 3+(Dungeon.depth/3) );
} }

View File

@ -23,15 +23,17 @@ package com.shatteredpixel.shatteredpixeldungeon.levels;
import com.shatteredpixel.shatteredpixeldungeon.Bones; import com.shatteredpixel.shatteredpixeldungeon.Bones;
import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Goo;
import com.shatteredpixel.shatteredpixeldungeon.items.Heap; import com.shatteredpixel.shatteredpixeldungeon.items.Heap;
import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.levels.builders.Builder; import com.shatteredpixel.shatteredpixeldungeon.levels.builders.Builder;
import com.shatteredpixel.shatteredpixeldungeon.levels.builders.LoopBuilder; import com.shatteredpixel.shatteredpixeldungeon.levels.builders.FigureEightBuilder;
import com.shatteredpixel.shatteredpixeldungeon.levels.painters.Painter;
import com.shatteredpixel.shatteredpixeldungeon.levels.painters.SewerPainter;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.Room; import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.Room;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.secret.RatKingRoom; import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.secret.RatKingRoom;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.sewerboss.GooBossRoom; import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.sewerboss.GooBossRoom;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.sewerboss.SewerBossEntranceRoom; import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.sewerboss.SewerBossEntranceRoom;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.sewerboss.SewerBossExitRoom;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.standard.StandardRoom; import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.standard.StandardRoom;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.watabou.utils.Bundle; import com.watabou.utils.Bundle;
@ -51,14 +53,23 @@ public class SewerBossLevel extends SewerLevel {
@Override @Override
protected ArrayList<Room> initRooms() { protected ArrayList<Room> initRooms() {
ArrayList<Room> initRooms = new ArrayList<>(); ArrayList<Room> initRooms = new ArrayList<>();
initRooms.add ( roomEntrance = roomExit = new SewerBossEntranceRoom());
//TODO it would be nice for these rooms to have some custom tile visuals
initRooms.add( roomEntrance = new SewerBossEntranceRoom() );
initRooms.add( roomExit = new SewerBossExitRoom() );
int standards = standardRooms(); int standards = standardRooms();
for (int i = 0; i < standards; i++) { for (int i = 0; i < standards; i++) {
initRooms.add(StandardRoom.createRoom()); StandardRoom s = StandardRoom.createRoom();
//force to normal size
s.setSizeCat(0, 0);
initRooms.add(s);
} }
initRooms.add(GooBossRoom.randomGooRoom()); //TODO need to improve the visual appearance of goo's nest
GooBossRoom gooRoom = GooBossRoom.randomGooRoom();
initRooms.add(gooRoom);
((FigureEightBuilder)builder).setLandmarkRoom(gooRoom);
initRooms.add(new RatKingRoom()); initRooms.add(new RatKingRoom());
return initRooms; return initRooms;
} }
@ -70,29 +81,18 @@ public class SewerBossLevel extends SewerLevel {
} }
protected Builder builder(){ protected Builder builder(){
return new LoopBuilder() return new FigureEightBuilder()
.setLoopShape( 2 , Random.Float(0.4f, 0.7f), Random.Float(0f, 0.5f))
.setPathLength(1f, new float[]{1}) .setPathLength(1f, new float[]{1})
.setTunnelLength(new float[]{0, 3, 1}, new float[]{1}); .setTunnelLength(new float[]{1, 2}, new float[]{1});
} }
@Override @Override
protected float waterFill(){ protected Painter painter() {
return 0.50f; return new SewerPainter()
} .setWater(0.50f, 5)
.setGrass(0.20f, 4)
@Override .setTraps(nTraps(), trapClasses(), trapChances());
protected int waterSmoothing(){
return 5;
}
@Override
protected float grassFill() {
return 0.20f;
}
@Override
protected int grassSmoothing() {
return 4;
} }
protected int nTraps() { protected int nTraps() {

View File

@ -0,0 +1,278 @@
/*
* Pixel Dungeon
* Copyright (C) 2012-2015 Oleg Dolya
*
* Shattered Pixel Dungeon
* Copyright (C) 2014-2019 Evan Debenham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package com.shatteredpixel.shatteredpixeldungeon.levels.builders;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.Room;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.connection.ConnectionRoom;
import com.watabou.utils.PointF;
import com.watabou.utils.Random;
import java.util.ArrayList;
public class FigureEightBuilder extends RegularBuilder {
//These methods allow for the adjusting of the shape of the loop
//by default the loop is a perfect circle, but it can be adjusted
//increasing the exponent will increase the the curvature, making the loop more oval shaped.
private int curveExponent = 0;
//This is a percentage (range 0-1) of the intensity of the curve function
// 0 makes for a perfect linear curve (circle)
// 1 means the curve is completely determined by the curve exponent
private float curveIntensity = 1;
//Adjusts the starting point along the loop.
// a common example, setting to 0.25 will make for a short fat oval instead of a long one.
private float curveOffset = 0;
public FigureEightBuilder setLoopShape(int exponent, float intensity, float offset){
this.curveExponent = Math.abs(exponent);
curveIntensity = intensity % 1f;
curveOffset = offset % 0.5f;
return this;
}
private float targetAngle( float percentAlong ){
percentAlong += curveOffset;
return 360f * (float)(
curveIntensity * curveEquation(percentAlong)
+ (1-curveIntensity)*(percentAlong)
- curveOffset);
}
private double curveEquation( double x ){
return Math.pow(4, 2*curveExponent)
*(Math.pow((x % 0.5f )-0.25, 2*curveExponent + 1))
+ 0.25 + 0.5*Math.floor(2*x);
}
private Room landmarkRoom;
public FigureEightBuilder setLandmarkRoom(Room room){
landmarkRoom = room;
return this;
}
ArrayList<Room> firstLoop, secondLoop;
PointF firstLoopCenter, secondLoopCenter;
@Override
public ArrayList<Room> build(ArrayList<Room> rooms) {
setupRooms(rooms);
//TODO might want to make this able to work without an exit. Probably a random room would be landmark and the landmark room would become exit
if (landmarkRoom == null){
landmarkRoom = Random.element(multiConnections);
}
if (multiConnections.contains(landmarkRoom)){
multiConnections.remove(landmarkRoom);
}
float startAngle = Random.Float(0, 180);
int roomsOnLoop = (int)(multiConnections.size()*pathLength) + Random.chances(pathLenJitterChances);
roomsOnLoop = Math.min(roomsOnLoop, multiConnections.size());
int roomsOnFirstLoop = roomsOnLoop/2;
if (roomsOnLoop % 2 == 1) roomsOnFirstLoop += Random.Int(2);
firstLoop = new ArrayList<>();
float[] pathTunnels = pathTunnelChances.clone();
for (int i = 0; i <= roomsOnFirstLoop; i++){
if (i == 0)
firstLoop.add(landmarkRoom);
else
firstLoop.add(multiConnections.remove(0));
int tunnels = Random.chances(pathTunnels);
if (tunnels == -1){
pathTunnels = pathTunnelChances.clone();
tunnels = Random.chances(pathTunnels);
}
pathTunnels[tunnels]--;
for (int j = 0; j < tunnels; j++){
firstLoop.add(ConnectionRoom.createRoom());
}
}
if (entrance != null) firstLoop.add((firstLoop.size()+1)/2, entrance);
int roomsOnSecondLoop = roomsOnLoop - roomsOnFirstLoop;
secondLoop = new ArrayList<>();
for (int i = 0; i <= roomsOnSecondLoop; i++){
if (i == 0)
secondLoop.add(landmarkRoom);
else
secondLoop.add(multiConnections.remove(0));
int tunnels = Random.chances(pathTunnels);
if (tunnels == -1){
pathTunnels = pathTunnelChances.clone();
tunnels = Random.chances(pathTunnels);
}
pathTunnels[tunnels]--;
for (int j = 0; j < tunnels; j++){
secondLoop.add(ConnectionRoom.createRoom());
}
}
if (exit != null) secondLoop.add((secondLoop.size()+1)/2, exit);
landmarkRoom.setSize();
landmarkRoom.setPos(0, 0);
Room prev = landmarkRoom;
float targetAngle;
for (int i = 1; i < firstLoop.size(); i++){
Room r = firstLoop.get(i);
targetAngle = startAngle + targetAngle( i / (float)firstLoop.size());
if (placeRoom(rooms, prev, r, targetAngle) != -1) {
prev = r;
if (!rooms.contains(prev))
rooms.add(prev);
} else {
//FIXME this is lazy, there are ways to do this without relying on chance
return null;
}
}
//FIXME this is still fairly chance reliant
// should just write a general function for stitching two rooms together in builder
while (!prev.connect(landmarkRoom)){
ConnectionRoom c = ConnectionRoom.createRoom();
if (placeRoom(firstLoop, prev, c, angleBetweenRooms(prev, entrance)) == -1){
return null;
}
firstLoop.add(c);
rooms.add(c);
prev = c;
}
prev = landmarkRoom;
startAngle += 180f;
for (int i = 1; i < secondLoop.size(); i++){
Room r = secondLoop.get(i);
targetAngle = startAngle + targetAngle( i / (float)secondLoop.size());
if (placeRoom(rooms, prev, r, targetAngle) != -1) {
prev = r;
if (!rooms.contains(prev))
rooms.add(prev);
} else {
//FIXME this is lazy, there are ways to do this without relying on chance
return null;
}
}
//FIXME this is still fairly chance reliant
// should just write a general function for stitching two rooms together in builder
while (!prev.connect(landmarkRoom)){
ConnectionRoom c = ConnectionRoom.createRoom();
if (placeRoom(secondLoop, prev, c, angleBetweenRooms(prev, entrance)) == -1){
return null;
}
secondLoop.add(c);
rooms.add(c);
prev = c;
}
if (shop != null) {
float angle;
int tries = 10;
do {
angle = placeRoom(firstLoop, entrance, shop, Random.Float(360f));
tries--;
} while (angle == -1 && tries >= 0);
if (angle == -1) return null;
}
firstLoopCenter = new PointF();
for (Room r : firstLoop){
firstLoopCenter.x += (r.left + r.right)/2f;
firstLoopCenter.y += (r.top + r.bottom)/2f;
}
firstLoopCenter.x /= firstLoop.size();
firstLoopCenter.y /= firstLoop.size();
secondLoopCenter = new PointF();
for (Room r : secondLoop){
secondLoopCenter.x += (r.left + r.right)/2f;
secondLoopCenter.y += (r.top + r.bottom)/2f;
}
secondLoopCenter.x /= secondLoop.size();
secondLoopCenter.y /= secondLoop.size();
ArrayList<Room> branchable = new ArrayList<>(firstLoop);
branchable.addAll(secondLoop);
branchable.remove(landmarkRoom); //remove once so it isn't present twice
ArrayList<Room> roomsToBranch = new ArrayList<>();
roomsToBranch.addAll(multiConnections);
roomsToBranch.addAll(singleConnections);
weightRooms(branchable);
createBranches(rooms, branchable, roomsToBranch, branchTunnelChances);
findNeighbours(rooms);
for (Room r : rooms){
for (Room n : r.neigbours){
if (!n.connected.containsKey(r)
&& Random.Float() < extraConnectionChance){
r.connect(n);
}
}
}
return rooms;
}
@Override
protected float randomBranchAngle( Room r ) {
PointF center;
if (firstLoop.contains(r)){
center = firstLoopCenter;
} else {
center = secondLoopCenter;
}
if (center == null)
return super.randomBranchAngle( r );
else {
//generate four angles randomly and return the one which points closer to the center
float toCenter = angleBetweenPoints( new PointF((r.left + r.right)/2f, (r.top + r.bottom)/2f), center);
if (toCenter < 0) toCenter += 360f;
float currAngle = Random.Float(360f);
for( int i = 0; i < 4; i ++){
float newAngle = Random.Float(360f);
if (Math.abs(toCenter - newAngle) < Math.abs(toCenter - currAngle)){
currAngle = newAngle;
}
}
return currAngle;
}
}
}

View File

@ -86,10 +86,9 @@ public class LoopBuilder extends RegularBuilder {
ArrayList<Room> loop = new ArrayList<>(); ArrayList<Room> loop = new ArrayList<>();
int roomsOnLoop = (int)(multiConnections.size()*pathLength) + Random.chances(pathLenJitterChances); int roomsOnLoop = (int)(multiConnections.size()*pathLength) + Random.chances(pathLenJitterChances);
roomsOnLoop = Math.min(roomsOnLoop, multiConnections.size()); roomsOnLoop = Math.min(roomsOnLoop, multiConnections.size());
roomsOnLoop++;
float[] pathTunnels = pathTunnelChances.clone(); float[] pathTunnels = pathTunnelChances.clone();
for (int i = 0; i < roomsOnLoop; i++){ for (int i = 0; i <= roomsOnLoop; i++){
if (i == 0) if (i == 0)
loop.add(entrance); loop.add(entrance);
else else
@ -137,14 +136,6 @@ public class LoopBuilder extends RegularBuilder {
prev = c; prev = c;
} }
loopCenter = new PointF();
for (Room r : loop){
loopCenter.x += (r.left + r.right)/2f;
loopCenter.y += (r.top + r.bottom)/2f;
}
loopCenter.x /= loop.size();
loopCenter.y /= loop.size();
if (shop != null) { if (shop != null) {
float angle; float angle;
int tries = 10; int tries = 10;
@ -155,6 +146,14 @@ public class LoopBuilder extends RegularBuilder {
if (angle == -1) return null; if (angle == -1) return null;
} }
loopCenter = new PointF();
for (Room r : loop){
loopCenter.x += (r.left + r.right)/2f;
loopCenter.y += (r.top + r.bottom)/2f;
}
loopCenter.x /= loop.size();
loopCenter.y /= loop.size();
ArrayList<Room> branchable = new ArrayList<>(loop); ArrayList<Room> branchable = new ArrayList<>(loop);
ArrayList<Room> roomsToBranch = new ArrayList<>(); ArrayList<Room> roomsToBranch = new ArrayList<>();

View File

@ -30,35 +30,11 @@ import com.watabou.utils.Point;
public class SewerBossEntranceRoom extends EntranceRoom { public class SewerBossEntranceRoom extends EntranceRoom {
@Override
public int minWidth() {
return 9;
}
@Override
public int maxWidth() {
return 9;
}
@Override @Override
public int minHeight() { public int minHeight() {
return 6; return 6;
} }
@Override
public int maxHeight() {
return 10;
}
//TODO perhaps I just want to deny all top-side connections
@Override
public boolean canConnect(Point p) {
//refuses connections on the center 3 tiles on the top side, and the top tile along left/right
return super.canConnect(p)
&& !(p.y == top && p.x >= (left + (width()/2 - 1)) && p.x <= (left + (width()/2 + 1)))
&& p.y != top+1;
}
public void paint(Level level ) { public void paint(Level level ) {
Painter.fill( level, this, Terrain.WALL ); Painter.fill( level, this, Terrain.WALL );
@ -67,9 +43,6 @@ public class SewerBossEntranceRoom extends EntranceRoom {
Painter.fill( level, left+1, top+1, width()-2, 1, Terrain.WALL_DECO); Painter.fill( level, left+1, top+1, width()-2, 1, Terrain.WALL_DECO);
Painter.fill( level, left+1, top+2, width()-2, 1, Terrain.WATER); Painter.fill( level, left+1, top+2, width()-2, 1, Terrain.WATER);
Painter.set( level, left+width()/2, top+1, Terrain.LOCKED_EXIT);
level.exit = level.pointToCell(new Point(left+width()/2, top+1));
do { do {
level.entrance = level.pointToCell(random(3)); level.entrance = level.pointToCell(random(3));
} while (level.findMob(level.entrance) != null); } while (level.findMob(level.entrance) != null);
@ -78,8 +51,8 @@ public class SewerBossEntranceRoom extends EntranceRoom {
for (Room.Door door : connected.values()) { for (Room.Door door : connected.values()) {
door.set( Room.Door.Type.REGULAR ); door.set( Room.Door.Type.REGULAR );
if (door.y == top){ if (door.y == top || door.y == top+1){
Painter.set( level, door.x, door.y+1, Terrain.WATER); Painter.drawInside( level, this, door, 1, Terrain.WATER);
} }
} }

View File

@ -0,0 +1,63 @@
/*
* Pixel Dungeon
* Copyright (C) 2012-2015 Oleg Dolya
*
* Shattered Pixel Dungeon
* Copyright (C) 2014-2019 Evan Debenham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package com.shatteredpixel.shatteredpixeldungeon.levels.rooms.sewerboss;
import com.shatteredpixel.shatteredpixeldungeon.levels.Level;
import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain;
import com.shatteredpixel.shatteredpixeldungeon.levels.painters.Painter;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.Room;
import com.shatteredpixel.shatteredpixeldungeon.levels.rooms.standard.ExitRoom;
import com.watabou.utils.Point;
public class SewerBossExitRoom extends ExitRoom {
@Override
public int minWidth() {
return Math.max(super.minWidth(), 7);
}
@Override
public int minHeight() {
return Math.max(super.minHeight(), 7);
}
public void paint(Level level) {
Painter.fill( level, this, Terrain.WALL );
Painter.fill( level, this, 1, Terrain.EMPTY );
for (Room.Door door : connected.values()) {
door.set( Room.Door.Type.REGULAR );
}
Point c = center();
Painter.fill( level, c.x-1, c.y-1, 3, 1, Terrain.WALL );
Painter.fill( level, c.x-1, c.y , 3, 1, Terrain.WALL_DECO );
Painter.fill( level, c.x-1, c.y+1, 3, 1, Terrain.WATER );
level.exit = level.pointToCell(c);
Painter.set( level, level.exit, Terrain.LOCKED_EXIT );
}
}