1148 lines
33 KiB
Java
1148 lines
33 KiB
Java
/*
|
|
* Pixel Dungeon
|
|
* Copyright (C) 2012-2015 Oleg Dolya
|
|
*
|
|
* Shattered Pixel Dungeon
|
|
* Copyright (C) 2014-2016 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;
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.Assets;
|
|
import com.shatteredpixel.shatteredpixeldungeon.Challenges;
|
|
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
|
|
import com.shatteredpixel.shatteredpixeldungeon.Statistics;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Alchemy;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.WellWater;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Awareness;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Blindness;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.MindVision;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Shadows;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroClass;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Bestiary;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
|
|
import com.shatteredpixel.shatteredpixeldungeon.effects.particles.FlowParticle;
|
|
import com.shatteredpixel.shatteredpixeldungeon.effects.particles.WindParticle;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.Dewdrop;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.Generator;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.Heap;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.Item;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.Stylus;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.Torch;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.armor.Armor;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.AlchemistsToolkit;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.DriedRose;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.TimekeepersHourglass;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.bags.ScrollHolder;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.bags.SeedPouch;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.food.Blandfruit;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.food.Food;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.potions.PotionOfHealing;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.potions.PotionOfMight;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.potions.PotionOfStrength;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfWealth;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.Scroll;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfMagicalInfusion;
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfUpgrade;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.features.Chasm;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.features.Door;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.features.HighGrass;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.painters.Painter;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.traps.Trap;
|
|
import com.shatteredpixel.shatteredpixeldungeon.mechanics.ShadowCaster;
|
|
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
|
|
import com.shatteredpixel.shatteredpixeldungeon.plants.BlandfruitBush;
|
|
import com.shatteredpixel.shatteredpixeldungeon.plants.Plant;
|
|
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
|
|
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite;
|
|
import com.shatteredpixel.shatteredpixeldungeon.ui.CustomTileVisual;
|
|
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
|
|
import com.watabou.noosa.Game;
|
|
import com.watabou.noosa.Group;
|
|
import com.watabou.noosa.audio.Sample;
|
|
import com.watabou.utils.Bundlable;
|
|
import com.watabou.utils.Bundle;
|
|
import com.watabou.utils.Random;
|
|
import com.watabou.utils.SparseArray;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
|
|
public abstract class Level implements Bundlable {
|
|
|
|
public static enum Feeling {
|
|
NONE,
|
|
CHASM,
|
|
WATER,
|
|
GRASS,
|
|
DARK
|
|
}
|
|
|
|
public static final int WIDTH = 32;
|
|
public static final int HEIGHT = 32;
|
|
public static final int LENGTH = WIDTH * HEIGHT;
|
|
|
|
public static final int[] NEIGHBOURS4 = {-WIDTH, +1, +WIDTH, -1};
|
|
public static final int[] NEIGHBOURS8 = {-WIDTH, +1-WIDTH, +1, +1+WIDTH, +WIDTH, -1+WIDTH, -1, -1-WIDTH};
|
|
public static final int[] NEIGHBOURS9 = {0, -WIDTH, +1-WIDTH, +1, +1+WIDTH, +WIDTH, -1+WIDTH, -1, -1-WIDTH};
|
|
|
|
//make sure to check insideMap() when using these, as there's a risk something may be outside the map
|
|
public static final int[] NEIGHBOURS8DIST2 = {+2+2*WIDTH, +1+2*WIDTH, 2*WIDTH, -1+2*WIDTH, -2+2*WIDTH,
|
|
+2+WIDTH, +1+WIDTH, +WIDTH, -1+WIDTH, -2+WIDTH,
|
|
+2, +1, -1, -2,
|
|
+2-WIDTH, +1-WIDTH, -WIDTH, -1-WIDTH, -2-WIDTH,
|
|
+2-2*WIDTH, +1-2*WIDTH, -2*WIDTH, -1-2*WIDTH, -2-2*WIDTH};
|
|
public static final int[] NEIGHBOURS9DIST2 = {+2+2*WIDTH, +1+2*WIDTH, 2*WIDTH, -1+2*WIDTH, -2+2*WIDTH,
|
|
+2+WIDTH, +1+WIDTH, +WIDTH, -1+WIDTH, -2+WIDTH,
|
|
+2, +1, 0, -1, -2,
|
|
+2-WIDTH, +1-WIDTH, -WIDTH, -1-WIDTH, -2-WIDTH,
|
|
+2-2*WIDTH, +1-2*WIDTH, -2*WIDTH, -1-2*WIDTH, -2-2*WIDTH};
|
|
|
|
|
|
protected static final float TIME_TO_RESPAWN = 50;
|
|
|
|
public static boolean resizingNeeded;
|
|
public static int loadedMapSize;
|
|
|
|
public int version;
|
|
public int[] map;
|
|
public boolean[] visited;
|
|
public boolean[] mapped;
|
|
|
|
public int viewDistance = Dungeon.isChallenged( Challenges.DARKNESS ) ? 3: 8;
|
|
|
|
public static boolean[] fieldOfView = new boolean[LENGTH];
|
|
|
|
public static boolean[] passable = new boolean[LENGTH];
|
|
public static boolean[] losBlocking = new boolean[LENGTH];
|
|
public static boolean[] flamable = new boolean[LENGTH];
|
|
public static boolean[] secret = new boolean[LENGTH];
|
|
public static boolean[] solid = new boolean[LENGTH];
|
|
public static boolean[] avoid = new boolean[LENGTH];
|
|
public static boolean[] water = new boolean[LENGTH];
|
|
public static boolean[] pit = new boolean[LENGTH];
|
|
|
|
public static boolean[] discoverable = new boolean[LENGTH];
|
|
|
|
public Feeling feeling = Feeling.NONE;
|
|
|
|
public int entrance;
|
|
public int exit;
|
|
|
|
//when a boss level has become locked.
|
|
public boolean locked = false;
|
|
|
|
public HashSet<Mob> mobs;
|
|
public SparseArray<Heap> heaps;
|
|
public HashMap<Class<? extends Blob>,Blob> blobs;
|
|
public SparseArray<Plant> plants;
|
|
public SparseArray<Trap> traps;
|
|
public HashSet<CustomTileVisual> customTiles;
|
|
|
|
protected ArrayList<Item> itemsToSpawn = new ArrayList<>();
|
|
|
|
protected Group visuals;
|
|
|
|
public int color1 = 0x004400;
|
|
public int color2 = 0x88CC44;
|
|
|
|
//FIXME this is sloppy. Should be able to keep track of this without static variables
|
|
protected static boolean pitRoomNeeded = false;
|
|
public static boolean weakFloorCreated = false;
|
|
|
|
private static final String VERSION = "version";
|
|
private static final String MAP = "map";
|
|
private static final String VISITED = "visited";
|
|
private static final String MAPPED = "mapped";
|
|
private static final String ENTRANCE = "entrance";
|
|
private static final String EXIT = "exit";
|
|
private static final String LOCKED = "locked";
|
|
private static final String HEAPS = "heaps";
|
|
private static final String PLANTS = "plants";
|
|
private static final String TRAPS = "traps";
|
|
private static final String CUSTOM_TILES= "customTiles";
|
|
private static final String MOBS = "mobs";
|
|
private static final String BLOBS = "blobs";
|
|
private static final String FEELING = "feeling";
|
|
|
|
public void create() {
|
|
|
|
resizingNeeded = false;
|
|
|
|
map = new int[LENGTH];
|
|
visited = new boolean[LENGTH];
|
|
Arrays.fill( visited, false );
|
|
mapped = new boolean[LENGTH];
|
|
Arrays.fill( mapped, false );
|
|
|
|
if (!(Dungeon.bossLevel() || Dungeon.depth == 21) /*final shop floor*/) {
|
|
addItemToSpawn( Generator.random( Generator.Category.FOOD ) );
|
|
|
|
int bonus = RingOfWealth.getBonus(Dungeon.hero, RingOfWealth.Wealth.class);
|
|
|
|
if (Dungeon.posNeeded()) {
|
|
if (Random.Float() > Math.pow(0.925, bonus))
|
|
addItemToSpawn( new PotionOfMight() );
|
|
else
|
|
addItemToSpawn( new PotionOfStrength() );
|
|
Dungeon.limitedDrops.strengthPotions.count++;
|
|
}
|
|
if (Dungeon.souNeeded()) {
|
|
if (Random.Float() > Math.pow(0.925, bonus))
|
|
addItemToSpawn( new ScrollOfMagicalInfusion() );
|
|
else
|
|
addItemToSpawn( new ScrollOfUpgrade() );
|
|
Dungeon.limitedDrops.upgradeScrolls.count++;
|
|
}
|
|
if (Dungeon.asNeeded()) {
|
|
if (Random.Float() > Math.pow(0.925, bonus))
|
|
addItemToSpawn( new Stylus() );
|
|
addItemToSpawn( new Stylus() );
|
|
Dungeon.limitedDrops.arcaneStyli.count++;
|
|
}
|
|
|
|
DriedRose rose = Dungeon.hero.belongings.getItem( DriedRose.class );
|
|
if (rose != null && !rose.cursed){
|
|
//this way if a rose is dropped later in the game, player still has a chance to max it out.
|
|
int petalsNeeded = (int) Math.ceil((float)((Dungeon.depth / 2) - rose.droppedPetals) / 3);
|
|
|
|
for (int i=1; i <= petalsNeeded; i++) {
|
|
//the player may miss a single petal and still max their rose.
|
|
if (rose.droppedPetals < 11) {
|
|
addItemToSpawn(new DriedRose.Petal());
|
|
rose.droppedPetals++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Dungeon.depth > 1) {
|
|
switch (Random.Int( 10 )) {
|
|
case 0:
|
|
if (!Dungeon.bossLevel( Dungeon.depth + 1 )) {
|
|
feeling = Feeling.CHASM;
|
|
}
|
|
break;
|
|
case 1:
|
|
feeling = Feeling.WATER;
|
|
break;
|
|
case 2:
|
|
feeling = Feeling.GRASS;
|
|
break;
|
|
case 3:
|
|
feeling = Feeling.DARK;
|
|
addItemToSpawn(new Torch());
|
|
viewDistance = (int)Math.ceil(viewDistance/3f);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
boolean pitNeeded = Dungeon.depth > 1 && weakFloorCreated;
|
|
|
|
do {
|
|
Arrays.fill( map, feeling == Feeling.CHASM ? Terrain.CHASM : Terrain.WALL );
|
|
|
|
pitRoomNeeded = pitNeeded;
|
|
weakFloorCreated = false;
|
|
|
|
mobs = new HashSet<>();
|
|
heaps = new SparseArray<>();
|
|
blobs = new HashMap<>();
|
|
plants = new SparseArray<>();
|
|
traps = new SparseArray<>();
|
|
customTiles = new HashSet<>();
|
|
|
|
} while (!build());
|
|
decorate();
|
|
|
|
buildFlagMaps();
|
|
cleanWalls();
|
|
|
|
createMobs();
|
|
createItems();
|
|
}
|
|
|
|
public void reset() {
|
|
|
|
for (Mob mob : mobs.toArray( new Mob[0] )) {
|
|
if (!mob.reset()) {
|
|
mobs.remove( mob );
|
|
}
|
|
}
|
|
createMobs();
|
|
}
|
|
|
|
@Override
|
|
public void restoreFromBundle( Bundle bundle ) {
|
|
|
|
version = bundle.getInt( VERSION );
|
|
|
|
mobs = new HashSet<>();
|
|
heaps = new SparseArray<>();
|
|
blobs = new HashMap<>();
|
|
plants = new SparseArray<>();
|
|
traps = new SparseArray<>();
|
|
customTiles = new HashSet<>();
|
|
|
|
map = bundle.getIntArray( MAP );
|
|
|
|
visited = bundle.getBooleanArray( VISITED );
|
|
mapped = bundle.getBooleanArray( MAPPED );
|
|
|
|
entrance = bundle.getInt( ENTRANCE );
|
|
exit = bundle.getInt( EXIT );
|
|
|
|
locked = bundle.getBoolean( LOCKED );
|
|
|
|
weakFloorCreated = false;
|
|
|
|
adjustMapSize();
|
|
|
|
//for pre-0.3.0c saves
|
|
if (version < 44){
|
|
map = Terrain.convertTrapsFrom43( map, traps );
|
|
}
|
|
|
|
Collection<Bundlable> collection = bundle.getCollection( HEAPS );
|
|
for (Bundlable h : collection) {
|
|
Heap heap = (Heap)h;
|
|
if (resizingNeeded) {
|
|
heap.pos = adjustPos( heap.pos );
|
|
}
|
|
if (!heap.isEmpty())
|
|
heaps.put( heap.pos, heap );
|
|
}
|
|
|
|
collection = bundle.getCollection( PLANTS );
|
|
for (Bundlable p : collection) {
|
|
Plant plant = (Plant)p;
|
|
if (resizingNeeded) {
|
|
plant.pos = adjustPos( plant.pos );
|
|
}
|
|
plants.put( plant.pos, plant );
|
|
}
|
|
|
|
collection = bundle.getCollection( TRAPS );
|
|
for (Bundlable p : collection) {
|
|
Trap trap = (Trap)p;
|
|
if (resizingNeeded) {
|
|
trap.pos = adjustPos( trap.pos );
|
|
}
|
|
traps.put( trap.pos, trap );
|
|
}
|
|
|
|
collection = bundle.getCollection( CUSTOM_TILES );
|
|
for (Bundlable p : collection) {
|
|
CustomTileVisual vis = (CustomTileVisual)p;
|
|
if (resizingNeeded) {
|
|
//TODO: add proper resizing logic here
|
|
}
|
|
customTiles.add( vis );
|
|
}
|
|
|
|
collection = bundle.getCollection( MOBS );
|
|
for (Bundlable m : collection) {
|
|
Mob mob = (Mob)m;
|
|
if (mob != null) {
|
|
if (resizingNeeded) {
|
|
mob.pos = adjustPos( mob.pos );
|
|
}
|
|
mobs.add( mob );
|
|
}
|
|
}
|
|
|
|
collection = bundle.getCollection( BLOBS );
|
|
for (Bundlable b : collection) {
|
|
Blob blob = (Blob)b;
|
|
blobs.put( blob.getClass(), blob );
|
|
}
|
|
|
|
feeling = bundle.getEnum( FEELING, Feeling.class );
|
|
if (feeling == Feeling.DARK)
|
|
viewDistance = (int)Math.ceil(viewDistance/3f);
|
|
|
|
buildFlagMaps();
|
|
cleanWalls();
|
|
}
|
|
|
|
@Override
|
|
public void storeInBundle( Bundle bundle ) {
|
|
bundle.put( VERSION, Game.versionCode );
|
|
bundle.put( MAP, map );
|
|
bundle.put( VISITED, visited );
|
|
bundle.put( MAPPED, mapped );
|
|
bundle.put( ENTRANCE, entrance );
|
|
bundle.put( EXIT, exit );
|
|
bundle.put( LOCKED, locked );
|
|
bundle.put( HEAPS, heaps.values() );
|
|
bundle.put( PLANTS, plants.values() );
|
|
bundle.put( TRAPS, traps.values() );
|
|
bundle.put( CUSTOM_TILES, customTiles );
|
|
bundle.put( MOBS, mobs );
|
|
bundle.put( BLOBS, blobs.values() );
|
|
bundle.put( FEELING, feeling );
|
|
}
|
|
|
|
public int tunnelTile() {
|
|
return feeling == Feeling.CHASM ? Terrain.EMPTY_SP : Terrain.EMPTY;
|
|
}
|
|
|
|
private void adjustMapSize() {
|
|
// For levels saved before 1.6.3
|
|
// Seeing as shattered started on 1.7.1 this is never used, but the code may be resused in future.
|
|
if (map.length < LENGTH) {
|
|
|
|
resizingNeeded = true;
|
|
loadedMapSize = (int)Math.sqrt( map.length );
|
|
|
|
int[] map = new int[LENGTH];
|
|
Arrays.fill( map, Terrain.WALL );
|
|
|
|
boolean[] visited = new boolean[LENGTH];
|
|
Arrays.fill( visited, false );
|
|
|
|
boolean[] mapped = new boolean[LENGTH];
|
|
Arrays.fill( mapped, false );
|
|
|
|
for (int i=0; i < loadedMapSize; i++) {
|
|
System.arraycopy( this.map, i * loadedMapSize, map, i * WIDTH, loadedMapSize );
|
|
System.arraycopy( this.visited, i * loadedMapSize, visited, i * WIDTH, loadedMapSize );
|
|
System.arraycopy( this.mapped, i * loadedMapSize, mapped, i * WIDTH, loadedMapSize );
|
|
}
|
|
|
|
this.map = map;
|
|
this.visited = visited;
|
|
this.mapped = mapped;
|
|
|
|
entrance = adjustPos( entrance );
|
|
exit = adjustPos( exit );
|
|
} else {
|
|
resizingNeeded = false;
|
|
}
|
|
}
|
|
|
|
public int adjustPos( int pos ) {
|
|
return (pos / loadedMapSize) * WIDTH + (pos % loadedMapSize);
|
|
}
|
|
|
|
public String tilesTex() {
|
|
return null;
|
|
}
|
|
|
|
public String waterTex() {
|
|
return null;
|
|
}
|
|
|
|
abstract protected boolean build();
|
|
|
|
abstract protected void decorate();
|
|
|
|
abstract protected void createMobs();
|
|
|
|
abstract protected void createItems();
|
|
|
|
public void seal(){
|
|
if (!locked) {
|
|
locked = true;
|
|
Buff.affect(Dungeon.hero, LockedFloor.class);
|
|
}
|
|
}
|
|
|
|
public void unseal(){
|
|
if (locked) {
|
|
locked = false;
|
|
}
|
|
}
|
|
|
|
public Group addVisuals() {
|
|
if (visuals == null || visuals.parent == null){
|
|
visuals = new Group();
|
|
} else {
|
|
visuals.clear();
|
|
}
|
|
for (int i=0; i < LENGTH; i++) {
|
|
if (pit[i]) {
|
|
visuals.add( new WindParticle.Wind( i ) );
|
|
if (i >= WIDTH && water[i-WIDTH]) {
|
|
visuals.add( new FlowParticle.Flow( i - WIDTH ) );
|
|
}
|
|
}
|
|
}
|
|
return visuals;
|
|
}
|
|
|
|
public int nMobs() {
|
|
return 0;
|
|
}
|
|
|
|
public Mob findMob( int pos ){
|
|
for (Mob mob : mobs){
|
|
if (mob.pos == pos){
|
|
return mob;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public Actor respawner() {
|
|
return new Actor() {
|
|
|
|
{
|
|
actPriority = 1; //as if it were a buff.
|
|
}
|
|
|
|
@Override
|
|
protected boolean act() {
|
|
if (mobs.size() < nMobs()) {
|
|
|
|
Mob mob = Bestiary.mutable( Dungeon.depth );
|
|
mob.state = mob.WANDERING;
|
|
mob.pos = randomRespawnCell();
|
|
if (Dungeon.hero.isAlive() && mob.pos != -1 && distance(Dungeon.hero.pos, mob.pos) >= 4) {
|
|
GameScene.add( mob );
|
|
if (Statistics.amuletObtained) {
|
|
mob.beckon( Dungeon.hero.pos );
|
|
}
|
|
}
|
|
}
|
|
spend( Dungeon.level.feeling == Feeling.DARK || Statistics.amuletObtained ? TIME_TO_RESPAWN / 2 : TIME_TO_RESPAWN );
|
|
return true;
|
|
}
|
|
};
|
|
}
|
|
|
|
public int randomRespawnCell() {
|
|
int cell;
|
|
do {
|
|
cell = Random.Int( LENGTH );
|
|
} while (!passable[cell] || Dungeon.visible[cell] || Actor.findChar( cell ) != null);
|
|
return cell;
|
|
}
|
|
|
|
public int randomDestination() {
|
|
int cell;
|
|
do {
|
|
cell = Random.Int( LENGTH );
|
|
} while (!passable[cell]);
|
|
return cell;
|
|
}
|
|
|
|
public void addItemToSpawn( Item item ) {
|
|
if (item != null) {
|
|
itemsToSpawn.add( item );
|
|
}
|
|
}
|
|
|
|
public Item findPrizeItem(){ return findPrizeItem(null); }
|
|
|
|
public Item findPrizeItem(Class<?extends Item> match){
|
|
if (itemsToSpawn.size() == 0)
|
|
return null;
|
|
|
|
if (match == null){
|
|
Item item = Random.element(itemsToSpawn);
|
|
itemsToSpawn.remove(item);
|
|
return item;
|
|
}
|
|
|
|
for (Item item : itemsToSpawn){
|
|
if (match.isInstance(item)){
|
|
itemsToSpawn.remove( item );
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected void buildFlagMaps() {
|
|
|
|
for (int i=0; i < LENGTH; i++) {
|
|
int flags = Terrain.flags[map[i]];
|
|
passable[i] = (flags & Terrain.PASSABLE) != 0;
|
|
losBlocking[i] = (flags & Terrain.LOS_BLOCKING) != 0;
|
|
flamable[i] = (flags & Terrain.FLAMABLE) != 0;
|
|
secret[i] = (flags & Terrain.SECRET) != 0;
|
|
solid[i] = (flags & Terrain.SOLID) != 0;
|
|
avoid[i] = (flags & Terrain.AVOID) != 0;
|
|
water[i] = (flags & Terrain.LIQUID) != 0;
|
|
pit[i] = (flags & Terrain.PIT) != 0;
|
|
}
|
|
|
|
int lastRow = LENGTH - WIDTH;
|
|
for (int i=0; i < WIDTH; i++) {
|
|
passable[i] = avoid[i] = false;
|
|
passable[lastRow + i] = avoid[lastRow + i] = false;
|
|
}
|
|
for (int i=WIDTH; i < lastRow; i += WIDTH) {
|
|
passable[i] = avoid[i] = false;
|
|
passable[i + WIDTH-1] = avoid[i + WIDTH-1] = false;
|
|
}
|
|
|
|
for (int i=WIDTH; i < LENGTH - WIDTH; i++) {
|
|
|
|
if (water[i]) {
|
|
map[i] = getWaterTile( i );
|
|
}
|
|
|
|
if (pit[i]) {
|
|
if (!pit[i - WIDTH]) {
|
|
int c = map[i - WIDTH];
|
|
if (c == Terrain.EMPTY_SP || c == Terrain.STATUE_SP) {
|
|
map[i] = Terrain.CHASM_FLOOR_SP;
|
|
} else if (water[i - WIDTH]) {
|
|
map[i] = Terrain.CHASM_WATER;
|
|
} else if ((Terrain.flags[c] & Terrain.UNSTITCHABLE) != 0) {
|
|
map[i] = Terrain.CHASM_WALL;
|
|
} else {
|
|
map[i] = Terrain.CHASM_FLOOR;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private int getWaterTile( int pos ) {
|
|
int t = Terrain.WATER_TILES;
|
|
for (int j=0; j < NEIGHBOURS4.length; j++) {
|
|
if ((Terrain.flags[map[pos + NEIGHBOURS4[j]]] & Terrain.UNSTITCHABLE) != 0) {
|
|
t += 1 << j;
|
|
}
|
|
}
|
|
return t;
|
|
}
|
|
|
|
public void destroy( int pos ) {
|
|
if ((Terrain.flags[map[pos]] & Terrain.UNSTITCHABLE) == 0) {
|
|
|
|
set( pos, Terrain.EMBERS );
|
|
|
|
} else {
|
|
boolean flood = false;
|
|
for (int j=0; j < NEIGHBOURS4.length; j++) {
|
|
if (water[pos + NEIGHBOURS4[j]]) {
|
|
flood = true;
|
|
break;
|
|
}
|
|
}
|
|
if (flood) {
|
|
set( pos, getWaterTile( pos ) );
|
|
} else {
|
|
set( pos, Terrain.EMBERS );
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void cleanWalls() {
|
|
for (int i=0; i < LENGTH; i++) {
|
|
|
|
boolean d = false;
|
|
|
|
for (int j=0; j < NEIGHBOURS9.length; j++) {
|
|
int n = i + NEIGHBOURS9[j];
|
|
if (n >= 0 && n < LENGTH && map[n] != Terrain.WALL && map[n] != Terrain.WALL_DECO) {
|
|
d = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (d) {
|
|
d = false;
|
|
|
|
for (int j=0; j < NEIGHBOURS9.length; j++) {
|
|
int n = i + NEIGHBOURS9[j];
|
|
if (n >= 0 && n < LENGTH && !pit[n]) {
|
|
d = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
discoverable[i] = d;
|
|
}
|
|
}
|
|
|
|
public static void set( int cell, int terrain ) {
|
|
Painter.set( Dungeon.level, cell, terrain );
|
|
|
|
if (terrain != Terrain.TRAP && terrain != Terrain.SECRET_TRAP && terrain != Terrain.INACTIVE_TRAP){
|
|
Dungeon.level.traps.remove( cell );
|
|
}
|
|
|
|
int flags = Terrain.flags[terrain];
|
|
passable[cell] = (flags & Terrain.PASSABLE) != 0;
|
|
losBlocking[cell] = (flags & Terrain.LOS_BLOCKING) != 0;
|
|
flamable[cell] = (flags & Terrain.FLAMABLE) != 0;
|
|
secret[cell] = (flags & Terrain.SECRET) != 0;
|
|
solid[cell] = (flags & Terrain.SOLID) != 0;
|
|
avoid[cell] = (flags & Terrain.AVOID) != 0;
|
|
pit[cell] = (flags & Terrain.PIT) != 0;
|
|
water[cell] = terrain == Terrain.WATER || terrain >= Terrain.WATER_TILES;
|
|
}
|
|
|
|
public Heap drop( Item item, int cell ) {
|
|
|
|
//This messy if statement deals will items which should not drop in challenges primarily.
|
|
if ((Dungeon.isChallenged( Challenges.NO_FOOD ) && (item instanceof Food || item instanceof BlandfruitBush.Seed)) ||
|
|
(Dungeon.isChallenged( Challenges.NO_ARMOR ) && item instanceof Armor) ||
|
|
(Dungeon.isChallenged( Challenges.NO_HEALING ) && item instanceof PotionOfHealing) ||
|
|
(Dungeon.isChallenged( Challenges.NO_HERBALISM ) && (item instanceof Plant.Seed || item instanceof Dewdrop || item instanceof SeedPouch)) ||
|
|
(Dungeon.isChallenged( Challenges.NO_SCROLLS ) && ((item instanceof Scroll && !(item instanceof ScrollOfUpgrade || item instanceof ScrollOfMagicalInfusion)) || item instanceof ScrollHolder)) ||
|
|
item == null) {
|
|
|
|
//create a dummy heap, give it a dummy sprite, don't add it to the game, and return it.
|
|
//effectively nullifies whatever the logic calling this wants to do, including dropping items.
|
|
Heap heap = new Heap();
|
|
ItemSprite sprite = heap.sprite = new ItemSprite();
|
|
sprite.link(heap);
|
|
return heap;
|
|
|
|
}
|
|
|
|
if ((map[cell] == Terrain.ALCHEMY) && (
|
|
!(item instanceof Plant.Seed || item instanceof Blandfruit) ||
|
|
item instanceof BlandfruitBush.Seed ||
|
|
(item instanceof Blandfruit && (((Blandfruit) item).potionAttrib != null || heaps.get(cell) != null))||
|
|
Dungeon.hero.buff(AlchemistsToolkit.alchemy.class) != null && Dungeon.hero.buff(AlchemistsToolkit.alchemy.class).isCursed())) {
|
|
int n;
|
|
do {
|
|
n = cell + NEIGHBOURS8[Random.Int( 8 )];
|
|
} while (map[n] != Terrain.EMPTY_SP);
|
|
cell = n;
|
|
}
|
|
|
|
Heap heap = heaps.get( cell );
|
|
if (heap == null) {
|
|
|
|
heap = new Heap();
|
|
heap.seen = Dungeon.visible[cell];
|
|
heap.pos = cell;
|
|
if (map[cell] == Terrain.CHASM || (Dungeon.level != null && pit[cell])) {
|
|
Dungeon.dropToChasm( item );
|
|
GameScene.discard( heap );
|
|
} else {
|
|
heaps.put( cell, heap );
|
|
GameScene.add( heap );
|
|
}
|
|
|
|
} else if (heap.type == Heap.Type.LOCKED_CHEST || heap.type == Heap.Type.CRYSTAL_CHEST) {
|
|
|
|
int n;
|
|
do {
|
|
n = cell + Level.NEIGHBOURS8[Random.Int( 8 )];
|
|
} while (!Level.passable[n] && !Level.avoid[n]);
|
|
return drop( item, n );
|
|
|
|
}
|
|
heap.drop(item);
|
|
|
|
if (Dungeon.level != null) {
|
|
press( cell, null );
|
|
}
|
|
|
|
return heap;
|
|
}
|
|
|
|
public Plant plant( Plant.Seed seed, int pos ) {
|
|
|
|
Plant plant = plants.get( pos );
|
|
if (plant != null) {
|
|
plant.wither();
|
|
}
|
|
|
|
if (map[pos] == Terrain.HIGH_GRASS ||
|
|
map[pos] == Terrain.EMPTY ||
|
|
map[pos] == Terrain.EMBERS ||
|
|
map[pos] == Terrain.EMPTY_DECO) {
|
|
map[pos] = Terrain.GRASS;
|
|
flamable[pos] = true;
|
|
GameScene.updateMap( pos );
|
|
}
|
|
|
|
plant = seed.couch( pos );
|
|
plants.put( pos, plant );
|
|
|
|
GameScene.add( plant );
|
|
|
|
return plant;
|
|
}
|
|
|
|
public void uproot( int pos ) {
|
|
plants.remove(pos);
|
|
}
|
|
|
|
public Trap setTrap( Trap trap, int pos ){
|
|
Trap existingTrap = traps.get(pos);
|
|
if (existingTrap != null){
|
|
traps.remove( pos );
|
|
if(existingTrap.sprite != null) existingTrap.sprite.kill();
|
|
}
|
|
trap.set( pos );
|
|
traps.put( pos, trap );
|
|
GameScene.add(trap);
|
|
return trap;
|
|
}
|
|
|
|
public void disarmTrap( int pos ) {
|
|
set(pos, Terrain.INACTIVE_TRAP);
|
|
GameScene.updateMap(pos);
|
|
}
|
|
|
|
public void discover( int cell ) {
|
|
set( cell, Terrain.discover( map[cell] ) );
|
|
Trap trap = traps.get( cell );
|
|
if (trap != null)
|
|
trap.reveal();
|
|
GameScene.updateMap( cell );
|
|
}
|
|
|
|
public int pitCell() {
|
|
return randomRespawnCell();
|
|
}
|
|
|
|
public void press( int cell, Char ch ) {
|
|
|
|
if (ch != null && pit[cell] && !ch.flying) {
|
|
if (ch == Dungeon.hero) {
|
|
Chasm.heroFall(cell);
|
|
} else if (ch instanceof Mob) {
|
|
Chasm.mobFall( (Mob)ch );
|
|
}
|
|
return;
|
|
}
|
|
|
|
Trap trap = null;
|
|
|
|
switch (map[cell]) {
|
|
|
|
case Terrain.SECRET_TRAP:
|
|
GLog.i( Messages.get(Level.class, "hidden_plate") );
|
|
case Terrain.TRAP:
|
|
trap = traps.get( cell );
|
|
break;
|
|
|
|
case Terrain.HIGH_GRASS:
|
|
HighGrass.trample( this, cell, ch );
|
|
break;
|
|
|
|
case Terrain.WELL:
|
|
WellWater.affectCell( cell );
|
|
break;
|
|
|
|
case Terrain.ALCHEMY:
|
|
if (ch == null) {
|
|
Alchemy.transmute( cell );
|
|
}
|
|
break;
|
|
|
|
case Terrain.DOOR:
|
|
Door.enter( cell );
|
|
break;
|
|
}
|
|
|
|
TimekeepersHourglass.timeFreeze timeFreeze = Dungeon.hero.buff(TimekeepersHourglass.timeFreeze.class);
|
|
|
|
if (trap != null) {
|
|
if (timeFreeze == null) {
|
|
|
|
if (ch == Dungeon.hero)
|
|
Dungeon.hero.interrupt();
|
|
|
|
trap.trigger();
|
|
|
|
} else {
|
|
|
|
Sample.INSTANCE.play(Assets.SND_TRAP);
|
|
|
|
discover(cell);
|
|
|
|
timeFreeze.setDelayedPress(cell);
|
|
|
|
}
|
|
}
|
|
|
|
Plant plant = plants.get( cell );
|
|
if (plant != null) {
|
|
plant.trigger();
|
|
}
|
|
}
|
|
|
|
public void mobPress( Mob mob ) {
|
|
|
|
int cell = mob.pos;
|
|
|
|
if (pit[cell] && !mob.flying) {
|
|
Chasm.mobFall( mob );
|
|
return;
|
|
}
|
|
|
|
Trap trap = null;
|
|
switch (map[cell]) {
|
|
|
|
case Terrain.TRAP:
|
|
trap = traps.get( cell );
|
|
break;
|
|
|
|
case Terrain.DOOR:
|
|
Door.enter( cell );
|
|
break;
|
|
}
|
|
|
|
if (trap != null) {
|
|
trap.trigger();
|
|
}
|
|
|
|
Plant plant = plants.get( cell );
|
|
if (plant != null) {
|
|
plant.trigger();
|
|
}
|
|
}
|
|
|
|
public boolean[] updateFieldOfView( Char c ) {
|
|
|
|
int cx = c.pos % WIDTH;
|
|
int cy = c.pos / WIDTH;
|
|
|
|
boolean sighted = c.buff( Blindness.class ) == null && c.buff( Shadows.class ) == null
|
|
&& c.buff( TimekeepersHourglass.timeStasis.class ) == null && c.isAlive();
|
|
if (sighted) {
|
|
ShadowCaster.castShadow( cx, cy, fieldOfView, c.viewDistance );
|
|
} else {
|
|
Arrays.fill( fieldOfView, false );
|
|
}
|
|
|
|
int sense = 1;
|
|
if (c.isAlive()) {
|
|
for (Buff b : c.buffs( MindVision.class )) {
|
|
sense = Math.max( ((MindVision)b).distance, sense );
|
|
}
|
|
}
|
|
|
|
if ((sighted && sense > 1) || !sighted) {
|
|
|
|
int ax = Math.max( 0, cx - sense );
|
|
int bx = Math.min( cx + sense, WIDTH - 1 );
|
|
int ay = Math.max( 0, cy - sense );
|
|
int by = Math.min( cy + sense, HEIGHT - 1 );
|
|
|
|
int len = bx - ax + 1;
|
|
int pos = ax + ay * WIDTH;
|
|
for (int y = ay; y <= by; y++, pos+=WIDTH) {
|
|
Arrays.fill( fieldOfView, pos, pos + len, true );
|
|
}
|
|
|
|
for (int i=0; i < LENGTH; i++) {
|
|
fieldOfView[i] &= discoverable[i];
|
|
}
|
|
}
|
|
|
|
if (c.isAlive()) {
|
|
if (c.buff( MindVision.class ) != null) {
|
|
for (Mob mob : mobs) {
|
|
int p = mob.pos;
|
|
fieldOfView[p] = true;
|
|
fieldOfView[p + 1] = true;
|
|
fieldOfView[p - 1] = true;
|
|
fieldOfView[p + WIDTH + 1] = true;
|
|
fieldOfView[p + WIDTH - 1] = true;
|
|
fieldOfView[p - WIDTH + 1] = true;
|
|
fieldOfView[p - WIDTH - 1] = true;
|
|
fieldOfView[p + WIDTH] = true;
|
|
fieldOfView[p - WIDTH] = true;
|
|
}
|
|
} else if (c == Dungeon.hero && ((Hero)c).heroClass == HeroClass.HUNTRESS) {
|
|
for (Mob mob : mobs) {
|
|
int p = mob.pos;
|
|
if (distance( c.pos, p) == 2) {
|
|
fieldOfView[p] = true;
|
|
fieldOfView[p + 1] = true;
|
|
fieldOfView[p - 1] = true;
|
|
fieldOfView[p + WIDTH + 1] = true;
|
|
fieldOfView[p + WIDTH - 1] = true;
|
|
fieldOfView[p - WIDTH + 1] = true;
|
|
fieldOfView[p - WIDTH - 1] = true;
|
|
fieldOfView[p + WIDTH] = true;
|
|
fieldOfView[p - WIDTH] = true;
|
|
}
|
|
}
|
|
}
|
|
if (c.buff( Awareness.class ) != null) {
|
|
for (Heap heap : heaps.values()) {
|
|
int p = heap.pos;
|
|
fieldOfView[p] = true;
|
|
fieldOfView[p + 1] = true;
|
|
fieldOfView[p - 1] = true;
|
|
fieldOfView[p + WIDTH + 1] = true;
|
|
fieldOfView[p + WIDTH - 1] = true;
|
|
fieldOfView[p - WIDTH + 1] = true;
|
|
fieldOfView[p - WIDTH - 1] = true;
|
|
fieldOfView[p + WIDTH] = true;
|
|
fieldOfView[p - WIDTH] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (Heap heap : heaps.values())
|
|
if (!heap.seen && fieldOfView[heap.pos] && c == Dungeon.hero)
|
|
heap.seen = true;
|
|
|
|
return fieldOfView;
|
|
}
|
|
|
|
public static int distance( int a, int b ) {
|
|
int ax = a % WIDTH;
|
|
int ay = a / WIDTH;
|
|
int bx = b % WIDTH;
|
|
int by = b / WIDTH;
|
|
return Math.max( Math.abs( ax - bx ), Math.abs( ay - by ) );
|
|
}
|
|
|
|
public static boolean adjacent( int a, int b ) {
|
|
int diff = Math.abs( a - b );
|
|
return diff == 1 || diff == WIDTH || diff == WIDTH + 1 || diff == WIDTH - 1;
|
|
}
|
|
|
|
//returns true if the input is a valid tile within the level
|
|
public static boolean insideMap( int tile ){
|
|
//outside map array
|
|
return !((tile <= -1 || tile >= LENGTH) ||
|
|
//top and bottom row
|
|
(tile <= 31 || tile >= LENGTH - WIDTH) ||
|
|
//left and right column
|
|
(tile % WIDTH == 0 || tile % WIDTH == 31));
|
|
}
|
|
|
|
public String tileName( int tile ) {
|
|
|
|
if (tile >= Terrain.WATER_TILES) {
|
|
return tileName( Terrain.WATER );
|
|
}
|
|
|
|
if (tile != Terrain.CHASM && (Terrain.flags[tile] & Terrain.PIT) != 0) {
|
|
return tileName( Terrain.CHASM );
|
|
}
|
|
|
|
switch (tile) {
|
|
case Terrain.CHASM:
|
|
return Messages.get(Level.class, "chasm_name");
|
|
case Terrain.EMPTY:
|
|
case Terrain.EMPTY_SP:
|
|
case Terrain.EMPTY_DECO:
|
|
case Terrain.SECRET_TRAP:
|
|
return Messages.get(Level.class, "floor_name");
|
|
case Terrain.GRASS:
|
|
return Messages.get(Level.class, "grass_name");
|
|
case Terrain.WATER:
|
|
return Messages.get(Level.class, "water_name");
|
|
case Terrain.WALL:
|
|
case Terrain.WALL_DECO:
|
|
case Terrain.SECRET_DOOR:
|
|
return Messages.get(Level.class, "wall_name");
|
|
case Terrain.DOOR:
|
|
return Messages.get(Level.class, "closed_door_name");
|
|
case Terrain.OPEN_DOOR:
|
|
return Messages.get(Level.class, "open_door_name");
|
|
case Terrain.ENTRANCE:
|
|
return Messages.get(Level.class, "entrace_name");
|
|
case Terrain.EXIT:
|
|
return Messages.get(Level.class, "exit_name");
|
|
case Terrain.EMBERS:
|
|
return Messages.get(Level.class, "embers_name");
|
|
case Terrain.LOCKED_DOOR:
|
|
return Messages.get(Level.class, "locked_door_name");
|
|
case Terrain.PEDESTAL:
|
|
return Messages.get(Level.class, "pedestal_name");
|
|
case Terrain.BARRICADE:
|
|
return Messages.get(Level.class, "barricade_name");
|
|
case Terrain.HIGH_GRASS:
|
|
return Messages.get(Level.class, "high_grass_name");
|
|
case Terrain.LOCKED_EXIT:
|
|
return Messages.get(Level.class, "locked_exit_name");
|
|
case Terrain.UNLOCKED_EXIT:
|
|
return Messages.get(Level.class, "unlocked_exit_name");
|
|
case Terrain.SIGN:
|
|
return Messages.get(Level.class, "sign_name");
|
|
case Terrain.WELL:
|
|
return Messages.get(Level.class, "well_name");
|
|
case Terrain.EMPTY_WELL:
|
|
return Messages.get(Level.class, "empty_well_name");
|
|
case Terrain.STATUE:
|
|
case Terrain.STATUE_SP:
|
|
return Messages.get(Level.class, "statue_name");
|
|
case Terrain.INACTIVE_TRAP:
|
|
return Messages.get(Level.class, "inactive_trap_name");
|
|
case Terrain.BOOKSHELF:
|
|
return Messages.get(Level.class, "bookshelf_name");
|
|
case Terrain.ALCHEMY:
|
|
return Messages.get(Level.class, "alchemy_name");
|
|
default:
|
|
return Messages.get(Level.class, "default_name");
|
|
}
|
|
}
|
|
|
|
public String tileDesc( int tile ) {
|
|
|
|
switch (tile) {
|
|
case Terrain.CHASM:
|
|
return Messages.get(Level.class, "chasm_desc");
|
|
case Terrain.WATER:
|
|
return Messages.get(Level.class, "water_desc");
|
|
case Terrain.ENTRANCE:
|
|
return Messages.get(Level.class, "entrance_desc");
|
|
case Terrain.EXIT:
|
|
case Terrain.UNLOCKED_EXIT:
|
|
return Messages.get(Level.class, "exit_desc");
|
|
case Terrain.EMBERS:
|
|
return Messages.get(Level.class, "embers_desc");
|
|
case Terrain.HIGH_GRASS:
|
|
return Messages.get(Level.class, "high_grass_desc");
|
|
case Terrain.LOCKED_DOOR:
|
|
return Messages.get(Level.class, "locked_door_desc");
|
|
case Terrain.LOCKED_EXIT:
|
|
return Messages.get(Level.class, "locked_exit_desc");
|
|
case Terrain.BARRICADE:
|
|
return Messages.get(Level.class, "barricade_desc");
|
|
case Terrain.SIGN:
|
|
return Messages.get(Level.class, "sign_desc");
|
|
case Terrain.INACTIVE_TRAP:
|
|
return Messages.get(Level.class, "inactive_trap_desc");
|
|
case Terrain.STATUE:
|
|
case Terrain.STATUE_SP:
|
|
return Messages.get(Level.class, "statue_desc");
|
|
case Terrain.ALCHEMY:
|
|
return Messages.get(Level.class, "alchemy_desc");
|
|
case Terrain.EMPTY_WELL:
|
|
return Messages.get(Level.class, "empty_well_desc");
|
|
default:
|
|
if (tile >= Terrain.WATER_TILES) {
|
|
return tileDesc( Terrain.WATER );
|
|
}
|
|
if ((Terrain.flags[tile] & Terrain.PIT) != 0) {
|
|
return tileDesc( Terrain.CHASM );
|
|
}
|
|
return Messages.get(Level.class, "default_desc");
|
|
}
|
|
}
|
|
}
|