/* * Pixel Dungeon * Copyright (C) 2012-2015 Oleg Dolya * * Shattered Pixel Dungeon * Copyright (C) 2014-2015 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.pixeldungeonunleashed.levels; import com.shatteredpixel.pixeldungeonunleashed.Bones; import com.shatteredpixel.pixeldungeonunleashed.Challenges; import com.shatteredpixel.pixeldungeonunleashed.Dungeon; import com.shatteredpixel.pixeldungeonunleashed.actors.Actor; import com.shatteredpixel.pixeldungeonunleashed.actors.buffs.Buff; import com.shatteredpixel.pixeldungeonunleashed.actors.mobs.Bestiary; import com.shatteredpixel.pixeldungeonunleashed.actors.mobs.ChaosMage; import com.shatteredpixel.pixeldungeonunleashed.actors.mobs.Minotaur; import com.shatteredpixel.pixeldungeonunleashed.actors.mobs.Mob; import com.shatteredpixel.pixeldungeonunleashed.actors.mobs.Tinkerer; import com.shatteredpixel.pixeldungeonunleashed.items.Generator; import com.shatteredpixel.pixeldungeonunleashed.items.Heap; import com.shatteredpixel.pixeldungeonunleashed.items.Item; import com.shatteredpixel.pixeldungeonunleashed.items.rings.RingOfWealth; import com.shatteredpixel.pixeldungeonunleashed.items.scrolls.Scroll; import com.shatteredpixel.pixeldungeonunleashed.levels.Room.Type; import com.shatteredpixel.pixeldungeonunleashed.levels.painters.Painter; import com.shatteredpixel.pixeldungeonunleashed.levels.painters.ShopPainter; import com.shatteredpixel.pixeldungeonunleashed.levels.traps.AlarmTrap; import com.shatteredpixel.pixeldungeonunleashed.levels.traps.FireTrap; import com.shatteredpixel.pixeldungeonunleashed.levels.traps.GrippingTrap; import com.shatteredpixel.pixeldungeonunleashed.levels.traps.LightningTrap; import com.shatteredpixel.pixeldungeonunleashed.levels.traps.ParalyticTrap; import com.shatteredpixel.pixeldungeonunleashed.levels.traps.PoisonTrap; import com.shatteredpixel.pixeldungeonunleashed.levels.traps.ToxicTrap; import com.watabou.utils.Bundle; import com.watabou.utils.Graph; import com.watabou.utils.Random; import com.watabou.utils.Rect; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; public abstract class RegularLevel extends Level { protected HashSet<Room> rooms; protected Room roomEntrance; protected Room roomExit; protected ArrayList<Room.Type> specials; public int secretDoors; @Override protected boolean build() { if (!initRooms()) { return false; } int distance; int retry = 0; int maxRoomRetry = 16; int minDistance = (int)Math.sqrt( rooms.size() ); do { do { roomEntrance = Random.element( rooms ); } while (roomEntrance.width() < 4 || roomEntrance.height() < 4); do { roomExit = Random.element( rooms ); } while (roomExit == roomEntrance || roomExit.width() < 4 || roomExit.height() < 4); Graph.buildDistanceMap( rooms, roomExit ); distance = roomEntrance.distance(); if (retry++ > maxRoomRetry) { return false; } } while (distance < minDistance); roomEntrance.type = Type.ENTRANCE; roomExit.type = Type.EXIT; HashSet<Room> connected = new HashSet<>(); connected.add( roomEntrance ); Graph.buildDistanceMap( rooms, roomExit ); List<Room> path = Graph.buildPath( rooms, roomEntrance, roomExit ); Room room = roomEntrance; for (Room next : path) { room.connect( next ); room = next; connected.add( room ); } Graph.setPrice( path, roomEntrance.distance ); Graph.buildDistanceMap( rooms, roomExit ); path = Graph.buildPath( rooms, roomEntrance, roomExit ); room = roomEntrance; for (Room next : path) { room.connect( next ); room = next; connected.add( room ); } int nConnected = (int)(rooms.size() * Random.Float( 0.5f, 0.7f )); // the higher this number is the more packed our map becomes while (connected.size() < nConnected) { Room cr = Random.element( connected ); Room or = Random.element( cr.neigbours ); if (!connected.contains( or )) { cr.connect( or ); connected.add( or ); } } if (Dungeon.shopOnLevel()) { Room shop = null; for (Room r : roomEntrance.connected.keySet()) { if (r.connected.size() == 1 && ((r.width()-1)*(r.height()-1) >= ShopPainter.spaceNeeded())) { shop = r; break; } } if (shop == null) { return false; } else { shop.type = Room.Type.SHOP; } } specials = new ArrayList<Room.Type>( Room.SPECIALS ); if (Dungeon.bossLevel( Dungeon.depth + 1 ) || Dungeon.specialLevel( Dungeon.depth + 1 )) { specials.remove( Room.Type.WEAK_FLOOR ); } if (Dungeon.isChallenged( Challenges.NO_ARMOR )){ //no sense in giving an armor reward room on a run with no armor. specials.remove( Room.Type.CRYPT ); } if (Dungeon.isChallenged( Challenges.NO_HERBALISM )){ //sorry warden, no lucky sungrass or blandfruit seeds for you! specials.remove( Room.Type.GARDEN ); } if (!assignRoomType()) return false; paint(); paintWater(); paintGrass(); placeTraps(); if (feeling == Feeling.BURNT) { // go through and burn all the grass and unlocked doors for (int i=WIDTH+1; i < LENGTH-WIDTH-1; i++) { if (map[i] == Terrain.GRASS || map[i] == Terrain.HIGH_GRASS || map[i] == Terrain.DOOR) { map[i] = Terrain.EMBERS; } } } return true; } protected void placeSign(){ while (true) { int pos = roomEntrance.random(); if (pos != entrance && traps.get(pos) == null) { map[pos] = Terrain.SIGN; break; } } } protected boolean initRooms() { rooms = new HashSet<>(); split( new Rect( 0, 0, WIDTH - 1, HEIGHT - 1 ) ); if (rooms.size() < 8) { return false; } Room[] ra = rooms.toArray( new Room[0] ); for (int i=0; i < ra.length-1; i++) { for (int j=i+1; j < ra.length; j++) { ra[i].addNeigbour( ra[j] ); } } return true; } protected boolean assignRoomType() { int specialRooms = 0; if (Dungeon.depth < Dungeon.altarLevel) { // make sure that this room doesn't appear too early specials.remove( Type.ALTAR ); } if (feeling == Feeling.BURNT) { specials.remove( Type.GARDEN ); } for (Room r : rooms) { if (r.type == Type.NULL && r.connected.size() == 1) { if (specials.size() > 0 && r.width() > 3 && r.height() > 3 && Random.Int( specialRooms * specialRooms + 2 ) < 3) { if (pitRoomNeeded) { r.type = Type.PIT; pitRoomNeeded = false; specials.remove( Type.ARMORY ); specials.remove( Type.CRYPT ); specials.remove( Type.LABORATORY ); specials.remove( Type.LIBRARY ); specials.remove( Type.STATUE ); specials.remove( Type.TREASURY ); specials.remove( Type.VAULT ); specials.remove( Type.WEAK_FLOOR ); specials.remove( Type.PRISON ); } else if (Dungeon.depth % 6 == 2 && specials.contains( Type.LABORATORY )) { r.type = Type.LABORATORY; } else if (Dungeon.depth >= Dungeon.transmutation && specials.contains( Type.MAGIC_WELL )) { r.type = Type.MAGIC_WELL; } else if (Dungeon.depth >= Dungeon.altarLevel && specials.contains( Type.ALTAR )) { r.type = Type.ALTAR; Dungeon.altarLevel = Dungeon.depth + Random.IntRange(3, 5); // no more altars for at least a few levels if (Dungeon.difficultyLevel > Dungeon.DIFF_NORM) { Dungeon.altarLevel += 1; } } else { int n = specials.size(); r.type = specials.get( Math.min( Random.Int( n ), Random.Int( n ) ) ); if (r.type == Type.WEAK_FLOOR) { weakFloorCreated = true; } } Room.useType( r.type ); specials.remove( r.type ); specialRooms++; } else if (Random.Int( 2 ) == 0){ HashSet<Room> neigbours = new HashSet<>(); for (Room n : r.neigbours) { if (!r.connected.containsKey( n ) && !Room.SPECIALS.contains( n.type ) && n.type != Type.PIT) { neigbours.add( n ); } } if (neigbours.size() > 1) { r.connect( Random.element( neigbours ) ); } } } } int count = 0; for (Room r : rooms) { if (r.type == Type.NULL) { int connections = r.connected.size(); if (connections > 0) { if (Random.Int(connections * connections) == 0) { r.type = Type.STANDARD; count++; } else { r.type = Type.TUNNEL; } } } } while (count < 6) { // convert 6 random tunnels into standard rooms Room r = randomRoom( Type.TUNNEL, 1 ); if (r != null) { r.type = Type.STANDARD; count++; } } /* GLog.i("Feeling: " + feeling.toString()); count = 0; for (Room r : rooms) { switch (r.type) { case NULL: case TUNNEL: case STANDARD: case PASSAGE: break; default: GLog.i("Room " + count + " : " + r.type.toString() + " (" + r.left + ", " + r.top + ", " + r.right + ", " + r.bottom + ")"); break; } count++; } GLog.i("AltarLevel = " + Dungeon.altarLevel); */ return true; } protected void paintWater() { boolean[] lake = water(); for (int i=0; i < LENGTH; i++) { if (map[i] == Terrain.EMPTY && lake[i]) { map[i] = Terrain.WATER; } } } protected void paintGrass() { boolean[] grass = grass(); if (feeling == Feeling.GRASS || feeling == Feeling.BURNT) { for (Room room : rooms) { if (room.type != Type.NULL && room.type != Type.PASSAGE && room.type != Type.TUNNEL) { grass[(room.left + 1) + (room.top + 1) * WIDTH] = true; grass[(room.right - 1) + (room.top + 1) * WIDTH] = true; grass[(room.left + 1) + (room.bottom - 1) * WIDTH] = true; grass[(room.right - 1) + (room.bottom - 1) * WIDTH] = true; } } } for (int i=WIDTH+1; i < LENGTH-WIDTH-1; i++) { if (map[i] == Terrain.EMPTY && grass[i]) { int count = 1; for (int n : NEIGHBOURS8) { if (grass[i + n]) { count++; } } map[i] = (Random.Float() < count / 12f) ? Terrain.HIGH_GRASS : Terrain.GRASS; } } } protected abstract boolean[] water(); protected abstract boolean[] grass(); protected void placeTraps() { int nTraps = nTraps(); float[] trapChances = trapChances(); for (int i=0; i < nTraps; i++) { int trapPos = Random.Int( LENGTH ); if (map[trapPos] == Terrain.EMPTY) { map[trapPos] = Terrain.SECRET_TRAP; switch (Random.chances( trapChances )) { case 0: setTrap( new ToxicTrap().hide(), trapPos); break; case 1: setTrap( new FireTrap().hide(), trapPos); break; case 2: setTrap( new ParalyticTrap().hide(), trapPos); break; case 3: setTrap( new PoisonTrap().hide(), trapPos); break; case 4: setTrap( new AlarmTrap().hide(), trapPos); break; case 5: setTrap( new LightningTrap().hide(), trapPos); break; case 6: setTrap( new GrippingTrap().hide(), trapPos); break; case 7: setTrap( new LightningTrap().hide(), trapPos); break; } } } } protected int nTraps() { return Dungeon.depth <= 1 ? 0 : Random.Int( 1, rooms.size() + Dungeon.depth ); } protected float[] trapChances() { float[] chances = { 1, 1, 1, 1, 1, 1, 1, 1 }; return chances; } protected int minRoomSize = 7; protected int maxRoomSize = 9; protected void split( Rect rect ) { int w = rect.width(); int h = rect.height(); if (w > maxRoomSize && h < minRoomSize) { int vw = Random.Int( rect.left + 3, rect.right - 3 ); split( new Rect( rect.left, rect.top, vw, rect.bottom ) ); split( new Rect( vw, rect.top, rect.right, rect.bottom ) ); } else if (h > maxRoomSize && w < minRoomSize) { int vh = Random.Int( rect.top + 3, rect.bottom - 3 ); split( new Rect( rect.left, rect.top, rect.right, vh ) ); split( new Rect( rect.left, vh, rect.right, rect.bottom ) ); } else if ((Math.random() <= (minRoomSize * minRoomSize / rect.square()) && w <= maxRoomSize && h <= maxRoomSize) || w < minRoomSize || h < minRoomSize) { rooms.add( (Room)new Room().set( rect ) ); } else { if (Random.Float() < (float)(w - 2) / (w + h - 4)) { int vw = Random.Int( rect.left + 3, rect.right - 3 ); split( new Rect( rect.left, rect.top, vw, rect.bottom ) ); split( new Rect( vw, rect.top, rect.right, rect.bottom ) ); } else { int vh = Random.Int( rect.top + 3, rect.bottom - 3 ); split( new Rect( rect.left, rect.top, rect.right, vh ) ); split( new Rect( rect.left, vh, rect.right, rect.bottom ) ); } } } protected void paint() { for (Room r : rooms) { if (r.type != Type.NULL) { placeDoors( r ); r.type.paint( this, r ); } else { if (feeling == Feeling.CHASM && Random.Int( 2 ) == 0) { Painter.fill( this, r, Terrain.WALL ); } } } for (Room r : rooms) { paintDoors( r ); } } private void placeDoors( Room r ) { for (Room n : r.connected.keySet()) { Room.Door door = r.connected.get( n ); if (door == null) { Rect i = r.intersect( n ); if (i.width() == 0) { door = new Room.Door( i.left, Random.Int( i.top + 1, i.bottom ) ); } else { door = new Room.Door( Random.Int( i.left + 1, i.right ), i.top); } r.connected.put( n, door ); n.connected.put( r, door ); } } } protected void paintDoors( Room r ) { for (Room n : r.connected.keySet()) { if (joinRooms( r, n )) { continue; } Room.Door d = r.connected.get( n ); int door = d.x + d.y * WIDTH; switch (d.type) { case EMPTY: map[door] = Terrain.EMPTY; break; case TUNNEL: map[door] = tunnelTile(); break; case REGULAR: if (Dungeon.depth <= 1) { map[door] = Terrain.DOOR; } else { boolean secret = (Dungeon.depth < 6 ? Random.Int( 12 - Dungeon.depth ) : Random.Int( 6 )) == 0; map[door] = secret ? Terrain.SECRET_DOOR : Terrain.DOOR; if (secret) { secretDoors++; } } break; case UNLOCKED: map[door] = Terrain.DOOR; break; case ARCHWAY: map[door] = Terrain.ARCHWAY; break; case HIDDEN: map[door] = Terrain.SECRET_DOOR; break; case BARRICADE: map[door] = Random.Int( 3 ) == 0 ? Terrain.BOOKSHELF : Terrain.BARRICADE; break; case LOCKED: map[door] = Terrain.LOCKED_DOOR; break; } } } protected boolean joinRooms( Room r, Room n ) { if (r.type != Room.Type.STANDARD || n.type != Room.Type.STANDARD) { return false; } Rect w = r.intersect( n ); if (w.left == w.right) { if (w.bottom - w.top < 3) { return false; } if (w.height() == Math.max( r.height(), n.height() )) { return false; } if (r.width() + n.width() > maxRoomSize) { return false; } w.top += 1; w.bottom -= 0; w.right++; Painter.fill( this, w.left, w.top, 1, w.height(), Terrain.EMPTY ); } else { if (w.right - w.left < 3) { return false; } if (w.width() == Math.max( r.width(), n.width() )) { return false; } if (r.height() + n.height() > maxRoomSize) { return false; } w.left += 1; w.right -= 0; w.bottom++; Painter.fill( this, w.left, w.top, w.width(), 1, Terrain.EMPTY ); } return true; } @Override public int nMobs() { switch(Dungeon.depth) { case 1: //mobs are not randomly spawned on floor 1. return 0; default: // the number of mobs slowly increases toward each boss battle switch (Dungeon.difficultyLevel) { case Dungeon.DIFF_TUTOR: case Dungeon.DIFF_EASY: return 3 + Dungeon.depth % 6 + Random.Int(5); case Dungeon.DIFF_HARD: case Dungeon.DIFF_NTMARE: return 4 + Dungeon.depth % 6 + Random.Int(7); default: return 3 + Dungeon.depth % 6 + Random.Int(6); } } } @Override protected void createMobs() { //on floor 1, enough rats are created so the player can get level 2. int mobsToSpawn = Dungeon.depth == 1 ? 13 : nMobs(); HashSet<Room> stdRooms = new HashSet<>(); for (Room room : rooms) { if (room.type == Type.STANDARD) stdRooms.add(room); } Iterator<Room> stdRoomIter = stdRooms.iterator(); while (mobsToSpawn > 0) { // cycle through the list of rooms if (!stdRoomIter.hasNext()) stdRoomIter = stdRooms.iterator(); Room roomToSpawn = stdRoomIter.next(); // add one or two mobs each time through Mob mob = Bestiary.mob( Dungeon.depth ); if (mob != null) { mob.pos = roomToSpawn.random(); if (findMob(mob.pos) == null && Level.passable[mob.pos]) { mobsToSpawn--; mobs.add(mob); if (mobsToSpawn > 0 && Random.Int(4) == 0) { mob = Bestiary.mob(Dungeon.depth); mob.scaleMob(); mob.pos = roomToSpawn.random(); if (findMob(mob.pos) == null && Level.passable[mob.pos]) { mobsToSpawn--; mobs.add(mob); } } } } } if (Dungeon.depth == 17) { Tinkerer tinker = new Tinkerer(); tinker.scaleMob(); do { tinker.pos = randomRespawnCell(); } while (tinker.pos == -1); mobs.add(tinker); } else if (Dungeon.depth == 23) { Minotaur minotaur = new Minotaur(); minotaur.scaleMob(); do { minotaur.pos = randomRespawnCell(); } while (minotaur.pos == -1); mobs.add(minotaur); } else if (Dungeon.depth == 29) { ChaosMage cmage = new ChaosMage(); cmage.scaleMob(); do { cmage.pos = randomRespawnCell(); } while (cmage.pos == -1); mobs.add(cmage); } } @Override public int randomRespawnCell() { int count = 0; int cell = -1; while (true) { if (++count > 30) { return -1; } Room room = randomRoom( Room.Type.STANDARD, 10 ); if (room == null) { continue; } cell = room.random(); if (!Dungeon.visible[cell] && Actor.findChar( cell ) == null && Level.passable[cell]) { return cell; } } } @Override public int randomDestination() { int cell = -1; while (true) { Room room = Random.element( rooms ); if (room == null) { continue; } cell = room.random(); if (Level.passable[cell]) { return cell; } } } @Override protected void createItems() { int nItems = (Dungeon.depth == 1) ? 3 : 4; int bonus = 0; for (Buff buff : Dungeon.hero.buffs(RingOfWealth.Wealth.class)) { bonus += ((RingOfWealth.Wealth) buff).level; } //just incase someone gets a ridiculous ring, cap this at 80% bonus = Math.min(bonus, 9); while (Random.Float() < (0.35f + bonus*0.05f)) { nItems++; } if (Dungeon.difficultyLevel == Dungeon.DIFF_NTMARE) { nItems = nItems / 2; } else if (Dungeon.difficultyLevel <= Dungeon.DIFF_EASY) { nItems += 2; } for (int i=0; i < nItems; i++) { Heap.Type type = null; switch (Random.Int( 20 )) { case 0: type = Heap.Type.SKELETON; break; case 1: case 2: case 3: case 4: if (feeling == Feeling.BURNT) { // burnt levels drop skeletons instead of chests type = Heap.Type.SKELETON; } else { type = Heap.Type.CHEST; } break; case 5: type = Dungeon.depth > 1 ? Heap.Type.MIMIC : Heap.Type.CHEST; break; default: type = Heap.Type.HEAP; } drop( Generator.random(), randomDropCell() ).type = type; } for (Item item : itemsToSpawn) { int cell = randomDropCell(); if (item instanceof Scroll) { while ((map[cell] == Terrain.TRAP || map[cell] == Terrain.SECRET_TRAP) && traps.get( cell ) instanceof FireTrap) { cell = randomDropCell(); } } drop( item, cell ).type = Heap.Type.HEAP; } Item item = Bones.get(); if (item != null) { drop( item, randomDropCell() ).type = Heap.Type.REMAINS; } } protected Room randomRoom( Room.Type type, int tries ) { for (int i=0; i < tries; i++) { Room room = Random.element( rooms ); if (room.type == type) { return room; } } return null; } public Room room( int pos ) { for (Room room : rooms) { if (room.type != Type.NULL && room.inside( pos )) { return room; } } return null; } protected int randomDropCell() { while (true) { Room room = randomRoom( Room.Type.STANDARD, 1 ); if (room != null) { int pos = room.random(); if (passable[pos]) { return pos; } } } } @Override public int pitCell() { for (Room room : rooms) { if (room.type == Type.PIT) { return room.random(); } } return super.pitCell(); } @Override public void storeInBundle( Bundle bundle ) { super.storeInBundle( bundle ); bundle.put( "rooms", rooms ); } @SuppressWarnings("unchecked") @Override public void restoreFromBundle( Bundle bundle ) { super.restoreFromBundle( bundle ); rooms = new HashSet<>( (Collection<Room>) ((Collection<?>) bundle.getCollection( "rooms" )) ); for (Room r : rooms) { if (r.type == Type.WEAK_FLOOR) { weakFloorCreated = true; break; } } } }