/*
* Copyright (c) 2003-onwards Shaven Puppy Ltd
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'Shaven Puppy' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package worm.features;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import net.puppygames.applet.Game;
import org.w3c.dom.Element;
import worm.MapRenderer;
import worm.Res;
import worm.SandboxParams;
import worm.SurvivalParams;
import worm.Worm;
import worm.WormGameState;
import worm.Xmas;
import worm.effects.WeatherEmitter;
import worm.generator.BaseMapGenerator;
import worm.generator.MapTemplate;
import com.shavenpuppy.jglib.Resources;
import com.shavenpuppy.jglib.interpolators.LinearInterpolator;
import com.shavenpuppy.jglib.resources.Data;
import com.shavenpuppy.jglib.resources.Feature;
import com.shavenpuppy.jglib.resources.ResourceArray;
import com.shavenpuppy.jglib.util.Util;
import com.shavenpuppy.jglib.util.XMLUtil;
/**
* Describes a level
*/
public class LevelFeature extends Feature {
private static final long serialVersionUID = 1L;
/** Autonaming hints counter */
private static int hintCount;
private static final String ENDLESS_INTRO = "endless.intro";
private static final String ENDLESS_STORY = "endless.story";
private static final String ENDLESS_SURROUNDED_STORY = "endless.surrounded.story";
private static final String ENDLESS_BOSS_STORY = "endless.boss.story";
private static final String ENDLESS_SURROUNDED_BOSS_STORY = "endless.surrounded-boss.story";
private static final String SURVIVAL_INTRO = "survival.intro";
private static final String XMAS_INTRO = "xmas.intro";
public static final int MIN_SIZE = 20;
public static final int MAX_SIZE = 72;
/** Compass bias: the bases will be clustered in a particular part of the map and the spawnpoints opposite. -1 means central bases. */
public static final int[] BIAS = {6, 2, 0, 4, 5, 1, 7, 3, -1, -1, 2, 6, 4, 0, 3, 5, 1, 7, -1, -1, 0, 4, 6, 2, -1, 1, 7, 3, 5, -1, 0, 1, 2, 3, 4, 5, -1, 6, 7, -1};
private static final int ENDLESS_MARCH = 1000; // Each level, add this notional value to research
private static final ArrayList<LevelFeature> LEVELS = new ArrayList<LevelFeature>(50);
// Note use of @Data as we need to copy LevelFeatures for Endless and Survival modes
/** Index */
protected int index;
/** World */
@Data
protected String world;
/** Title */
@Data
protected String title;
/** Level coloration */
@Data
protected String colors;
/** Bosses */
protected ResourceArray bosses;
/** The stories */
protected StoryFeature[] storyFeature;
/** Scenery */
@Data
protected String scenery;
/** Hints */
protected HintFeature[] eventFeature;
/** Formation */
@Data
protected String formation;
/** Width */
protected int width;
/** Height */
protected int height;
/** Bias */
protected int bias;
/** Xmas level */
private boolean xmas;
/** Weather emitter */
@Data
protected String weather;
/*
* Transient data
*/
protected transient WorldFeature worldFeature;
protected transient LevelColorsFeature colorsFeature;
protected transient SceneryFeature sceneryFeature;
protected transient WeatherEmitter weatherFeature;
/**
* C'tor
*/
public LevelFeature() {
setAutoCreated();
}
/**
* C'tor
* @param name
*/
public LevelFeature(String name) {
super(name);
setAutoCreated();
setName("level."+name);
}
@Override
protected void doRegister() {
if (!xmas) {
index = LEVELS.size();
LEVELS.add(this);
}
}
@Override
protected void doDeregister() {
if (!xmas) {
LEVELS.remove(this);
}
}
@Override
public void load(Element element, Loader loader) throws Exception {
super.load(element, loader);
List<Element> children = XMLUtil.getChildren(element, "story");
storyFeature = new StoryFeature[children.size()];
int count = 0;
for (Element child : children) {
storyFeature[count] = new StoryFeature();
storyFeature[count].load(child, loader);
count ++;
}
List<Element> hintChildren = XMLUtil.getChildren(element, "event");
eventFeature = new HintFeature[hintChildren.size()];
int eventCount = 0;
for (Element child : hintChildren) {
if (child.hasAttribute("name")) {
eventFeature[eventCount] = new HintFeature(XMLUtil.getString(child, "name"));
eventFeature[eventCount].register();
} else {
eventFeature[eventCount] = new HintFeature();
eventFeature[eventCount].setName("hintfeature."+(hintCount++));
}
eventFeature[eventCount].load(child, loader);
Resources.put(eventFeature[eventCount]);
eventCount ++;
}
}
/**
* @return the events
*/
public HintFeature[] getEvents() {
return eventFeature;
}
/**
* @return the colors for this level
*/
public LevelColorsFeature getColors() {
return colorsFeature;
}
/**
* @param colorsFeature the colorsFeature to set
*/
public void setColors(LevelColorsFeature colorsFeature) {
this.colorsFeature = colorsFeature;
}
/**
* @return the world this level is in
*/
public WorldFeature getWorld() {
return worldFeature;
}
/**
* @return the number of levels
*/
public static int getNumLevels() {
return LEVELS.size();
}
/**
* Gets a campaign level by its index
* @param idx
* @return a level
*/
public static LevelFeature getLevel(int idx) {
return LEVELS.get(idx % LEVELS.size());
}
/**
* @return the bosses
*/
public ResourceArray getBosses() {
return bosses;
}
/**
* @return the story
*/
public StoryFeature[] getStories() {
return storyFeature;
}
@Override
protected void doCreate() {
super.doCreate();
for (StoryFeature element : storyFeature) {
element.create();
}
for (HintFeature element : eventFeature) {
element.create();
}
calculateDimensions();
assert weather == null || (weather != null && weatherFeature != null);
}
protected void calculateDimensions() {
int levelInWorld = index % WormGameState.LEVELS_IN_WORLD;
bias = getBias(index);
if (index <= 5) {
width = height = getMinHeight();
} else {
width = height = (int) LinearInterpolator.instance.interpolate
(
LinearInterpolator.instance.interpolate(MIN_SIZE, MAX_SIZE, index / WormGameState.LEVELS_IN_WORLD / 5.0f),
LinearInterpolator.instance.interpolate(MIN_SIZE, WormGameState.ABS_MAX_SIZE, (index / WormGameState.LEVELS_IN_WORLD + 1) / 5.0f),
levelInWorld / (float) (WormGameState.LEVELS_IN_WORLD - 1)
);
}
switch (getBias()) {
case -1:
height = Math.min(WormGameState.ABS_MAX_SIZE, (int)(height * 1.5f));
width = Math.min(WormGameState.ABS_MAX_SIZE, (int)(height * 1.5f));
break;
case 0:
case 4:
width = Math.min(WormGameState.ABS_MAX_SIZE, (int)(height * 1.25f));
break;
case 2:
case 6:
height = Math.min(WormGameState.ABS_MAX_SIZE, (int)(height * 1.25f));
break;
default:
// No adjustment
}
// Make sure map's an even width / height
height = Math.max(height, getMinHeight()) & 0xFFFE;
width = Math.max(width, getMinWidth()) & 0xFFFE;
}
/**
* @return the bias
*/
public int getBias() {
return bias;
}
private static int getMinWidth() {
int displayWidth = Game.getWidth();
int displayHeight = Game.getHeight();
int smallerDimension = Math.min(displayWidth, displayHeight);
int res = smallerDimension / Game.getScale();
int fits = displayWidth / res; // This is the largest number of pixels
return Math.max(MIN_SIZE, fits / MapRenderer.TILE_SIZE + 2);
}
private static int getMinHeight() {
int displayWidth = Game.getWidth();
int displayHeight = Game.getHeight();
int smallerDimension = Math.min(displayWidth, displayHeight);
int res = smallerDimension / Game.getScale();
int fits = displayHeight / res; // This is the largest number of pixels
return Math.max(MIN_SIZE, fits / MapRenderer.TILE_SIZE + 2);
}
@Override
protected void doDestroy() {
super.doDestroy();
for (StoryFeature element : storyFeature) {
element.destroy();
}
for (HintFeature element : eventFeature) {
element.destroy();
}
}
/**
* @return the title
*/
public String getTitle() {
return title;
}
public SceneryFeature getScenery() {
return sceneryFeature;
}
public MapTemplate getTemplate() {
return worldFeature.getTemplate();
}
public int getNumSpawnPoints() {
return formation.length();
}
/**
* Generates a random level
* @param level Level number
* @return an unnamed LevelFeature
*/
public static LevelFeature generateEndless(int level) {
Random r = new Random((long) level ^ Game.getPlayerSlot().getName().hashCode() << 32L); // Random, but always the same sequence
float rf = r.nextFloat();
int idx = (int)(rf * (LEVELS.size() - 1));
idx %= Worm.getMaxLevel(WormGameState.GAME_MODE_ENDLESS) + 1;
LevelFeature ret = new EndlessLevelFeature(level, getLevel(idx));
ret.create();
return ret;
}
/**
* Generates a survival level
* @param worldIndex World index, 0...4
* @param size Size in tiles
* @return an unnamed LevelFeature
*/
public static LevelFeature generateSurvival(SurvivalParams params) {
LevelFeature ret = new SurvivalLevelFeature(params);
ret.create();
return ret;
}
/**
* Generates an Xmas level
* @return an unnamed LevelFeature
*/
public static LevelFeature generateXmas() {
LevelFeature ret = new XmasLevelFeature();
ret.create();
return ret;
}
public static LevelFeature generateSandbox(SandboxParams params) {
LevelFeature ret = new SandboxLevelFeature(params);
ret.create();
return ret;
}
public String getFormation() {
return formation;
}
public boolean useFixedSpawnPoints() {
return true;
}
public GidrahFeature getAngryGidrah(int type) {
return worldFeature.getAngryGidrah(type);
}
public GidrahFeature getGidrah(int type) {
return worldFeature.getGidrah(type);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
private static int getBias(int level) {
if (level == -1) {
// Survival mode
return -1;
}
return BIAS[level % BIAS.length];
}
/**
* Endless levels
*/
public static class EndlessLevelFeature extends LevelFeature {
private static final long serialVersionUID = 1L;
private ArrayList<GidrahFeature> gidrahs = new ArrayList<GidrahFeature>(4);
private ArrayList<GidrahFeature> angryGidrahs = new ArrayList<GidrahFeature>(4);
private static boolean hasBosses(int level) {
if (level < 9) {
// No bosses for first 10 levels
return false;
} else if (level == 9 || level == 19) {
// Boss on levels 10, 20
return true;
} else if (level >= 20 && level <= 40) {
// Boss every 5 levels
return level % 5 == 4;
} else {
// Boss every 3 levels
return level % 3 == 2;
}
}
/**
* C'tor
*/
public EndlessLevelFeature(int level, LevelFeature src) {
index = level;
title = "LEVEL "+(level + 1);
colors = src.colors;
scenery = src.scenery;
world = src.world;
weather = src.weather;
weatherFeature = src.weatherFeature;
boolean central = BaseMapGenerator.isBaseCentralForLevel(level, WormGameState.GAME_MODE_ENDLESS);
boolean bosses = hasBosses(level);
String storyType;
if (level == 0) {
storyType = ENDLESS_INTRO;
} else {
if (bosses) {
if (central) {
storyType = ENDLESS_SURROUNDED_BOSS_STORY;
} else {
storyType = ENDLESS_BOSS_STORY;
}
} else if (central) {
storyType = ENDLESS_SURROUNDED_STORY;
} else {
storyType = ENDLESS_STORY;
}
}
storyFeature = new StoryFeature[] {(StoryFeature) Resources.get(storyType)};
eventFeature = new HintFeature[0]; // No events
// Spawnpoints are arranged in 10 level groups: small to begin with, up to a larger figure. We interpolate between them in the 10 intervening levels.
int levelInGroup = level % WormGameState.LEVELS_IN_WORLD;
int minSpawnpoints = level / WormGameState.LEVELS_IN_WORLD + 1; // 1, 2, 3, etc
int maxSpawnpoints = Math.min(14, minSpawnpoints + 4 + level / (WormGameState.LEVELS_IN_WORLD * 2)); // 5, 6, 7 etc
int numSpawnpoints = (int) LinearInterpolator.instance.interpolate(minSpawnpoints, maxSpawnpoints, levelInGroup / (WormGameState.LEVELS_IN_WORLD - 1.0f));
if (level > 0) {
numSpawnpoints ++;
}
if (level > 1) {
numSpawnpoints ++;
}
// Find out which gidrahs are unlocked
@SuppressWarnings("unchecked")
List<GidrahFeature>[] gidrahFeatures = new ArrayList[4];
@SuppressWarnings("unchecked")
List<GidrahFeature>[] angryGidrahFeatures = new ArrayList[4];
for (int i = 0; i < 4; i ++) {
gidrahFeatures[i] = new ArrayList<GidrahFeature>(4);
angryGidrahFeatures[i] = new ArrayList<GidrahFeature>(4);
ResourceArray ga = Res.getEndlessGidrahs(i);
ResourceArray aga = Res.getEndlessAngryGidrahs(i);
assert ga.getNumResources() == aga.getNumResources();
for (int j = 0; j < ga.getNumResources(); j ++) {
GidrahFeature gf = (GidrahFeature) ga.getResource(j);
if (gf != null && gf.isUnlocked()) {
gidrahFeatures[i].add(gf);
GidrahFeature agf = (GidrahFeature) aga.getResource(j);
if (agf != null && agf.isUnlocked()) {
angryGidrahFeatures[i].add(agf);
} else {
// Downgrade
angryGidrahFeatures[i].add(gf);
}
}
}
}
// Gradually adjust ratio of gidrahs over the 10 level group, biasing heavily towards difficult and exotic
int type4ratio = gidrahFeatures[3].size() == 0 ? 0 : levelInGroup * 2;
int type3ratio = gidrahFeatures[2].size() == 0 ? 0 : levelInGroup * 5;
int type2ratio = gidrahFeatures[1].size() == 0 ? 0 : levelInGroup * 10;
int type1ratio = 40 - (int) Math.min(20.0f, Worm.getGameState().getBasicDifficulty() * 20.0f); // Less rank and file the better you are
int total = type4ratio + type3ratio + type2ratio + type1ratio;
int numType4 = numSpawnpoints * type4ratio / total;
int numType3 = numSpawnpoints * type3ratio / total;
int numType2 = numSpawnpoints * type2ratio / total;
int numType1 = Math.max(0, numSpawnpoints - (numType4 + numType3 + numType2));
System.out.println("Ratios: 1:"+type1ratio+" 2:"+type2ratio+" 3:"+type3ratio+" 4:"+type4ratio);
System.out.println("Numbers: 1:"+numType1+" 2:"+numType2+" 3:"+numType3+" 4:"+numType4);
System.out.println("Num spawnpoints:"+numSpawnpoints);
int[] numOfEachType = {numType1, numType2, numType3, numType4};
for (int i = 0; i < 4; i ++) {
if (numOfEachType[i] > 0) {
// Pick a random type
int idx = Util.random(0, gidrahFeatures[i].size() - 1);
GidrahFeature gf = gidrahFeatures[i].get(idx);
GidrahFeature agf = angryGidrahFeatures[i].get(idx);
gidrahs.add(gf);
System.out.println("Gidrah Type "+i+" is "+gf);
angryGidrahs.add(agf);
} else {
// Pad with a null
gidrahs.add(null);
angryGidrahs.add(null);
}
}
// Ok, that's up to four types of gidrah. Allocate spawnpoints according to ratio
StringBuilder sb = new StringBuilder(numSpawnpoints);
for (int i = 0; i < numType1; i ++) {
sb.append('1');
}
for (int i = 0; i < numType2; i ++) {
sb.append('2');
}
for (int i = 0; i < numType3; i ++) {
sb.append('3');
}
for (int i = 0; i < numType4; i ++) {
sb.append('4');
}
formation = sb.toString();
System.out.println("Formation:" +formation);
}
@Override
public GidrahFeature getAngryGidrah(int type) {
GidrahFeature gf = angryGidrahs.get(type);
if (gf == null) {
return getGidrah(type);
}
return gf;
}
@Override
public GidrahFeature getGidrah(int type) {
return gidrahs.get(type);
}
}
/**
* Survival level
*/
public static class SurvivalLevelFeature extends LevelFeature {
private static final long serialVersionUID = 1L;
private ArrayList<GidrahFeature> gidrahs = new ArrayList<GidrahFeature>(4);
private ArrayList<GidrahFeature> angryGidrahs = new ArrayList<GidrahFeature>(4);
private MapTemplate template;
/**
* C'tor
*/
public SurvivalLevelFeature(SurvivalParams params) {
index = -1;
worldFeature = params.getWorld();
template = params.getTemplate();
title = "SURVIVAL";
// Pick random colours
LevelFeature src = LevelFeature.getLevel(Util.random(0, WormGameState.LEVELS_IN_WORLD - 1) + worldFeature.getIndex() * WormGameState.LEVELS_IN_WORLD);
colors = src.colors;
scenery = src.scenery;
weather = src.weather;
weatherFeature = src.weatherFeature;
world = worldFeature.getName();
storyFeature = new StoryFeature[] {(StoryFeature) Resources.get(SURVIVAL_INTRO)};
eventFeature = new HintFeature[0]; // No events
formation = "";
width = params.getSize();
height = params.getSize();
bias = -1;
}
@Override
public int getNumSpawnPoints() {
return width / 2; // Loads! Approx 1/8th of the edge is spawnpoints.
}
@Override
public MapTemplate getTemplate() {
return template;
}
@Override
protected void calculateDimensions() {
// Do nothing
}
@Override
public GidrahFeature getAngryGidrah(int type) {
GidrahFeature gf = angryGidrahs.get(type);
if (gf == null) {
return getGidrah(type);
}
return gf;
}
@Override
public GidrahFeature getGidrah(int type) {
GidrahFeature ret = gidrahs.get(type);
if (ret == null) {
throw new RuntimeException("Failed to get gidrah of type "+type);
}
return ret;
}
@Override
public boolean useFixedSpawnPoints() {
return false;
}
}
/**
* Xmas level
*/
public static class XmasLevelFeature extends LevelFeature {
private static final long serialVersionUID = 1L;
/**
* C'tor
*/
public XmasLevelFeature() {
index = -1;
title = "XMAS";
worldFeature = Resources.get(Xmas.XMAS_WORLD);
LevelFeature src = Resources.get(Xmas.XMAS_LEVEL);
colors = src.colors;
scenery = src.scenery;
weather = src.weather;
weatherFeature = src.weatherFeature;
world = worldFeature.getName();
storyFeature = src.getStories();
eventFeature = src.getEvents();
formation = "";
width = Xmas.XMAS_WIDTH;
height = Xmas.XMAS_HEIGHT;
bias = 6; // Base in the south
}
@Override
public int getNumSpawnPoints() {
return 1; // 1 stream of gids
}
@Override
public MapTemplate getTemplate() {
return worldFeature.getTemplate();
}
@Override
protected void calculateDimensions() {
// Do nothing
}
@Override
public GidrahFeature getAngryGidrah(int type) {
return null;
}
@Override
public GidrahFeature getGidrah(int type) {
return null;
}
}
/**
* Sandbox level
*/
public static class SandboxLevelFeature extends LevelFeature {
private static final long serialVersionUID = 1L;
private ArrayList<GidrahFeature> gidrahs = new ArrayList<GidrahFeature>(4);
private ArrayList<GidrahFeature> angryGidrahs = new ArrayList<GidrahFeature>(4);
private MapTemplate template;
/**
* C'tor
*/
public SandboxLevelFeature(SandboxParams params) {
index = -1;
worldFeature = params.getWorld();
template = params.getTemplate();
title = "SANDBOX";
// Pick random colours
LevelFeature src = LevelFeature.getLevel(Util.random(0, WormGameState.LEVELS_IN_WORLD - 1) + worldFeature.getIndex() * WormGameState.LEVELS_IN_WORLD);
colors = src.colors;
scenery = src.scenery;
weatherFeature = src.weatherFeature;
world = worldFeature.getName();
storyFeature = new StoryFeature[0]; // No story
eventFeature = new HintFeature[0]; // No events
formation = "";
width = params.getSize();
height = params.getSize();
bias = -1;
}
@Override
public int getNumSpawnPoints() {
return 0; // none, we're going to add them later somehow
}
@Override
public MapTemplate getTemplate() {
return template;
}
@Override
protected void calculateDimensions() {
// Do nothing
}
@Override
public GidrahFeature getAngryGidrah(int type) {
GidrahFeature gf = angryGidrahs.get(type);
if (gf == null) {
return getGidrah(type);
}
return gf;
}
@Override
public GidrahFeature getGidrah(int type) {
GidrahFeature ret = gidrahs.get(type);
if (ret == null) {
throw new RuntimeException("Failed to get gidrah of type "+type);
}
return ret;
}
@Override
public boolean useFixedSpawnPoints() {
return false;
}
}
/**
* @return the weather, if any
*/
public WeatherEmitter getWeather() {
return weatherFeature;
}
}