Browse Source

Merge branch 'master' of git.hammerle.me:kjhammerle/opengl-super-snuvi

Kajetan Johannes Hammerle 3 years ago
parent
commit
1957b4c16e
67 changed files with 3226 additions and 1764 deletions
  1. BIN
      levels/00-Tech_Demo.map
  2. 123 0
      levels/00-Tech_Demo.snuvi
  3. BIN
      levels/01-Tech_Demo.map
  4. 10 0
      levels/01-Tech_Demo.snuvi
  5. 0 0
      levels/02-Parabola.snuvi
  6. 0 0
      levels/03-Higher_Ground.snuvi
  7. 0 0
      levels/04-Out_of_Reach.snuvi
  8. 0 0
      levels/05-Like_Ice.snuvi
  9. BIN
      resources/platform.png
  10. BIN
      resources/platform.xcf
  11. 229 49
      src/me/hammerle/supersnuvi/Game.java
  12. 2 22
      src/me/hammerle/supersnuvi/Keys.java
  13. 477 230
      src/me/hammerle/supersnuvi/entity/Entity.java
  14. 85 31
      src/me/hammerle/supersnuvi/entity/EntityBuilder.java
  15. 18 0
      src/me/hammerle/supersnuvi/entity/components/BoxMovement.java
  16. 4 12
      src/me/hammerle/supersnuvi/entity/components/DefaultEnergy.java
  17. 3 23
      src/me/hammerle/supersnuvi/entity/components/DefaultHealth.java
  18. 15 18
      src/me/hammerle/supersnuvi/entity/components/DefaultMovement.java
  19. 2 9
      src/me/hammerle/supersnuvi/entity/components/Energy.java
  20. 2 16
      src/me/hammerle/supersnuvi/entity/components/Health.java
  21. 10 0
      src/me/hammerle/supersnuvi/entity/components/IDespawn.java
  22. 9 23
      src/me/hammerle/supersnuvi/entity/components/Movement.java
  23. 22 5
      src/me/hammerle/supersnuvi/entity/components/StoneMovement.java
  24. 45 0
      src/me/hammerle/supersnuvi/entity/components/ai/BoxController.java
  25. 7 13
      src/me/hammerle/supersnuvi/entity/components/ai/Controller.java
  26. 33 23
      src/me/hammerle/supersnuvi/entity/components/ai/HumanController.java
  27. 73 60
      src/me/hammerle/supersnuvi/entity/components/ai/LondonerController.java
  28. 135 0
      src/me/hammerle/supersnuvi/entity/components/ai/PlatformController.java
  29. 12 37
      src/me/hammerle/supersnuvi/entity/components/ai/StartScreenHeroController.java
  30. 5 8
      src/me/hammerle/supersnuvi/entity/components/ai/StoneController.java
  31. 217 0
      src/me/hammerle/supersnuvi/gamelogic/FileLevel.java
  32. 178 581
      src/me/hammerle/supersnuvi/gamelogic/Level.java
  33. 4 0
      src/me/hammerle/supersnuvi/gamelogic/LevelData.java
  34. 28 376
      src/me/hammerle/supersnuvi/gamelogic/StartScreenLevel.java
  35. 7 0
      src/me/hammerle/supersnuvi/math/IVector.java
  36. 95 0
      src/me/hammerle/supersnuvi/math/Vector.java
  37. 8 0
      src/me/hammerle/supersnuvi/rendering/IRenderer.java
  38. 491 0
      src/me/hammerle/supersnuvi/rendering/LevelRenderer.java
  39. 109 0
      src/me/hammerle/supersnuvi/rendering/Light.java
  40. 49 0
      src/me/hammerle/supersnuvi/rendering/TileUpdater.java
  41. 7 0
      src/me/hammerle/supersnuvi/rendering/UpdateConsumer.java
  42. 48 0
      src/me/hammerle/supersnuvi/snuviscript/SnuviLogger.java
  43. 85 0
      src/me/hammerle/supersnuvi/snuviscript/SnuviScheduler.java
  44. 29 1
      src/me/hammerle/supersnuvi/tiles/BaseBoxTile.java
  45. 41 0
      src/me/hammerle/supersnuvi/tiles/BaseCollisionTile.java
  46. 17 0
      src/me/hammerle/supersnuvi/tiles/BaseMoveCollisionTile.java
  47. 6 4
      src/me/hammerle/supersnuvi/tiles/BaseTile.java
  48. 23 26
      src/me/hammerle/supersnuvi/tiles/BottledSoulTile.java
  49. 21 31
      src/me/hammerle/supersnuvi/tiles/CrumblingStoneTile.java
  50. 5 7
      src/me/hammerle/supersnuvi/tiles/GoalTile.java
  51. 76 0
      src/me/hammerle/supersnuvi/tiles/HeadHitOnceTile.java
  52. 27 0
      src/me/hammerle/supersnuvi/tiles/HeadHitTile.java
  53. 34 0
      src/me/hammerle/supersnuvi/tiles/InteractTile.java
  54. 4 6
      src/me/hammerle/supersnuvi/tiles/KillTile.java
  55. 3 3
      src/me/hammerle/supersnuvi/tiles/NullTile.java
  56. 58 0
      src/me/hammerle/supersnuvi/tiles/PressureTile.java
  57. 40 0
      src/me/hammerle/supersnuvi/tiles/RampTile.java
  58. 3 8
      src/me/hammerle/supersnuvi/tiles/SlipperyTile.java
  59. 6 8
      src/me/hammerle/supersnuvi/tiles/SpikeTile.java
  60. 8 2
      src/me/hammerle/supersnuvi/tiles/StartTile.java
  61. 33 83
      src/me/hammerle/supersnuvi/tiles/Tile.java
  62. 3 3
      src/me/hammerle/supersnuvi/tiles/TrampolinTile.java
  63. 8 15
      src/me/hammerle/supersnuvi/tiles/WaterTile.java
  64. 19 9
      src/me/hammerle/supersnuvi/util/BlockDataStorage.java
  65. 4 3
      src/me/hammerle/supersnuvi/util/Face.java
  66. 2 2
      src/me/hammerle/supersnuvi/util/SoundUtils.java
  67. 109 17
      src/me/hammerle/supersnuvi/util/Utils.java

BIN
levels/00-Tech_Demo.map


+ 123 - 0
levels/00-Tech_Demo.snuvi

@@ -0,0 +1,123 @@
+event.load("level_reset");
+event.load("tile_interact");
+event.load("tile_hit");
+event.load("auto_tile_interact");
+//event.load("entity_spawn");
+
+//sgoto(0, @loop);
+
+@main
+wait();
+if(event == "level_reset")
+{
+    onLevelReset();
+}
+elseif(event == "tile_interact")
+{
+    onInteract(tile_x, tile_y, entity);
+}
+elseif(event == "tile_hit")
+{
+    onTileHit(tile_x, tile_y, entity);
+}
+elseif(event == "auto_tile_interact")
+{
+    onAutoInteract(tile_x, tile_y, entity);
+}
+elseif(event == "entity_spawn")
+{
+    print(entity.getType(entity));
+}
+goto(@main);
+
+
+//@loop
+//light.setPositionSmooth(0, entity.getX(entity.getHero()) + 16, entity.getY(entity.getHero()) + 32);
+//sgoto(0, @loop);
+//goto(@main);
+
+
+function onLevelReset()
+{
+    //light.setAmbient(0.0, 0.0, 0.0);
+    //light.setColor(0, 1.2, 0.9, 0);
+    //light.setPosition(0, entity.getX(entity.getHero()), entity.getY(entity.getHero()));
+    //light.setStrength(0, 0.005);
+    
+    hero = entity.getHero();
+    x = tile.toTileCoord(entity.getX(hero));
+    y = tile.toTileCoord(entity.getY(hero));
+
+    y -= 1;
+
+    x = tile.toLevelCoord(x);
+    y = tile.toLevelCoord(y);
+
+    entity.teleport(hero, x, y);
+
+    index = level.getBackgroundIndex();
+    
+    level.setTile(index, 0, 12, 72);
+    level.setTile(index, 2, 12, 72);
+    level.setTile(index, 10, 8, 72);
+    
+    level.setTile(index, 4, 12, -1);
+    level.setTile(index, 4, 7, -1);
+    
+    level.setTile(index, 1, 9, 70);
+    level.setTile(index, 4, 8, 71);
+    
+    level.setTile(index, 7, 11, 73);
+    level.setTile(index, 8, 10, 74);
+    
+    $platform = platform.spawn(tile.toLevelCoord(4), tile.toLevelCoord(11), 3);
+    platform.addmove($platform, tile.toLevelCoord(4), tile.toLevelCoord(10), 6, 9, 1);
+    platform.addmove($platform, tile.toLevelCoord(3), tile.toLevelCoord(6), 6, 9, 1);
+    
+    entity.spawn(5, tile.toLevelCoord(11), tile.toLevelCoord(4));
+}
+
+function onInteract(x, y, ent)
+{
+    if(x == 0 && y == 12)
+    {
+        level.setTile(level.getBackgroundIndex(), 4, 12, 3);
+        level.addMessage("Bla bla, I new block has appeared.");
+        level.addMessage("This is another page.");
+        level.addMessage("And another page.");
+    }
+    elseif(x == 2 && y == 12)
+    {
+        entity.setMotionY(entity.getHero(), -100);
+    }
+    elseif(x == 10 && y == 8)
+    {
+        entity.spawn(2, tile.toLevelCoord(14), tile.toLevelCoord(4));
+    }
+}
+
+function onTileHit(x, y, ent)
+{
+    if(x == 1 && y == 9)
+    {
+        level.addMessage("I'm an info block. ");
+    }
+    elseif(x == 4 && y == 8)
+    {
+        level.setTile(level.getBackgroundIndex(), 4, 7, 3);
+    }
+}
+
+function onAutoInteract(x, y, ent)
+{
+    if(x == 7 && y == 11)
+    {
+        entity.setMotionY(ent, tile.scale(-40));
+    }
+    elseif(x == 8 && y == 10)
+    {
+        //entity.setMotionY(ent, tile.scale(-80));
+        //entity.addHealth(entity.getHero(), -1);
+    }
+}
+

BIN
levels/01-Tech_Demo.map


+ 10 - 0
levels/01-Tech_Demo.snuvi

@@ -0,0 +1,10 @@
+print("Hi");
+event.load("level_reset");
+
+@main
+wait();
+
+print(event, 2);
+
+goto(@main);
+

+ 0 - 0
levels/02-Parabola.snuvi


+ 0 - 0
levels/03-Higher_Ground.snuvi


+ 0 - 0
levels/04-Out_of_Reach.snuvi


+ 0 - 0
levels/05-Like_Ice.snuvi


BIN
resources/platform.png


BIN
resources/platform.xcf


+ 229 - 49
src/me/hammerle/supersnuvi/Game.java

@@ -8,11 +8,20 @@ import me.hammerle.snuviengine.api.Engine;
 import me.hammerle.snuviengine.api.FontRenderer;
 import me.hammerle.snuviengine.api.KeyBinding;
 import me.hammerle.snuviengine.api.Shader;
+import me.hammerle.snuviscript.code.SnuviParser;
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.entity.EntityBuilder;
+import me.hammerle.supersnuvi.entity.components.ai.PlatformController;
+import me.hammerle.supersnuvi.gamelogic.FileLevel;
 import me.hammerle.supersnuvi.gamelogic.Level;
 import me.hammerle.supersnuvi.gamelogic.StartScreenLevel;
+import me.hammerle.supersnuvi.rendering.LevelRenderer;
 import me.hammerle.supersnuvi.savegame.SimpleConfig;
+import me.hammerle.supersnuvi.snuviscript.SnuviLogger;
+import me.hammerle.supersnuvi.snuviscript.SnuviScheduler;
 import me.hammerle.supersnuvi.tiles.*;
 import me.hammerle.supersnuvi.util.SoundUtils;
+import me.hammerle.supersnuvi.util.Utils;
 
 public class Game extends Engine
 {
@@ -35,7 +44,7 @@ public class Game extends Engine
     public static final NullTile FALLBACK_TILE = new NullTile();
     
     // tiles
-    private final Tile[] registeredTiles = new Tile[70];
+    private final Tile[] registeredTiles = new Tile[75];
     
     // levels
     private Level currentLevel = null;
@@ -43,6 +52,11 @@ public class Game extends Engine
     private int levelIndex = 0;
     private final StartScreenLevel startScreenLevel = new StartScreenLevel();
     
+    // scripting
+    private final SnuviLogger snuviLogger = new SnuviLogger();
+    private final SnuviScheduler snuviScheduler = new SnuviScheduler();
+    private final SnuviParser snuviParser = new SnuviParser(snuviLogger, snuviScheduler);
+    
     // config and savegames
     private final SimpleConfig config = new SimpleConfig("options.txt", true);
     private final SimpleConfig[] saveSlots = new SimpleConfig[] 
@@ -61,30 +75,43 @@ public class Game extends Engine
     
     // sound
     private boolean sound = config.getBoolean("sound", false);
+    
+    // renderer
+    private final LevelRenderer levelRenderer = new LevelRenderer();
+    // this dummy will be removed later
+    private final Shader dummy = new Shader();
 
     public Game()
     {
         instance = this;
         registerTiles();
+        addSnuviCommands();
         
         File[] files = new File("./levels").listFiles();
         Arrays.sort(files, (o1, o2) -> o1.compareTo(o2));
-        ArrayList<Level> levelList = new ArrayList<>();
+        ArrayList<FileLevel> levelList = new ArrayList<>();
         for(File file : files)
         {
-            if(file.isFile())
+            if(file.isFile() && file.getName().endsWith(".map"))
             {
-                Level l = new Level(file);
-                levelList.add(l);
+                try
+                {
+                    FileLevel l = new FileLevel(file);
+                    levelList.add(l);
+                }
+                catch(IllegalArgumentException ex)
+                {
+                    //System.out.println(ex.getMessage());
+                }
             }
         }
-        levels = levelList.toArray(new Level[levelList.size()]);
+        levels = levelList.toArray(new FileLevel[levelList.size()]); 
     }
     
     @Override
     public void init()
     {
-        setMaxFps(120);
+        setMaxFps(60);
         setNanosPerTick(MS_PER_TICK * 1_000_000);
     }
     
@@ -100,10 +127,15 @@ public class Game extends Engine
             SoundUtils.playSound(SoundUtils.Sound.SONG_1);
             SoundUtils.stopSound(SoundUtils.Sound.MENU_MUSIC);
             
-            currentLevel.tick();
+            snuviScheduler.setActiveLevel(currentLevel);
+            //if(Keys.TEST.isReleased())
+            {
+                currentLevel.tick();
+            }
+            snuviScheduler.tick(currentLevel);
             
             // doing that here to prevent concurent modification
-            if(currentLevel.shouldFinish())
+            if(currentLevel.isFinished())
             {
                 String base = "level." + currentLevel.getFileName();
                 SimpleConfig sp = saveSlots[slotScreenIndex];
@@ -128,17 +160,14 @@ public class Game extends Engine
                 // final save
                 sp.save();
                 
-                currentLevel.resetLevel();
+                currentLevel.reset();
                 currentLevel = null;
                 return;
             }
             
             if(currentLevel.shouldReset())
             {
-                if(currentLevel.resetLevel())
-                {
-                    currentLevel = null;
-                }
+                currentLevel.reset();
             }
             
             if(Keys.ESCAPE.isReleased())
@@ -230,13 +259,13 @@ public class Game extends Engine
                                 }
                                 optionsDirty = true;
                                 break;
-                            case 15: // save options
+                            case 9: // save options
                                 Keys.write(config);
                                 config.set("sound", sound);
                                 config.save();
                                 optionsDirty = false;
                                 break;
-                            case 16: // go back
+                            case 10: // go back
                                 screen = 0;
                                 break;
                             default: // rebind keys
@@ -280,8 +309,8 @@ public class Game extends Engine
         }
     }
     
-    private final static int COLOR_BROWN = 0xFF13458B;
-    private final static int COLOR_OVERLAY = 0x77000000;
+    private final static int COLOR_OVERLAY = 0x77808080;
+    private final static int COLOR_OVERLAY2 = 0x77202020;
 
     private String getKeyName(KeyBinding key)
     {
@@ -297,10 +326,12 @@ public class Game extends Engine
     {
         if(currentLevel != null)
         {
-            currentLevel.renderTick(lag);
+            levelRenderer.renderTick(dummy, lag, currentLevel);
+            //currentLevel.renderTick(lag);
             return;
         }
-        startScreenLevel.renderTick(lag);
+        levelRenderer.renderTick(dummy, lag, startScreenLevel);
+
         Shader.translateTo(0.0f, 0.0f);
         Shader.updateMatrix();
         switch(screen)
@@ -329,7 +360,7 @@ public class Game extends Engine
                 Shader.setBlendingEnabled(true);
                 cr.drawRectangle(left, top, right, bottom, COLOR_OVERLAY);
                 float base = top + (3 + startScreenIndex) * line;
-                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY);
+                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY2);
                 Shader.setBlendingEnabled(false);
                         
                 Shader.setTextureEnabled(true);
@@ -365,7 +396,7 @@ public class Game extends Engine
                 Shader.setBlendingEnabled(true);
                 cr.drawRectangle(left, top, right, bottom, COLOR_OVERLAY);
                 float base = top + (3 + slotScreenIndex) * line;
-                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY);
+                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY2);
                 Shader.setBlendingEnabled(false);
                         
                 Shader.setTextureEnabled(true);
@@ -390,8 +421,8 @@ public class Game extends Engine
                 
                 float left = width * 0.2f;
                 float right = width * 0.8f;
-                float top = (height - line * 21.0f) * 0.5f;
-                float bottom = top + line * 21.0f;
+                float top = (height - line * 15.0f) * 0.5f;
+                float bottom = top + line * 15.0f;
                 
                 Shader.setTextureEnabled(false);
                 Shader.setColorEnabled(true);
@@ -402,7 +433,7 @@ public class Game extends Engine
                 Shader.setBlendingEnabled(true);
                 cr.drawRectangle(left, top, right, bottom, COLOR_OVERLAY);
                 float base = top + (3 + optionScreenIndex) * line;
-                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY);
+                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY2);
                 Shader.setBlendingEnabled(false);
                         
                 Shader.setTextureEnabled(true);
@@ -429,18 +460,6 @@ public class Game extends Engine
                 y = fr.drawString(secLeft, y, getKeyName(Keys.ESCAPE)); 
                 fr.drawString(left, y, "K: Enter");
                 y = fr.drawString(secLeft, y, getKeyName(Keys.ENTER)); 
-                fr.drawString(left, y, "K: Combat");
-                y = fr.drawString(secLeft, y, getKeyName(Keys.COMBAT)); 
-                fr.drawString(left, y, "K: Switch Face");
-                y = fr.drawString(secLeft, y, getKeyName(Keys.COMBAT_SWITCH_FACE)); 
-                fr.drawString(left, y, "K: Dash/Dodge");
-                y = fr.drawString(secLeft, y, getKeyName(Keys.COMBAT_DASH)); 
-                fr.drawString(left, y, "K: Dash/Dodge");
-                y = fr.drawString(secLeft, y, getKeyName(Keys.COMBAT_DODGE)); 
-                fr.drawString(left, y, "K: Block");
-                y = fr.drawString(secLeft, y, getKeyName(Keys.COMBAT_BLOCK)); 
-                fr.drawString(left, y, "K: Attack");
-                y = fr.drawString(secLeft, y, getKeyName(Keys.COMBAT_ATTACK)); 
                 if(optionsDirty)
                 {
                     y = fr.drawString(left, y, true, "&cSave");
@@ -507,7 +526,7 @@ public class Game extends Engine
                     baseIndex = half;
                 }
                 float base = top + (3 + baseIndex) * line;
-                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY);
+                cr.drawRectangle(left, base, right, base + line, COLOR_OVERLAY2);
                 Shader.setBlendingEnabled(false);
                         
                 Shader.setTextureEnabled(true);
@@ -528,15 +547,15 @@ public class Game extends Engine
                 {
                     Level l = levels[i];
                     fr.drawString(left, y, l.getName()); 
-                    fr.drawString(secLeft, y, l.formatBottles(sc.getInt("level." + l.getFileName() + ".bottles", 0))); 
-                    y = fr.drawString(thirdLeft, y, l.formatTime(sc.getFloat("level." + l.getFileName() + ".time", 0))); 
+                    fr.drawString(secLeft, y, LevelRenderer.formatBottles(sc.getInt("level." + l.getFileName() + ".bottles", 0), l.getMaxBottles())); 
+                    y = fr.drawString(thirdLeft, y, LevelRenderer.formatTime(sc.getFloat("level." + l.getFileName() + ".time", 0))); 
                 }
                 if(lastIndex == levels.length)
                 {
                     Level l = levels[lastIndex - 1];
                     fr.drawString(left, y, l.getName()); 
-                    fr.drawString(secLeft, y, l.formatBottles(sc.getInt("level." + l.getFileName() + ".bottles", 0))); 
-                    fr.drawString(thirdLeft, y, l.formatTime(sc.getFloat("level." + l.getFileName() + ".time", 0))); 
+                    fr.drawString(secLeft, y, LevelRenderer.formatBottles(sc.getInt("level." + l.getFileName() + ".bottles", 0), l.getMaxBottles())); 
+                    fr.drawString(thirdLeft, y, LevelRenderer.formatTime(sc.getFloat("level." + l.getFileName() + ".time", 0))); 
                 }
                 else
                 {
@@ -600,12 +619,12 @@ public class Game extends Engine
         registeredTiles[34] = new DecoShroomTile(0.0625f, 0.21875f, 0.125f, 0.25f);
         registeredTiles[35] = new DecoShroomTile(0.0625f, 0.1875f, 0.125f, 0.21875f);
         // ramp
-        registeredTiles[36] = new Ramp(0.375f, 0.0f, 0.4375f, 0.0625f, 0.0f, Tile.SIZE, Tile.SIZE, 0.0f);
-        registeredTiles[37] = new Ramp(0.4375f, 0.0f, 0.5f, 0.0625f, 0.0f, Tile.SIZE, Tile.SIZE, Tile.SIZE * 0.5f);
-        registeredTiles[38] = new Ramp(0.5f, 0.0f, 0.5625f, 0.0625f, 0.0f, Tile.SIZE * 0.5f, Tile.SIZE, 0.0f);
-        registeredTiles[39] = new Ramp(0.375f, 0.0625f, 0.4375f, 0.125f, Tile.SIZE, Tile.SIZE, 0.0f, 0.0f);
-        registeredTiles[40] = new Ramp(0.4375f, 0.0625f, 0.5f, 0.125f, Tile.SIZE, Tile.SIZE, 0.0f, Tile.SIZE * 0.5f);
-        registeredTiles[41] = new Ramp(0.5f, 0.0625f, 0.5625f, 0.125f, Tile.SIZE, Tile.SIZE * 0.5f, 0.0f, 0.0f);
+        registeredTiles[36] = new RampTile(0.375f, 0.0f, 0.4375f, 0.0625f, 0.0f, Tile.SIZE, Tile.SIZE, 0.0f);
+        registeredTiles[37] = new RampTile(0.4375f, 0.0f, 0.5f, 0.0625f, 0.0f, Tile.SIZE, Tile.SIZE, Tile.SIZE * 0.5f);
+        registeredTiles[38] = new RampTile(0.5f, 0.0f, 0.5625f, 0.0625f, 0.0f, Tile.SIZE * 0.5f, Tile.SIZE, 0.0f);
+        registeredTiles[39] = new RampTile(0.375f, 0.0625f, 0.4375f, 0.125f, Tile.SIZE, Tile.SIZE, 0.0f, 0.0f);
+        registeredTiles[40] = new RampTile(0.4375f, 0.0625f, 0.5f, 0.125f, Tile.SIZE, Tile.SIZE, 0.0f, Tile.SIZE * 0.5f);
+        registeredTiles[41] = new RampTile(0.5f, 0.0625f, 0.5625f, 0.125f, Tile.SIZE, Tile.SIZE * 0.5f, 0.0f, 0.0f);
         
         // london stuff
         // street
@@ -622,6 +641,17 @@ public class Game extends Engine
                 registeredTiles[45 + 5 * y + x] = new BaseTile(0.0625f * x, 0.25f + 0.0625f * y, 0.0625f * (x + 1), 0.25f + 0.0625f * (y + 1));
             }
         }
+        
+        // !-Tile
+        registeredTiles[70] = new HeadHitTile(0.5625f, 0.0f, 0.625f, 0.0625f);
+        // ?-Tile
+        registeredTiles[71] = new HeadHitOnceTile(0.625f, 0.0f, 0.6875f, 0.0625f, 0.6875f, 0.0f, 0.75f, 0.0625f);
+        // button
+        registeredTiles[72] = new InteractTile(0.75f, 0.0f, 0.8125f, 0.0625f);
+        // wooden pressure plate
+        registeredTiles[73] = new PressureTile(0.568359375f, 0.072265625f, 0.619140625f, 0.078125f);
+        // stone pressure plate
+        registeredTiles[74] = new PressureTile(0.568359375f, 0.087890625f, 0.619140625f, 0.09375f);        
     }
     
     public Tile getTile(int id)
@@ -633,7 +663,7 @@ public class Game extends Engine
         return registeredTiles[id];
     }
     
-    public void resetTiles(Level l)
+    public void resetTiles(FileLevel l)
     {
         for(Tile t : registeredTiles)
         {
@@ -688,4 +718,154 @@ public class Game extends Engine
     {
         SoundUtils.closeSounds();
     }
+    
+    public SnuviParser getParser()
+    {
+        return snuviParser;
+    }
+    
+    public Level getCurrentLevel()
+    {
+        return currentLevel;
+    }
+    
+    public void setCurrentLevel(Level l)
+    {
+        snuviScheduler.setActiveLevel(l);
+        currentLevel = l;
+    }
+    
+    private void addSnuviCommands()
+    {
+        snuviParser.registerFunction("level.getname", (sc, in) -> currentLevel.getName());
+        snuviParser.registerFunction("level.getwidth", (sc, in) -> (double) currentLevel.getData().getWidth());
+        snuviParser.registerFunction("level.getheight", (sc, in) -> (double) currentLevel.getData().getHeight());
+        snuviParser.registerFunction("level.getlayers", (sc, in) -> (double) currentLevel.getData().getLayers());
+        snuviParser.registerFunction("level.getbackgroundindex", (sc, in) -> (double) currentLevel.getData().getBackgroundIndex());
+        snuviParser.registerFunction("level.gettile", (sc, in) -> currentLevel.getData().getTile(in[0].getInt(sc), in[1].getInt(sc), in[2].getInt(sc)));
+        snuviParser.registerFunction("level.settile", (sc, in) -> 
+        {
+            int layer = in[0].getInt(sc);
+            int x = in[1].getInt(sc);
+            int y = in[2].getInt(sc);
+            int tile = in[3].getInt(sc);
+            currentLevel.getData().setTile(layer, x, y, tile);
+            currentLevel.getTileUpdater().add(layer, x, y);
+            Game.get().getTile(tile).reset(x, y, currentLevel);
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("level.addmessage", (sc, in) -> 
+        {
+            currentLevel.addMessage(in[0].getString(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("level.finish", (sc, in) -> 
+        {
+            currentLevel.finishLevel();
+            return Void.TYPE;
+        });
+        
+        snuviParser.registerFunction("tile.totilecoord", (sc, in) -> (double) Utils.toBlock(in[0].getFloat(sc)));
+        snuviParser.registerFunction("tile.tolevelcoord", (sc, in) -> (double) Utils.toCoord(in[0].getInt(sc)));
+        snuviParser.registerFunction("tile.scale", (sc, in) -> in[0].getDouble(sc) * Tile.SIZE_SCALE);
+        
+        snuviParser.registerFunction("entity.gethero", (sc, in) -> currentLevel.getHero());
+        snuviParser.registerFunction("entity.getx", (sc, in) -> (double) ((Entity) in[0].get(sc)).getX());
+        snuviParser.registerFunction("entity.gety", (sc, in) -> (double) ((Entity) in[0].get(sc)).getY());
+        snuviParser.registerFunction("entity.teleport", (sc, in) -> 
+        {
+            Entity ent = (Entity) in[0].get(sc);
+            float x = in[1].getFloat(sc);
+            float y = in[2].getFloat(sc);
+            ent.setPosition(x, y);
+            ent.setPosition(x, y);
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("entity.getmotionx", (sc, in) -> (double) ((Entity) in[0].get(sc)).getOwnForceX());
+        snuviParser.registerFunction("entity.getmotiony", (sc, in) -> (double) ((Entity) in[0].get(sc)).getOwnForceY());
+        snuviParser.registerFunction("entity.setmotionx", (sc, in) -> 
+        {
+            ((Entity) in[0].get(sc)).applyForce(in[1].getFloat(sc), 0.0f);
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("entity.setmotiony", (sc, in) -> 
+        {
+            ((Entity) in[0].get(sc)).applyForce(0.0f, in[1].getFloat(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("entity.ishero", (sc, in) -> currentLevel.getHero() == in[0].get(sc));
+        snuviParser.registerFunction("entity.gethealth", (sc, in) -> ((Entity) in[0].get(sc)).getHealth().getHealthPercent());
+        snuviParser.registerFunction("entity.addhealth", (sc, in) -> 
+        {
+            ((Entity) in[0].get(sc)).getHealth().addHealthPercent(in[1].getFloat(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("entity.getenergy", (sc, in) -> ((Entity) in[0].get(sc)).getEnergy().getEnergyPercent());
+        snuviParser.registerFunction("entity.addenergy", (sc, in) -> 
+        {
+            ((Entity) in[0].get(sc)).getEnergy().addEnergyPercent(in[1].getFloat(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("entity.spawn", (sc, in) -> 
+        {
+            Entity ent = EntityBuilder.fromId(in[0].getInt(sc), currentLevel, in[1].getFloat(sc), in[2].getFloat(sc));
+            if(ent != null)
+            {
+                currentLevel.spawnEntity(ent);
+            }
+            return ent;
+        });
+        snuviParser.registerFunction("entity.remove", (sc, in) -> 
+        {
+            Entity ent = (Entity) in[0].get(sc);
+            // move entity out of world so it is removed
+            ent.setPosition(0.0f, Float.MAX_VALUE * 0.5f);
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("entity.gettype", (sc, in) -> ((Entity) in[0].get(sc)).getType());
+        
+        snuviParser.registerFunction("platform.spawn", (sc, in) -> 
+        {
+            Entity ent = EntityBuilder.buildPlatform(currentLevel, in[0].getInt(sc), in[1].getFloat(sc), in[2].getInt(sc));
+            currentLevel.spawnEntity(ent);
+            return ent;
+        });
+        snuviParser.registerFunction("platform.addmove", (sc, in) -> 
+        {
+            PlatformController controller = (PlatformController) ((Entity) in[0].get(sc)).getController();
+            controller.addMoveData(in[1].getFloat(sc), in[2].getFloat(sc), in[3].getFloat(sc) * Tile.SIZE_SCALE, in[4].getFloat(sc) * Tile.SIZE_SCALE, in[5].getInt(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("platform.clear", (sc, in) -> 
+        {
+            ((PlatformController) ((Entity) in[0].get(sc)).getController()).clearMoveData();
+            return Void.TYPE;
+        });
+        
+        snuviParser.registerFunction("light.setambient", (sc, in) -> 
+        {
+            currentLevel.setAmbientLight(in[0].getFloat(sc), in[1].getFloat(sc), in[2].getFloat(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("light.setcolor", (sc, in) -> 
+        {
+            currentLevel.getLight(in[0].getInt(sc)).setColor(in[1].getFloat(sc), in[2].getFloat(sc), in[3].getFloat(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("light.setposition", (sc, in) -> 
+        {
+            currentLevel.getLight(in[0].getInt(sc)).setPosition(in[1].getFloat(sc), in[2].getFloat(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("light.setpositionsmooth", (sc, in) -> 
+        {
+            currentLevel.getLight(in[0].getInt(sc)).setPositionSmooth(in[1].getFloat(sc), in[2].getFloat(sc));
+            return Void.TYPE;
+        });
+        snuviParser.registerFunction("light.setstrength", (sc, in) -> 
+        {
+            currentLevel.getLight(in[0].getInt(sc)).setStrength(in[1].getFloat(sc));
+            return Void.TYPE;
+        });
+    }
 }

+ 2 - 22
src/me/hammerle/supersnuvi/Keys.java

@@ -17,18 +17,12 @@ public class Keys
     public final static KeyBinding ESCAPE = KeyHandler.register(GLFW_KEY_ESCAPE);
     public final static KeyBinding ENTER = KeyHandler.register(GLFW_KEY_ENTER);
     
-    public final static KeyBinding COMBAT = KeyHandler.register(GLFW_KEY_A);
-    public final static KeyBinding COMBAT_SWITCH_FACE = KeyHandler.register(GLFW_KEY_S);
-    public final static KeyBinding COMBAT_DASH = KeyHandler.register(GLFW_KEY_D);
-    public final static KeyBinding COMBAT_DODGE = KeyHandler.register(GLFW_KEY_F);
-    public final static KeyBinding COMBAT_BLOCK = KeyHandler.register(GLFW_KEY_G);
-    public final static KeyBinding COMBAT_ATTACK = KeyHandler.register(GLFW_KEY_H);
+    public final static KeyBinding TEST = KeyHandler.register(GLFW_KEY_T);
     
     private final static KeyBinding[] KEYS = 
     {
         UP, DOWN, LEFT, RIGHT, 
-        JUMP, RUN, ESCAPE, ENTER, 
-        COMBAT, COMBAT_SWITCH_FACE, COMBAT_DASH, COMBAT_DODGE, COMBAT_BLOCK, COMBAT_ATTACK
+        JUMP, RUN, ESCAPE, ENTER, TEST
     };
     
     public static void rebind(KeyBinding binding)
@@ -57,13 +51,6 @@ public class Keys
         KeyHandler.rebind(RUN, config.getInt("key.run", GLFW_KEY_LEFT_SHIFT));
         KeyHandler.rebind(ESCAPE, config.getInt("key.escape", GLFW_KEY_ESCAPE));
         KeyHandler.rebind(ENTER, config.getInt("key.enter", GLFW_KEY_ENTER));
-        
-        KeyHandler.rebind(COMBAT, config.getInt("key.combat", GLFW_KEY_A));
-        KeyHandler.rebind(COMBAT_SWITCH_FACE, config.getInt("key.combat.switchface", GLFW_KEY_S));
-        KeyHandler.rebind(COMBAT_DASH, config.getInt("key.combat.dash", GLFW_KEY_D));
-        KeyHandler.rebind(COMBAT_DODGE, config.getInt("key.combat.dodge", GLFW_KEY_F));
-        KeyHandler.rebind(COMBAT_BLOCK, config.getInt("key.combat.block", GLFW_KEY_G));
-        KeyHandler.rebind(COMBAT_ATTACK, config.getInt("key.combat.attack", GLFW_KEY_H));
     }
     
     public static void write(SimpleConfig config)
@@ -77,12 +64,5 @@ public class Keys
         config.set("key.run", RUN.getKey());
         config.set("key.escape", ESCAPE.getKey());
         config.set("key.enter", ENTER.getKey());
-
-        config.set("key.combat", COMBAT.getKey());
-        config.set("key.combat.switchface", COMBAT_SWITCH_FACE.getKey());
-        config.set("key.combat.dash", COMBAT_DASH.getKey());
-        config.set("key.combat.dodge", COMBAT_DODGE.getKey());
-        config.set("key.combat.block", COMBAT_BLOCK.getKey());
-        config.set("key.combat.attack", COMBAT_ATTACK.getKey());
     }
 }

+ 477 - 230
src/me/hammerle/supersnuvi/entity/Entity.java

@@ -1,86 +1,72 @@
 package me.hammerle.supersnuvi.entity;
 
-import java.util.List;
 import me.hammerle.supersnuvi.entity.components.ai.Controller;
 import me.hammerle.supersnuvi.entity.components.Energy;
 import me.hammerle.supersnuvi.entity.components.Health;
 import me.hammerle.supersnuvi.entity.components.ItemCollector;
 import me.hammerle.supersnuvi.entity.components.Movement;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.util.CollisionObject;
+import me.hammerle.supersnuvi.tiles.Tile;
 import me.hammerle.supersnuvi.util.Face;
 import me.hammerle.supersnuvi.util.Utils;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.math.Vector;
+import me.hammerle.supersnuvi.entity.components.IDespawn;
 
 public final class Entity
 {
-    public static final float GRAVITY = 8.0f;
-    public static final float STEP = 0.0625f;
+    public static final float GRAVITY = 8.0f * Tile.SIZE_SCALE;
+    public static final float STEP = 0.015625f;
     // this one is a little bit bigger to prevent wrong calculation
-    // while joing upwars
+    // while going upwards
     public static final float UP_STEP = STEP + 0.00390625f;
     
     // the last position is used for interpolation during rendering
-    private float lastPosX;
-    private float lastPosY;
+    private final Vector lastPos = new Vector();
     // the current position of the entity
-    private float posX;
-    private float posY;
-    
-    // the collision box of the entity
-    private final CollisionObject box;
-    
-    // the motion before the movement collision check
-    private float preMotionX;
-    private float preMotionY;
-    // the motion after the movement collision check
-    private float motionX;
-    private float motionY;
-    
+    private final Vector pos = new Vector();
+    // the width of the entity
+    private final float width;
+    // the height of the entity
+    private final float height;  
+    // own force used by the controller component
+    private final Vector ownForce = new Vector();
+    // the final motion of the last tick
+    private final Vector lastMotion = new Vector();
+    // the motion, reduced by the movement collision check
+    private final Vector motion = new Vector();
+    // the friction reducing motion each tick
+    private final Vector baseFriction = new Vector(0.0f, 1.0f);
+    private final Vector friction = new Vector(0.0f, 1.0f);
     // a flag indicating that the entity is on the ground
     private boolean onGround = true;
-    
-    // the level of the entity
-    private final ILevel level;
-    
     // entity components
-    protected Controller controller;
-    protected Health health;
-    protected Energy energy;
-    protected Movement move;
-    protected ItemCollector itemCollector;
-    
-    // face
-    private Face face = Face.RIGHT;
-    
-    protected Entity(ILevel level, float x, float y, CollisionObject box)
+    private final Controller controller;
+    private final Health health;
+    private final Energy energy;
+    private final Movement move;
+    private final ItemCollector itemCollector;
+    private final IDespawn onDespawn;
+    // the type of the entity, used by snuvi script
+    private final String type;
+    private Face direction = Face.NULL;
+    
+    protected Entity(float width, float height, Controller c, Health h, Energy e, Movement m, ItemCollector ic, IDespawn d, String type)
     {
-        lastPosX = x;
-        lastPosY = y;
-        posX = x;
-        posY = y;
-
-        // ensure the box cannot be modified from the outside
-        this.box = box.copy().offset(x, y);
-        
-        preMotionX = 0.0f;
-        preMotionY = 0.0f;
-        motionX = 0.0f;
-        motionY = 0.0f;
-        
-        this.level = level;
-        
-        // components
-        this.controller = Controller.NULL;
-        this.health = Health.NULL;
-        this.energy = Energy.NULL;
-        this.move =  Movement.NULL;
-        this.itemCollector =  ItemCollector.NULL;
+        this.width = width;
+        this.height = height;
+        this.controller = c;
+        this.health = h;
+        this.energy = e;
+        this.move = m;
+        this.itemCollector = ic;
+        this.onDespawn = d;
+        this.type = type;
     }
     
-    public ILevel getLevel()
+    public String getType()
     {
-        return level;
-    }   
+        return type;
+    }
     
     //--------------------------------------------------------------------------
     // components
@@ -91,6 +77,11 @@ public final class Entity
         return controller.isAnimated();
     }
     
+    public Controller getController()
+    {
+        return controller;
+    }
+    
     public Health getHealth()
     {
         return health;
@@ -111,286 +102,536 @@ public final class Entity
         return itemCollector;
     }
     
+    public void onDespawn()
+    {
+        onDespawn.onDespawn(this);
+    }
+    
     //--------------------------------------------------------------------------
     // basic stuff
     //--------------------------------------------------------------------------
     
     public float getSquaredDistance(Entity e)
     {
-        return Utils.getSquaredDistance(
-                posX + box.getWidth() * 0.5f, posY + box.getHeight() * 0.5f, 
-                e.posX + e.box.getWidth() * 0.5f, e.posY + e.box.getHeight() * 0.5f);
-    }
-    
-    public CollisionObject getBox()
-    {
-        return box;
+        return Utils.getSquaredDistance(getCenterX(), getCenterY(), e.getCenterX(), e.getCenterY());
     }
     
     public float getX()
     {
-        return posX;
+        return pos.getX();
     }
     
     public float getY()
     {
-        return posY;
+        return pos.getY();
     }
     
     public void setPosition(float x, float y)
     {
-        lastPosX = x;
-        lastPosY = y;
-        posX = x;
-        posY = y;
-        box.reset().offset(posX, posY);
+        lastPos.set(x, y);
+        pos.set(x, y);
     }
     
     public float getLastX()
     {
-        return lastPosX;
+        return lastPos.getX();
     }
     
     public float getLastY()
     {
-        return lastPosY;
+        return lastPos.getY();
     }
     
     public float getCenterX()
     {
-        return posX + box.getWidth() * 0.5f;
+        return pos.getX() + width * 0.5f;
     }
     
     public float getCenterY()
     {
-        return posY + box.getHeight() * 0.5f;
+        return pos.getY() + height * 0.5f;
+    }
+    
+    public float getWidth()
+    {
+        return width;
     }
     
     public float getHeight()
     {
-        return box.getHeight();
+        return height;
+    }
+    
+    public float getMotionX()
+    {
+        return motion.getX();
     }
     
-    public Face getFace()
+    public float getMotionY()
     {
-        if(motionX == 0.0f)
-        {
-            return face;
-        }
-        face = motionX < 0.0f ? Face.LEFT : Face.RIGHT;
-        return face;
+        return motion.getY();
     }
     
-    public void updateFace()
+    public float getOwnForceX()
     {
-        
+        return ownForce.getX();
     }
     
-    public float getMotionX()
+    public float getOwnForceY()
+    {
+        return ownForce.getY();
+    }
+
+    public void applyOwnForce(float x, float y)
     {
-        return motionX;
+        ownForce.add(x, y);
     }
     
-    public void setMotionX(float motionX)
+    public void applyForce(float x, float y)
     {
-        this.motionX = motionX;
+        motion.add(x, y);
     }
     
-    public float getMotionY()
+    public void setBaseFriction(float fx, float fy)
+    {
+        baseFriction.set(fx, fy);
+    }
+    
+    public Vector getFriction()
     {
-        return motionY;
+        return friction;
     }
     
-    public void setMotionY(float motionY)
+    public boolean isAt(float x, float y)
     {
-        this.motionY = motionY;
+        return Math.abs(x - pos.getX()) < STEP && Math.abs(y - pos.getY()) < STEP;
     }
     
-    public float getPreMotionX()
+    public boolean isColliding(float minX, float minY, float maxX, float maxY)
     {
-        return preMotionX;
+        return maxX > getX() && getX() + width > minX && maxY > getY() && getY() + height > minY;
+    }
+    
+    public boolean isColliding(Entity ent)
+    {
+        return isColliding(ent.getX(), ent.getY(), ent.getX() + ent.width, ent.getY() + ent.height);
+    }
+    
+    private Face getCollidingFace(Entity o)
+    {
+        return Utils.getCollidingFace(
+                o.getX(), o.getY(), o.getX() + o.getWidth(), o.getY() + o.getHeight(),
+                getX(), getY(), getX() + getWidth(), getY() + getHeight());
+    }
+    
+    public boolean jump() 
+    {
+        if(onGround)
+        {
+            ownForce.addY(-move.getJumpPower());
+            return true;
+        }
+        return false;
     }
     
-    public float getPreMotionY()
+    public Face getDirection()
     {
-        return preMotionY;
+        return direction;
     }
     
     //--------------------------------------------------------------------------
     // ticking
     //--------------------------------------------------------------------------
     
-    public void tick() 
+    public void tick(Level level) 
     {
-        lastPosX = posX;
-        lastPosY = posY;
+        // last motion is current motion, last motion will be set in move
+        motion.set(lastMotion);
+        lastMotion.set(0.0f, 0.0f);
         
-        controller.tick();
-        energy.tick();
+        // reset own force
+        ownForce.set(0.0f, 0.0f);      
         
-        preMotionX = motionX;
-        preMotionY = motionY;
+        // update onGround before controller tick
+        onGround = isOnTileGround(level);
+        if(onGround)
+        {
+            if(!move.canMoveOnTiles())
+            {
+                motion.setY(0.0f);
+            }
+        }
+        else
+        {
+            friction.set(0.2f, 1.0f);
+        }
+        
+        // tick components
+        controller.tick(this, level);
+        energy.tick(this);
+        health.tick();
+        
+        // apply gravity if needed
+        if(move.hasGravity(this))
+        {
+            motion.addY(GRAVITY * move.getGravityFactor());
+        }
+        
+        // apply friction
+        motion.mul(friction);
+        if(move.isInWater())
+        {
+            motion.mul(friction);
+            move.setInWater(false);
+        }
+        friction.set(baseFriction);
+        
+        // modify current motion by own force
+        motion.add(ownForce);
         
-        if(move.hasGravity())
+        if(ownForce.getX() < 0.0f)
         {
-            preMotionY += GRAVITY * move.getGravityFactor();
+            direction = Face.LEFT;
         }
+        else if(ownForce.getX() > 0.0f)
+        {
+            direction = Face.RIGHT;
+        }
+    }
+    
+    public void tickCollision(Level level)
+    {
+        // (entity <-> tile) collision
+        doTileCollision(level);
+        // (entity <-> entity) collision
+        level.forEachCollidingEntity(this, (ent) -> 
+        {
+            controller.onCollideWithEntity(this, ent, getCollidingFace(ent));
+        });
+        
+        slideOnEdge(level);
+        
+        // remeber last position for rendering
+        lastPos.set(pos);
+    }
+    
+    public int move(Level level)
+    {
+        if(motion.getX() == 0.0f && motion.getY() == 0.0f)
+        {
+            return 0;
+        }
+        Vector maxMotion = new Vector();
+        maxMotion.set(motion);
+        
+        if(!move.canMoveOnEntities() || !move.canMoveOnTiles())
+        {
+            reduceMotion(level);
+        }
+        pos.add(motion);
+        
+        int r = (motion.getX() == 0.0f && motion.getY() == 0.0f) ? 0 : 1;
+        
+        lastMotion.add(motion);
+        maxMotion.sub(motion);
+        motion.set(maxMotion);
+        
+        return r;
+    }
+    
+    private boolean isOnTileGround(Level level)
+    {
+        float minX = pos.getX();
+        float minY = pos.getY() + height;
+        float maxX = pos.getX() + width;
+        float maxY = pos.getY() + height + STEP * 2;
+        int startX = Utils.toBlock(minX);
+        int startY = Utils.toBlock(minY);
+        int endX = Utils.toBlock(maxX);
+        int endY = Utils.toBlock(maxY);
 
-        if(move.canMoveEverywhere())
+        for(int x = startX; x <= endX; x++)
         {
-            motionX = preMotionX;
-            motionY = preMotionY;
+            for(int y = startY; y <= endY; y++)
+            {
+                Tile t = level.getInteractionTile(x, y);
+                if(t.isMoveColliding(minX, minY, maxX, maxY, x, y, level))
+                {
+                    return true;
+                }
+            }
         }
-        else
+        
+        for(Entity ent : level.getEntities())
+        {
+            if(ent == this)
+            {
+                continue;
+            }
+            if(ent.getMovement().isSolid() && ent.isColliding(minX, minY, maxX, maxY))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    private void doTileCollision(Level level)
+    {
+        float minX = pos.getX() - STEP;
+        float minY = pos.getY() - STEP;
+        float maxX = pos.getX() + width + STEP;
+        float maxY = pos.getY() + height + STEP;
+        int startX = Utils.toBlock(minX);
+        int startY = Utils.toBlock(minY);
+        int endX = Utils.toBlock(maxX);
+        int endY = Utils.toBlock(maxY);
+
+        for(int x = startX; x <= endX; x++)
+        {
+            for(int y = startY; y <= endY; y++)
+            {
+                Tile t = level.getInteractionTile(x, y);
+                if(t.isColliding(minX, minY, maxX, maxY, x, y, level))
+                {
+                    Face f = t.getCollidingFace(minX, minY, maxX, maxY, x, y, level);
+                    controller.onCollideWithTile(this, x, y, level, t, f);
+                    t.onEntityCollide(this, x, y, f.getOpposite(), level);
+                }
+            }
+        }
+    }
+    
+    private boolean isCollidingWithTiles(int startX, int startY, int endX, int endY, Level level)
+    {
+        if(move.canMoveOnTiles())
         {
-            CollisionObject testBox = box.copy().expand(preMotionX, preMotionY).expand(0.0f, -UP_STEP);
-            List<CollisionObject> boxes = level.getMovementBoxesAt(testBox, this);
-            if(!boxes.isEmpty())
+            return false;
+        }
+        float minX = getX();
+        float minY = getY();
+        float maxX = minX + width;
+        float maxY = minY + height;
+        for(int x = startX; x <= endX; x++)
+        {
+            for(int y = startY; y <= endY; y++)
             {
-                float mx = preMotionX;
-                float my = preMotionY;
-                
-                testBox.reset();
-                
-                float oldX = testBox.getMinX();
-                float oldY = testBox.getMinY();
-                
-                while(mx != 0.0 || my != 0.0)
+                if(level.getInteractionTile(x, y).isMoveColliding(minX, minY, maxX, maxY, x, y, level))
                 {
-                    testBox.save();
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+    
+    private boolean isCollidingWithEntities(Level level)
+    {
+        if(move.canMoveOnEntities())
+        {
+            return false;
+        }
+        float minX = pos.getX();
+        float minY = pos.getY();
+        float maxX = pos.getX() + width;
+        float maxY = pos.getY() + height;
+        for(Entity ent : level.getEntities())
+        {
+            if(ent == this)
+            {
+                continue;
+            }
+            if(ent.isColliding(minX, minY, maxX, maxY) && (move.isSolid() || ent.move.isSolid()))
+            {
+                return true;
+            }
+        }
+        for(Entity ent : level.getEntitiesInQueue())
+        {
+            if(ent == this)
+            {
+                continue;
+            }
+            if(ent.isColliding(minX, minY, maxX, maxY) && (move.isSolid() || ent.move.isSolid()))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
 
-                    if(mx < 0.0)
+    public void reduceMotion(Level level)
+    {
+        if(motion.getX() == 0.0f && motion.getY() == 0.0f)
+        {
+            return;
+        }
+        
+        float mx = motion.getX();
+        float my = motion.getY();
+        
+        int startX;
+        int endX;
+        if(mx < 0.0f)
+        {
+            startX = Utils.toBlock(pos.getX() + mx);
+            endX = Utils.toBlock(pos.getX() + width);
+        }
+        else
+        {
+            startX = Utils.toBlock(pos.getX());
+            endX = Utils.toBlock(pos.getX() + width + mx);
+        }
+        
+        int startY;
+        int endY;
+        if(my < 0.0f)
+        {
+            startY = Utils.toBlock(pos.getY() + my);
+            endY = Utils.toBlock(pos.getY() + height);
+        }
+        else
+        {
+            startY = Utils.toBlock(pos.getY());
+            endY = Utils.toBlock(pos.getY() + height + my);
+        }
+        
+        
+        float oldPosX = pos.getX();
+        float oldPosY = pos.getY();
+        
+        while(mx != 0.0 || my != 0.0)
+        {
+            if(mx != 0.0f)
+            {
+                float oldX = pos.getX();
+                if(mx < 0.0)
+                {
+                    if(mx > -STEP)
                     {
-                        if(mx > -STEP)
-                        {
-                            testBox.offsetX(mx);
-                            mx = 0.0f;
-                        }
-                        else
-                        {
-                            testBox.offsetX(-STEP);
-                            mx += STEP;
-                        }
+                        pos.addX(mx);
+                        mx = 0.0f;
                     }
-                    else if(mx > 0.0)
+                    else
                     {
-                        if(mx < STEP)
-                        {
-                            testBox.offsetX(mx);
-                            mx = 0.0f;
-                        }
-                        else
-                        {
-                            testBox.offsetX(STEP);
-                            mx -= STEP;
-                        }
+                        pos.addX(-STEP);
+                        mx += STEP;
                     }
+                }
+                else if(mx > 0.0)
+                {
+                    if(mx < STEP)
+                    {
+                        pos.addX(mx);
+                        mx = 0.0f;
+                    }
+                    else
+                    {
+                        pos.addX(STEP);
+                        mx -= STEP;
+                    }
+                }
 
-                    for(CollisionObject cb : boxes)
+                if(isCollidingWithTiles(startX, startY, endX, endY, level) || isCollidingWithEntities(level))
+                {
+                    pos.addY(-UP_STEP);
+                    if(isCollidingWithTiles(startX, startY, endX, endY, level) || isCollidingWithEntities(level))
                     {
-                        if(cb.isColliding(testBox))
-                        {
-                            testBox.offsetY(-UP_STEP);
-                            for(CollisionObject cb2 : boxes)
-                            {
-                                if(cb2.isColliding(testBox))
-                                {
-                                    mx = 0.0f;
-                                    testBox.reset();
-                                    break;
-                                }
-                            }
-                            break;
-                        }
+                        pos.addY(UP_STEP);
+                        pos.setX(oldX);
+                        mx = 0.0f;
                     }
-                    
-                    testBox.save();
+                }
+            }
+            if(my != 0.0f)
+            {
+                float oldY = pos.getY();
 
-                    if(my < 0.0)
+                if(my < 0.0)
+                {
+                    if(my > -STEP)
                     {
-                        if(my > -STEP)
-                        {
-                            testBox.offsetY(my);
-                            my = 0.0f;
-                        }
-                        else
-                        {
-                            testBox.offsetY(-STEP);
-                            my += STEP;
-                        }
+                        pos.addY(my);
+                        my = 0.0f;
                     }
-                    else if(my > 0.0)
+                    else
                     {
-                        if(my < STEP)
-                        {
-                            testBox.offsetY(my);
-                            my = 0.0f;
-                        }
-                        else
-                        {
-                            testBox.offsetY(STEP);
-                            my -= STEP;
-                        }
+                        pos.addY(-STEP);
+                        my += STEP;
                     }
-
-                    for(CollisionObject cb : boxes)
+                }
+                else if(my > 0.0)
+                {
+                    if(my < STEP)
+                    {
+                        pos.addY(my);
+                        my = 0.0f;
+                    }
+                    else
                     {
-                        if(cb.isColliding(testBox))
-                        {
-                            my = 0.0f;
-                            testBox.reset();
-                            break;
-                        }
+                        pos.addY(STEP);
+                        my -= STEP;
                     }
                 }
-                
-                motionX = testBox.getMinX() - oldX;
-                motionY = testBox.getMinY() - oldY;
-            }
-            else
-            {
-                motionX = preMotionX;
-                motionY = preMotionY;
+
+                if(isCollidingWithTiles(startX, startY, endX, endY, level) || isCollidingWithEntities(level))
+                {
+                    my = 0.0f;
+                    pos.setY(oldY);
+                }
             }
         }
+
+        motion.set(pos.getX() - oldPosX, pos.getY() - oldPosY);
+        pos.set(oldPosX, oldPosY);
+    }
+    
+    private void slideOnEdge(Level level)
+    {
+        float minY = pos.getY() + height;
+        float maxY = pos.getY() + height + STEP * 2;
         
-        posX += motionX;
-        posY += motionY;
-        box.reset().offset(posX, posY);
+        float minX = pos.getX();
+        float midX = pos.getX() + width * 0.5f;
+        float maxX = pos.getX() + width;
         
-        if(!move.canMoveEverywhere())
-        {
-            //onGround = preMotionY > 0.0f && motionY == 0;
-            onGround = !level.getMovementBoxesAt(box.copy().expand(0.0f, STEP * 2), this).isEmpty();
+        int startX = Utils.toBlock(minX);
+        int startY = Utils.toBlock(minY);
+        int endX = Utils.toBlock(maxX);
+        int endY = Utils.toBlock(maxY);
+        
+        int leftEdges = 0;
+        int rightEdges = 0;
 
-            move.setInWater(false);
-            move.setFrictionFactor(0.6f);
-            // apply collision
-            CollisionObject cb = box.copy();
-            for(Face f : Face.values())
+        for(int x = startX; x <= endX; x++)
+        {
+            for(int y = startY; y <= endY; y++)
             {
-                cb.reset();
-                cb.expand(f.getCollisionOffsetX(), f.getCollisionOffsetY());
-                level.getEntitiesCollidingWith(this, cb).forEach(ent -> this.controller.onCollideWithEntity(ent, f));
-                level.getCollisionBoxesAt(cb).forEach(loc -> 
+                Tile t = level.getInteractionTile(x, y);
+                if(t.isMoveColliding(minX, minY, midX, maxY, x, y, level))
                 {
-                    controller.onCollideWithTile(loc, f);
-                    loc.getTile().onEntityCollide(this, loc.getX(), loc.getY(), f.getOpposite(), level);
-                });
-            }
-
-            motionX *= move.getFrictionFactor();
-            if(Math.abs(motionX) < 0.3)
-            {
-                motionX = 0.0f;
+                    rightEdges++;
+                }
+                if(t.isMoveColliding(midX, minY, maxX, maxY, x, y, level))
+                {
+                    leftEdges++;
+                }
             }
         }
-        health.tick();
+        
+        if(leftEdges == 0)
+        {
+            applyForce(STEP * 25, 0.0f);
+        }
+        if(rightEdges == 0)
+        {
+            applyForce(-STEP * 25, 0.0f);
+        }
     }
     
     public void renderTick(float lag)
     {
-        controller.renderTick(lag);
+        controller.renderTick(this, lag);
     }
     
     //--------------------------------------------------------------------------
@@ -401,4 +642,10 @@ public final class Entity
     {
         return onGround;
     }   
+
+    @Override
+    public String toString()
+    {
+        return String.format("(%f, %f, %f, %f)", pos.getX(), pos.getY(), pos.getX() + width, pos.getY() + height);
+    }
 }

+ 85 - 31
src/me/hammerle/supersnuvi/entity/EntityBuilder.java

@@ -1,65 +1,113 @@
 package me.hammerle.supersnuvi.entity;
 
+import me.hammerle.supersnuvi.entity.components.BoxMovement;
 import me.hammerle.supersnuvi.entity.components.DefaultHealth;
 import me.hammerle.supersnuvi.entity.components.DefaultMovement;
 import me.hammerle.supersnuvi.entity.components.DefaultEnergy;
+import me.hammerle.supersnuvi.entity.components.Energy;
+import me.hammerle.supersnuvi.entity.components.Health;
 import me.hammerle.supersnuvi.entity.components.StoneMovement;
 import me.hammerle.supersnuvi.entity.components.ai.HumanController;
 import me.hammerle.supersnuvi.entity.components.ai.LondonerController;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.tiles.Tile;
-import me.hammerle.supersnuvi.entity.components.IDeath;
 import me.hammerle.supersnuvi.entity.components.ItemCollector;
-import me.hammerle.supersnuvi.entity.components.NoHealth;
+import me.hammerle.supersnuvi.entity.components.ai.PlatformController;
 import me.hammerle.supersnuvi.entity.components.ai.StartScreenHeroController;
 import me.hammerle.supersnuvi.entity.components.ai.StoneController;
-import me.hammerle.supersnuvi.util.CollisionBox;
 import me.hammerle.supersnuvi.util.SoundUtils.Sound;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.tiles.Tile;
+import me.hammerle.supersnuvi.entity.components.IDespawn;
+import me.hammerle.supersnuvi.entity.components.ai.BoxController;
 
 public final class EntityBuilder 
 {
-    public static Entity buildHero(ILevel level, float x, float y)
+    public static Entity buildHero(Level level, float x, float y)
     {
-        Entity hero = new Entity(level, x, y, new CollisionBox(0.0f, 5.0f, 27.0f, 64.0f));
-        hero.controller = new HumanController(hero);
-        hero.health = new DefaultHealth(hero, (ent) -> ent.getLevel().scheduleReset(), 100.0f, null, Sound.MIRROR_CRACK, Sound.MIRROR_BREAK);
-        hero.energy = new DefaultEnergy(hero, 100.0f);
-        hero.move = new DefaultMovement(hero, 12.0f, 0.0f, 50.0f);
-        hero.itemCollector = ItemCollector.HERO;
+        Entity hero = new Entity(Tile.SIZE * 0.84375f, Tile.SIZE * 1.84375f, 
+                new HumanController(), 
+                new DefaultHealth(null, Sound.MIRROR_CRACK, Sound.MIRROR_BREAK), 
+                new DefaultEnergy(),
+                new DefaultMovement(12.0f, 0.0f, 50.0f, false), 
+                ItemCollector.HERO, 
+                (ent) -> level.scheduleReset(), 
+                "Hero");
+        hero.setPosition(x, y);
         return hero;
     }
     
-    public static Entity buildStartScreenHero(ILevel level, float x, float y)
+    public static Entity buildStartScreenHero(Level level, float x, float y)
     {
-        Entity hero = new Entity(level, x, y, new CollisionBox(0.0f, 5.0f, 27.0f, 64.0f));
-        hero.controller = new StartScreenHeroController(hero);
-        hero.health = new DefaultHealth(hero, (ent) -> ent.getLevel().scheduleReset(), 100.0f, null, Sound.MIRROR_CRACK, Sound.MIRROR_BREAK);
-        hero.energy = new DefaultEnergy(hero, 100.0f);
-        hero.move = new DefaultMovement(hero, 12.0f, 0.0f, 50.0f);
-        hero.itemCollector = ItemCollector.HERO;
+        Entity hero = new Entity(Tile.SIZE * 0.84375f, Tile.SIZE * 1.84375f, 
+                new StartScreenHeroController(), 
+                new DefaultHealth(null, Sound.MIRROR_CRACK, Sound.MIRROR_BREAK), 
+                new DefaultEnergy(),
+                new DefaultMovement(12.0f, 0.0f, 50.0f, false), 
+                ItemCollector.HERO, 
+                (ent) -> level.scheduleReset(), 
+                "Hero");
+        hero.setPosition(x, y);
         return hero;
     }
     
-    public static Entity buildLondoner(ILevel level, float x, float y, boolean evil)
+    public static Entity buildLondoner(Level level, float x, float y, boolean evil)
     {
-        Entity londoner = new Entity(level, x, y, new CollisionBox(7.0f, 19.0f, 21.0f, 64.0f));
-        londoner.controller = new LondonerController(londoner, evil);
-        londoner.health = new DefaultHealth(londoner, IDeath.NULL, 100.0f, null, null, null);
-        londoner.energy = new DefaultEnergy(londoner, 100.0f);
-        londoner.move = new DefaultMovement(londoner, 3.0f, 0.0f, 50.0f);
+        Entity londoner = new Entity(Tile.SIZE * 0.4375f, Tile.SIZE * 1.40625f, 
+                new LondonerController(evil), 
+                new DefaultHealth(null, null, null),
+                new DefaultEnergy(),
+                new DefaultMovement(3.0f, 0.0f, 50.0f, true),
+                ItemCollector.NULL,
+                IDespawn.NULL, 
+                evil ? "EvilLondoner" : "Londoner");
+        londoner.setPosition(x, y);
         return londoner;
     }
     
-    public static Entity buildCrumblingStone(ILevel level, float x, float y)
+    public static Entity buildCrumblingStone(Level level, float x, float y)
     {
-        Entity stone = new Entity(level, x, y, new CollisionBox(0.0f, 0.0f, Tile.SIZE, Tile.SIZE));
-        stone.controller = new StoneController(stone);
-        stone.move = new StoneMovement(stone);
-        stone.health = new NoHealth(stone);
+        Entity stone = new Entity(Tile.SIZE, Tile.SIZE, 
+                new StoneController(),
+                Health.NULL,
+                Energy.NULL,
+                StoneMovement.STONE,
+                ItemCollector.NULL,
+                IDespawn.NULL, 
+                "CrumblingStone");
+        stone.setPosition(x, y);
         return stone;
     }
     
-    public static Entity fromId(int id, ILevel level, float x, float y)
+    public static Entity buildPlatform(Level level, float x, float y, int tx)
+    {
+        // render offset: -0.03125, 0
+        Entity platform = new Entity(Tile.SIZE * tx, Tile.SIZE * 0.78125f,
+                new PlatformController(),
+                Health.NULL,
+                Energy.NULL,
+                StoneMovement.PLATFORM,
+                ItemCollector.NULL,
+                IDespawn.NULL,
+                "Platform");
+        platform.setPosition(x, y);
+        platform.setBaseFriction(0.0f, 0.0f);
+        return platform;
+    }
+    
+    public static Entity buildBox(Level level, float x, float y, int type)
+    {
+        Entity box = new Entity(Tile.SIZE, Tile.SIZE,
+                new BoxController(type),
+                Health.NULL,
+                Energy.NULL,
+                new BoxMovement(),
+                ItemCollector.NULL,
+                IDespawn.NULL,
+                "Box");
+        box.setPosition(x, y);
+        return box;
+    }
+    
+    public static Entity fromId(int id, Level level, float x, float y)
     {
         switch(id)
         {
@@ -67,6 +115,12 @@ public final class EntityBuilder
                 return buildLondoner(level, x, y, true);
             case 2:
                 return buildLondoner(level, x, y, false);
+            case 3:
+                return buildBox(level, x, y, 0);
+            case 4:
+                return buildBox(level, x, y, 1);
+            case 5:
+                return buildBox(level, x, y, 2);
         }
         return null;
     }

+ 18 - 0
src/me/hammerle/supersnuvi/entity/components/BoxMovement.java

@@ -0,0 +1,18 @@
+package me.hammerle.supersnuvi.entity.components;
+
+import me.hammerle.supersnuvi.entity.Entity;
+
+public class BoxMovement extends Movement
+{
+    @Override
+    public boolean isSolid()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean hasGravity(Entity ent)
+    {
+        return true;
+    }
+}

+ 4 - 12
src/me/hammerle/supersnuvi/entity/components/DefaultEnergy.java

@@ -5,23 +5,15 @@ import me.hammerle.supersnuvi.entity.Entity;
 
 public class DefaultEnergy extends Energy
 {
-    private final float maxEnergy;
-    private float energy;
+    private final float maxEnergy = 100.0f;
+    private float energy = maxEnergy;
     
     private static final float BASE = 1.0f / Game.getTicksForMillis(3000);
-    
-    public DefaultEnergy(Entity ent, float maxEnergy) 
-    {
-        super(ent);
-        this.maxEnergy = maxEnergy;
-        this.energy = maxEnergy;
-    }
 
     @Override
-    public void tick() 
+    public void tick(Entity ent) 
     {
-        float mx = ent.getMotionX();
-        
+        float mx = ent.getOwnForceX();
         if(mx == 0.0f)
         {
             addEnergyPercent(BASE);

+ 3 - 23
src/me/hammerle/supersnuvi/entity/components/DefaultHealth.java

@@ -1,32 +1,23 @@
 package me.hammerle.supersnuvi.entity.components;
 
 import me.hammerle.supersnuvi.Game;
-import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.tiles.Tile;
 import me.hammerle.supersnuvi.util.SoundUtils;
 import me.hammerle.supersnuvi.util.SoundUtils.Sound;
 
 public class DefaultHealth extends Health
 {
-    private final float maxHealth;
-    private float health;
+    private final float maxHealth = 100.0f;
+    private float health = maxHealth;
     
     private int hurtTicks = 0;
     private int invincibility = 0;
     
-    private final IDeath death;
-    
     private final Sound soundHeal;
     private final Sound soundHurt;
     private final Sound soundDeath;
     
-    public DefaultHealth(Entity ent, IDeath death, float maxHealth, 
-            Sound soundHeal, Sound soundHurt, Sound soundDeath) 
+    public DefaultHealth(Sound soundHeal, Sound soundHurt, Sound soundDeath) 
     {
-        super(ent);
-        this.death = death;
-        this.maxHealth = maxHealth;
-        this.health = maxHealth;
         this.soundHeal = soundHeal;
         this.soundHurt = soundHurt;
         this.soundDeath = soundDeath;
@@ -48,11 +39,6 @@ public class DefaultHealth extends Health
         {
             hurtTicks++;
         }
-        
-        if(shouldDespawn())
-        {
-            death.onDeath(ent);
-        }
     }
 
     @Override
@@ -61,12 +47,6 @@ public class DefaultHealth extends Health
         return health <= 0.0f;
     }
     
-    @Override
-    public boolean shouldDespawn() 
-    {
-        return (isDead() && !ent.isAnimated()) || (ent.getY() > ent.getLevel().getHeight() * Tile.SIZE);
-    }
-    
     @Override
     public boolean wasHurt() 
     {

+ 15 - 18
src/me/hammerle/supersnuvi/entity/components/DefaultMovement.java

@@ -1,6 +1,7 @@
 package me.hammerle.supersnuvi.entity.components;
 
 import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.tiles.Tile;
 
 public class DefaultMovement extends Movement
 {
@@ -9,14 +10,15 @@ public class DefaultMovement extends Movement
     private final float vx;
     private final float vy;
     private float friction = 1.0f;
+    private boolean solid;
     
-    public DefaultMovement(Entity ent, float vx, float vy, float jumpPower) 
+    public DefaultMovement(float vx, float vy, float jumpPower, boolean solid) 
     {
-        super(ent);
-        this.jumpPower = jumpPower;
-        this.vx = vx;
-        this.vy = vy;
-    }
+        this.jumpPower = jumpPower * Tile.SIZE_SCALE;
+        this.vx = vx * Tile.SIZE_SCALE;
+        this.vy = vy * Tile.SIZE_SCALE;
+        this.solid = solid;
+    } 
     
     private float getFactor()
     {
@@ -41,17 +43,6 @@ public class DefaultMovement extends Movement
         return vy * getFactor();
     }
 
-    @Override
-    public boolean jump() 
-    {
-        if(ent.isOnGround())
-        {
-            ent.setMotionY((ent.getMotionY() < 0.0f ? ent.getMotionY() : 0.0f) - getJumpPower());
-            return true;
-        }
-        return false;
-    }
-
     @Override
     public float getJumpPower() 
     {
@@ -59,7 +50,7 @@ public class DefaultMovement extends Movement
     }
 
     @Override
-    public boolean hasGravity()
+    public boolean hasGravity(Entity ent)
     {
         return true;
     }
@@ -87,4 +78,10 @@ public class DefaultMovement extends Movement
     {
         return friction;
     }
+
+    @Override
+    public boolean isSolid()
+    {
+        return solid;
+    }
 }

+ 2 - 9
src/me/hammerle/supersnuvi/entity/components/Energy.java

@@ -4,16 +4,9 @@ import me.hammerle.supersnuvi.entity.Entity;
 
 public class Energy
 {
-    public final static Energy NULL = new Energy(null);
+    public final static Energy NULL = new Energy();
     
-    protected final Entity ent;
-    
-    protected Energy(Entity ent)
-    {
-        this.ent = ent;
-    }
-    
-    public void tick()
+    public void tick(Entity ent)
     {
     }
 

+ 2 - 16
src/me/hammerle/supersnuvi/entity/components/Health.java

@@ -1,17 +1,8 @@
 package me.hammerle.supersnuvi.entity.components;
 
-import me.hammerle.supersnuvi.entity.Entity;
-
 public class Health
 {
-    public final static Health NULL = new Health(null);
-    
-    protected final Entity ent;
-    
-    protected Health(Entity ent)
-    {
-        this.ent = ent;
-    }
+    public final static Health NULL = new Health();
     
     public void tick()
     {
@@ -21,12 +12,7 @@ public class Health
     {
         return false;
     }
-    
-    public boolean shouldDespawn()
-    {
-        return false;
-    }
-    
+
     public boolean wasHurt()
     {
         return false;

+ 10 - 0
src/me/hammerle/supersnuvi/entity/components/IDespawn.java

@@ -0,0 +1,10 @@
+package me.hammerle.supersnuvi.entity.components;
+
+import me.hammerle.supersnuvi.entity.Entity;
+
+public interface IDespawn
+{
+    public static final IDespawn NULL = (Entity ent) -> {};
+    
+    public void onDespawn(Entity ent);
+}

+ 9 - 23
src/me/hammerle/supersnuvi/entity/components/Movement.java

@@ -4,14 +4,7 @@ import me.hammerle.supersnuvi.entity.Entity;
 
 public class Movement
 {
-    public final static Movement NULL = new Movement(null);
-    
-    protected final Entity ent;
-    
-    public Movement(Entity ent) 
-    {
-        this.ent = ent;
-    }
+    public final static Movement NULL = new Movement();
     
     public float getVelocityX()
     {
@@ -23,16 +16,12 @@ public class Movement
         return 0.0f;
     }
     
-    public boolean canMoveEverywhere()
+    public boolean canMoveOnTiles()
     {
         return false;
     }
     
-    //--------------------------------------------------------------------------
-    // jumping
-    //--------------------------------------------------------------------------
-    
-    public boolean jump()
+    public boolean canMoveOnEntities()
     {
         return false;
     }
@@ -42,11 +31,7 @@ public class Movement
         return 0.0f;
     }
     
-    //--------------------------------------------------------------------------
-    // gravity
-    //--------------------------------------------------------------------------
-    
-    public boolean hasGravity()
+    public boolean hasGravity(Entity ent)
     {
         return false;
     }
@@ -56,10 +41,6 @@ public class Movement
         return 1.0f;
     }
     
-    //--------------------------------------------------------------------------
-    // friction
-    //--------------------------------------------------------------------------
-    
     public void setInWater(boolean b)
     {
     }
@@ -77,4 +58,9 @@ public class Movement
     {
         return 1.0f;
     }
+    
+    public boolean isSolid()
+    {
+        return false;
+    }
 }

+ 22 - 5
src/me/hammerle/supersnuvi/entity/components/StoneMovement.java

@@ -4,13 +4,18 @@ import me.hammerle.supersnuvi.entity.Entity;
 
 public class StoneMovement extends Movement
 {
-    public StoneMovement(Entity ent) 
+    public final static StoneMovement STONE = new StoneMovement(true);
+    public final static StoneMovement PLATFORM = new StoneMovement(false);
+    
+    private final boolean entityPassing;
+
+    private StoneMovement(boolean entityPassing)
     {
-        super(ent);
+        this.entityPassing = entityPassing;
     }
-
+    
     @Override
-    public boolean hasGravity() 
+    public boolean hasGravity(Entity ent) 
     {
         return !ent.isAnimated();
     }
@@ -22,8 +27,20 @@ public class StoneMovement extends Movement
     }
     
     @Override
-    public boolean canMoveEverywhere() 
+    public boolean canMoveOnTiles() 
     {
         return true;
     }  
+
+    @Override
+    public boolean isSolid()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean canMoveOnEntities()
+    {
+        return entityPassing;
+    }
 }

+ 45 - 0
src/me/hammerle/supersnuvi/entity/components/ai/BoxController.java

@@ -0,0 +1,45 @@
+package me.hammerle.supersnuvi.entity.components.ai;
+
+import me.hammerle.snuviengine.api.Shader;
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.rendering.LevelRenderer;
+import me.hammerle.supersnuvi.tiles.Tile;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.util.Utils;
+
+public class BoxController extends Controller
+{
+    private final float startX;
+    private final float endX;
+    
+    public BoxController(int type)
+    {
+        startX = (256.0f + Tile.SIZE * type) / 512.0f;
+        endX = (288.0f + Tile.SIZE * type) / 512.0f;
+    }
+
+    @Override
+    public void renderTick(Entity ent, float lag)
+    {
+        float x = Utils.interpolate(ent.getLastX(), ent.getX(), lag);
+        float y = Utils.interpolate(ent.getLastY(), ent.getY(), lag);
+        LevelRenderer.TILES.bind();      
+        Shader.getTextureRenderer().drawRectangle(x, y,
+                x + ent.getWidth(), y + ent.getHeight(), 
+                startX, 0.125f, 
+                endX, 0.1875f);
+    }
+
+    @Override
+    public void onCollideWithEntity(Entity ent, Entity other, Face face)
+    {
+        if(face == Face.LEFT && other.getOwnForceX() > 0.0f)
+        {
+            ent.applyForce(other.getOwnForceX(), 0.0f);
+        }
+        else if(face == Face.RIGHT && other.getOwnForceX() < 0.0f)
+        {
+            ent.applyForce(other.getOwnForceX(), 0.0f);
+        }
+    }
+}

+ 7 - 13
src/me/hammerle/supersnuvi/entity/components/ai/Controller.java

@@ -1,25 +1,19 @@
 package me.hammerle.supersnuvi.entity.components.ai;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.tiles.Location;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.tiles.Tile;
 import me.hammerle.supersnuvi.util.Face;
 
 public class Controller
 {
-    public final static Controller NULL = new Controller(null);
+    public final static Controller NULL = new Controller();
     
-    protected final Entity ent;
-    
-    protected Controller(Entity ent)
-    {
-        this.ent = ent;
-    }
-    
-    public void tick()
+    public void tick(Entity ent, Level level)
     {
     }
     
-    public void renderTick(float lag)
+    public void renderTick(Entity ent, float lag)
     {
     }
     
@@ -28,11 +22,11 @@ public class Controller
         return false;
     }
     
-    public void onCollideWithTile(Location loc, Face face)
+    public void onCollideWithTile(Entity ent, int x, int y, Level l, Tile t, Face face)
     {
     }
     
-    public void onCollideWithEntity(Entity ent, Face face)
+    public void onCollideWithEntity(Entity ent, Entity other, Face face)
     {
     }
 }

+ 33 - 23
src/me/hammerle/supersnuvi/entity/components/ai/HumanController.java

@@ -4,6 +4,8 @@ import me.hammerle.snuviengine.api.Shader;
 import me.hammerle.snuviengine.api.Texture;
 import me.hammerle.supersnuvi.Keys;
 import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.tiles.Tile;
 import me.hammerle.supersnuvi.util.Face;
 import me.hammerle.supersnuvi.util.SoundUtils;
 import me.hammerle.supersnuvi.util.SoundUtils.Sound;
@@ -14,6 +16,8 @@ public class HumanController extends Controller
     protected final static int SIZE = 1024;
     private final static Texture HERO = new Texture("resources/hero.png");
     
+    private final static float OFFSET_Y = -Tile.SIZE * 0.15625f;
+    
     protected float ox = 0.0f;
     protected float oy = 0.0f;
     protected float w = 0.0f;
@@ -25,11 +29,8 @@ public class HumanController extends Controller
     private int idleCounter = 0;
     protected int idleFrame = 0;
     protected int deathFrame = 0;
-
-    public HumanController(Entity ent) 
-    {
-        super(ent);
-    }
+    
+    protected Face face = Face.RIGHT;
     
     protected void nextWalkFrame()
     {
@@ -71,14 +72,14 @@ public class HumanController extends Controller
     }
     
     @Override
-    public void tick() 
+    public void tick(Entity ent, Level level) 
     {
         if(ent.getHealth().isDead())
         {
-            ox = ent.getFace() == Face.RIGHT ? 0.0f: -37.0f;
+            ox = face == Face.RIGHT ? 0.0f: -1.15625f * Tile.SIZE;
             oy = 0.0f;
-            h = 64.0f;
-            w = 64.0f;
+            h = 2.0f;
+            w = 2.0f;
             
             tx = (deathFrame * 64.0f) / SIZE;
             if(deathFrame < 16)
@@ -103,33 +104,33 @@ public class HumanController extends Controller
         
         if(Keys.LEFT.isDown())
         {
-            ent.setMotionX(-speed);
+            ent.applyOwnForce(-speed, 0.0f);
             SoundUtils.playSound(ent.getMovement().isInWater() ? SoundUtils.Sound.WALK_WATER : SoundUtils.Sound.WALK);
         }
         
         if(Keys.RIGHT.isDown())
         {
-            ent.setMotionX(speed);
+            ent.applyOwnForce(speed, 0.0f);
             SoundUtils.playSound(ent.getMovement().isInWater() ? SoundUtils.Sound.WALK_WATER : SoundUtils.Sound.WALK);
         }
         
         if(Keys.JUMP.isDown())
         {
-            if(ent.getEnergy().getEnergyPercent() >= 0.1f && ent.getMovement().jump())
+            if(ent.getEnergy().getEnergyPercent() >= 0.1f && ent.jump())
             {
                 SoundUtils.playSound(Sound.JUMP);
                 ent.getEnergy().addEnergyPercent(-0.1f);
             }
         }
         
-        ox = ent.getFace() == Face.RIGHT ? 0.0f: -5.0f;
+        ox = face == Face.RIGHT ? 0.0f : -0.15625f * Tile.SIZE;
         oy = 0.0f;
-        h = 64.0f;
-        w = 32.0f;
+        h = 2.0f;
+        w = 1.0f;
         
         if(ent.isOnGround())
         {
-            if(ent.getMotionX() == 0.0f)
+            if(ent.getOwnForceX() == 0.0f)
             {
                 tx = (idleFrame * 32.0f) / SIZE;
                 ty = 128.0f / SIZE;
@@ -148,10 +149,19 @@ public class HumanController extends Controller
             tx = 0.0f;
             ty = 0.0f;
         }
+        
+        if(ent.getOwnForceX() > 0)
+        {
+            face = Face.RIGHT;
+        }
+        else if(ent.getOwnForceX() < 0)
+        {
+            face = Face.LEFT;
+        }
     }
 
     @Override
-    public void renderTick(float lag)
+    public void renderTick(Entity ent, float lag)
     {
         if(ent.getHealth().wasHurt())
         {
@@ -172,21 +182,21 @@ public class HumanController extends Controller
 
         float m1;
         float m2;
-        if(ent.getFace() == Face.LEFT)
+        if(face == Face.LEFT)
         {
-            m1 = w;
+            m1 = w * Tile.SIZE;
             m2 = 0.0f;
         }
         else
         {
             m1 = 0.0f;
-            m2 = w;
+            m2 = w * Tile.SIZE;
         }
         Shader.getTextureRenderer().drawRectangle(
-                x + ox + m1, y + oy, 
-                x + ox + m2, y + oy + h, 
+                x + ox + m1, y + oy + OFFSET_Y, 
+                x + ox + m2, y + oy + h * Tile.SIZE + OFFSET_Y, 
                 tx, ty, 
-                tx + (w / SIZE), ty + (h / SIZE));
+                tx + (w * 32.0f / SIZE), ty + (h * 32.0f / SIZE));
         
         if(ent.getHealth().wasHurt() || ent.getHealth().wasHealed())
         {

+ 73 - 60
src/me/hammerle/supersnuvi/entity/components/ai/LondonerController.java

@@ -3,8 +3,9 @@ package me.hammerle.supersnuvi.entity.components.ai;
 import me.hammerle.snuviengine.api.Shader;
 import me.hammerle.snuviengine.api.Texture;
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.tiles.Location;
-import me.hammerle.supersnuvi.tiles.Ramp;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.tiles.RampTile;
+import me.hammerle.supersnuvi.tiles.Tile;
 import me.hammerle.supersnuvi.util.Face;
 import me.hammerle.supersnuvi.util.SoundUtils;
 import me.hammerle.supersnuvi.util.Utils;
@@ -13,6 +14,11 @@ public class LondonerController extends Controller
 {
     private final static Texture LONDONER = new Texture("resources/londoner.png");
     
+    // for render offset: 0.21875f, 0.59375f, 0.65625f, 2.0f
+    // render offset: -0.21875, -0.59375
+    private final static float OFFSET_X = -Tile.SIZE * 0.21875f;
+    private final static float OFFSET_Y = -Tile.SIZE * 0.59375f;
+    
     private final boolean evil;
     
     private float ox = 0.0f;
@@ -24,16 +30,15 @@ public class LondonerController extends Controller
     private int deathCounter = 0;
     private int deathFrame = 0;
     
+    private Face oldFace = Face.LEFT;
     private Face direction = Face.LEFT;
     
     private boolean shouldJump = false;
     
-    private Entity hurt = null;
-    private Entity attacker = null;
-
-    public LondonerController(Entity ent, boolean evil) 
+    private int hurtTicks = 0;
+    
+    public LondonerController(boolean evil) 
     {
-        super(ent);
         this.evil = evil;
     }
     
@@ -49,22 +54,27 @@ public class LondonerController extends Controller
     }
     
     @Override
-    public void tick() 
+    public void tick(Entity ent, Level level) 
     {
+        if(hurtTicks > 0)
+        {
+            hurtTicks--;
+        }
+        
         if(evil)
         {
             if(transformFrame != -1)
             {
                 if(transformFrame < 3)
                 {
-                    ox = ent.getFace() == Face.RIGHT ? 0.0f: -36.0f;
+                    ox = direction == Face.RIGHT ? 0.0f: -1.125f * Tile.SIZE;
                     tx = transformFrame * 0.125f + 0.125f;
                     ty = 0.375f;
                     transformFrame++;
                     return;
                 }
             }
-            else if(ent.getLevel().getHero().getSquaredDistance(ent) <= 16384) // 4 Tiles
+            else if(level.getHero().getSquaredDistance(ent) <= 16384) // 4 Tiles
             {
                 transformFrame = 0;
             }
@@ -72,7 +82,7 @@ public class LondonerController extends Controller
         
         if(ent.getHealth().isDead())
         {
-            ox = ent.getFace() == Face.RIGHT ? -32.0f: -4.0f;
+            ox = direction == Face.RIGHT ? -Tile.SIZE: -0.125f * Tile.SIZE;
             
             tx = deathFrame * 0.125f;
             ty = hasRedEyes() ? 0.125f : 0.25f;
@@ -89,68 +99,42 @@ public class LondonerController extends Controller
         
         if(hasRedEyes())
         {
-            float hx = ent.getLevel().getHero().getCenterX();
+            float hx = level.getHero().getCenterX();
             if(hx < ent.getCenterX())
             {
-                ent.setMotionX(-ent.getMovement().getVelocityX());
+                ent.applyOwnForce(-ent.getMovement().getVelocityX(), 0.0f);
+                direction = Face.LEFT;
             }
             else
             {
-                ent.setMotionX(ent.getMovement().getVelocityX());
+                ent.applyOwnForce(ent.getMovement().getVelocityX(), 0.0f);
+                direction = Face.RIGHT;
             }
         }
         else
         {
             if(direction == Face.LEFT)
             {
-                ent.setMotionX(-ent.getMovement().getVelocityX());
+                ent.applyOwnForce(-ent.getMovement().getVelocityX(), 0.0f);
             }
             else
             {
-                ent.setMotionX(ent.getMovement().getVelocityX());
+                ent.applyOwnForce(ent.getMovement().getVelocityX(), 0.0f);
             }
         }
         
         if(shouldJump)
         {
             SoundUtils.playSound(SoundUtils.Sound.LONDONER_JUMP);
-            ent.getMovement().jump();
+            ent.jump();
             shouldJump = false;
         }
         
-        if(attacker != null)
-        {
-            this.ent.getHealth().addHealthPercent(-0.201f);
-            attacker.setMotionY(-attacker.getMovement().getJumpPower());
-            
-            attacker = null;
-            hurt = null;
-        }
-        
-        if(hurt != null)
-        {
-            hurt.getHealth().addHealthPercent(-0.1f);
-
-            hurt.setMotionY(ent.getMotionY() - 20.0f);
-
-            float hx = hurt.getLevel().getHero().getCenterX();
-            if(hx < hurt.getCenterX())
-            {
-                hurt.setMotionX(hurt.getMotionX() - 10.0f);
-            }
-            else
-            {
-                hurt.setMotionX(hurt.getMotionX() + 10.0f);
-            }
-            
-            hurt = null;
-        }
-        
-        ox = ent.getFace() == Face.RIGHT ? 0.0f: -36.0f;
+        ox = direction == Face.RIGHT ? 0.0f: -1.125f * Tile.SIZE;
         
         if(ent.isOnGround())
         {
-            if(ent.getMotionX() == 0.0f)
+            if(ent.getOwnForceX() == 0.0f)
             {
                 tx = 0.875f;
                 ty = hasRedEyes() ? 0.25f : 0.375f;
@@ -170,10 +154,12 @@ public class LondonerController extends Controller
             tx = 0.125f;
             ty = hasRedEyes() ? 0.5f : 0.625f;
         }
+        
+        oldFace = direction;
     }
 
     @Override
-    public void renderTick(float lag)
+    public void renderTick(Entity ent, float lag)
     {
         if(ent.getHealth().wasHurt())
         {
@@ -194,19 +180,19 @@ public class LondonerController extends Controller
         
         float m1;
         float m2;
-        if(ent.getFace() == Face.LEFT)
+        if(oldFace == Face.LEFT)
         {
-            m1 = 64.0f;
+            m1 = 2.0f * Tile.SIZE;
             m2 = 0.0f;
         }
         else
         {
             m1 = 0.0f;
-            m2 = 64.0f;
+            m2 = 2.0f * Tile.SIZE;
         }
         Shader.getTextureRenderer().drawRectangle(
-                x + ox + m1, y,
-                x + ox + m2, y + 64.0f, 
+                x + ox + m1 + OFFSET_X, y + OFFSET_Y,
+                x + ox + m2 + OFFSET_X, y + 2.0f * Tile.SIZE + OFFSET_Y, 
                 tx, ty, 
                 tx + 0.125f, ty + 0.125f);
         
@@ -218,18 +204,22 @@ public class LondonerController extends Controller
     }
     
     @Override
-    public void onCollideWithTile(Location loc, Face face)
+    public void onCollideWithTile(Entity ent, int x, int y, Level l, Tile t, Face face)
     {
+        if(ent.getHealth().isDead())
+        {
+            return;
+        }
         if(hasRedEyes())
         {
-            if(!(loc.getTile() instanceof Ramp) && face == ent.getFace() && ent.getMotionX() == 0.0f)
+            if(!(t instanceof RampTile) && face == direction && ent.getOwnForceX() == 0.0f)
             {
                 shouldJump = true;
             }
         }
         else
         {
-            if(loc.getTile().shouldAiUseCollisionBox(loc.getX(), loc.getY(), loc.getLevel()))
+            if(t.shouldAiUseCollisionBox(x, y, l))
             {
                 switch(face)
                 {
@@ -245,19 +235,42 @@ public class LondonerController extends Controller
     }
 
     @Override
-    public void onCollideWithEntity(Entity ent, Face face)
+    public void onCollideWithEntity(Entity ent, Entity other, Face face)
     {
+        if(ent.getHealth().isDead())
+        {
+            return;
+        }
         switch(face)
         {
             case LEFT:
             case RIGHT:
-                if(ent.getItemCollector().isHero() && hasRedEyes())
+                if(hurtTicks > 0)
+                {
+                    return;
+                }
+                if(other.getItemCollector().isHero() && hasRedEyes())
+                {
+                    other.getHealth().addHealthPercent(-0.1f);
+                    other.applyForce(0.0f, -20.0f * Tile.SIZE_SCALE);
+                    if(ent.getCenterX() < other.getCenterX())
+                    {
+                        other.applyForce(20.0f * Tile.SIZE_SCALE, 0.0f);
+                    }
+                    else
+                    {
+                        other.applyForce(-20.0f * Tile.SIZE_SCALE, 0.0f);
+                    }
+                }
+                else
                 {
-                    hurt = ent;
+                    direction = direction.getOpposite();
                 }
                 break;
             case UP:
-                attacker = ent;
+                ent.getHealth().addHealthPercent(-0.201f);
+                other.applyForce(0.0f, -other.getMotionY() - other.getMovement().getJumpPower() * 0.5f);
+                hurtTicks = 10;
                 break;
         }
     }

+ 135 - 0
src/me/hammerle/supersnuvi/entity/components/ai/PlatformController.java

@@ -0,0 +1,135 @@
+package me.hammerle.supersnuvi.entity.components.ai;
+
+import java.util.ArrayList;
+import me.hammerle.snuviengine.api.Shader;
+import me.hammerle.snuviengine.api.Texture;
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.tiles.Tile;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.util.Utils;
+
+public class PlatformController extends Controller
+{
+    private final static Texture TEXTURE = new Texture("resources/platform.png");
+    
+    private static class MoveData
+    {
+        private final float x;
+        private final float y;
+        private final float speedX;
+        private final float speedY;
+        private final int waitTicks;
+        
+        public MoveData(float x, float y, float speedX, float speedY, int waitTicks)
+        {
+            this.x = x;
+            this.y = y;
+            this.speedX = Math.abs(speedX);
+            this.speedY = Math.abs(speedY);
+            this.waitTicks = waitTicks;
+        }
+    }
+    
+    private final ArrayList<MoveData> dataList = new ArrayList<>();
+    private int moveIndex = 0;
+    private int waitTicks = 0;
+    
+    private int samePosX = 0;
+    private int samePosY = 0;
+  
+    @Override
+    public boolean isAnimated()
+    {
+        return true;
+    }
+    
+    public void addMoveData(float x, float y, float speedX, float speedY, int waitTicks)
+    {
+        dataList.add(new MoveData(x, y, speedX, speedY, waitTicks));
+    }
+    
+    public void clearMoveData()
+    {
+        dataList.clear();
+    }
+    
+    private float clamp(float f, float min, float max)
+    {
+        if(f < min)
+        {
+            return min;
+        }
+        else if(f > max)
+        {
+            return max;
+        }
+        return f;
+    }
+    
+    @Override
+    public void tick(Entity ent, Level level) 
+    {
+        if(dataList.isEmpty())
+        {
+            return;
+        }
+        
+        MoveData data = dataList.get(moveIndex);
+        if(ent.isAt(data.x, data.y))
+        {
+            waitTicks++;
+            if(waitTicks >= data.waitTicks)
+            {
+                waitTicks = 0;
+                moveIndex = (moveIndex + 1) % dataList.size();
+            }
+        }
+        else
+        {
+            float motionX = clamp(data.x - ent.getX(), -data.speedX, data.speedX);
+            float motionY = clamp(data.y - ent.getY(), -data.speedY, data.speedY);
+            ent.applyOwnForce(motionX, motionY);
+        }
+    }
+
+    @Override
+    public void renderTick(Entity ent, float lag)
+    {
+        TEXTURE.bind();
+        float x = Utils.interpolate(ent.getLastX(), ent.getX(), lag);
+        float y = Utils.interpolate(ent.getLastY(), ent.getY(), lag);
+        
+        int mid = (int) Math.ceil((ent.getWidth() / Tile.SIZE) - 2);
+        float endY = y + ent.getHeight();
+        
+        Shader.getTextureRenderer().drawRectangle(x, y, x + Tile.SIZE, endY, 0.0f, 0.25f, 0.25f, 0.4453125f);
+        x += Tile.SIZE;
+        for(int i = 0; i < mid; i++)
+        {
+            Shader.getTextureRenderer().drawRectangle(x, y, x + Tile.SIZE, endY, 0.25f, 0.25f, 0.5f, 0.4453125f);
+            x += Tile.SIZE;
+        }
+        Shader.getTextureRenderer().drawRectangle(x, y, x + Tile.SIZE, endY, 0.5f, 0.25f, 0.75f, 0.4453125f);
+    }
+
+    @Override
+    public void onCollideWithEntity(Entity ent, Entity other, Face face)
+    {
+        if(face == Face.UP)
+        {
+            if(ent.getOwnForceY() < 0)
+            {
+                other.applyForce(ent.getOwnForceX(), ent.getOwnForceY() - Entity.GRAVITY * other.getMovement().getGravityFactor());
+            }
+            else
+            {
+                other.applyForce(ent.getOwnForceX(), ent.getOwnForceY());
+            }
+        }
+        else if(face == Face.LEFT || face == Face.RIGHT)
+        {
+            other.applyForce(ent.getOwnForceX(), 0.0f);
+        }
+    }
+}

+ 12 - 37
src/me/hammerle/supersnuvi/entity/components/ai/StartScreenHeroController.java

@@ -1,59 +1,34 @@
 package me.hammerle.supersnuvi.entity.components.ai;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.tiles.Location;
-import me.hammerle.supersnuvi.tiles.Ramp;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.tiles.RampTile;
+import me.hammerle.supersnuvi.tiles.Tile;
 import me.hammerle.supersnuvi.util.Face;
 
 public class StartScreenHeroController extends HumanController
 {
     private boolean shouldJump = false;
     
-    public StartScreenHeroController(Entity ent) 
-    {
-        super(ent);
-    }
-    
     @Override
-    public void tick() 
+    public void tick(Entity ent, Level level) 
     {
-        if(ent.getHealth().isDead())
-        {
-            ox = ent.getFace() == Face.RIGHT ? 0.0f: -37.0f;
-            oy = 0.0f;
-            h = 64.0f;
-            w = 64.0f;
-            
-            tx = (deathFrame * 64.0f) / SIZE;
-            if(deathFrame < 16)
-            {
-                ty = 192.0f / SIZE;
-            }
-            else
-            {
-                ty = 256.0f / SIZE;
-            }
-            
-            nextDeathFrame();
-            return;
-        }
-        
-        ent.setMotionX(ent.getMovement().getVelocityX());
+        ent.applyOwnForce(ent.getMovement().getVelocityX(), 0.0f);
         
         if(shouldJump)
         {
             shouldJump = false;
-            ent.getMovement().jump();
+            ent.jump();
         }
         
-        ox = ent.getFace() == Face.RIGHT ? 0.0f: -5.0f;
+        ox = 0.0f;
         oy = 0.0f;
-        h = 64.0f;
-        w = 32.0f;
+        h = 2.0f;
+        w = 1.0f;
         
         if(ent.isOnGround())
         {
-            if(ent.getMotionX() == 0.0f)
+            if(ent.getOwnForceX() == 0.0f)
             {
                 tx = (idleFrame * 32.0f) / SIZE;
                 ty = 128.0f / SIZE;
@@ -75,9 +50,9 @@ public class StartScreenHeroController extends HumanController
     }
 
     @Override
-    public void onCollideWithTile(Location loc, Face face)
+    public void onCollideWithTile(Entity ent, int x, int y, Level l, Tile t, Face face)
     {
-        if(face == Face.RIGHT && !(loc.getTile() instanceof Ramp))
+        if(face == Face.RIGHT && !(t instanceof RampTile))
         {
             shouldJump = true;
         }

+ 5 - 8
src/me/hammerle/supersnuvi/entity/components/ai/StoneController.java

@@ -3,6 +3,8 @@ package me.hammerle.supersnuvi.entity.components.ai;
 import me.hammerle.snuviengine.api.Shader;
 import me.hammerle.snuviengine.api.Texture;
 import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.tiles.Tile;
 import me.hammerle.supersnuvi.util.Utils;
 
 public class StoneController extends Controller
@@ -10,11 +12,6 @@ public class StoneController extends Controller
     private final static Texture STONE = new Texture("resources/stone.png");
     
     private int frame = 0;
-    
-    public StoneController(Entity ent)
-    {
-        super(ent);
-    }
 
     @Override
     public boolean isAnimated()
@@ -23,7 +20,7 @@ public class StoneController extends Controller
     }
 
     @Override
-    public void renderTick(float lag)
+    public void renderTick(Entity ent, float lag)
     {
         STONE.bind();
         float x = Utils.interpolate(ent.getLastX(), ent.getX(), lag);
@@ -32,12 +29,12 @@ public class StoneController extends Controller
         float row = (frame / 16) * 0.3125f;
         float column = (frame % 16) * 0.0625f;
         Shader.getTextureRenderer().drawRectangle(
-                x, y, x + 32.0f, y + 160.0f, 
+                x, y, x + Tile.SIZE, y + 5.0f * Tile.SIZE, 
                 column, row, column + 0.0625f, row + 0.3125f);
     }
 
     @Override
-    public void tick()
+    public void tick(Entity ent, Level level)
     {
         if(frame < 40)
         {

+ 217 - 0
src/me/hammerle/supersnuvi/gamelogic/FileLevel.java

@@ -0,0 +1,217 @@
+package me.hammerle.supersnuvi.gamelogic;
+
+import java.io.File;
+import java.util.LinkedList;
+import java.util.TreeSet;
+import java.util.function.Consumer;
+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.Tile;
+import me.hammerle.supersnuvi.util.Utils;
+
+public final class FileLevel extends Level
+{
+    // level data
+    private final LevelData data;
+    private final String name;
+    private final String fileName;
+    // level stats
+    private int souls = 0;
+    private int maxSouls = 0;
+    private float time = 0.0f;
+    private final LinkedList<String> messages = new LinkedList<>();
+    // controlling
+    private TreeSet<Point> spawns = new TreeSet<>();
+    // scripting
+    private Script levelScript = null;
+
+    public FileLevel(File f)
+    {
+        this.data = new LevelData(f);
+        if(!data.load())
+        {
+            throw new IllegalArgumentException(f + " is not a valid level.");
+        }
+        
+        this.name = data.getString("name", "error");
+        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());
+        
+        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
+        reset();
+        
+        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); 
+    }
+    
+    // -------------------------------------------------------------------------
+    // level data
+    // -------------------------------------------------------------------------
+
+    @Override
+    public LevelData getData()
+    {
+        return data;
+    }
+    
+    @Override
+    public String getName() 
+    {
+        return name;
+    }
+    
+    @Override
+    public String getFileName() 
+    {
+        return fileName;
+    }
+    
+    @Override
+    public void addMessage(String message)
+    {
+        messages.add(message);
+    }
+    
+    @Override
+    public String getMessage()
+    {
+        return messages.isEmpty() ? null : messages.getFirst();
+    }
+    
+    // -------------------------------------------------------------------------
+    // level stats
+    // -------------------------------------------------------------------------
+    
+    @Override
+    public int getCurrentBottles()
+    {
+        return souls;
+    }
+    
+    @Override
+    public int getMaxBottles()
+    {
+        return maxSouls;
+    }
+    
+    @Override
+    public void addBottles(int score)
+    {
+        souls += score;
+    }
+    
+    @Override
+    public float getTime()
+    {
+        return time;
+    }
+    
+    // -------------------------------------------------------------------------
+    // controlling
+    // -------------------------------------------------------------------------
+    
+    @Override
+    public void tick()
+    {
+        if(!messages.isEmpty() && Keys.ENTER.getTime() == 1)
+        {
+            messages.removeFirst();
+        }
+
+        time += Game.SECS_PER_TICK;
+        Game.get().tickTiles();
+
+        tickEntities();
+    }  
+    
+    @Override
+    protected void onReset()
+    {
+        Game.get().resetTiles(this);
+        data.activateEntities();
+        souls = 0;
+        time = 0.0f;
+        Entity h = spawnHero(true);
+        setHero(h);
+        removeEntities();
+        spawnEntity(h);
+        messages.clear();
+        setAmbientLight(1.0f, 1.0f, 1.0f);
+        
+        getTileUpdater().updateAll();
+        
+        callEvent("level_reset");
+    }
+    
+    public Entity spawnHero(boolean first)
+    {
+        Point p;
+        if(getHero() == null || getHero().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(getHero().getX()), Utils.toBlock(getHero().getY())));
+            if(p == null)
+            {
+                p = spawns.first();
+            }
+        }       
+        return EntityBuilder.buildHero(this, Utils.toCoord(p.getX()), Utils.toCoord(p.getY()));
+    }
+         
+    // -------------------------------------------------------------------------
+    // scripting
+    // -------------------------------------------------------------------------
+    
+    @Override
+    public void callEvent(String name, Consumer<Script> before, Consumer<Script> after)
+    {
+        if(levelScript != null)
+        {
+            //Level l = Game.get().getCurrentLevel();
+            //Game.get().setCurrentLevel(this);
+            Game.get().getParser().callEvent(name, levelScript, before, after);
+            //Game.get().setCurrentLevel(l);
+        }
+    }
+}

+ 178 - 581
src/me/hammerle/supersnuvi/gamelogic/Level.java

@@ -1,624 +1,257 @@
 package me.hammerle.supersnuvi.gamelogic;
 
-import java.io.File;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedList;
-import java.util.List;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-import me.hammerle.snuviengine.api.Shader;
-import me.hammerle.snuviengine.api.Texture;
-import me.hammerle.snuviengine.api.TextureRenderer;
+import java.util.function.Consumer;
+import me.hammerle.snuviscript.code.Script;
 import me.hammerle.supersnuvi.Game;
 import me.hammerle.supersnuvi.entity.Entity;
 import me.hammerle.supersnuvi.entity.EntityBuilder;
-import me.hammerle.supersnuvi.tiles.Location;
+import me.hammerle.supersnuvi.rendering.Light;
+import me.hammerle.supersnuvi.rendering.TileUpdater;
 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 abstract class Level
 {
-    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;
-    
+    // level data
+    private final TileUpdater tileUpdater = new TileUpdater();
+    // entity related
     private final HashMap<Integer, Entity> entities = new HashMap<>();
     private final LinkedList<Entity> spawnQueue = new LinkedList<>();
-    private Entity hero;
+    private Entity hero = null;
     private int entityCounter = 0;
-    
+    // controlling
     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 boolean done = false;
+    // lighting
+    private float red = 1.0f;
+    private float green = 1.0f;
+    private float blue = 1.0f;
+    private final Light[] lights = new Light[32];
     
-    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<Point> spawns = new TreeSet<>();
-    
-    public Level(File f)
+    public Level()
     {
-        this.data = new LevelData(f);
-        this.worldLoaded = data.load();
-        
-        if(worldLoaded)
+        for(int i = 0; i < lights.length; i++)
         {
-            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());
+            lights[i] = new Light();
         }
-        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));
-        }
-
-        resetLevel();
     }
-
+    
     // -------------------------------------------------------------------------
-    // basic stuff
+    // level data
     // -------------------------------------------------------------------------
-
-    @Override
-    public Entity getHero()
-    {
-        return hero;
-    }
     
-    public String getFileName() 
-    {
-        return fileName;
-    }
+    public abstract LevelData getData();
     
-    public String getName() 
+    public String getName()
     {
-        return name;
+        return "Unknown";
     }
     
-    @Override
-    public void finishLevel()
+    public String getFileName()
     {
-        shouldReset = true;
-        done = true;
+        return "Unknown";
     }
     
-    public boolean shouldFinish()
+    public final TileUpdater getTileUpdater()
     {
-        return done;
+        return tileUpdater;
     }
     
-    public boolean shouldReset()
-    {
-        return shouldReset;
-    }
+    // -------------------------------------------------------------------------
+    // level stats
+    // -------------------------------------------------------------------------
     
-    @Override
-    public void scheduleReset()
+    public int getCurrentBottles()
     {
-        shouldReset = true;
+        return 0;
     }
     
-    public boolean resetLevel()
+    public int getMaxBottles()
     {
-        /*boolean dead = false;
-        if(!done) // hero just died
-        {
-            tries--;
-            if(tries <= 0)
-            {
-                tries = 7;
-                dead = true;
-            }
-        }*/
-        Game.get().resetTiles(this);
-        data.activateEntities();
-        souls = 0;
-        time = 0.0f;
-        shouldReset = false;
-        done = false;
-        Entity h = spawnHero(true);
-        hero = h;
-        entities.clear();
-        entities.put(entityCounter++, h);
-        
-        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();
-                }
-            }
-        }
-        return false;
+        return 0;
     }
     
-    @Override
-    public void spawnEntity(Entity ent)
+    public void addBottles(int bottles)
     {
-        spawnQueue.add(ent);
     }
     
-    public Entity spawnHero(boolean first)
+    public float getTime()
     {
-        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;
+        return 0;
     }
     
-    @Override
-    public void increaseSouls(int score)
+    public void addMessage(String message)
     {
-        souls += score;
     }
     
-    public int getCurrentBottles()
+    public String getMessage()
     {
-        return souls;
+        return null;
     }
     
-    public int getMaxBottles()
-    {
-        return maxSouls;
-    }
+    // -------------------------------------------------------------------------
+    // entity related
+    // -------------------------------------------------------------------------
     
-    public LevelData getData()
+    public final Entity getHero()
     {
-        return data;
+        return hero;
     }
     
-    public float getTime()
+    public final void setHero(Entity hero)
     {
-        return time;
-    }  
+        this.hero = hero;
+    }
     
-    @Override
-    public int getWidth()
+    public final void removeEntities()
     {
-        return data.getWidth();
+        entities.clear();
     }
     
-    @Override
-    public int getHeight()
+    public final void spawnEntity(Entity ent)
     {
-        return data.getHeight();
+        spawnQueue.add(ent);
     }
-
-    // -------------------------------------------------------------------------
-    // tick
-    // -------------------------------------------------------------------------
     
-    public void tick()
+    public final void tickEntities()
     {
-        if(worldLoaded)
+        entities.values().removeIf(ent -> 
         {
-            time += Game.SECS_PER_TICK;
-            Game.get().tickTiles();
-            
-            // doing entity logic first
-            entities.values().removeIf(entity -> 
+            ent.tick(this);
+            if((ent.getHealth().isDead() && !ent.isAnimated()) || (ent.getY() > getData().getHeight() * Tile.SIZE))
             {
-                entity.tick();
-                return entity.getHealth().shouldDespawn();
-            });
-            
-            if(!spawnQueue.isEmpty())
+                ent.onDespawn();
+                callEvent("entity_despawn", (sc) -> 
+                {
+                    sc.setVar("entity", ent);
+                }, null);
+                return true;
+            }
+            return false;
+        });
+        entities.values().forEach(ent -> ent.tickCollision(this));
+        int loops = 20;
+        int changes = 1;
+        while(changes > 0)
+        {
+            loops--;
+            if(loops == 0)
+            {
+                System.out.println(System.nanoTime() + " Loop end reached ...");
+                break;
+            }
+            changes = 0;
+            for(Entity ent : entities.values())
             {
-                spawnQueue.forEach(ent -> entities.put(entityCounter++, ent));
-                spawnQueue.clear();
+                changes += ent.move(this);
             }
-            
-            // 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(!spawnQueue.isEmpty())
+        {
+            spawnQueue.forEach(ent -> 
             {
-                if(tile > 0)
+                entities.put(entityCounter++, ent);
+                callEvent("entity_spawn", (sc) -> 
                 {
-                    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);
+                    sc.setVar("entity", ent);
+                }, null);
+            });
+            spawnQueue.clear();
         }
-    }   
+    }
     
-    public String formatBottles(int bottles)
+    public final void forEachCollidingEntity(Entity ent, Consumer<Entity> c)
     {
-        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] = '/';
-        if(maxSouls <= 9)
-        {
-            c[3] = '0';
-            c[4] = (char) (maxSouls + '0');
-        }
-        else if(maxSouls > 99)
-        {
-            c[3] = 'X';
-            c[4] = 'X';
-        }
-        else
-        {
-            c[3] = (char) ((maxSouls / 10) + '0');
-            c[4] = (char) ((maxSouls % 10) + '0');
-        }
-        return new String(c);
+        float minX = ent.getX() - Entity.STEP;
+        float minY = ent.getY() - Entity.STEP;
+        float maxX = ent.getX() + ent.getWidth() + Entity.STEP;
+        float maxY = ent.getY() + ent.getHeight() + Entity.STEP;
+        entities.values().stream().filter(other -> ent != other && other.isColliding(minX, minY, maxX, maxY)).forEach(c);
     }
     
-    public String formatTime(float time)
+    public final void forEachEntity(Consumer<Entity> c)
     {
-        if(time == -1.0f)
-        {
-            return "-----";
-        }
-        else if(time >= 999.9f)
-        {
-            return "999.9";
-        }
-        return String.format("%05.1f", time);
-    }   
+        entities.values().forEach(c);
+    }
     
-    private float getViewX(float x) 
+    public final Collection<Entity> getEntities()
     {
-        x -= Shader.getViewWidth() >> 1;
-        if(x < 0)
-        {
-            return 0;
-        }
-        float max = data.getWidth() * Tile.SIZE - Shader.getViewWidth();
-        if(x > max)
-        {
-            return max;
-        }
-        return x;
+        return entities.values();
     }
     
-    private float getViewY(float y) 
+    public final Collection<Entity> getEntitiesInQueue()
     {
-        y -= Shader.getViewHeight() >> 1;
-        if(y < 0)
-        {
-            return 0;
-        }
-        float max = data.getHeight() * Tile.SIZE - Shader.getViewHeight();
-        if(y > max)
-        {
-            return max;
-        }
-        return y;
+        return spawnQueue;
     }
     
-    @Override
-    public void updateTile(int layer, int x, int y)
+    // -------------------------------------------------------------------------
+    // controlling
+    // -------------------------------------------------------------------------
+    
+    public abstract void tick();
+    
+    public final void finishLevel()
     {
-        if(layer > data.getBackgroundIndex())
-        {
-            layer--;
-        }
-        meshes[layer][x / meshSize][y / meshSize].clear();
+        shouldReset = true;
+        done = true;
     }
     
-    @Override
-    public void updateTile(int x, int y)
+    public final boolean isFinished()
     {
-        updateTile(data.getBackgroundIndex(), x, y);
+        return done;
     }
     
-    private void drawMesh(int l, int tl, int mx, int my)
+    public final boolean shouldReset()
     {
-        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() + ERROR, t.getTextureMinY() + ERROR,
-                                t.getTextureMaxX() - ERROR, t.getTextureMaxY() - ERROR);
-                    }
-                }
-            }
-            tr.build();
-        }
-        meshes[l][mx][my].draw();
+        return shouldReset;
     }
     
-    public void renderTick(float lag)
+    public final void scheduleReset()
     {
-        if(worldLoaded)
+        shouldReset = true;
+    }
+    
+    public final void reset()
+    {
+        // reset lights
+        for(Light l : lights)
         {
-            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
-            
-            entities.values().forEach(entity -> 
-            {
-                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++)
+            l.reset();
+        }
+        
+        onReset();
+        shouldReset = false;
+        done = false;
+        
+        // spawn entities
+        getData().forEachEntity((x, y, tile) -> 
+        {
+            if(tile > 0)
             {
-                for(int mx = startX; mx < endX; mx++)
+                Entity ent = EntityBuilder.fromId(tile, this, Utils.toCoord(x), Utils.toCoord(y));
+                if(ent != null)
                 {
-                    for(int my = startY; my < endY; my++)
-                    {
-                        drawMesh(l - 1, l, mx, my);
-                    }
+                    entities.put(entityCounter++, ent);
                 }
             }
-            
-            // 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);
-            
-            float w = Shader.getViewWidth();
-
-            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;
-            }
-            
-            // 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);    
-        }
+        }, 0, getData().getWidth(), 0, getData().getHeight());
     }
     
+    protected void onReset()
+    {
+    };
+    
     // -------------------------------------------------------------------------
-    // collision box, interaction layer
+    // collision
     // -------------------------------------------------------------------------
     
-    private Tile getInteractionTile(int x, int y)
+    public Tile getInteractionTile(int x, int y)
     {
-        int i = data.getInteractionTile(x, y);
+        int i = getData().getInteractionTile(x, y);
         if(i == -1)
         {
             return Game.FALLBACK_TILE;
@@ -626,83 +259,47 @@ public final class Level implements ILevel
         return Game.get().getTile(i);
     }
     
-    private CollisionObject getMovementBox(int x, int y)
+    // -------------------------------------------------------------------------
+    // scripting
+    // -------------------------------------------------------------------------
+    
+    public void callEvent(String name, Consumer<Script> before, Consumer<Script> after)
     {
-        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<CollisionObject> getMovementBoxesAt(CollisionObject box, Entity not)
+    public final void callEvent(String name)
     {
-        List<CollisionObject> 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;
+        callEvent(name, null, null);
     }
     
-    private CollisionObject getCollisionBox(int x, int y)
+    // -------------------------------------------------------------------------
+    // lighting
+    // -------------------------------------------------------------------------
+    
+    public final void setAmbientLight(float r, float g, float b)
     {
-        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));
+        red = r;
+        green = g;
+        blue = b;
     }
     
-    @Override
-    public List<Location> getCollisionBoxesAt(CollisionObject cb)
+    public final float getAmbientRed()
     {
-        LinkedList<Location> 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;
+        return red;
+    }
+    
+    public final float getAmbientGreen()
+    {
+        return green;
+    }
+    
+    public final float getAmbientBlue()
+    {
+        return blue;
     }
     
-    @Override
-    public List<Entity> getEntitiesCollidingWith(Entity not, CollisionObject cb)
+    public Light getLight(int index)
     {
-        return entities.values().stream().filter(ent -> ent != not && ent.getBox().isColliding(cb)).collect(Collectors.toList());
+        return lights[index];
     }
 }

+ 4 - 0
src/me/hammerle/supersnuvi/gamelogic/LevelData.java

@@ -85,6 +85,10 @@ public class LevelData
         int length = readInt(in);
         byte[] b = new byte[length];
         in.read(b);
+        if(length > 1024 * 1024)
+        {
+            throw new IOException("invalid level file");
+        }
         return new String(b);
     }
     

+ 28 - 376
src/me/hammerle/supersnuvi/gamelogic/StartScreenLevel.java

@@ -1,71 +1,40 @@
 package me.hammerle.supersnuvi.gamelogic;
 
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Random;
-import java.util.stream.Collectors;
-import me.hammerle.snuviengine.api.Shader;
-import me.hammerle.snuviengine.api.TextureRenderer;
+import java.util.Random;;
 import me.hammerle.supersnuvi.Game;
 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 StartScreenLevel implements ILevel
+public final class StartScreenLevel extends Level
 {
+    // level data
     private final LevelData data;
- 
-    private final HashMap<Integer, Entity> entities = new HashMap<>();
-    private final LinkedList<Entity> spawnQueue = new LinkedList<>();
-    private final Entity hero;
-    private int entityCounter = 0;
-   
-    private float cameraX = 0.0f;
-    private float cameraY = 0.0f;
-    private float oldCameraX = 0.0f;
-    private float oldCameraY = 0.0f;
-    
+    // level stats
+    private float time = 0.0f;
+    // controlling
     private float heroSpawnX;
     private float heroSpawnY;
     
-    private final int meshSize = 16;
-    private final int meshLayers;
-    private final int meshWidth;
-    private final int meshHeight;
-    private final TextureRenderer[][][] meshes;   
-    
     public StartScreenLevel()
     {
-        hero = EntityBuilder.buildStartScreenHero(this, 0.0f, 0.0f);
+        Entity hero = EntityBuilder.buildStartScreenHero(this, 0.0f, 0.0f);
+        setHero(hero);
         spawnEntity(hero);
         
         this.data = generate();
-
-        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);
-                }
-            }
-        }
-        
-        oldCameraX = -getViewX(hero.getCenterX());
-        oldCameraY = -getViewY(hero.getCenterY());
-        cameraX = oldCameraX;
-        cameraY = oldCameraY;
     }
     
+    // -------------------------------------------------------------------------
+    // level data
+    // -------------------------------------------------------------------------
+    
+    @Override
+    public LevelData getData()
+    {
+        return data;
+    }
+        
     private void placeLamp(LevelData ld, int ox, int oy)
     {
         ox -= 2;
@@ -89,7 +58,7 @@ public final class StartScreenLevel implements ILevel
         
         heroSpawnX = 0;
         heroSpawnY = (hh - 2) * Tile.SIZE;
-        hero.setPosition(heroSpawnX, heroSpawnY);
+        getHero().setPosition(heroSpawnX, heroSpawnY);
         
         LevelData ld = new LevelData(null, 1, l, w, h);
         
@@ -330,344 +299,27 @@ public final class StartScreenLevel implements ILevel
         return ld;
     }
 
-    // -------------------------------------------------------------------------
-    // basic stuff
-    // -------------------------------------------------------------------------
-
     @Override
-    public Entity getHero()
+    public float getTime()
     {
-        return hero;
-    }
-    
-    @Override
-    public void finishLevel()
-    {
-    }
-    
-    @Override
-    public void scheduleReset()
-    {
-    }
-    
-    @Override
-    public void spawnEntity(Entity ent)
-    {
-        spawnQueue.add(ent);
-    }
-    
-    @Override
-    public void increaseSouls(int score)
-    {
-    }
-    
-    @Override
-    public int getWidth()
-    {
-        return data.getWidth();
-    }
-    
-    @Override
-    public int getHeight()
-    {
-        return data.getHeight();
+        return time;
     }
 
     // -------------------------------------------------------------------------
-    // tick
+    // controlling
     // -------------------------------------------------------------------------
     
+    @Override
     public void tick()
     {
+        time += Game.SECS_PER_TICK;
         Game.get().tickTiles();
 
-        // doing entity logic first
-        entities.values().removeIf(entity -> 
-        {
-            entity.tick();
-            return entity.getHealth().shouldDespawn();
-        });
-        
-        if(!spawnQueue.isEmpty())
-        {
-            spawnQueue.forEach(ent -> entities.put(entityCounter++, ent));
-            spawnQueue.clear();
-        }
+        tickEntities();
         
-        if(hero.getX() > data.getWidth() * Tile.SIZE)
+        if(getHero().getX() > data.getWidth() * Tile.SIZE)
         {
-            hero.setPosition(heroSpawnX, heroSpawnY);
-            // calculate new camera position
-            cameraX = -getViewX(hero.getCenterX());
-            cameraY = -getViewY(hero.getCenterY());
-            oldCameraX = cameraX;
-            oldCameraY = cameraY;
+            getHero().setPosition(heroSpawnX, heroSpawnY);
         }
-        else
-        {
-            // 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);
     }   
-    
-    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())
-        {
-            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() + Level.ERROR, t.getTextureMinY() + Level.ERROR,
-                                t.getTextureMaxX() - Level.ERROR, t.getTextureMaxY() - Level.ERROR);
-                    }
-                }
-            }
-            tr.build();
-        }
-        meshes[l][mx][my].draw();
-    }
-    
-    public void renderTick(float lag)
-    {
-        float camX = Utils.interpolate(oldCameraX, cameraX, lag);
-        float camY = Utils.interpolate(oldCameraY, cameraY, lag);
-        
-        //System.out.println("CAM " + hero.getCenterX()+ " " + hero.getCenterY());
-        //System.out.println("CAM " + oldCameraX + " " + oldCameraY + " " + cameraX + " " + cameraY);
-
-        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);
-        Level.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
-
-        entities.values().forEach(entity -> 
-        {
-            entity.renderTick(lag);
-        });
-        
-        // foreground
-        Shader.setColorEnabled(false);
-        Shader.setTextureEnabled(true);
-        Shader.setBlendingEnabled(true);
-        Level.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);
-                }
-            }
-        }
-    }
-    
-    // -------------------------------------------------------------------------
-    // 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<CollisionObject> getMovementBoxesAt(CollisionObject box, Entity not)
-    {
-        List<CollisionObject> 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<Location> getCollisionBoxesAt(CollisionObject cb)
-    {
-        LinkedList<Location> 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<Entity> getEntitiesCollidingWith(Entity not, CollisionObject cb)
-    {
-        return entities.values().stream().filter(ent -> ent != not && ent.getBox().isColliding(cb)).collect(Collectors.toList());
-    }
-}
+}

+ 7 - 0
src/me/hammerle/supersnuvi/math/IVector.java

@@ -0,0 +1,7 @@
+package me.hammerle.supersnuvi.math;
+
+public interface IVector
+{
+    public float getX();
+    public float getY();
+}

+ 95 - 0
src/me/hammerle/supersnuvi/math/Vector.java

@@ -0,0 +1,95 @@
+package me.hammerle.supersnuvi.math;
+
+public class Vector implements IVector
+{
+    private float x = 0.0f;
+    private float y = 0.0f;
+    
+    public Vector(float x, float y)
+    {
+        this.x = x;
+        this.y = y;
+    }
+    
+    public Vector()
+    {
+        this(0.0f, 0.0f);
+    }
+    
+    public void setX(float x)
+    {
+        this.x = x;
+    }
+    
+    public void setY(float y)
+    {
+        this.y = y;
+    }
+    
+    public void set(float x, float y)
+    {
+        this.x = x;
+        this.y = y;
+    }
+    
+    public void set(Vector v)
+    {
+        set(v.x, v.y);
+    }
+    
+    public void add(float x, float y)
+    {
+        this.x += x;
+        this.y += y;
+    }
+    
+    public void add(Vector v)
+    {
+        add(v.x, v.y);
+    }
+    
+    public void sub(Vector v)
+    {
+        add(-v.x, -v.y);
+    }
+    
+    public void addY(float y)
+    {
+        this.y += y;
+    }
+    
+    public void addX(float x)
+    {
+        this.x += x;
+    }
+    
+    public void mul(Vector v)
+    {
+        x *= v.x;
+        y *= v.y;
+    }
+    
+    public void mul(float fx, float fy)
+    {
+        x *= fx;
+        y *= fy;
+    }
+
+    @Override
+    public float getX()
+    {
+        return x;
+    }
+
+    @Override
+    public float getY()
+    {
+        return y;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("Vector(x = %f, y = %f)", x, y);
+    }
+}

+ 8 - 0
src/me/hammerle/supersnuvi/rendering/IRenderer.java

@@ -0,0 +1,8 @@
+package me.hammerle.supersnuvi.rendering;
+
+import me.hammerle.snuviengine.api.Shader;
+
+public interface IRenderer<E>
+{
+    public void renderTick(Shader sh, float lag, E e);
+}

+ 491 - 0
src/me/hammerle/supersnuvi/rendering/LevelRenderer.java

@@ -0,0 +1,491 @@
+package me.hammerle.supersnuvi.rendering;
+
+import java.util.ArrayList;
+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.supersnuvi.Game;
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.gamelogic.LevelData;
+import me.hammerle.supersnuvi.tiles.Tile;
+import me.hammerle.supersnuvi.util.Utils;
+
+public class LevelRenderer implements IRenderer<Level>
+{
+    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 static int MESH_SIZE = 16;
+    
+    private float cameraX = 0.0f;
+    private float cameraY = 0.0f;
+    
+    private Level lastRenderedLevel = null;
+    private int meshLayers = 0;
+    private int meshWidth = 0;
+    private int meshHeight = 0;
+    private TextureRenderer[][][] meshes = null;
+    
+    @Override
+    public void renderTick(Shader sh, float lag, Level level)
+    {
+        updateCamera(sh, lag, level);
+        if(lastRenderedLevel != level)
+        {
+            updateMeshes(level);
+            lastRenderedLevel = level;
+            
+            for(int i = 0; i < 32; i++)
+            {
+                level.getLight(i).markDirty();
+            }
+        }
+
+        Shader.setAmbientLight(level.getAmbientRed(), level.getAmbientGreen(), level.getAmbientBlue());
+        Shader.setLightEnabled(true);
+        for(int i = 0; i < 1; i++)
+        {
+            Light l = level.getLight(i);
+            if(l.isDirty())
+            {
+                sh.setLightColor(i, l.getRed(), l.getGreen(), l.getBlue());
+                sh.setLightStrength(i, l.getStrength());
+                l.clearDirtyFlag();
+            }
+            sh.setLightLocation(i, 
+                    Utils.interpolate(l.getLastX(), l.getX(), lag) + cameraX, 
+                    Utils.interpolate(l.getLastY(), l.getY(), lag) + cameraY);
+        }
+        renderTilesAndEntities(sh, lag, level);
+        Shader.setLightEnabled(false);
+        renderOverlay(sh, level);
+    }
+
+    private float getView(int screenSize, int levelSize, float pos) 
+    {
+        pos -= screenSize >> 1;
+        if(pos < 0)
+        {
+            return 0;
+        }
+        float max = levelSize * Tile.SIZE - screenSize;
+        if(pos > max)
+        {
+            return max;
+        }
+        return pos;
+    }
+    
+    private void updateCamera(Shader sh, float lag, Level level)
+    {
+        Entity hero = level.getHero();
+        
+        int levelWidth = level.getData().getWidth();
+        int levelHeight = level.getData().getHeight();
+        
+        int screenWidth = sh.getViewWidth();
+        int screenHeight = sh.getViewHeight();
+        
+        float halfHeroWidth = hero.getWidth() * 0.5f;
+        float halfHeroHeight = hero.getHeight() * 0.5f;
+        
+        float oldCamX = -getView(screenWidth, levelWidth, hero.getLastX() + halfHeroWidth);
+        float oldCamY = -getView(screenHeight, levelHeight, hero.getLastY() + halfHeroHeight);
+        float camX = -getView(screenWidth, levelWidth, hero.getX() + halfHeroWidth);
+        float camY = -getView(screenHeight, levelHeight, hero.getY() + halfHeroHeight);
+
+        cameraX = Utils.interpolate(oldCamX, camX, lag);
+        cameraY = Utils.interpolate(oldCamY, camY, lag);
+
+        sh.translateTo(cameraX, cameraY);
+        sh.updateMatrix();
+    }
+    
+    private void updateMeshes(Level level)
+    {
+        if(meshes != null)
+        {
+            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].delete();
+                    }
+                }
+            }
+        }
+        
+        meshLayers = level.getData().getLayers() - 1;
+        meshWidth = (int) Math.ceil((double) level.getData().getWidth() / MESH_SIZE);
+        meshHeight = (int) Math.ceil((double) level.getData().getHeight() / MESH_SIZE);
+        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(MESH_SIZE * MESH_SIZE * 2, false);
+                }
+            }
+        }
+    }
+    
+    private void drawMesh(Level level, int l, int tl, int mx, int my)
+    {
+        TextureRenderer tr = meshes[l][mx][my];
+        if(!tr.isBuilt())
+        {
+            LevelData data = level.getData();
+            int tsx = mx * MESH_SIZE;
+            int tsy = my * MESH_SIZE;
+            int tex = Math.min(tsx + MESH_SIZE, data.getWidth());
+            int tey = Math.min(tsy + MESH_SIZE, 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, level))
+                    {
+                        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, level) + ERROR, t.getTextureMinY(x, y, level) + ERROR,
+                                t.getTextureMaxX(x, y, level) - ERROR, t.getTextureMaxY(x, y, level) - ERROR);
+                    }
+                }
+            }
+            tr.build();
+        }
+        tr.draw();
+    }
+    
+    private void renderTilesAndEntities(Shader sh, float lag, Level level)
+    {
+        int startX = (int) (-cameraX / (MESH_SIZE * Tile.SIZE));
+        int startY = (int) (-cameraY / (MESH_SIZE * Tile.SIZE));
+        int endX = (int) Math.ceil((-cameraX + sh.getViewWidth()) / (MESH_SIZE * Tile.SIZE));
+        int endY = (int) Math.ceil((-cameraY + sh.getViewHeight()) / (MESH_SIZE * 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);
+
+        // clear meshes which need an update
+        if(level.getTileUpdater().shouldUpdateAll())
+        {
+            if(meshes != null)
+            {
+                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].clear();
+                        }
+                    }
+                }
+            }
+        }
+        else
+        {
+            level.getTileUpdater().forEach((layer, x, y) -> 
+            {
+                if(layer == level.getData().getBackgroundIndex() + 1)
+                {
+                    // do not update changes on entity layer
+                    return;
+                }
+                if(layer > level.getData().getBackgroundIndex())
+                {
+                    layer--;
+                }
+                meshes[layer][x / MESH_SIZE][y / MESH_SIZE].clear();
+            });
+        }
+        level.getTileUpdater().clear();
+        
+        // background
+        sh.setColorEnabled(false);
+        sh.setTextureEnabled(true);
+        sh.setBlendingEnabled(true);
+        TILES.bind();
+        
+        LevelData data = level.getData();
+
+        int fromLayer = 0;
+        int toLayer = level.getData().getBackgroundIndex() + 1;
+
+        for(int l = fromLayer; l < toLayer; l++)
+        {
+            for(int mx = startX; mx < endX; mx++)
+            {
+                for(int my = startY; my < endY; my++)
+                {
+                    drawMesh(level, l, l, mx, my);
+                }
+            }
+        }
+
+        // entities
+        level.forEachEntity(entity -> entity.renderTick(lag));
+
+        // foreground
+        sh.setColorEnabled(false);
+        sh.setTextureEnabled(true);
+        sh.setBlendingEnabled(true);
+        TILES.bind();
+
+        fromLayer = toLayer + 1;
+        toLayer = level.getData().getLayers();
+
+        for(int l = fromLayer; l < toLayer; l++)
+        {
+            for(int mx = startX; mx < endX; mx++)
+            {
+                for(int my = startY; my < endY; my++)
+                {
+                    drawMesh(level, l - 1, l, mx, my);
+                }
+            }
+        }
+    }
+    
+    public static String formatBottles(int bottles, int maxBottles)
+    {
+        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, maxBottles);
+        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 static String formatTime(float time)
+    {
+        if(time == -1.0f)
+        {
+            return "-----";
+        }
+        else if(time >= 999.9f)
+        {
+            return "999.9";
+        }
+        return String.format("%05.1f", time);
+    }   
+    
+    private String[] split(String s)
+    {
+        ArrayList<String> 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()]);
+    }
+    
+    private void renderMessage(Shader sh, Level level)
+    {
+        String message = level.getMessage();
+        if(message != null)
+        {
+            float lineWidth = sh.getFontRenderer().getWidth();
+            String[] messageParts = split(message);
+            int index = 0;
+            ArrayList<StringBuilder> list = new ArrayList<>();
+            list.add(new StringBuilder());
+            float currentWidth = 0;
+
+            float w = sh.getViewWidth() - 26;
+            for(String s : messageParts)
+            {
+                if(s.equals("\n"))
+                {
+                    currentWidth = w;
+                    continue;
+                }
+                Rectangle rec = sh.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() * sh.getFontRenderer().getHeight();
+
+            sh.setColorEnabled(true);
+            sh.setTextureEnabled(false);
+            float messageY = sh.getViewHeight() - height - 26;
+            sh.getColorRenderer().drawRectangle(0.0f, messageY, sh.getViewWidth(), sh.getViewHeight(), 0x77000000);
+            messageY += 13;
+            sh.setTextureEnabled(true);
+            for(StringBuilder sb : list)
+            {
+                messageY = sh.getFontRenderer().drawString(13.0f, messageY, sb.toString());
+            }
+            sh.setColorEnabled(false);
+        }
+    }
+    
+    private void renderOverlay(Shader sh, Level level)
+    {
+        // menu rendering
+        sh.translateTo(0.0f, 0.0f);
+        sh.updateMatrix();
+
+        // grey background of clock and bottles
+        float lineHeight = sh.getFontRenderer().getHeight();
+        float lineWidth = sh.getFontRenderer().getWidth();
+        sh.setColorEnabled(true);
+        sh.setTextureEnabled(false);
+        sh.getColorRenderer().drawRectangle(0.0f, 0.0f, (lineWidth * 6.0f) + 10.0f, (lineHeight * 2.0f + 10.0f), 0x77000000);
+        sh.setTextureEnabled(true);
+        float y = 5.0f;
+        y = sh.getFontRenderer().drawString(13.0f, y, formatBottles(level.getCurrentBottles(), level.getMaxBottles()));
+        sh.getFontRenderer().drawString(13.0f, y, formatTime(level.getTime()));
+        sh.setColorEnabled(false);
+
+        // draw messages
+        renderMessage(sh, level);
+
+        GUI.bind();
+        GUI_RENDERER.clear();
+
+        int scale = sh.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 = sh.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) (level.getHero().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 = level.getHero().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
+        sh.setColorEnabled(true);
+        sh.setTextureEnabled(false);
+        switch(scale)
+        {
+            case 1: sh.translateTo(8.5f, y + 4.5f); break;
+            case 2: sh.translateTo(8.75f, y + 4.25f); break;
+            default: sh.translateTo(8.8333333333f, y + 4.16666667f); break;
+        }
+        sh.rotate(-level.getTime() * 72.0f);
+        sh.updateMatrix();
+        sh.getColorRenderer().drawRectangle(-0.5f / scale, -0.5f / scale, 0.5f / scale, 4.0f - 0.5f * scale, 0xFF000000);
+    }
+}

+ 109 - 0
src/me/hammerle/supersnuvi/rendering/Light.java

@@ -0,0 +1,109 @@
+package me.hammerle.supersnuvi.rendering;
+
+public class Light
+{
+    private float red = 0.0f;
+    private float green = 0.0f;
+    private float blue = 0.0f;
+    private float lastX = 0.0f;
+    private float lastY = 0.0f;
+    private float x = 0.0f;
+    private float y = 0.0f;
+    private float strength = 0.0f;
+    private boolean dirty = false;
+    
+    public void setColor(float r, float g, float b)
+    {
+        markDirty();
+        red = r;
+        green = g;
+        blue = b;
+    }
+    
+    public void setPosition(float x, float y)
+    {
+        markDirty();
+        this.x = x;
+        this.y = y;
+        lastX = x;
+        lastY = y;
+    }
+    
+    public void setPositionSmooth(float x, float y)
+    {
+        markDirty();
+        lastX = this.x;
+        lastY = this.y;
+        this.x = x;
+        this.y = y;
+    }
+    
+    public void setStrength(float f)
+    {
+        markDirty();
+        strength = f;
+    }
+
+    public float getRed()
+    {
+        return red;
+    }
+    
+    public float getGreen()
+    {
+        return green;
+    }
+    
+    public float getBlue()
+    {
+        return blue;
+    }
+    
+    public float getX()
+    {
+        return x;
+    }
+    
+    public float getY()
+    {
+        return y;
+    }
+    
+    public float getLastX()
+    {
+        return lastX;
+    }
+    
+    public float getLastY()
+    {
+        return lastY;
+    }
+    
+    public float getStrength()
+    {
+        return strength;
+    }
+    
+    public boolean isDirty()
+    {
+        return dirty;
+    }
+    
+    public void markDirty()
+    {
+        dirty = true;
+    }
+    
+    public void clearDirtyFlag()
+    {
+        dirty = false;
+    }
+    
+    public void reset()
+    {
+        markDirty();
+        setColor(0.0f, 0.0f, 0.0f);
+        setPosition(0.0f, 0.0f);
+        setStrength(0.0f);
+    }
+}

+ 49 - 0
src/me/hammerle/supersnuvi/rendering/TileUpdater.java

@@ -0,0 +1,49 @@
+package me.hammerle.supersnuvi.rendering;
+
+import java.util.LinkedList;
+
+public class TileUpdater
+{
+    private static class Update
+    {
+        private final int l;
+        private final int x;
+        private final int y;
+        
+        public Update(int l, int x, int y)
+        {
+            this.l = l;
+            this.x = x;
+            this.y = y;
+        }
+    }
+    
+    private final LinkedList<Update> updates = new LinkedList<>();
+    private boolean updateAll = false;
+
+    public void add(int layer, int x, int y)
+    {
+        updates.add(new Update(layer, x, y));
+    }
+    
+    public void forEach(UpdateConsumer uc)
+    {
+        updates.forEach((up) -> uc.accept(up.l, up.x, up.y));
+    }
+    
+    public void updateAll()
+    {
+        updateAll = true;
+    }
+    
+    public boolean shouldUpdateAll()
+    {
+        return updateAll;
+    }
+    
+    public void clear()
+    {
+        updates.clear();
+        updateAll = false;
+    }
+}

+ 7 - 0
src/me/hammerle/supersnuvi/rendering/UpdateConsumer.java

@@ -0,0 +1,7 @@
+package me.hammerle.supersnuvi.rendering;
+
+@FunctionalInterface
+public interface UpdateConsumer
+{
+    public void accept(int layer, int x, int y);
+}

+ 48 - 0
src/me/hammerle/supersnuvi/snuviscript/SnuviLogger.java

@@ -0,0 +1,48 @@
+package me.hammerle.supersnuvi.snuviscript;
+
+import me.hammerle.snuviscript.code.ISnuviLogger;
+import me.hammerle.snuviscript.code.Script;
+
+public class SnuviLogger implements ISnuviLogger
+{
+    private void printListElement(String name, String content)
+    {
+        System.out.print(" - ");
+        System.out.print(name);
+        System.out.print(": ");
+        System.out.println(content);
+    }
+    
+    @Override
+    public void print(String message, Exception ex, String function, String scriptname, Script sc, int line) 
+    {
+        if(scriptname != null)
+        {
+            if(sc != null)
+            {
+                System.out.println("error in '" + scriptname + "' " + sc.getId());
+            }
+            else
+            {
+                System.out.println("error in '" + scriptname + "'");
+            }
+        }
+        if(message != null)
+        {
+            printListElement("message", message);
+        }
+        if(ex != null)
+        {
+            printListElement("exception", ex.getClass().getSimpleName());
+            printListElement("exception", ex.getMessage());
+        }
+        if(function != null)
+        {
+            printListElement("function", function);
+        }
+        if(line != -1)
+        {
+            printListElement("line", String.valueOf(line));
+        }
+    }
+}

+ 85 - 0
src/me/hammerle/supersnuvi/snuviscript/SnuviScheduler.java

@@ -0,0 +1,85 @@
+package me.hammerle.supersnuvi.snuviscript;
+
+import java.util.ArrayList;
+import me.hammerle.snuviscript.code.ISnuviScheduler;
+import me.hammerle.supersnuvi.gamelogic.Level;
+
+public class SnuviScheduler implements ISnuviScheduler
+{
+    private static class Task
+    {
+        private final Runnable r;
+        private long ticks;
+        private final Level owner;
+        
+        private Task(Runnable r, long ticks, Level owner)
+        {
+            this.r = r;
+            this.ticks = ticks;
+            this.owner = owner;
+        }
+        
+        private boolean tick(Level current)
+        {
+            if(current != owner && owner != null)
+            {
+                return false;
+            }
+            ticks--;
+            if(ticks <= 0)
+            {
+                r.run();
+                return true;
+            }
+            return false;
+        }
+    }
+    
+    private int id = 0;
+    private final ArrayList<Task> queue = new ArrayList<>();
+    
+    private Level currentLevel = null;
+    
+    public void setActiveLevel(Level l)
+    {
+        currentLevel = l;
+    }
+    
+    @Override
+    public int scheduleTask(Runnable r, long delay)
+    { 
+        queue.add(new Task(r, Math.max(delay, 2), currentLevel));
+        return id++;
+    }
+    
+    @Override
+    public int scheduleTask(Runnable r)
+    { 
+        return scheduleTask(r, 2);
+    }
+    
+    public void tick(Level current)
+    {
+        int i = 0;
+        while(i < queue.size())
+        {
+            if(queue.get(i).tick(current))
+            {
+                if(i + 1 == queue.size())
+                {
+                    // last element
+                    queue.remove(i);
+                }
+                else
+                {
+                    // swap last
+                    queue.set(i, queue.get(queue.size() - 1));
+                    queue.remove(queue.size() - 1);
+                    // current element is a new one, skip i++
+                    continue;
+                }
+            }
+            i++;
+        }
+    }    
+}

+ 29 - 1
src/me/hammerle/supersnuvi/tiles/BaseBoxTile.java

@@ -1,10 +1,38 @@
 package me.hammerle.supersnuvi.tiles;
 
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.util.Utils;
+
 public class BaseBoxTile extends BaseTile
 {
     public BaseBoxTile(float tMinX, float tMinY, float tMaxX, float tMaxY)
     {
         super(tMinX, tMinY, tMaxX, tMaxY);
-        super.setDefaultCollisionBox();
+    }
+
+    @Override
+    public boolean isMoveColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        return isColliding(minX, minY, maxX, maxY, x, y, l);
+    }
+
+    @Override
+    public boolean isColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        float minTileX = Utils.toCoord(x);
+        float minTileY = Utils.toCoord(y);
+        return maxX > minTileX && minTileX + Tile.SIZE > minX && 
+                maxY > minTileY && minTileY + Tile.SIZE > minY;
+    }
+
+    @Override
+    public Face getCollidingFace(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        float minTileX = Utils.toCoord(x);
+        float minTileY = Utils.toCoord(y);
+        return Utils.getCollidingFace(
+                minTileX, minTileY, minTileX + Tile.SIZE, minTileY + Tile.SIZE,
+                minX, minY, maxX, maxY);
     }
 }

+ 41 - 0
src/me/hammerle/supersnuvi/tiles/BaseCollisionTile.java

@@ -0,0 +1,41 @@
+package me.hammerle.supersnuvi.tiles;
+
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.util.Utils;
+
+public class BaseCollisionTile extends BaseTile
+{
+    private final float x1;
+    private final float y1;
+    private final float x2;
+    private final float y2;
+    
+    public BaseCollisionTile(float tMinX, float tMinY, float tMaxX, float tMaxY, float x1, float y1, float x2, float y2)
+    {
+        super(tMinX, tMinY, tMaxX, tMaxY);
+        this.x1 = x1;
+        this.y1 = y1;
+        this.x2 = x2;
+        this.y2 = y2;
+    }
+    
+    @Override
+    public boolean isColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        float minTileX = Utils.toCoord(x);
+        float minTileY = Utils.toCoord(y);
+        return maxX > minTileX + x1 && minTileX + x2 > minX && 
+                maxY > minTileY + y1 && minTileY + y2 > minY;
+    }
+
+    @Override
+    public Face getCollidingFace(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        float minTileX = Utils.toCoord(x);
+        float minTileY = Utils.toCoord(y);
+        return Utils.getCollidingFace(
+                minTileX + x1, minTileY + y1, minTileX + x2, minTileY + y2,
+                minX, minY, maxX, maxY);
+    }
+}

+ 17 - 0
src/me/hammerle/supersnuvi/tiles/BaseMoveCollisionTile.java

@@ -0,0 +1,17 @@
+package me.hammerle.supersnuvi.tiles;
+
+import me.hammerle.supersnuvi.gamelogic.Level;
+
+public class BaseMoveCollisionTile extends BaseCollisionTile
+{
+    public BaseMoveCollisionTile(float tMinX, float tMinY, float tMaxX, float tMaxY, float x1, float y1, float x2, float y2)
+    {
+        super(tMinX, tMinY, tMaxX, tMaxY, x1, y1, x2, y2);
+    }
+
+    @Override
+    public boolean isMoveColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        return super.isColliding(minX, minY, maxX, maxY, x, y, l);
+    }
+}

+ 6 - 4
src/me/hammerle/supersnuvi/tiles/BaseTile.java

@@ -1,5 +1,7 @@
 package me.hammerle.supersnuvi.tiles;
 
+import me.hammerle.supersnuvi.gamelogic.Level;
+
 public class BaseTile extends Tile
 {
     private final float tMinX;
@@ -16,25 +18,25 @@ public class BaseTile extends Tile
     }
     
     @Override
-    public float getTextureMinX()
+    public float getTextureMinX(int x, int y, Level l)
     {
         return tMinX;
     }
     
     @Override
-    public float getTextureMaxX()
+    public float getTextureMaxX(int x, int y, Level l)
     {
         return tMaxX;
     }
     
     @Override
-    public float getTextureMinY()
+    public float getTextureMinY(int x, int y, Level l)
     {
         return tMinY;
     }
     
     @Override
-    public float getTextureMaxY()
+    public float getTextureMaxY(int x, int y, Level l)
     {
         return tMaxY;
     }

+ 23 - 26
src/me/hammerle/supersnuvi/tiles/BottledSoulTile.java

@@ -1,71 +1,68 @@
 package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
 import me.hammerle.supersnuvi.util.BlockDataStorage;
-import me.hammerle.supersnuvi.util.CollisionBox;
-import me.hammerle.supersnuvi.util.CollisionObject;
 import me.hammerle.supersnuvi.util.Face;
 import me.hammerle.supersnuvi.util.SoundUtils;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
-public class BottledSoulTile extends BaseTile
+public class BottledSoulTile extends BaseCollisionTile
 {
     private final BlockDataStorage states = new BlockDataStorage();
     private final int score;
     
     public BottledSoulTile(int score)
     {
-        super(0.0625f * (score - 1), 0.0625f, 0.0625f * score, 0.125f);
-        super.setCollisionBox(CollisionBox.createScaledTileBox(0.2f, 0.2f, 0.8f, 0.8f));
+        super(0.0625f * (score - 1), 0.0625f, 0.0625f * score, 0.125f,
+                Tile.SIZE * 0.2f, Tile.SIZE * 0.2f, Tile.SIZE * 0.8f, Tile.SIZE * 0.8f);
         this.score = score;
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
-        if(ent.getItemCollector().canCollect())
+        if(ent.getItemCollector().canCollect() && states.add(x, y, l))
         {
-            if(states.add(x, y, l))
+            l.getTileUpdater().add(l.getData().getBackgroundIndex(), x, y);
+            if(ent.getItemCollector().isHero())
             {
-                ent.getLevel().updateTile(x, y);
-                if(ent.getItemCollector().isHero())
-                {
-                    SoundUtils.playSound(SoundUtils.Sound.COLLECT);
-                    ent.getLevel().increaseSouls(score);
-                }
-                ent.getHealth().addHealthPercent(0.143f * score);
+                SoundUtils.playSound(SoundUtils.Sound.COLLECT);
+                l.addBottles(score);
             }
+            ent.getHealth().addHealthPercent(0.143f * score);
         }
     }
-    
+
     @Override
-    public CollisionObject getCollisionBox(int x, int y, ILevel l) 
+    public boolean isColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
     {
-        if(states.contains(x, y, l))
-        {
-            return CollisionObject.NULL_BOX;
-        }
-        return super.getCollisionBox(x, y, l);
+        return !states.contains(x, y, l) && super.isColliding(minX, minY, maxX, maxY, x, y, l);
     }
 
     @Override
-    public boolean shouldAiUseCollisionBox(int x, int y, ILevel l) 
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l) 
     {
         return false;
     }
     
     @Override
-    public boolean shouldRender(int x, int y, ILevel l) 
+    public boolean shouldRender(int x, int y, Level l) 
     {
         return !states.contains(x, y, l);
     }
     
     @Override
-    public void reset(ILevel l) 
+    public void reset(Level l) 
     {
         states.clear(l);
     } 
     
+    @Override
+    public void reset(int x, int y, Level l)
+    {
+        states.clear(x, y, l);
+    }
+    
     @Override
     public int getBottleScore()
     {

+ 21 - 31
src/me/hammerle/supersnuvi/tiles/CrumblingStoneTile.java

@@ -2,69 +2,59 @@ package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.supersnuvi.entity.Entity;
 import me.hammerle.supersnuvi.entity.EntityBuilder;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
 import me.hammerle.supersnuvi.util.BlockDataStorage;
-import me.hammerle.supersnuvi.util.CollisionBox;
-import me.hammerle.supersnuvi.util.CollisionObject;
 import me.hammerle.supersnuvi.util.Face;
 import me.hammerle.supersnuvi.util.SoundUtils;
 import me.hammerle.supersnuvi.util.Utils;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
-public class CrumblingStoneTile extends BaseTile
+public class CrumblingStoneTile extends BaseMoveCollisionTile
 {
     private final BlockDataStorage states = new BlockDataStorage();
     
     public CrumblingStoneTile() 
     {
-        super(0.1875f, 0.125f, 0.25f, 0.1875f);
-        super.setCollisionBox(CollisionBox.createScaledTileBox(0.0f, 0.0f, 1.0f, 0.6f));
-        super.setMovementBox(CollisionBox.createScaledTileBox(0.0f, 0.0f, 1.0f, 0.6f));
+        super(0.1875f, 0.125f, 0.25f, 0.1875f, 0.0f, 0.0f, Tile.SIZE, Tile.SIZE * 0.6f);
     }
 
     @Override
-    public CollisionObject getCollisionBox(int x, int y, ILevel l) 
+    public boolean isColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
     {
-        if(states.contains(x, y, l))
-        {
-            return CollisionObject.NULL_BOX;
-        }
-        return super.getCollisionBox(x, y, l);
+        return !states.contains(x, y, l) && super.isColliding(minX, minY, maxX, maxY, x, y, l);
     }
 
     @Override
-    public CollisionObject getMovementBox(int x, int y, ILevel l) 
+    public boolean isMoveColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
     {
-        if(states.contains(x, y, l))
-        {
-            return CollisionObject.NULL_BOX;
-        }
-        return super.getMovementBox(x, y, l);
+        return !states.contains(x, y, l) && super.isMoveColliding(minX, minY, maxX, maxY, x, y, l);
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
-        super.onEntityCollide(ent, x, y, face, l);
-        if(face == Face.UP)
+        if(face == Face.UP && !ent.getMovement().canMoveOnTiles() && states.add(x, y, l))
         {
-            if(states.add(x, y, l))
-            {
-                ent.getLevel().updateTile(x, y);         
-                l.spawnEntity(EntityBuilder.buildCrumblingStone(l, Utils.toCoord(x), Utils.toCoord(y)));
-                SoundUtils.playSound(SoundUtils.Sound.STONE_CRUMBLING);
-            }
+            l.getTileUpdater().add(l.getData().getBackgroundIndex(), x, y);      
+            l.spawnEntity(EntityBuilder.buildCrumblingStone(l, Utils.toCoord(x), Utils.toCoord(y)));
+            SoundUtils.playSound(SoundUtils.Sound.STONE_CRUMBLING);
         }
     }
 
     @Override
-    public boolean shouldRender(int x, int y, ILevel l)
+    public boolean shouldRender(int x, int y, Level l)
     {
         return !states.contains(x, y, l);
     }
 
     @Override
-    public void reset(ILevel l) 
+    public void reset(Level l) 
     {
         states.clear(l);
     } 
-}
+
+    @Override
+    public void reset(int x, int y, Level l)
+    {
+        states.clear(x, y, l);
+    }
+}

+ 5 - 7
src/me/hammerle/supersnuvi/tiles/GoalTile.java

@@ -1,24 +1,22 @@
 package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.util.CollisionBox;
 import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
-public class GoalTile extends BaseTile
+public class GoalTile extends BaseCollisionTile
 {
     public GoalTile(float tMinX, float tMinY, float tMaxX, float tMaxY) 
     {
-        super(tMinX, tMinY, tMaxX, tMaxY);
-        super.setCollisionBox(CollisionBox.createScaledTileBox(0.1f, 0.1f, 0.9f, 0.9f));
+        super(tMinX, tMinY, tMaxX, tMaxY, Tile.SIZE * 0.1f, Tile.SIZE * 0.1f, Tile.SIZE * 0.9f, Tile.SIZE * 0.9f);
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
         if(ent.getItemCollector().isHero())
         {
-            ent.getLevel().finishLevel();
+            l.finishLevel();
         }
     }
 

+ 76 - 0
src/me/hammerle/supersnuvi/tiles/HeadHitOnceTile.java

@@ -0,0 +1,76 @@
+package me.hammerle.supersnuvi.tiles;
+
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.util.BlockDataStorage;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
+
+public class HeadHitOnceTile extends BaseBoxTile
+{
+    private final BlockDataStorage states = new BlockDataStorage();
+    
+    private final float tMinX2;
+    private final float tMaxX2;
+    private final float tMinY2;
+    private final float tMaxY2;
+    
+    public HeadHitOnceTile(float tMinX, float tMinY, float tMaxX, float tMaxY, float tMinX2, float tMinY2, float tMaxX2, float tMaxY2)
+    {
+        super(tMinX, tMinY, tMaxX, tMaxY);
+        this.tMinX2 = tMinX2;
+        this.tMaxX2 = tMaxX2;
+        this.tMinY2 = tMinY2;
+        this.tMaxY2 = tMaxY2;
+    }
+
+    @Override
+    public float getTextureMinX(int x, int y, Level l)
+    {
+        return states.contains(x, y, l) ? tMinX2 : super.getTextureMinX(x, y, l);
+    }
+
+    @Override
+    public float getTextureMinY(int x, int y, Level l)
+    {
+        return states.contains(x, y, l) ? tMinY2 : super.getTextureMinY(x, y, l);
+    }
+
+    @Override
+    public float getTextureMaxX(int x, int y, Level l)
+    {
+        return states.contains(x, y, l) ? tMaxX2 : super.getTextureMaxX(x, y, l);
+    }
+
+    @Override
+    public float getTextureMaxY(int x, int y, Level l)
+    {
+        return states.contains(x, y, l) ? tMaxY2 : super.getTextureMaxY(x, y, l); 
+    }
+
+    @Override
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l)
+    {
+        if(face == Face.DOWN && ent.getItemCollector().isHero() && ent.getMotionY() > 0 && states.add(x, y, l))
+        {
+            l.getTileUpdater().add(l.getData().getBackgroundIndex(), x, y);
+            l.callEvent("tile_hit", (sc) -> 
+            {
+                sc.setVar("tile_x", (double) x);
+                sc.setVar("tile_y", (double) y);
+                sc.setVar("entity", ent);
+            }, null);
+        }
+    }
+    
+    @Override
+    public void reset(Level l) 
+    {
+        states.clear(l);
+    } 
+    
+    @Override
+    public void reset(int x, int y, Level l)
+    {
+        states.clear(x, y, l);
+    }
+}

+ 27 - 0
src/me/hammerle/supersnuvi/tiles/HeadHitTile.java

@@ -0,0 +1,27 @@
+package me.hammerle.supersnuvi.tiles;
+
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
+
+public class HeadHitTile extends BaseBoxTile
+{
+    public HeadHitTile(float tMinX, float tMinY, float tMaxX, float tMaxY)
+    {
+        super(tMinX, tMinY, tMaxX, tMaxY);
+    }
+
+    @Override
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l)
+    {
+        if(face == Face.DOWN && ent.getItemCollector().isHero() && ent.getMotionY() > 0)
+        {
+            l.callEvent("tile_hit", (sc) -> 
+            {
+                sc.setVar("tile_x", (double) x);
+                sc.setVar("tile_y", (double) y);
+                sc.setVar("entity", ent);
+            }, null);
+        }
+    }
+}

+ 34 - 0
src/me/hammerle/supersnuvi/tiles/InteractTile.java

@@ -0,0 +1,34 @@
+package me.hammerle.supersnuvi.tiles;
+
+import me.hammerle.supersnuvi.Keys;
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
+
+public class InteractTile extends BaseCollisionTile
+{
+    public InteractTile(float tMinX, float tMinY, float tMaxX, float tMaxY)
+    {
+        super(tMinX, tMinY, tMaxX, tMaxY, 0.0f, 0.0f, Tile.SIZE, Tile.SIZE);
+    }
+
+    @Override
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l)
+    {
+        return false;
+    }
+
+    @Override
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l)
+    {
+        if(ent.getItemCollector().isHero() && Keys.UP.getTime() == 1)
+        {
+            l.callEvent("tile_interact", (sc) -> 
+            {
+                sc.setVar("tile_x", (double) x);
+                sc.setVar("tile_y", (double) y);
+                sc.setVar("entity", ent);
+            }, null);
+        }
+    }
+}

+ 4 - 6
src/me/hammerle/supersnuvi/tiles/KillTile.java

@@ -1,20 +1,18 @@
 package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.util.CollisionBox;
 import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
-public class KillTile extends BaseTile
+public class KillTile extends BaseCollisionTile
 {
     public KillTile(float tMinX, float tMinY, float tMaxX, float tMaxY) 
     {
-        super(tMinX, tMinY, tMaxX, tMaxY);
-        super.setCollisionBox(CollisionBox.createScaledTileBox(0.1f, 0.1f, 0.9f, 0.9f));
+        super(tMinX, tMinY, tMaxX, tMaxY, Tile.SIZE * 0.1f, Tile.SIZE * 0.1f, Tile.SIZE * 0.9f, Tile.SIZE * 0.9f);
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
         super.onEntityCollide(ent, x, y, face, l);
         ent.getHealth().addHealth(-14.3f);

+ 3 - 3
src/me/hammerle/supersnuvi/tiles/NullTile.java

@@ -1,6 +1,6 @@
 package me.hammerle.supersnuvi.tiles;
 
-import me.hammerle.supersnuvi.gamelogic.ILevel;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
 public class NullTile extends Tile
 {
@@ -9,13 +9,13 @@ public class NullTile extends Tile
     }
 
     @Override
-    public boolean shouldRender(int x, int y, ILevel l)
+    public boolean shouldRender(int x, int y, Level l)
     {
         return false;
     }
 
     @Override
-    public boolean shouldAiUseCollisionBox(int x, int y, ILevel l)
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l)
     {
         return false;
     }

+ 58 - 0
src/me/hammerle/supersnuvi/tiles/PressureTile.java

@@ -0,0 +1,58 @@
+package me.hammerle.supersnuvi.tiles;
+
+import me.hammerle.supersnuvi.entity.Entity;
+import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
+
+public class PressureTile extends BaseCollisionTile
+{
+    public PressureTile(float tMinX, float tMinY, float tMaxX, float tMaxY)
+    {
+        super(tMinX, tMinY, tMaxX, tMaxY,
+                Tile.SIZE * 0.09375f, Tile.SIZE * 0.90625f, Tile.SIZE * 0.90625f, Tile.SIZE);
+    }
+    
+    @Override
+    public float getWidth()
+    {
+        return Tile.SIZE * 0.8125f;
+    }
+
+    @Override
+    public float getOffsetX()
+    {
+        return Tile.SIZE * 0.09375f;
+    }
+
+    @Override
+    public float getHeight()
+    {
+        return Tile.SIZE * 0.09375f;
+    }
+
+    @Override
+    public float getOffsetY()
+    {
+        return Tile.SIZE * 0.90625f;
+    }
+
+    @Override
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l)
+    {
+        return false;
+    }
+
+    @Override
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l)
+    {
+        if(face == Face.UP && ent.getItemCollector().isHero())
+        {
+            l.callEvent("auto_tile_interact", (sc) -> 
+            {
+                sc.setVar("tile_x", (double) x);
+                sc.setVar("tile_y", (double) y);
+                sc.setVar("entity", ent);
+            }, null);
+        }
+    }
+}

+ 40 - 0
src/me/hammerle/supersnuvi/tiles/RampTile.java

@@ -0,0 +1,40 @@
+package me.hammerle.supersnuvi.tiles;
+
+import me.hammerle.supersnuvi.gamelogic.Level;
+import me.hammerle.supersnuvi.util.Utils;
+
+public class RampTile extends BaseTile
+{
+    private final float lx1;
+    private final float ly1;
+    private final float lx2;
+    private final float ly2;
+    
+    public RampTile(float tMinX, float fMinY, float tMaxX, float tMaxY, float x1, float y1, float x2, float y2)
+    {
+        super(tMinX, fMinY, tMaxX, tMaxY);
+        lx1 = x1;
+        ly1 = y1;
+        lx2 = x2;
+        ly2 = y2;
+    }
+
+    @Override
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l)
+    {
+        return false;
+    }
+
+    @Override
+    public boolean isMoveColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        float x1 = Utils.toCoord(x) + lx1;
+        float y1 = Utils.toCoord(y) + ly1;
+        float x2 = Utils.toCoord(x) + lx2;
+        float y2 = Utils.toCoord(y) + ly2;
+        return Utils.intersect(x1, y1, x2, y2, minX, minY, maxX, minY) || 
+                Utils.intersect(x1, y1, x2, y2, maxX, minY, maxX, maxY) ||
+                Utils.intersect(x1, y1, x2, y2, maxX, maxY, minX, maxY) ||
+                Utils.intersect(x1, y1, x2, y2, minX, maxY, minX, minY);
+    }
+}

+ 3 - 8
src/me/hammerle/supersnuvi/tiles/SlipperyTile.java

@@ -1,9 +1,8 @@
 package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
 import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
 public class SlipperyTile extends BaseBoxTile
 {
@@ -13,15 +12,11 @@ public class SlipperyTile extends BaseBoxTile
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
         if(face == Face.UP)
         {
-            if(ent.getPreMotionY() != Entity.GRAVITY && Math.abs(ent.getMotionX()) <= 0.07)
-            {
-                ent.setMotionX(ent.getFace() == Face.RIGHT ? 5.0f : -5.0f);
-            }
-            ent.getMovement().setFrictionFactor(0.85f);
+            ent.getFriction().set(0.65f, 1.0f);
         }
     }
 }

+ 6 - 8
src/me/hammerle/supersnuvi/tiles/SpikeTile.java

@@ -2,14 +2,13 @@ package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.snuviengine.api.Texture;
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.gamelogic.Level;
-import me.hammerle.supersnuvi.util.CollisionBox;
+import me.hammerle.supersnuvi.rendering.LevelRenderer;
 import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
-public class SpikeTile extends BaseTile
+public class SpikeTile extends BaseCollisionTile
 {
-    private final static Texture.Animation SPIKES = Level.TILES.addAnimation(96, 32, 
+    private final static Texture.Animation SPIKES = LevelRenderer.TILES.addAnimation(96, 32, 
             "resources/spike_trap/spike_trap_frame1.png", "resources/spike_trap/spike_trap_frame2.png",
             "resources/spike_trap/spike_trap_frame3.png", "resources/spike_trap/spike_trap_frame4.png",
             "resources/spike_trap/spike_trap_frame5.png");
@@ -19,14 +18,13 @@ public class SpikeTile extends BaseTile
     
     public SpikeTile() 
     {
-        super(0.1875f, 0.0625f, 0.25f, 0.125f);
+        super(0.1875f, 0.0625f, 0.25f, 0.125f, Tile.SIZE * 0.1f, Tile.SIZE * 0.1f, Tile.SIZE * 0.9f, Tile.SIZE * 0.9f);
         counter = 0;
         frame = 0;
-        super.setCollisionBox(CollisionBox.createScaledTileBox(0.1f, 0.1f, 0.9f, 0.9f));
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
         super.onEntityCollide(ent, x, y, face, l);
         if(frame >= 1)

+ 8 - 2
src/me/hammerle/supersnuvi/tiles/StartTile.java

@@ -1,11 +1,17 @@
 package me.hammerle.supersnuvi.tiles;
 
-import me.hammerle.supersnuvi.gamelogic.ILevel;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
 public class StartTile extends Tile
 {
     @Override
-    public boolean shouldRender(int x, int y, ILevel l)
+    public boolean shouldRender(int x, int y, Level l)
+    {
+        return false;
+    }
+
+    @Override
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l)
     {
         return false;
     }

+ 33 - 83
src/me/hammerle/supersnuvi/tiles/Tile.java

@@ -1,103 +1,33 @@
 package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.util.CollisionObject;
 import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
 public abstract class Tile 
 {
     public final static int SIZE = 32;
+    public final static float SIZE_SCALE = SIZE / 32.0f;
     
-    private CollisionObject movementCollision;
-    private CollisionObject collisionBox;
-    
-    public Tile()
-    {
-        movementCollision = null;
-        collisionBox = null;
-    }
-
-    /** Sets the collision box for movement
-     *
-     * @param cb a collision box
-     * @return the tile which the change was applied to
-     */
-    public Tile setMovementBox(CollisionObject cb)
-    {
-        if(cb == CollisionObject.NULL_BOX)
-        {
-            this.movementCollision = null;
-        }
-        else
-        {
-            this.movementCollision = cb.copy();
-        }
-        return this;
-    }
-    
-    /** Sets the collision box for normal collision
-     *
-     * @param cb a collision box
-     * @return the tile which the change was applied to
-     */
-    public Tile setCollisionBox(CollisionObject cb)
-    {
-        if(cb == CollisionObject.NULL_BOX)
-        {
-            this.collisionBox = null;
-        }
-        else
-        {
-            this.collisionBox = cb.copy();
-        }
-        return this;
-    }
-    
-    /** Sets the default collision box for movement and for collision
-     *
-     * @return the tile which the change was applied to
-     */
-    public Tile setDefaultCollisionBox()
-    {
-        this.movementCollision = CollisionObject.DEFAULT_TILE_BOX.copy();
-        this.collisionBox = CollisionObject.DEFAULT_TILE_BOX.copy();
-        return this;
-    }
-    
-    public CollisionObject getCollisionBox(int x, int y, ILevel l)
-    {
-        if(collisionBox != null)
-        {
-            return collisionBox;
-        }
-        return CollisionObject.NULL_BOX.reset();
-    }
-    
-    public boolean shouldAiUseCollisionBox(int x, int y, ILevel l)
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l)
     {
         return true;
     }
     
-    public CollisionObject getMovementBox(int x, int y, ILevel l)
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l)
     {
-        if(movementCollision != null)
-        {
-            return movementCollision;
-        }
-        return CollisionObject.NULL_BOX.reset();
+        
     }
     
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l)
+    public void tick()
     {
-        
     }
     
-    public void tick()
+    public void reset(Level l)
     {
     }
     
-    public void reset(ILevel l)
+    public void reset(int x, int y, Level l)
     {
     }
     
@@ -106,22 +36,22 @@ public abstract class Tile
         return 0;
     }
     
-    public float getTextureMinX()
+    public float getTextureMinX(int x, int y, Level l)
     {
         return 0.0f;
     }
     
-    public float getTextureMaxX()
+    public float getTextureMaxX(int x, int y, Level l)
     {
         return 0.0625f;
     }
     
-    public float getTextureMinY()
+    public float getTextureMinY(int x, int y, Level l)
     {
         return 0.0f;
     }
     
-    public float getTextureMaxY()
+    public float getTextureMaxY(int x, int y, Level l)
     {
         return 0.0625f;
     }
@@ -146,8 +76,28 @@ public abstract class Tile
         return Tile.SIZE;
     }
     
-    public boolean shouldRender(int x, int y, ILevel l)
+    public boolean shouldRender(int x, int y, Level l)
     {
         return true;
     }
+    
+    public boolean isMoveColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        return false;
+    }
+    
+    public boolean isColliding(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        return false;
+    }
+    
+    public Face getCollidingFace(float minX, float minY, float maxX, float maxY, int x, int y, Level l)
+    {
+        return Face.NULL;
+    }
+    
+    public final boolean hasMoveBox(Level l)
+    {
+        return isMoveColliding(0.0f, 0.0f, Tile.SIZE, Tile.SIZE, 0, 0, l);
+    }
 }

+ 3 - 3
src/me/hammerle/supersnuvi/tiles/TrampolinTile.java

@@ -1,9 +1,9 @@
 package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
 import me.hammerle.supersnuvi.util.Face;
 import me.hammerle.supersnuvi.util.SoundUtils;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
 public class TrampolinTile extends BaseBoxTile
 {
@@ -13,11 +13,11 @@ public class TrampolinTile extends BaseBoxTile
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
         if(face == Face.UP)
         {
-            ent.setMotionY(-ent.getMovement().getJumpPower());
+            ent.applyForce(0.0f, -ent.getMovement().getJumpPower());
             SoundUtils.playSound(SoundUtils.Sound.JUMP_ON_BOUNCE_SHROOM);
         }
     }

+ 8 - 15
src/me/hammerle/supersnuvi/tiles/WaterTile.java

@@ -2,15 +2,13 @@ package me.hammerle.supersnuvi.tiles;
 
 import me.hammerle.snuviengine.api.Texture.Animation;
 import me.hammerle.supersnuvi.entity.Entity;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
-import me.hammerle.supersnuvi.gamelogic.Level;
-import me.hammerle.supersnuvi.util.CollisionBox;
-import me.hammerle.supersnuvi.util.CollisionObject;
+import me.hammerle.supersnuvi.rendering.LevelRenderer;
 import me.hammerle.supersnuvi.util.Face;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
-public class WaterTile extends BaseTile
+public class WaterTile extends BaseCollisionTile
 {
-    private final static Animation WATER = Level.TILES.addAnimation(160, 0, 
+    private final static Animation WATER = LevelRenderer.TILES.addAnimation(160, 0, 
             "resources/water/water_frame1.png", "resources/water/water_frame2.png",
             "resources/water/water_frame3.png", "resources/water/water_frame4.png",
             "resources/water/water_frame5.png", "resources/water/water_frame6.png",
@@ -21,25 +19,20 @@ public class WaterTile extends BaseTile
     
     public WaterTile(int level) 
     {
-        super(0.3125f, 0.00390625f * (15 - level), 0.375f, 0.0625f);
+        super(0.3125f, 0.00390625f * (15 - level), 0.375f, 0.0625f, 
+                0.03125f * Tile.SIZE, Tile.SIZE - (Tile.SIZE / 16) * (level + 1), 0.96875f * Tile.SIZE, Tile.SIZE);
         this.level = level;
-        super.setCollisionBox(CollisionBox.createScaledTileBox(0.03125f, getOffsetY() / Tile.SIZE, 0.96875f, 1.0f));
-        super.setMovementBox(CollisionObject.NULL_BOX);
     }
 
     @Override
-    public void onEntityCollide(Entity ent, int x, int y, Face face, ILevel l) 
+    public void onEntityCollide(Entity ent, int x, int y, Face face, Level l) 
     {
         super.onEntityCollide(ent, x, y, face, l);
         ent.getMovement().setInWater(true);
-        if(ent.getMotionY() > 0.0f)
-        {
-            ent.setMotionY(0.0f);
-        }
     }
 
     @Override
-    public boolean shouldAiUseCollisionBox(int x, int y, ILevel l)
+    public boolean shouldAiUseCollisionBox(int x, int y, Level l)
     {
         return false;
     }

+ 19 - 9
src/me/hammerle/supersnuvi/util/BlockDataStorage.java

@@ -2,17 +2,17 @@ package me.hammerle.supersnuvi.util;
 
 import java.util.HashSet;
 import java.util.Objects;
-import me.hammerle.supersnuvi.gamelogic.ILevel;
+import me.hammerle.supersnuvi.gamelogic.Level;
 
 public class BlockDataStorage
 {
     private static class Data
     {
-        private final int x;
-        private final int y;
-        private final ILevel level;
+        private int x;
+        private int y;
+        private Level level;
         
-        private Data(int x, int y, ILevel level)
+        private Data(int x, int y, Level level)
         {
             this.x = x;
             this.y = y;
@@ -57,6 +57,8 @@ public class BlockDataStorage
         }
     }
     
+    private final Data containCache = new Data(0, 0, null);
+    
     private final HashSet<Data> data = new HashSet<>();
     
     public BlockDataStorage()
@@ -64,18 +66,26 @@ public class BlockDataStorage
         
     }
     
-    public boolean add(int x, int y, ILevel level)
+    public boolean add(int x, int y, Level level)
     {
         return data.add(new Data(x, y, level));
     }
     
-    public boolean contains(int x, int y, ILevel level)
+    public boolean contains(int x, int y, Level level)
     {
-        return data.contains(new Data(x, y, level));
+        containCache.x = x;
+        containCache.y = y;
+        containCache.level = level;
+        return data.contains(containCache);
     }
     
-    public void clear(ILevel level)
+    public void clear(Level level)
     {
         data.removeIf(d -> d.level == level);
     }
+    
+    public void clear(int x, int y, Level level)
+    {
+        data.removeIf(d -> d.level == level && x == d.x && y == d.y);
+    }
 }

+ 4 - 3
src/me/hammerle/supersnuvi/util/Face.java

@@ -4,13 +4,14 @@ import me.hammerle.supersnuvi.entity.Entity;
 
 public enum Face 
 {
+    NULL(0, 0),
     LEFT(-Entity.STEP, 0),
     RIGHT(Entity.STEP, 0),
     UP(0, -Entity.STEP),
     DOWN(0, Entity.STEP);
     
-    private float offsetX;
-    private float offsetY;
+    private final float offsetX;
+    private final float offsetY;
     
     Face(float offsetX, float offsetY)
     {
@@ -27,7 +28,7 @@ public enum Face
             case UP: return DOWN;
             case DOWN: return UP;
         }
-        return UP;
+        return NULL;
     }
     
     public float getCollisionOffsetX()

+ 2 - 2
src/me/hammerle/supersnuvi/util/SoundUtils.java

@@ -81,7 +81,7 @@ public class SoundUtils
             catch(Exception ex)
             {
                 System.out.println("Sound '" + f + "' failed.");
-                ex.printStackTrace();
+                //ex.printStackTrace();
             }
         }
         
@@ -142,7 +142,7 @@ public class SoundUtils
             catch(Exception ex)
             {
                 System.out.println("Sound '" + f + "' failed.");
-                ex.printStackTrace();
+                //ex.printStackTrace();
             }
         }
 

+ 109 - 17
src/me/hammerle/supersnuvi/util/Utils.java

@@ -4,28 +4,11 @@ import me.hammerle.supersnuvi.tiles.Tile;
 
 public class Utils 
 {
-    public static float round(float d)
-    {
-        return Math.round(d * 10000) / 10000f;
-    }
-    
     public static float interpolate(float from, float to, float factor)
     {
         return from + (to - from) * factor;
     }
     
-    public static float interpolateY(float x1, float y1, float x2, float y2, float x3)
-    {
-        float k = (y2 - y1) / (x2 - x1);
-        return k * x3 + y1 - k * x1;
-    }
-    
-    public static float interpolateX(float x1, float y1, float x2, float y2, float y3)
-    {
-        float k = (x2 - x1) / (y2 - y1);
-        return (y3 - y1) * k + x1;
-    }
-    
     public static int toBlock(float c)
     {
         return (int) (c / Tile.SIZE);
@@ -40,4 +23,113 @@ public class Utils
     {
         return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
     }
+    
+    public static Face getCollidingFace(
+            float xMin1, float yMin1, float xMax1, float yMax1, 
+            float xMin2, float yMin2, float xMax2, float yMax2)
+    {
+        float diffUp = yMax1 - yMin2;
+        float diffRight = xMax2 - xMin1;
+        float diffDown = yMax2 - yMin1;
+        float diffLeft = xMax1 - xMin2;
+        
+        float min = diffUp;
+        
+        Face f = Face.UP;
+        if(min > diffRight)
+        {
+            min = diffRight;
+            f = Face.RIGHT;
+        }
+        if(min > diffDown)
+        {
+            min = diffDown;
+            f = Face.DOWN;
+        }
+        if(min > diffLeft)
+        {
+            f = Face.LEFT;
+        }
+        
+        int c = 0;
+        c += (min == diffUp) ? 1 : 0;
+        c += (min == diffRight) ? 1 : 0;
+        c += (min == diffDown) ? 1 : 0;
+        c += (min == diffLeft) ? 1 : 0;
+        if(c >= 2)
+        {
+            return Face.NULL;
+        }
+        
+        return f;
+    }
+    
+    public static boolean intersect(float x11, float y11, float x12, float y12, float x21, float y21, float x22, float y22)
+    {
+        if(compareFloats(x11, x12))
+        {
+            if(compareFloats(x21, x22))
+            {
+                return false;
+            }
+            else
+            {
+                if(!isBetween(x11, x21, x22))
+                {
+                    return false;
+                }
+                float k = (y21 - y22) / (x21 - x22);
+                float d = y22 - k * x22;
+                float y = d + x11 * k;
+                return isBetween(y, y11, y12) && isBetween(y, y21, y22);
+            }
+        }
+        else
+        {
+            if(compareFloats(x21, x22))
+            {
+                if(!isBetween(x21, x11, x12))
+                {
+                    return false;
+                }
+                float k = (y11 - y12) / (x11 - x12);
+                float d = y12 - k * x12;
+                float y = d + x21 * k;
+                return isBetween(y, y11, y12) && isBetween(y, y21, y22);
+            }
+            else
+            {
+                float k1 = (y11 - y12) / (x11 - x12);
+                float k2 = (y21 - y22) / (x21 - x22);
+                if(compareFloats(k1, k2))
+                {
+                    return false;
+                }
+                float d1 = y12 - k1 * x12;
+                float d2 = y22 - k2 * x22;
+                
+                float x = (d1 - d2) / (k2 - k1);
+                if(!isBetween(x, x11, x12) || !isBetween(x, x21, x22))
+                {
+                    return false;
+                }
+                float y = k1 * x + d1;
+                return isBetween(y, y11, y12) && isBetween(y, y21, y22);
+            }
+        }
+    }
+    
+    private static final float ERROR = 1.0f / 512.0f;
+    
+    private static boolean isBetween(float y, float y1, float y2)
+    {
+        float min = Math.min(y1, y2) - ERROR;
+        float max = Math.max(y1, y2) + ERROR;
+        return y >= min && y <= max;
+    }
+    
+    private static boolean compareFloats(float a, float b)
+    {
+        return Math.abs(a - b) < ERROR;
+    }
 }