/* * Copyright 2012-2013 Ivan Gadzhega * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package net.ivang.axonix.main.actors.game.level; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.math.*; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Group; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; import net.ivang.axonix.main.actors.game.level.blocks.Block; import net.ivang.axonix.main.actors.game.level.blocks.BlocksParticlesHolder; import net.ivang.axonix.main.actors.game.level.bonuses.Bonus; import net.ivang.axonix.main.actors.game.level.bonuses.LifeBonus; import net.ivang.axonix.main.actors.game.level.bonuses.SlowBonus; import net.ivang.axonix.main.actors.game.level.bonuses.SpeedBonus; import net.ivang.axonix.main.actors.game.level.enemies.BlueEnemy; import net.ivang.axonix.main.actors.game.level.enemies.Enemy; import net.ivang.axonix.main.actors.game.level.enemies.PurpleEnemy; import net.ivang.axonix.main.actors.game.level.enemies.RedEnemy; import net.ivang.axonix.main.events.facts.EnemyBounceFact; import net.ivang.axonix.main.events.facts.ObtainedPointsFact; import net.ivang.axonix.main.events.facts.TailBlockFact; import net.ivang.axonix.main.events.facts.level.LevelProgressFact; import net.ivang.axonix.main.events.facts.level.LevelScoreFact; import net.ivang.axonix.main.events.intents.game.DestroyBlockIntent; import net.ivang.axonix.main.events.intents.game.LevelScoreIntent; import net.ivang.axonix.main.events.intents.game.NotificationIntent; import net.ivang.axonix.main.screens.GameScreen; import java.util.*; import static net.ivang.axonix.main.actors.game.KinematicActor.Direction; import static net.ivang.axonix.main.actors.game.level.blocks.Block.Type; /** * @author Ivan Gadzhega * @since 0.1 */ public class Level extends Group { private State state; private EventBus eventBus; private int mapWidth; private int mapHeight; private Block[][] levelMap; private int levelIndex; private int score; private byte percentComplete; private int filledBlocks; private Protagonist protagonist; private List<Enemy> enemies; private List<Block> tailBlocks; private Group bonuses; private BlocksParticlesHolder blocksParticles; private boolean containsRedBlocks; private float redBlocksDelta; private Skin skin; @Inject public Level(int levelIndex, Pixmap pixmap, Skin skin, EventBus eventBus) { // register with the event bus this.eventBus = eventBus; eventBus.register(this); this.skin = skin; this.mapWidth = pixmap.getWidth(); this.mapHeight = pixmap.getHeight(); this.levelMap = new Block[mapWidth][mapHeight]; this.tailBlocks = new ArrayList<Block>(); this.enemies = new ArrayList<Enemy>(); this.bonuses = new Group(); this.blocksParticles = new BlocksParticlesHolder(skin, eventBus); initFromPixmap(pixmap); addActor(blocksParticles); addActor(bonuses); setScore(0); updateLevelProgress(0); this.levelIndex = levelIndex; String level = Integer.toString(levelIndex); showNotification("Level " + level + ". Go-go-go!", 0.25f, 1.5f); } private void initFromPixmap(Pixmap pixmap) { final int BLOCK_BLUE_HARD = 0x000055; final int ENEMY_RED = 0xFF0000; final int ENEMY_PURPLE = 0xFF00FF; final int ENEMY_BLUE_U = 0x0000FC; final int ENEMY_BLUE_R = 0x0000FD; final int ENEMY_BLUE_D = 0x0000FE; final int ENEMY_BLUE_L = 0x0000FF; final int PROTAGONIST = 0x00FF00; for (int x = 0; x < mapWidth; x++) { for (int y = 0; y < mapHeight; y++) { int pix = (pixmap.getPixel(x, mapHeight-y-1) >>> 8) & 0xffffff; switch (pix) { case BLOCK_BLUE_HARD: case PROTAGONIST: levelMap[x][y] = new Block(x, y, Type.BLUE_HARD, skin); break; default: levelMap[x][y] = new Block(x, y, Type.EMPTY, skin); break; } addActor(levelMap[x][y]); switch (pix) { case PROTAGONIST: protagonist = new Protagonist(x + 0.5f, y + 0.5f, this, skin, eventBus); break; case ENEMY_RED: Enemy redEnemy = new RedEnemy(x + 0.5f, y + 0.5f, skin, eventBus); enemies.add(redEnemy); break; case ENEMY_PURPLE: Enemy purpleEnemy = new PurpleEnemy(x + 0.5f, y + 0.5f, skin, eventBus); enemies.add(purpleEnemy); break; case ENEMY_BLUE_U: Enemy blueEnemyU = new BlueEnemy(x + 0.5f, y + 0.8f, skin, Direction.UP, eventBus); enemies.add(blueEnemyU); break; case ENEMY_BLUE_R: Enemy blueEnemyR = new BlueEnemy(x + 0.8f, y + 0.5f, skin, Direction.RIGHT, eventBus); enemies.add(blueEnemyR); break; case ENEMY_BLUE_D: Enemy blueEnemyD = new BlueEnemy(x + 0.5f, y + 0.2f, skin, Direction.DOWN, eventBus); enemies.add(blueEnemyD); break; case ENEMY_BLUE_L: Enemy blueEnemyL = new BlueEnemy(x + 0.2f, y + 0.5f, skin, Direction.LEFT, eventBus); enemies.add(blueEnemyL); break; } } } addActor(protagonist); for (Enemy enemy : enemies) { addActor(enemy); } } @Override public void act(float delta) { if (hasState(State.PLAYING)) { super.act(delta); checkTail(delta); checkEnemies(delta); checkProtagonist(); checkPercentComplete(); } } public void unregister() { eventBus.unregister(this); eventBus.unregister(blocksParticles); eventBus.unregister(protagonist); for (Enemy enemy : enemies) { eventBus.unregister(enemy); } } //--------------------------------------------------------------------- // Subscribers //--------------------------------------------------------------------- @Subscribe @SuppressWarnings("unused") public void onGameScreenStateChange(GameScreen.State gameScreenState) { switch (gameScreenState) { case PLAYING: setState(State.PLAYING); break; case PAUSED: case GAME_OVER: setState(State.PAUSED); break; } } @Subscribe @SuppressWarnings("unused") public void onProtagonistStateChange(Protagonist.State protagonistState) { switch (protagonistState) { case DYING: clearTail(Type.EMPTY); } } @Subscribe @SuppressWarnings("unused") public void onScoreChange(LevelScoreIntent event) { int scoreDelta = event.getScoreDelta(); setScore(score + scoreDelta); if (scoreDelta > 0) { showObtainedPoints(scoreDelta); } } @Subscribe @SuppressWarnings("unused") public void destroyBlock(DestroyBlockIntent intent) { Block block = intent.getBlock(); block.setType(Type.EMPTY); // update the adjacent blocks int bx = (int) block.getX(); int by = (int) block.getY(); for (int i = bx - 1; i <= bx + 1; i++) { for (int j = by - 1; j <= by + 1; j++) { Block adjacentBlock = getBlock(i, j); if (adjacentBlock.hasType(Type.GREEN)) { adjacentBlock.setType(Type.BLUE); } } } // update score and progress eventBus.post(new LevelScoreIntent(-1)); updateLevelProgress(-1); } //--------------------------------------------------------------------- // Helper methods //--------------------------------------------------------------------- @SuppressWarnings("StatementWithEmptyBody") private void checkTail(float delta) { if (containsRedBlocks) { redBlocksDelta += delta; float interval = 1 / (protagonist.getSpeed() * 3); if (redBlocksDelta > interval) { redBlocksDelta = 0; int tailSize = tailBlocks.size() - 1; for (int i = 0; i <= tailSize; i++) { if (tailBlocks.get(i).hasType(Type.RED)) { // burn previous block if (i > 0) tailBlocks.get(i-1).setType(Type.RED); // skip consequent red blocks while (i < tailSize && tailBlocks.get(++i).hasType(Type.RED)); // check if we caught up the protagonist if (i == tailSize && tailBlocks.get(i).hasType(Type.RED)) { protagonist.setState(Protagonist.State.DYING); } else { tailBlocks.get(i).setType(Type.RED); } } } } } } private void checkEnemies(float delta) { for (Enemy enemy : enemies) { checkEnemyCollisionWithProtagonist(enemy); checkEnemyCollisionsWithBonuses(enemy); checkEnemyCollisionsWithBlocks(enemy, delta); } } private void checkEnemyCollisionWithProtagonist(Enemy enemy) { Circle enemyCircle = enemy.getCollisionCircle(); Circle protagonistCircle = protagonist.getCollisionCircle(); if (Intersector.overlapCircles(enemyCircle, protagonistCircle)) { protagonist.setState(Protagonist.State.DYING); } } private void checkEnemyCollisionsWithBonuses(Enemy enemy) { for (Actor actor : bonuses.getChildren()) { Bonus bonus = (Bonus) actor; if (Intersector.overlapCircles(enemy.getCollisionCircle(), bonus.getCollisionCircle())) { bonus.removeSmoothly(); } } } private void checkEnemyCollisionsWithBlocks(Enemy enemy, float delta) { if (enemy.isBouncingOffBlocks()) { checkBouncingEnemyCollisionsWithBlocks(enemy); } else { checkCrawlingEnemyCollisionsWithBlocks(enemy, delta); } } private void checkBouncingEnemyCollisionsWithBlocks(Enemy enemy) { Vector2 direction = enemy.getDirection(); Vector2 signum = new Vector2(Math.signum(direction.x), Math.signum(direction.y)); Block b1 = getBlock(enemy.getX() + signum.x, enemy.getY()); Block b2 = getBlock(enemy.getX(), enemy.getY() + signum.y); Block b3 = getBlock(enemy.getX() + signum.x, enemy.getY() + signum.y); List<Block> collisions = new ArrayList<Block>(); if (!b1.isEmpty()) { Rectangle r1 = b1.getCollisionRectangle(); if (Intersector.overlapCircleRectangle(enemy.getCollisionCircle(), r1)) { collisions.add(b1); direction.x = - direction.x; } } if (!b2.isEmpty()) { Rectangle r2 = b2.getCollisionRectangle(); if (Intersector.overlapCircleRectangle(enemy.getCollisionCircle(), r2)) { collisions.add(b2); direction.y = - direction.y; } } if (collisions.isEmpty() && !b3.isEmpty()) { Rectangle r3 = b3.getCollisionRectangle(); if (Intersector.overlapCircleRectangle(enemy.getCollisionCircle(), r3)) { collisions.add(b3); direction.x = - direction.x; direction.y = - direction.y; } } if (!collisions.isEmpty()) { // direction has changed eventBus.post(new EnemyBounceFact(direction)); // burn tail for (Block block : collisions) { switch (block.getType()) { case TAIL: block.setType(Type.RED); containsRedBlocks = true; break; case BLUE: case GREEN: if (enemy.isDestroyingBlocks()) { eventBus.post(new DestroyBlockIntent(block)); } break; } } } } private void checkCrawlingEnemyCollisionsWithBlocks(Enemy enemy, float delta) { float dx = enemy.getDirection().x; float dy = enemy.getDirection().y; float speed = enemy.getSpeed(); // next block float nx = enemy.getX() + delta * speed * dx; float ny = enemy.getY() + delta * speed * dy; Block nextBlock = getBlock(nx, ny); // check whether enemy should turn in CW/CCW direction int cwFactor = enemy.isMovingClockwise() ? 1: -1; if (!nextBlock.isEmpty()) { enemy.getDirection().set(cwFactor * dy, cwFactor * -dx); } else { // CW: left block (90 degrees) // CCW: right block (-90 degrees) float rx = enemy.getX() - cwFactor * dy; float ry = enemy.getY() + cwFactor * dx; Block rightBlock = getBlock(rx, ry); // CW: left rear block (135 degrees) // CCW: right rear block (-135 degrees) float rrx = enemy.getX() - 0.7f * dx - 0.7f * cwFactor * dy; float rry = enemy.getY() + 0.7f * cwFactor * dx - 0.7f * dy; Block rightRearBlock = getBlock(rrx , rry); // check whether enemy should turn in the opposite direction if (rightBlock.isEmpty() && !rightRearBlock.isEmpty()) { enemy.getDirection().set(cwFactor * -dy, cwFactor *dx); } } } private void checkProtagonist() { if(protagonist.hasState(Protagonist.State.ALIVE) && protagonist.isOnNewBlock()) { // check bonuses for (Actor actor : bonuses.getChildren()) { Bonus bonus = (Bonus) actor; if (bonus.isActive()) { Circle protagonistCircle = protagonist.getCollisionCircle(); Circle bonusCircle = bonus.getCollisionCircle(); if (Intersector.overlapCircles(protagonistCircle, bonusCircle)) { eventBus.post(bonus); bonus.removeSmoothly(); } } } // check blocks Block currentBlock = getBlock(protagonist.getX(), protagonist.getY()); switch (currentBlock.getType()) { case EMPTY: currentBlock.setType(Type.TAIL); float duration = 0.5f / protagonist.getSpeed(); currentBlock.addAction(Actions.sequence(Actions.delay(duration), Actions.fadeIn(duration))); tailBlocks.add(currentBlock); eventBus.post(new TailBlockFact()); break; case TAIL: protagonist.setState(Protagonist.State.DYING); break; case GREEN: case BLUE: case BLUE_HARD: Block prevBlock = getBlock(protagonist.getPrevX(), protagonist.getPrevY()); if (prevBlock.hasType(Type.TAIL)) { // convert tail int newBlocks = tailBlocks.size(); clearTail(Type.BLUE); // fill areas newBlocks += fillAreas(); // update level score float bonus = 1 + newBlocks / 200f; int obtainedPoints = (int) (newBlocks * bonus); eventBus.post(new LevelScoreIntent(obtainedPoints)); // update percentage updateLevelProgress(newBlocks); // add bonus with some probability addBonus(); } break; } } } private void checkPercentComplete() { if (percentComplete > 80) { setState(State.LEVEL_COMPLETED); } } /** * Fills fenced areas if they do not contain enemies. * * @return the number of filled blocks */ private int fillAreas() { // thanks to http://habrahabr.ru/post/119244/ byte[][] tmpState = new byte[mapWidth][mapHeight]; byte spotNum = 0; int blocks = 0; Map<Byte, List<Vector2>> spots = new HashMap<Byte, List<Vector2>>(); for(int i = 1; i < mapWidth - 1; i++) { for(int j = 1; j < mapHeight - 1; j++) { Block A = levelMap[i][j]; if (A.hasType(Type.EMPTY)) { byte B = tmpState[i][j-1]; byte C = tmpState[i-1][j]; if ( B == 0) { if (C == 0) { // New Spot spotNum++; tmpState[i][j] = spotNum; List<Vector2> spot = new ArrayList<Vector2>(); spot.add(new Vector2(i,j)); spots.put(spotNum, spot); } else { // C!=0 tmpState[i][j] = C; spots.get(C).add(new Vector2(i,j)); } } if (B != 0) { if(C == 0) { tmpState[i][j] = B; spots.get(B).add(new Vector2(i,j)); } else { // C != 0 tmpState[i][j] = B; spots.get(B).add(new Vector2(i,j)); if (B != C) { for(int m = 1; m < mapWidth - 1; m++) { for(int n = 1; n < mapHeight; n++) { if (tmpState[m][n] == C) { tmpState[m][n] = B; } } } spots.get(B).addAll(spots.get(C)); spots.remove(C); } } } } } } Iterator<Byte> iterator = spots.keySet().iterator(); while (iterator.hasNext()) { check_spot_points: for(Vector2 pos: spots.get(iterator.next())) { for (Enemy enemy : enemies) { if ((pos.x == (int) enemy.getX()) && (pos.y == (int) enemy.getY())) { iterator.remove(); break check_spot_points; } } } } for(List<Vector2> spot : spots.values()) { for(Vector2 pos : spot) { levelMap[(int) pos.x][(int) pos.y].setType(Type.GREEN); blocks++; } } return blocks; } private void showObtainedPoints(int points) { // calculate new position for label float protX = protagonist.getX(); float protY = protagonist.getY(); float labelX = (protX) * getScaleX() + this.getX(); float labelY = (protY) * getScaleY() + this.getY(); // movement distance and direction float moveY = ((protY > mapHeight/2) ? -3 : 3) * getScaleY(); // correct position if is on the right side boolean subtractBounds = protX > mapWidth/2; // post event eventBus.post(new ObtainedPointsFact(points, labelX, labelY, moveY, subtractBounds)); } private void showNotification(String text, float showDelay, float hideDelay) { eventBus.post(new NotificationIntent(text, showDelay, hideDelay)); } private void clearTail(Type newType) { for (Block block : tailBlocks) { block.setType(newType); } tailBlocks.clear(); containsRedBlocks = false; } private void addBonus() { float probability = 0.1f + (levelIndex * 0.01f); if (probability > MathUtils.random()) { int x = MathUtils.random(1, mapWidth - 2); int y = MathUtils.random(1, mapHeight - 2); switch (MathUtils.random(2)) { case 0: bonuses.addActor(new SpeedBonus(x + 0.5f, y + 0.5f, skin)); break; case 1: bonuses.addActor(new SlowBonus(x + 0.5f, y + 0.5f, skin)); break; case 2: bonuses.addActor(new LifeBonus(x + 0.5f, y + 0.5f, skin)); break; } } } private void updateLevelProgress(int blocksDelta) { filledBlocks += blocksDelta; percentComplete = (byte) (((float) filledBlocks / ((mapWidth - 2) * (mapHeight - 2))) * 100) ; eventBus.post(new LevelProgressFact(percentComplete)); } private boolean hasState(State state) { return this.state == state; } //--------------------------------------------------------------------- // Getters & Setters //--------------------------------------------------------------------- public Block getBlock(int x, int y) { return levelMap[x][y]; } public Block getBlock(float x, float y) { return getBlock((int) x, (int) y); } public float getMapWidth() { return mapWidth; } public float getMapHeight() { return mapHeight; } public int getScore() { return score; } public void setScore(int score) { this.score = score; eventBus.post(new LevelScoreFact(score)); } public void setState(State state) { this.state = state; eventBus.post(state); } public Protagonist getProtagonist() { return protagonist; } //--------------------------------------------------------------------- // Nested Classes //--------------------------------------------------------------------- public enum State { PLAYING, PAUSED, LEVEL_COMPLETED } }