/* * 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.generator; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import net.puppygames.applet.Game; import org.lwjgl.util.Point; import org.lwjgl.util.ReadablePoint; import worm.IntGrid; import worm.MapRenderer; import worm.Worm; import worm.WormGameState; import worm.features.LevelFeature; import worm.path.Topology; import worm.tiles.Crystal; import worm.tiles.Ruin; import com.shavenpuppy.jglib.algorithms.Bresenham; import com.shavenpuppy.jglib.interpolators.CosineInterpolator; import com.shavenpuppy.jglib.interpolators.Interpolator; import com.shavenpuppy.jglib.interpolators.LinearInterpolator; import com.shavenpuppy.jglib.interpolators.SineInterpolator; import com.shavenpuppy.jglib.util.IntList; import com.shavenpuppy.jglib.util.Util; /** * Draws bases and spawnpoints in a standard way. */ public abstract class BaseMapGenerator extends AbstractMapGenerator { // public static void main(String[] args) { // for (int level = 0; level < 50; level ++) { // int num; // if (level % 10 == 9) { // num = BASE_CRYSTALS_BOSS; // } else if (LevelFeature.BIAS[level % LevelFeature.BIAS.length] == -1) { // num = BASE_CRYSTALS_CENTRAL; // } else { // num = BASE_CRYSTALS; // } // int worldIndex = level / WormGameState.LEVELS_IN_WORLD; // //int c = (int) ((CRYSTALS_PER_LEVEL[Math.min(CRYSTALS_PER_LEVEL.length - 1, worldIndex)] * level) + num + worldIndex * BASE_CRYSTALS_PER_WORLD); // int c = (int) ((BASE_CRYSTALS_PER_LEVEL * level) + num + worldIndex * BASE_CRYSTALS_PER_WORLD); // int c2 = (int) ((0.2f * level) + num + worldIndex * BASE_CRYSTALS_PER_WORLD); // System.out.print("Level "+level+": $"+(c * 1000)); // System.out.print(" $"+(c2 * 1000)); // System.out.println(" $"+((c - c2) * 1000)); // } // } private static final int MAX_ATTEMPTS = 500; private static final int MAX_MID_SPAWNERS = 4; private static final int SPAWN_POINT_MARGIN = 5; protected static final int OBSTACLE_MARGIN = 2; private static final int MIN_BASE_RANGE_TO_SPAWNPOINT = 20; private static final int MID_SPAWNERS_DIVISOR = 8; private static final int MID_SPAWNERS_THRESHOLD = 25; private static final int MIN_CRYSTAL_DISTANCE = 4; private static final int BASE_MARGIN = (MapRenderer.FADE_SIZE * 2) / 3; private static final int BASE_CRYSTALS_PER_WORLD = 1; private static final int BASE_CRYSTALS = 2; private static final int EXTRA_CRYSTALS_CENTRAL = 2; private static final int EXTRA_CRYSTALS_BOSS = 4; private static final int BASE_CRYSTALS_SURVIVAL = 0; private static final float BASE_CRYSTALS_PER_LEVEL = 0.5f; private static final float MAX_MONEY = 25000.0f; // unimpeded up to 25k.. private static final float ABS_MAX_MONEY = 25000.0f; // then squeezed up to 50k private int totalRuins, totalCrystals; /** * C'tor * @param template * @param level * @param levelFeature TODO */ public BaseMapGenerator(MapTemplate template, MapGeneratorParams mapGeneratorParams) { super(template, mapGeneratorParams); } public static boolean isBaseCentralForLevel(int level, int gameMode) { return gameMode == WormGameState.GAME_MODE_SURVIVAL || LevelFeature.getLevel(level).getBias() == -1; } @Override protected final void generateBases() { int ww = getWidth() - BASE_MARGIN * 2; int hh = getHeight() - BASE_MARGIN * 2; boolean[] used = new boolean[ww * hh]; int ox, oy; switch (levelFeature.getBias()) { case 0: ox = ww * 2 / 3; oy = hh / 3; break; case 1: ox = ww * 2 / 3; oy = hh * 2 / 3; break; case 2: ox = ww / 3; oy = hh * 2 / 3; break; case 3: ox = 0; oy = hh * 2 / 3; break; case 4: ox = 0; oy = hh / 3; break; case 5: ox = 0; oy = 0; break; case 6: ox = ww / 3; oy = 0; break; case 7: ox = ww * 2 / 3; oy = 0; break; default: assert false; case -1: ox = ww / 3; oy = hh / 3; break; } // Choose a set of points, then centre them according to level bias. int x = Util.random(3, ww / 3 - 6) + ox + BASE_MARGIN; int y = mapGeneratorParams.getGameMode() == WormGameState.GAME_MODE_XMAS ? BASE_MARGIN + 3 : Util.random(3, hh / 3 - 6) + oy + BASE_MARGIN; // Ensure not proximal to any other base. boolean ok; do { ok = isEmpty(x, y, used) && isEmpty(x + 1, y, used) && isEmpty(x + 1, y + 1, used) && isEmpty(x + 1, y + 2, used) && isEmpty(x + 2, y, used) && isEmpty(x + 2, y + 1, used) && isEmpty(x + 2, y + 2, used) && isEmpty(x, y + 2, used) && isEmpty(x, y + 1, used) ; Thread.yield(); } while (!ok); // Bases are 4 x 3 tiles for (int yy = 0; yy < 3; yy ++) { for (int xx = 0; xx < 4; xx ++) { setUsed(x + xx, y + yy, used); } } addBase(x, y); } private void setUsed(int x, int y, boolean[] used) { if (x < 0 || y < 0 || x >= getWidth() / 3 || y >= getHeight() / 3) { return; } used[x + y * (getWidth() / 3)] = true; } protected final boolean isEmpty(int x, int y, boolean[] used) { if (x < 0 || y < 0 || x >= getWidth() / 3 || y >= getHeight() / 3) { return true; } return !used[x + y * (getWidth() / 3)]; } protected abstract float getMinSpawnTunnelSize(); protected abstract float getMaxSpawnTunnelSize(); @Override protected void clean() { // Now find and fill any areas which are not either connected to a base or a spawn point IntGrid temp = new IntGrid(getWidth(), getHeight(), 0); // First, mark walls SolidCheck solidCheck = new SolidCheck() { @Override public boolean isSolid(int x, int y, int value) { return x < 0 || y < 0 || x >= getWidth() || y >= getHeight() || value == WALL || value == RUIN || value == IMPASSABLE || value == TOTAL_IMPASSABLE || value == RUIN_IMPASSABLE || value == CRYSTAL || value == SPECIAL_IMPASSABLE; } }; for (int y = 0; y < getHeight(); y ++) { for (int x = 0; x < getWidth(); x ++) { if (solidCheck.isSolid(x, y, getValue(x, y))) { temp.setValue(x, y, 1); } } } SolidCheck tempCheck = new SolidCheck() { @Override public boolean isSolid(int x, int y, int value) { return value == 1; } }; IntList path = new IntList(true, getWidth() + getHeight()); int[] steps = new int[1]; // Second, scan for (int y = 0; y < getHeight(); y ++) { for (int x = 0; x < getWidth(); x ++) { if (!tempCheck.isSolid(x, y, temp.getValue(x, y))) { // Find a path to any base or spawnpoint. If successful, flood fill temp with 1 so we don't // scan any more in this area. If unsuccessful, also fill the real map with wall, overwriting // any other tiles. boolean found = false; // First the bases for (Iterator<Point> i = getBases().iterator(); i.hasNext(); ) { Point p = i.next(); if (findPath(x, y, p.getX(), p.getY(), path, solidCheck, steps)) { // Yay! found a base. found = true; break; } } if (!found) { // Then spawnpoints for (Iterator<Point> i = getSpawnPoints().iterator(); i.hasNext(); ) { Point p = i.next(); if (findPath(x, y, p.getX(), p.getY(), path, solidCheck, steps)) { // Yay! found a spawnpoint. found = true; break; } } } if (!found) { // Flood fill the real map with wall floodFill(getMap(), x, y, WALL, solidCheck); } // Flood fill temp with wall floodFill(temp, x, y, 1, tempCheck); } } } } private final void floodFill(IntGrid grid, int x, int y, int value, SolidCheck check) { if (check.isSolid(x, y, grid.getValue(x, y))) { return; } else { grid.setValue(x, y, value); } int[] xx = new int[grid.getWidth() * grid.getHeight() * 2]; int[] yy = new int[grid.getWidth() * grid.getHeight() * 2]; IntGrid visited = new IntGrid(grid.getWidth(), grid.getHeight(), 0); visited.setValue(x, y, 1); int size = 1; xx[0] = x; yy[0] = y; while (size > 0) { size --; int xxx = xx[size]; int yyy = yy[size]; assert visited.getValue(xxx, yyy) ==1; // Push empty neighbours if (xxx < grid.getWidth() - 1 && visited.getValue(xxx + 1, yyy) == 0 && !check.isSolid(xxx + 1, yyy, grid.getValue(xxx + 1, yyy))) { visited.setValue(xxx + 1, yyy, 1); xx[size] = xxx + 1; yy[size] = yyy; grid.setValue(xxx + 1, yyy, value); size ++; } if (xxx > 0 && visited.getValue(xxx - 1, yyy) == 0 && !check.isSolid(xxx - 1, yyy, grid.getValue(xxx - 1, yyy))) { visited.setValue(xxx - 1, yyy, 1); xx[size] = xxx - 1; yy[size] = yyy; grid.setValue(xxx - 1, yyy, value); size ++; } if (yyy < grid.getHeight() - 1 && visited.getValue(xxx, yyy + 1) == 0 && !check.isSolid(xxx, yyy + 1, grid.getValue(xxx, yyy + 1))) { visited.setValue(xxx, yyy + 1, 1); xx[size] = xxx; yy[size] = yyy + 1; grid.setValue(xxx, yyy + 1, value); size ++; } if (yyy > 0 && visited.getValue(xxx, yyy - 1) == 0 & !check.isSolid(xxx, yyy, grid.getValue(xxx, yyy - 1))) { visited.setValue(xxx, yyy - 1, 1); xx[size] = xxx; yy[size] = yyy - 1; grid.setValue(xxx, yyy - 1, value); size ++; } } } @Override protected final void generateSpawnPoints() { // Number of central spawn points to generate is up to (level - 50) / 10; // the remaining spawn points are edge ones. // No central spawn point may be placed within 5 squares of any base. // Choose formation int totalSpawners = levelFeature.getNumSpawnPoints(); int totalMid = mapGeneratorParams.getGameMode() == WormGameState.GAME_MODE_CAMPAIGN ? Math.max(0, level - (MID_SPAWNERS_THRESHOLD - MID_SPAWNERS_DIVISOR)) / MID_SPAWNERS_DIVISOR : mapGeneratorParams.getGameMode() == WormGameState.GAME_MODE_ENDLESS ? (mapGeneratorParams.getWorldFeature().getIndex() > 1 ? Math.max(0, level - (MID_SPAWNERS_THRESHOLD - MID_SPAWNERS_DIVISOR)) / MID_SPAWNERS_DIVISOR : 0) : 0; int totalEdge = Math.max(0, totalSpawners - totalMid); assert totalSpawners > 0; assert totalEdge > 0; //System.out.println(totalEdge+", "+totalSpawners); for (int i = 0; i < totalEdge; i ++) { int x, y; do { // Pick a random edge int edge; // increasing probability as worlds progress that spawnpoints won't be at the biased edge... int bias = levelFeature.getBias(); switch (bias) { case 0: // Util.random(0, 20) < level / WormGameState.LEVELS_IN_WORLD gives us a slowly increasing chance ranging from 0:20 to 4:20 // of putting gidrahs on a non-biased edge if (Util.random(0, 20) < level / WormGameState.LEVELS_IN_WORLD) { edge = Util.random() < 0.5 ? 0 : 2; } else { edge = 3; } break; case 1: edge = Util.random() < 0.5 ? 2 : 3; break; case 2: if (Util.random(0, 20) < level / WormGameState.LEVELS_IN_WORLD) { edge = Util.random() < 0.5 ? 1 : 3; } else { edge = 2; } break; case 3: edge = Util.random() < 0.5 ? 1 : 2; break; case 4: if (Util.random(0, 20) < level / WormGameState.LEVELS_IN_WORLD) { edge = Util.random() < 0.5 ? 0 : 2; } else { edge = 1; } break; case 5: edge = Util.random() < 0.5 ? 0 : 1; break; case 6: if (Util.random(0, 20) < level / WormGameState.LEVELS_IN_WORLD && mapGeneratorParams.getGameMode() != WormGameState.GAME_MODE_XMAS) { // Xmas: always from the north. edge = Util.random() < 0.5 ? 1 : 3; } else { edge = 0; } break; case 7: edge = Util.random() < 0.5 ? 0 : 3; break; default: assert false; case -1: edge = Util.random(0, 3); break; } // Pick a point along the edge, excluding the corners switch (edge) { case 0: // NORTH if (bias == 1 || bias == 0 || bias == 7) { x = Util.random(1, getWidth() / 2 - 2); } else if (bias == 3 || bias == 4 || bias== 5) { x = Util.random(getWidth() / 2 + 2, getWidth() - 2); } else { x = Util.random(1, getWidth() - 2); } y = getHeight() - 1; break; case 1: // EAST x = getWidth() - 1; if (bias == 5 || bias == 6 || bias == 7) { y = Util.random(getHeight() / 2 + 2, getHeight() - 2); } else if (bias == 1 || bias == 2 || bias== 3) { y = Util.random(1, getHeight() / 2 - 2); } else { y = Util.random(1, getHeight() - 2); } break; case 2: // SOUTH if (bias == 1 || bias == 0 || bias == 7) { x = Util.random(1, getWidth() / 2 - 2); } else if (bias == 3 || bias == 4 || bias== 5) { x = Util.random(getWidth() / 2 + 2, getWidth() - 2); } else { x = Util.random(1, getWidth() - 2); } y = 0; break; case 3: // WEST x = 0; if (bias == 5 || bias == 6 || bias == 7) { y = Util.random(getHeight() / 2 + 2, getHeight() - 2); } else if (bias == 1 || bias == 2 || bias== 3) { y = Util.random(1, getHeight() / 2 - 2); } else { y = Util.random(1, getHeight() - 2); } break; default: assert false; x = 0; y = 0; } } while (getValue(x, y) == SPAWN); // Carve a line from the spawnpoint to the nearest base, until we reach FLOOR. ReadablePoint p = getClosestBase(x, y); drawLineWithStop(x, y, p.getX(), p.getY(), getMinSpawnTunnelSize(), getMaxSpawnTunnelSize()); addSpawnPoint(x, y); } // Minimum range to spawn point from a base: int minRange = (int) LinearInterpolator.instance.interpolate(Math.max(getWidth(), getHeight()) / 2, MIN_BASE_RANGE_TO_SPAWNPOINT, getLevel() / 50.0f); for (int i = 0; i < totalMid; i ++) { // Pick a random point int x = -1, y = -1; boolean ok = false; again: for (int attempts = 0; attempts < MAX_ATTEMPTS; attempts ++) { Point basePos = getBases().get(0); double angle = Util.random() * Math.PI * 2.0; double distance = Util.random(minRange, Math.max(minRange, (int) (minRange * (2.0 - Worm.getGameState().getBasicDifficulty())))); x = (int) (basePos.getX() + Math.cos(angle) * distance); y = (int) (basePos.getY() + Math.sin(angle) * distance); if (x < SPAWN_POINT_MARGIN || y < SPAWN_POINT_MARGIN || x >= getWidth() - SPAWN_POINT_MARGIN || y >= getHeight() - SPAWN_POINT_MARGIN) { continue; } // x = Util.random(SPAWN_POINT_MARGIN, getWidth() - SPAWN_POINT_MARGIN - 1); // y = Util.random(SPAWN_POINT_MARGIN, getHeight() - SPAWN_POINT_MARGIN - 1); // Ensure it's clear if (getValue(x, y) != FLOOR) { continue; } // Ensure not proximal to a wall for (int xx = x - 1; xx <= x + 1; xx ++) { for (int yy = y - 1; yy <= y + 1; yy ++) { if (getValue(xx, yy) == WALL) { continue again; } } } ok = true; break; } if (ok) { addSpawnPoint(x, y); } } } protected final void drawLine(int sx, int sy, int tx, int ty, float minSize, float maxSize, int value) { Bresenham bh = new Bresenham(); float startSize = Util.random() * (maxSize - minSize) + minSize; float endSize = Util.random() * (maxSize - minSize) + minSize; float steps = bh.plot(sx, sy, tx, ty); for (int step = 0; bh.next(); step ++) { int x = bh.getX(); int y = bh.getY(); drawArea(x, y, LinearInterpolator.instance.interpolate(startSize, endSize, step / steps), value); } } static final Interpolator INTERPOLATOR; static { INTERPOLATOR = new Interpolator() { private static final long serialVersionUID = 1L; @Override public float interpolate(float a, float b, float ratio) { if (ratio < 0.5f) { return SineInterpolator.instance.interpolate(a, b, ratio * 2.0f); } else { return SineInterpolator.instance.interpolate(b, a, (ratio - 0.5f) * 2.0f); } } }; } protected final void drawLineWithStop(int sx, int sy, int tx, int ty, float minSize, float maxSize) { Bresenham bh = new Bresenham(); bh.plot(sx, sy, tx, ty); for (int step = 0; bh.next(); step ++) { int x = bh.getX(); int y = bh.getY(); if (getValue(x, y) == FLOOR) { tx = x; ty = y; break; } } // Randomly wiggle between sx, sy and tx, ty, varying size as we go. If we encounter FLOOR, we're done, and we can then // go and carve floor on the points we traversed. List<Point> points = new ArrayList<Point>(Math.abs(tx - sx) + Math.abs(ty - sy)); double x = sx; double y = sy; points.add(new Point(sx, sy)); double sdx = tx - sx; double sdy = ty - sy; double originalDistance = Math.sqrt(sdx * sdx + sdy * sdy); double currentAngle = Math.atan2(sdy, sdx); while (Math.abs(x - tx) > 1 || Math.abs(y - ty) > 1) { // Get angle to destination from where we are... double targetAngle = Math.atan2(ty - y, tx - x); // Deviation of this angle based on distance. When very far from or very near the destination, deviation is very small; // deviation is at its greatest when precisely halfway. double dx = x - tx; double dy = y - ty; double distance = Math.sqrt(dx * dx + dy * dy); double ratio = distance / originalDistance; // Current angle meanders all over the place... currentAngle += Util.random() * Math.PI / 2.0 - Math.PI / 4.0; // Now: become more and more like the direct angle, based on distance double difference = Util.getAngleDifference(currentAngle, targetAngle); double moveAngle = Util.moveToAngle((float) currentAngle, (float) targetAngle, INTERPOLATOR.interpolate((float) difference, 0.0f, (float) ratio)); // Move in this direction int oldx = (int) x; int oldy = (int) y; x += Math.cos(moveAngle); y += Math.sin(moveAngle); if (oldx == x && oldy == y) { continue; } Point p = new Point((int) x, (int) y); int idx = points.indexOf(p); if (idx == -1) { points.add(p); } else { points = new ArrayList<Point>(points.subList(0, idx)); } // Hit floor? We're done if (getValue((int) x, (int) y) == FLOOR) { break; } } float size = Util.random(); for (Iterator<Point> i = points.iterator(); i.hasNext(); ) { ReadablePoint p = i.next(); drawArea(p.getX(), p.getY(), LinearInterpolator.instance.interpolate(minSize, maxSize, size), FLOOR); int d = Util.random(0, 4); if (d == 0) { size = Math.max(minSize, size - 0.125f); } else if (d == 1) { size = Math.min(maxSize, size + 0.125f); } } } protected final void drawArea(int x, int y, float radius, int value) { for (int yy = (int) (y - radius); yy <= (int) (y + radius); yy ++) { for (int xx = (int) (x - radius); xx <= (int) (x + radius); xx ++) { float dx = xx - x; float dy = yy - y; if (Math.sqrt(dx * dx + dy * dy) <= radius && getValue(xx, yy) != BASE && getValue(xx, yy) != SPAWN) { setValue(xx, yy, value); } } } } @Override protected void generateObstacles() { // Just a random number of obstacles. int numObstacles = Util.random(scenery.getMinObstacles(), scenery.getMaxObstacles()); for (int i = 0; i < numObstacles; i ++) { int x = Util.random(OBSTACLE_MARGIN, getWidth() - OBSTACLE_MARGIN - 1); int y = Util.random(OBSTACLE_MARGIN, getHeight() - OBSTACLE_MARGIN - 1); if (getValue(x, y) == FLOOR) { setValue(x, y, OBSTACLE); } } } @Override protected void generateRuins() { totalRuins = 0; int numRuinAreas = Util.random(0, scenery.getMaxRuinClusters()); for (int i = 0; i < numRuinAreas; i ++) { int x = Util.random(0, getWidth()); int y = Util.random(0, getHeight()); float radius = Util.random() * getAreaScale() * (scenery.getMaxRuinClusterSize() - scenery.getMinRuinClusterSize()) + getAreaScale() * scenery.getMinRuinClusterSize(); generateRuinArea(x, y, radius); if (totalRuins >= (int) (scenery.getAbsMaxRuins() * getAreaScale())) { return; } } } /** * Generates an area of ruins * @param x * @param y * @param radius */ private void generateRuinArea(int x, int y, float radius) { int numRuins = Util.random((int) radius, (int) (scenery.getMaxRuins() * getAreaScale())); outer: for (int i = 0; i < numRuins; i ++) { // Pick a random point inside the radius... double angle = Util.random() * Math.PI * 2.0; double distance = Util.random() * radius; int xx = x + (int) (Math.cos(angle) * distance); int yy = y + (int) (Math.sin(angle) * distance); // Size is dependent on ratio of distance from radius int size = (int) LinearInterpolator.instance.interpolate(4.9f, 1.0f, (float) distance / radius); int width = 0; int height = 0; Ruin ruin = null; for (int j = 0; j < 10; j ++) { width = Util.random(1, size); height = Util.random(1, size); if (height > width) { int temp = width; width = height; height = temp; } ruin = scenery.getRuin(width, height); if (ruin == null) { continue; } } if (ruin == null) { continue; } // Ensure that we're clear to fit it in for (int yyy = yy; yyy < yy + height; yyy ++) { for (int xxx = xx; xxx < xx + width; xxx ++) { if (getValue(xxx, yyy) != FLOOR) { continue outer; } } } // Also ensure we're not within 5 squares of a base for (Iterator<Point> j = getBases().iterator(); j.hasNext(); ) { Point base = j.next(); int dx = base.getX() - xx; int dy = base.getY() - yy; if (Math.sqrt(dx * dx + dy * dy) < 5.0) { continue outer; } } // Ok, write ruin addRuin(xx, yy, width, height); totalRuins ++; if (totalRuins == (int) (scenery.getAbsMaxRuins() * getAreaScale())) { return; } } } @Override protected void generateCrystals() { totalCrystals = 0; if (Game.DEBUG) { System.out.println("Generating "+getNumCrystalsToCreate()+" crystals: min distance "+getCrystalMinDistance(1)+", max distance "+getCrystalMaxDistance(3)); } while (totalCrystals < getNumCrystalsToCreate()) { // pick a base //System.out.println("Total crystals now "+totalCrystals+" of "+getNumCrystalsToCreate()); List<Point> bases = getBases(); Point base = bases.get(Util.random(0, bases.size() - 1)); // Choose a distance from the base, higher the level, further away int x, y; int num = getNumCrystalsToCreate() - totalCrystals; int size = Math.min(Math.min(3, Util.random(1, 3 + num / 5)), num); // Bias toward larger crystals do { // larger crystals more likely to be further away. // Also take an average of 2 readings to get a bell shaped distribution - not too far, too too close float rand0 = Util.random(), rand1 = Util.random(); Interpolator i; switch (size) { case 1: i = CosineInterpolator.instance; break; case 2: i = LinearInterpolator.instance; break; case 3: i = SineInterpolator.instance; break; default: assert false; return; } rand0 = i.interpolate(0.0f, 1.0f, rand0); rand1 = i.interpolate(0.0f, 1.0f, rand1); double dist = ( LinearInterpolator.instance.interpolate(getCrystalMinDistance(size), getCrystalMaxDistance(size), rand0) + LinearInterpolator.instance.interpolate(getCrystalMinDistance(size), getCrystalMaxDistance(size), rand1) ) / 2.0; double angle = Util.random() * Math.PI * 2.0; x = base.getX() + (int) (Math.cos(angle) * dist); y = base.getY() + (int) (Math.sin(angle) * dist); } while (x < 5 || y < 5 || x >= getWidth() - 5 || y >= getHeight() - 5); generateCrystal(x, y, size); } } private int getCrystalMinDistance(int size) { return (int) LinearInterpolator.instance.interpolate(MIN_CRYSTAL_DISTANCE + size, MIN_CRYSTAL_DISTANCE * size + size, mapGeneratorParams.getBasicDifficulty()); } private int getCrystalMaxDistance(int size) { int crystalMinDistance = (int) (getCrystalMinDistance(size) * 1.5f); return (int) LinearInterpolator.instance.interpolate(crystalMinDistance, Math.max(crystalMinDistance, Math.max(getHeight(), getWidth()) * 0.7f), mapGeneratorParams.getBasicDifficulty()); } /** * Generates a crystal * @param x * @param y */ private void generateCrystal(int x, int y, int size) { Crystal crystal = null; for (int j = 0; j < 10; j ++) { crystal = scenery.getCrystal(size); if (crystal == null) { if (Game.DEBUG) { System.out.println("Warning: no crystal of size "+size+" available"); } continue; } } if (crystal == null) { return; } // Ensure that we're clear to fit it in int cw = crystal.getWidth(); int ch = crystal.getHeight(); for (int yyy = y; yyy < y + ch; yyy ++) { for (int xxx = x; xxx < x + cw; xxx ++) { if (getValue(xxx, yyy) != FLOOR || roadsOverlay.getValue(xxx, yyy) != 0) { return; } } } // Also ensure we're not within 4 squares of a base for (Iterator<Point> j = getBases().iterator(); j.hasNext(); ) { Point base = j.next(); int dx = base.getX() + 2 - x; int dy = base.getY() + 1 - y; if (Math.sqrt(dx * dx + dy * dy) < 4.0) { return; } } // Ok, write crystal if (addCrystal(x, y, size)) { totalCrystals += size; if (totalCrystals >= getNumCrystalsToCreate()) { return; } } } protected int getNumCrystalsToCreate() { int num; if (level == -1) { // Survival mode return BASE_CRYSTALS_SURVIVAL; } else if (level == 0) { return 0; // No crystals on first level } else { num = BASE_CRYSTALS; if (mapGeneratorParams.getLevelFeature().getBosses() != null) { num += EXTRA_CRYSTALS_BOSS + mapGeneratorParams.getLevelFeature().getBosses().getNumResources(); } if (levelFeature.getBias() == -1) { num += EXTRA_CRYSTALS_CENTRAL; } } int money = mapGeneratorParams.getMoney(); float ratio = money - MAX_MONEY > 0 ? Math.min(ABS_MAX_MONEY, (money - MAX_MONEY)) / ABS_MAX_MONEY : 0.0f; int worldIndex = level / WormGameState.LEVELS_IN_WORLD; float baseCrystals = BASE_CRYSTALS_PER_LEVEL * level + num + worldIndex * BASE_CRYSTALS_PER_WORLD; return (int) CosineInterpolator.instance.interpolate(baseCrystals, 1.0f, ratio); } protected float getAreaScale() { return (float) Math.sqrt(getWidth() * getHeight()) / LevelFeature.MIN_SIZE; } @Override protected void generateRoads() { // Get a collection of base coordinates and ruin coordinates and edges-of-map coordinates, // then randomly join them up. List<Point> points = new LinkedList<Point>(); // First get bases... List<Point> bases = getBases(); // Offset roads by random amounts Point basePoint = null; for (Iterator<Point> i = bases.iterator(); i.hasNext(); ) { Point p = i.next(); points.add(basePoint = new Point(p.getX() + Util.random(0, 3), p.getY() + 1)); } // Now the ruins... Map<Point, Ruin> ruins = getRuins(); int numRuins = 0; for (Entry<Point, Ruin> entry : ruins.entrySet()) { Point p = entry.getKey(); Ruin r = entry.getValue(); if (r.getRoads()) { // Choose a spawn point to draw the road to points.add(new Point(p.getX() + Util.random(-1, r.getWidth()), p.getY() + Util.random(-1, r.getHeight()))); numRuins ++; //System.out.println("ruins to draw roads from = "+numRuins); } else { //System.out.println("No road to this ruin "+p+","+r); } } // Add spawnpoints List<Point> spawnPoints = new ArrayList<Point>(getSpawnPoints()); for (Iterator<Point> i = spawnPoints.iterator(); i.hasNext(); ) { Point spawnPoint = i.next(); points.add(spawnPoint); } float startProgress = getProgress(); // Now randomly join up stuff int[] steps = new int[1]; // Ensure at least 1 road from base to a spawnpoint for (int i = 0; i < 1 + level / WormGameState.LEVELS_IN_WORLD && spawnPoints.size() > 0; i ++) { int idx = Util.random(0, spawnPoints.size() - 1); Point spawnPoint = spawnPoints.remove(idx); drawRoad(basePoint.getX(), basePoint.getY(), spawnPoint.getX(), spawnPoint.getY(), false, steps); } int edges = Util.random(1, (bases.size() + numRuins) / 2); if (points.size() + edges - 2 == 0) { // No roads return; } Collections.shuffle(points); for (int i = 0; i < points.size() - 1; i ++) { Point start = points.get(i); Point end = points.get(i + 1); boolean ok = drawRoad(start.getX(), start.getY(), end.getX(), end.getY(), false, steps); setProgress(LinearInterpolator.instance.interpolate(startProgress, ROADS_COMPLETED_PROGRESS, (float) i / (points.size() + edges - 2))); if (!ok) {// || Util.random() < 0.2) { // Break the road here i ++; } if (steps[0] > getMaxRoadSteps()) { // That's enough roads break; } } // // startProgress = getProgress(); // // Now pick some random spawn points // for (int i = 0; i < edges; i ++) { // int tx, ty; // switch (Util.random(0, 3)) { // case 0: // North // tx = Util.random(0, getWidth() / 2) + getWidth() / 2; // ty = 0; // break; // case 1: // South // tx = Util.random(0, getWidth() / 2) + getWidth() / 2; // ty = getHeight() - 1; // break; // case 2: // West // tx = 0; // ty = Util.random(0, getHeight() / 2) + getHeight() / 2; // break; // case 3: // East // tx = getWidth() - 1; // ty = Util.random(0, getHeight() / 2) + getHeight() / 2; // break; // default: // assert false; // return; // } // // // And randomly join up things to the edges // if (getValue(tx, ty) == FLOOR) { // Point s = (Point) points.get(Util.random(0, points.size() - 1)); // drawRoad(s.getX(), s.getY(), tx, ty, true, steps); // } // setProgress(LinearInterpolator.instance.interpolate(startProgress, ROADS_COMPLETED_PROGRESS, (float) i / (edges - 1))); // if (steps[0] > getMaxRoadSteps()) { // // That's enough roads // break; // } // } } private int getMaxRoadSteps() { return getWidth() * getHeight() * 10; } private boolean drawRoad(int sx, int sy, int tx, int ty, final boolean stopAtEdge, int[] steps) { sx /= 2; sx *= 2; sy /= 2; sy *= 2; tx /= 2; tx *= 2; ty /= 2; ty *= 2; IntList path = new IntList(getWidth() + getHeight()); path.add(IntGridTopology.pack(sx, sy)); if (!findPath(sx, sy, tx, ty, path, new Topology() { @Override public int getWidth() { return BaseMapGenerator.this.getWidth(); } @Override public int getHeight() { return BaseMapGenerator.this.getHeight(); } @Override public int getCost(int from, int to) { return 1; } @Override public int getDistance(int from, int to) { int fromX = IntGridTopology.getX(from); int fromY = IntGridTopology.getY(from); int toX = IntGridTopology.getX(to); int toY = IntGridTopology.getY(to); // Scale it a bit int h = Math.abs(fromX - toX) + Math.abs(fromY - toY); h *= 1.0f + 1.0f / WormGameState.ABS_MAX_SIZE; return h; } @Override public void getNeighbours(int node, int parent, IntList dest) { dest.clear(); int x = IntGridTopology.getX(node); int y = IntGridTopology.getY(node); boolean lrOK, udOK; if (x % 2 == 1 && y % 2 == 0) { // Only left and right possible lrOK = true; udOK = false; } else if (x % 2 == 0 && y % 2 == 1) { // Only up and down possible lrOK = false; udOK = true; } else { lrOK = true; udOK = true; } int n = IntGridTopology.pack(x, y + 1); int v = getMap().getValue(x, y + 1); if (n != parent && udOK && !isSolid(x, y + 1, v)) { dest.add(n); } n = IntGridTopology.pack(x, y - 1); v = getMap().getValue(x, y - 1); if (n != parent && udOK && !isSolid(x, y - 1, v)) { dest.add(n); } n = IntGridTopology.pack(x + 1, y); v = getMap().getValue(x + 1, y); if (n != parent && lrOK && !isSolid(x + 1, y, v)) { dest.add(n); } n = IntGridTopology.pack(x - 1, y); v = getMap().getValue(x - 1, y); if (n != parent && lrOK && !isSolid(x - 1, y, v)) { dest.add(n); } } boolean isSolid(int x, int y, int value) { return x < 0 || y < 0 || x > getWidth() - 1 || y > getHeight() - 1 || value == WALL || value == WALLEDGE || value == RUIN || value == CRYSTAL || value == RUIN_IMPASSABLE || value == IMPASSABLE || value == SPECIAL_IMPASSABLE; } }, steps)) { // System.out.println("Failed to plot road between "+sx+","+sy+","+tx+","+ty); return false; } // System.out.println("Plot road between "+sx+","+sy+","+tx+","+ty+":"+path.size()+" steps"); // Follow the path for (int i = 0; i < path.size(); i ++) { int p = path.get(i); int x = IntGridTopology.getX(p); int y = IntGridTopology.getY(p); if (getRoadsOverlay().getValue(x, y) == 1) { break; } getRoadsOverlay().setValue(x, y, 1); if (stopAtEdge && (x == 0 || y == 0 || x == getWidth() - 1 || y == getHeight() -1)) { break; } } getRoadsOverlay().setValue(sx, sy, 1); return true; } }