package me.hammerle.supersnuvi.gamelogic; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.TreeSet; import java.util.function.Consumer; import java.util.stream.Collectors; import me.hammerle.snuviengine.api.Shader; import me.hammerle.snuviengine.api.Texture; import me.hammerle.snuviengine.api.TextureRenderer; import me.hammerle.snuviengine.util.Rectangle; import me.hammerle.snuviscript.code.Script; import me.hammerle.supersnuvi.Game; import me.hammerle.supersnuvi.Keys; import me.hammerle.supersnuvi.entity.Entity; import me.hammerle.supersnuvi.entity.EntityBuilder; import me.hammerle.supersnuvi.tiles.Location; import me.hammerle.supersnuvi.tiles.Tile; import me.hammerle.supersnuvi.util.CollisionObject; import me.hammerle.supersnuvi.util.Utils; public final class Level implements ILevel { public final static float ERROR = 1f / 65536f; public final static Texture TILES = new Texture("resources/tiles.png"); private final static Texture GUI = new Texture("resources/gui.png"); private final static TextureRenderer GUI_RENDERER = new TextureRenderer(60); private final boolean worldLoaded; private final LevelData data; private final String fileName; private final String name; private final HashMap entities = new HashMap<>(); private final LinkedList spawnQueue = new LinkedList<>(); private Entity hero; private int entityCounter = 0; private boolean shouldReset = false; private boolean done = true; // this will be reseted in resetLevel() private int souls; private int maxSouls; private float time = 0.0f; private float cameraX = 0.0f; private float cameraY = 0.0f; private float oldCameraX = 0.0f; private float oldCameraY = 0.0f; private final int meshSize = 16; private final int meshLayers; private final int meshWidth; private final int meshHeight; private final TextureRenderer[][][] meshes; //private int tries = 7; private TreeSet spawns = new TreeSet<>(); private Script levelScript = null; private final LinkedList messages = new LinkedList<>(); public Level(File f) { this.data = new LevelData(f); this.worldLoaded = data.load(); if(worldLoaded) { this.name = data.getString("name", "error"); meshLayers = data.getLayers() - 1; meshWidth = (int) Math.ceil((double) data.getWidth() / meshSize); meshHeight = (int) Math.ceil((double) data.getHeight() / meshSize); meshes = new TextureRenderer[meshLayers][meshWidth][meshHeight]; for(int l = 0; l < meshLayers; l++) { for(int mx = 0; mx < meshWidth; mx++) { for(int my = 0; my < meshHeight; my++) { meshes[l][mx][my] = new TextureRenderer(meshSize * meshSize * 2); } } } // debug stuff /*if(name.equals("01-Tech Demo")) { int l = data.getBackgroundIndex(); }*/ // end debug stuff maxSouls = 0; data.forEachInteractTile((x, y, tile) -> { Tile t = Game.get().getTile(tile); if(t != null) { maxSouls += t.getBottleScore(); } if(tile == 24) // start tile { spawns.add(new Point(x, y - 1)); } }); data.forEachEntity((x, y, tile) -> { if(tile == 1) { maxSouls++; } }, 0, data.getWidth(), 0, data.getHeight()); } else { this.name = f.getName(); maxSouls = 0; meshLayers = 0; meshWidth = 0; meshHeight = 0; meshes = new TextureRenderer[0][0][0]; } fileName = f.getName(); // there must be at least one spawn if(spawns.isEmpty()) { spawns.add(new Point(5, 5)); } // make sure hero is spawned before any script starts resetLevel(); String scriptName = f.getPath().replace(".map", ""); // mark current level as active to make currentLevel calls work Level l = Game.get().getCurrentLevel(); Game.get().setCurrentLevel(this); levelScript = Game.get().getParser().startScript(false, ".snuvi", scriptName); // call level reset here, because levelScript was null in resetLevel() callEvent("level_reset"); // mark previous level as active Game.get().setCurrentLevel(l); } // ------------------------------------------------------------------------- // basic stuff // ------------------------------------------------------------------------- @Override public Entity getHero() { return hero; } public String getFileName() { return fileName; } public String getName() { return name; } @Override public void finishLevel() { shouldReset = true; done = true; } public boolean shouldFinish() { return done; } public boolean shouldReset() { return shouldReset; } @Override public void scheduleReset() { shouldReset = true; } public boolean resetLevel() { Game.get().resetTiles(this); data.activateEntities(); souls = 0; time = 0.0f; shouldReset = false; done = false; Entity h = spawnHero(true); hero = h; entities.clear(); spawnEntity(hero); for(int l = 0; l < meshLayers; l++) { for(int x = 0; x < meshWidth; x++) { for(int y = 0; y < meshHeight; y++) { meshes[l][x][y].clear(); } } } messages.clear(); callEvent("level_reset"); return false; } @Override public void spawnEntity(Entity ent) { spawnQueue.add(ent); } public Entity spawnHero(boolean first) { Entity h = hero; Point p; if(h == null || hero.getX() < 0 || first) { // first spawn or out of map, use first spawn p = spawns.first(); } else { // hero is somewhere in the map, getting last spawn p = spawns.floor(new Point(Utils.toBlock(hero.getX()), Utils.toBlock(hero.getY()))); if(p == null) { p = spawns.first(); } } Entity newHero = EntityBuilder.buildHero(this, Utils.toCoord(p.getX()), Utils.toCoord(p.getY())); // reset the camera oldCameraX = -getViewX(newHero.getCenterX()); oldCameraY = -getViewY(newHero.getCenterY()); cameraX = oldCameraX; cameraY = oldCameraY; return newHero; } @Override public void increaseSouls(int score) { souls += score; } public int getCurrentBottles() { return souls; } public LevelData getData() { return data; } public float getTime() { return time; } @Override public int getWidth() { return data.getWidth(); } @Override public int getHeight() { return data.getHeight(); } // ------------------------------------------------------------------------- // tick // ------------------------------------------------------------------------- public void tick() { if(worldLoaded) { if(!messages.isEmpty() && Keys.ENTER.getTime() == 1) { messages.removeFirst(); } time += Game.SECS_PER_TICK; Game.get().tickTiles(); // doing entity logic first entities.values().removeIf(entity -> { entity.tick(); if(entity.getHealth().shouldDespawn()) { callEvent("entity_despawn", (sc) -> { sc.setVar("entity", entity); }, null); return true; } return false; }); if(!spawnQueue.isEmpty()) { spawnQueue.forEach(ent -> { entities.put(entityCounter++, ent); callEvent("entity_spawn", (sc) -> { sc.setVar("entity", ent); }, null); }); spawnQueue.clear(); } // calculate new camera position oldCameraX = cameraX; oldCameraY = cameraY; cameraX = -getViewX(hero.getCenterX()); cameraY = -getViewY(hero.getCenterY()); // entity spawn layer after camera update int startX = (int) (-cameraX / Tile.SIZE); int startY = (int) (-cameraY / Tile.SIZE); int endX = Math.min((int) Math.ceil((-cameraX + Shader.getViewWidth()) / Tile.SIZE), data.getWidth()); int endY = Math.min((int) Math.ceil((-cameraY + Shader.getViewHeight()) / Tile.SIZE), data.getWidth()); data.forEachEntity((x, y, tile) -> { if(tile > 0) { data.deactivateEntity(x, y); Entity ent = EntityBuilder.fromId(tile, this, Utils.toCoord(x), Utils.toCoord(y)); if(ent != null) { entities.put(entityCounter++, ent); } } }, startX, endX, startY, endY); } } public String formatBottles(int bottles) { char[] c = new char[5]; if(bottles <= 9) { c[0] = '0'; c[1] = (char) (bottles + '0'); } else if(bottles > 99) { c[0] = 'X'; c[1] = 'X'; } else { c[0] = (char) ((bottles / 10) + '0'); c[1] = (char) ((bottles % 10) + '0'); } c[2] = '/'; int currentMaxSouls = Math.max(bottles, maxSouls); if(currentMaxSouls <= 9) { c[3] = '0'; c[4] = (char) (currentMaxSouls + '0'); } else if(currentMaxSouls > 99) { c[3] = 'X'; c[4] = 'X'; } else { c[3] = (char) ((currentMaxSouls / 10) + '0'); c[4] = (char) ((currentMaxSouls % 10) + '0'); } return new String(c); } public String formatTime(float time) { if(time == -1.0f) { return "-----"; } else if(time >= 999.9f) { return "999.9"; } return String.format("%05.1f", time); } private float getViewX(float x) { x -= Shader.getViewWidth() >> 1; if(x < 0) { return 0; } float max = data.getWidth() * Tile.SIZE - Shader.getViewWidth(); if(x > max) { return max; } return x; } private float getViewY(float y) { y -= Shader.getViewHeight() >> 1; if(y < 0) { return 0; } float max = data.getHeight() * Tile.SIZE - Shader.getViewHeight(); if(y > max) { return max; } return y; } @Override public void updateTile(int layer, int x, int y) { if(layer == data.getBackgroundIndex() + 1) { // do not update changes on entity layer return; } if(layer > data.getBackgroundIndex()) { layer--; } meshes[layer][x / meshSize][y / meshSize].clear(); } @Override public void updateTile(int x, int y) { updateTile(data.getBackgroundIndex(), x, y); } private void drawMesh(int l, int tl, int mx, int my) { TextureRenderer tr = meshes[l][mx][my]; if(!tr.isBuilt()) { int tsx = mx * meshSize; int tsy = my * meshSize; int tex = Math.min(tsx + meshSize, data.getWidth()); int tey = Math.min(tsy + meshSize, data.getHeight()); for(int x = tsx; x < tex; x++) { for(int y = tsy; y < tey; y++) { Tile t = Game.get().getTile(data.getTile(tl, x, y)); if(t.shouldRender(x, y, this)) { float minX = x * Tile.SIZE + t.getOffsetX(); float minY = y * Tile.SIZE + t.getOffsetY(); tr.addRectangle(minX, minY, minX + t.getWidth(), minY + t.getHeight(), t.getTextureMinX(x, y, this) + ERROR, t.getTextureMinY(x, y, this) + ERROR, t.getTextureMaxX(x, y, this) - ERROR, t.getTextureMaxY(x, y, this) - ERROR); } } } tr.build(); } meshes[l][mx][my].draw(); } private String[] split(String s) { ArrayList list = new ArrayList<>(); int old = 0; int index = 0; while(index < s.length()) { switch(s.charAt(index)) { case '\n': list.add(s.substring(old, index)); list.add("\n"); old = index + 1; break; case ' ': list.add(s.substring(old, index)); old = index + 1; break; } index++; } if(old < s.length()) { list.add(s.substring(old, index)); } return list.toArray(new String[list.size()]); } public void renderTick(float lag) { if(worldLoaded) { float camX = Utils.interpolate(oldCameraX, cameraX, lag); float camY = Utils.interpolate(oldCameraY, cameraY, lag); Shader.translateTo(camX, camY); Shader.updateMatrix(); int startX = (int) (-camX / (meshSize * Tile.SIZE)); int startY = (int) (-camY / (meshSize * Tile.SIZE)); int endX = (int) Math.ceil((-camX + Shader.getViewWidth()) / (meshSize * Tile.SIZE)); int endY = (int) Math.ceil((-camY + Shader.getViewHeight()) / (meshSize * Tile.SIZE)); startX = Math.min(Math.max(startX, 0), meshWidth); startY = Math.min(Math.max(startY, 0), meshHeight); endX = Math.min(Math.max(endX, 0), meshWidth); endY = Math.min(Math.max(endY, 0), meshHeight); // background Shader.setColorEnabled(false); Shader.setTextureEnabled(true); Shader.setBlendingEnabled(true); TILES.bind(); int fromLayer = 0; int toLayer = data.getBackgroundIndex() + 1; for(int l = fromLayer; l < toLayer; l++) { for(int mx = startX; mx < endX; mx++) { for(int my = startY; my < endY; my++) { drawMesh(l, l, mx, my); } } } // entities for(Entity entity : entities.values()) { entity.renderTick(lag); } // foreground Shader.setColorEnabled(false); Shader.setTextureEnabled(true); Shader.setBlendingEnabled(true); TILES.bind(); fromLayer = toLayer + 1; toLayer = data.getLayers(); for(int l = fromLayer; l < toLayer; l++) { for(int mx = startX; mx < endX; mx++) { for(int my = startY; my < endY; my++) { drawMesh(l - 1, l, mx, my); } } } // menu rendering Shader.translateTo(0.0f, 0.0f); Shader.updateMatrix(); // grey background of clock and bottles float lineHeight = Shader.getFontRenderer().getHeight(); float lineWidth = Shader.getFontRenderer().getWidth(); Shader.setColorEnabled(true); Shader.setTextureEnabled(false); Shader.getColorRenderer().drawRectangle(0.0f, 0.0f, (lineWidth * 6.0f) + 10.0f, (lineHeight * 2.0f + 10.0f), 0x77000000); Shader.setTextureEnabled(true); float y = 5.0f; y = Shader.getFontRenderer().drawString(13.0f, y, formatBottles(souls)); Shader.getFontRenderer().drawString(13.0f, y, formatTime(time)); Shader.setColorEnabled(false); // draw messages if(!messages.isEmpty()) { String[] message = split(messages.getFirst()); int index = 0; ArrayList list = new ArrayList<>(); list.add(new StringBuilder()); float currentWidth = 0; float w = Shader.getViewWidth() - 26; for(String s : message) { if(s.equals("\n")) { currentWidth = w; continue; } Rectangle rec = Shader.getFontRenderer().getSize(s); // + lineWidth for the space if(currentWidth + rec.getWidth() + lineWidth < w) { currentWidth += rec.getWidth(); StringBuilder sb = list.get(index); if(sb.length() == 0) { sb.append(s); } else { sb.append(" "); sb.append(s); currentWidth += lineWidth; } } else { StringBuilder sb = new StringBuilder(); list.add(sb); index++; sb.append(s); currentWidth = rec.getWidth(); } } float height = list.size() * lineHeight; Shader.setColorEnabled(true); Shader.setTextureEnabled(false); float messageY = Shader.getViewHeight() - height - 26; Shader.getColorRenderer().drawRectangle(0.0f, messageY, Shader.getViewWidth(), Shader.getViewHeight(), 0x77000000); messageY += 13; Shader.setTextureEnabled(true); for(StringBuilder sb : list) { messageY = Shader.getFontRenderer().drawString(13.0f, messageY, sb.toString()); } Shader.setColorEnabled(false); } /*Shader.setColorEnabled(true); Shader.setTextureEnabled(false); Shader.getColorRenderer().drawRectangle(0.0f, 0.0f, (lineWidth * 6.0f) + 10.0f, (lineHeight * 2.0f + 10.0f), 0x77000000); Shader.setTextureEnabled(true); y = 5.0f; y = Shader.getFontRenderer().drawString(13.0f, y, formatBottles(souls)); Shader.getFontRenderer().drawString(13.0f, y, formatTime(time)); Shader.setColorEnabled(false);*/ GUI.bind(); GUI_RENDERER.clear(); int scale = Shader.getViewScale(); // bottles switch(scale) { case 1: GUI_RENDERER.addRectangle(6.0f, 4.0f, 12.0f, 14.0f, 0.0f, 0.046875f, 0.01171875f, 0.068359375f); break; case 2: GUI_RENDERER.addRectangle(6.0f, 4.0f, 12.0f, 14.0f, 0.01171875f, 0.046875f, 0.037109375f, 0.0859375f); break; default: GUI_RENDERER.addRectangle(6.0f, 4.0f, 12.0f, 14.0f, 0.037109375f, 0.046875f, 0.06640625f, 0.10546875f); break; } // clock switch(scale) { case 1: GUI_RENDERER.addRectangle(4.0f, y, 13.0f, y + 9.0f, 0.0f, 0.265625f, 0.017578125f, 0.283203125f); break; case 2: GUI_RENDERER.addRectangle(4.5f, y, 13.0f, y + 8.5f, 0.017578125f, 0.265625f, 0.05078125f, 0.298828125f); break; default: GUI_RENDERER.addRectangle(4.666666666f, y, 13.0f, y + 8.333333333f, 0.05078125f, 0.265625f, 0.099609375f, 0.314453125f); break; } float w = Shader.getViewWidth(); // gui background GUI_RENDERER.addRectangle(w - 111.0f, 0.0f, w - 44.0f, 24.0f, 0.0f, 0.0f, 0.130859375f, 0.046875f); GUI_RENDERER.addRectangle(w - 44.0f, 0.0f, w - 18.0f, 16.0f, 0.130859375f, 0.0f, 0.181640625f, 0.03125f); GUI_RENDERER.addRectangle(w - 76.0f, 24.0f, w - 45.0f, 57.0f, 0.068359375f, 0.046875f, 0.12890625f, 0.111328125f); // health mirror int healthFrame = (int) (hero.getHealth().getHealthPercent() * 7); float leftMirror = (7 - healthFrame) * 0.0625f; GUI_RENDERER.addRectangle(w - 39.0f, 8.0f, w - 7.0f, 46.0f, leftMirror, 0.15625f, leftMirror + 0.0625f, 0.23046875f); // energy float energy = hero.getEnergy().getEnergyPercent(); float fullEnd = w - 109.0f + 64.0f * energy; GUI_RENDERER.addRectangle(w - 109.0f, 13.0f, fullEnd, 21.0f, 0.0f, 0.140625f, 0.125f * energy, 0.15625f); GUI_RENDERER.addRectangle(fullEnd, 13.0f, w - 45.0f, 21.0f, 0.125f * energy, 0.125f, 0.125f, 0.140625f); // gui foreground GUI_RENDERER.addRectangle(w - 49.0f, 0.0f, w, 64.0f, 0.201171875f, 0.0f, 0.296875f, 0.125f); GUI_RENDERER.addRectangle(w - 109.0f, 15.0f, w - 106.0f, 18.0f, 0.15625f, 0.03125f, 0.162109375f, 0.037109375f); GUI_RENDERER.addRectangle(w - 97.0f, 15.0f, w - 92.0f, 20.0f, 0.1796875f, 0.03125f, 0.189453125f, 0.041015625f); // health number overlay GUI_RENDERER.addRectangle(w - 30.0f, 53.0f, w - 12.0f, 62.0f, leftMirror, 0.23828125f, leftMirror + 0.03515625f, 0.255859375f); GUI_RENDERER.build(); GUI_RENDERER.draw(); // dynamic clock hand Shader.setColorEnabled(true); Shader.setTextureEnabled(false); switch(scale) { case 1: Shader.translateTo(8.5f, y + 4.5f); break; case 2: Shader.translateTo(8.75f, y + 4.25f); break; default: Shader.translateTo(8.8333333333f, y + 4.16666667f); break; } Shader.rotate(-time * 72.0f); Shader.updateMatrix(); Shader.getColorRenderer().drawRectangle(-0.5f / scale, -0.5f / scale, 0.5f / scale, 4.0f - 0.5f * scale, 0xFF000000); } } // ------------------------------------------------------------------------- // collision box, interaction layer // ------------------------------------------------------------------------- private Tile getInteractionTile(int x, int y) { int i = data.getInteractionTile(x, y); if(i == -1) { return Game.FALLBACK_TILE; } return Game.get().getTile(i); } private CollisionObject getMovementBox(int x, int y) { int i = data.getInteractionTile(x, y); if(i == -1) { return CollisionObject.NULL_BOX; } return Game.get().getTile(i).getMovementBox(x, y, this).reset().offset(Utils.toCoord(x), Utils.toCoord(y)); } @Override public List getMovementBoxesAt(CollisionObject box, Entity not) { List boxes; if(not != null) { boxes = getEntitiesCollidingWith(not, box).stream().map(ent -> ent.getBox()).collect(Collectors.toList()); } else { boxes = new LinkedList<>(); } int startX = Utils.toBlock(box.getMinX()); int endX = Utils.toBlock(box.getMaxX()); int startY = Utils.toBlock(box.getMinY()); int endY = Utils.toBlock(box.getMaxY()); for(int x = startX; x <= endX; x++) { for(int y = startY; y <= endY; y++) { CollisionObject cb = getMovementBox(x, y); if(cb.mayCollide(box) && cb != CollisionObject.NULL_BOX) { boxes.add(cb.copy()); } } } return boxes; } private CollisionObject getCollisionBox(int x, int y) { int i = data.getInteractionTile(x, y); if(i == -1) { return CollisionObject.NULL_BOX; } Tile tile = Game.get().getTile(i); return tile.getCollisionBox(x, y, this).reset().offset(Utils.toCoord(x), Utils.toCoord(y)); } @Override public List getCollisionBoxesAt(CollisionObject cb) { LinkedList boxes = new LinkedList<>(); int startX = Utils.toBlock(cb.getMinX()); int endX = Utils.toBlock(cb.getMaxX()); int startY = Utils.toBlock(cb.getMinY()); int endY = Utils.toBlock(cb.getMaxY()); for(int x = startX; x <= endX; x++) { for(int y = startY; y <= endY; y++) { if(getCollisionBox(x, y).isColliding(cb)) { boxes.add(new Location(getInteractionTile(x, y), this, x, y)); } } } return boxes; } @Override public List getEntitiesCollidingWith(Entity not, CollisionObject cb) { return entities.values().stream().filter(ent -> ent != not && ent.getBox().isColliding(cb)).collect(Collectors.toList()); } @Override public void callEvent(String name, Consumer