package me.hammerle.supersnuvi.gamelogic; import java.io.File; import java.util.Arrays; import java.util.HashMap; import me.hammerle.supersnuvi.javafx.IKeyBinding; import me.hammerle.supersnuvi.javafx.KeyHandler; import me.hammerle.supersnuvi.savegame.SimpleConfig; import me.hammerle.supersnuvi.tiles.*; import me.hammerle.supersnuvi.util.SoundUtils; import me.hammerle.supersnuvi.rendering.IRenderer; public class StateRenderer { // constants public static final ColoredBaseTile FALLBACK_TILE = new ColoredBaseTile(0, 0, 0, 0); // rendering private final IRenderer renderer; // tiles private final HashMap registeredTiles; // levels private Level currentLevel; private final Level[] levels; private int levelIndex; public static final SimpleConfig CONFIG = new SimpleConfig("options.txt", true); private static final SimpleConfig[] SAVE_SLOTS = new SimpleConfig[] { new SimpleConfig("slot1.txt", true), new SimpleConfig("slot2.txt", true), new SimpleConfig("slot3.txt", true) }; private int screen = 0; private int startScreenIndex = 0; private int optionScreenIndex = 0; private int slotScreenIndex = 0; private boolean optionsDirty = false; // sound private static boolean sound = CONFIG.getBoolean("sound", false); public static boolean isSoundEnabled() { return sound; } public StateRenderer(IRenderer renderer) { this.renderer = renderer; this.registeredTiles = new HashMap<>(); registerTiles(); File[] files = new File("./levels").listFiles(); Arrays.sort(files, (o1, o2) -> o1.compareTo(o2)); levels = new Level[files.length]; for(int i = 0; i < levels.length; i++) { levels[i] = new Level(this, files[i]); } currentLevel = null; levelIndex = 0; } // ------------------------------------------------------------------------- // tile stuff // ------------------------------------------------------------------------- private void registerTiles() { // dirt for(int i = 0; i < 16; i++) { registeredTiles.put(i, new BaseBoxTile("dirt/dirt" + i)); } // grass registeredTiles.put(20, new BaseBoxTile("grass/grass4")); registeredTiles.put(21, new BaseBoxTile("grass/grass9")); registeredTiles.put(22, new BaseBoxTile("grass/grass8")); registeredTiles.put(23, new BaseBoxTile("grass/grass10")); registeredTiles.put(28, new BaseBoxTile("grass/grass7")); registeredTiles.put(29, new BaseBoxTile("grass/grass15")); registeredTiles.put(30, new BaseBoxTile("grass/grass16")); registeredTiles.put(31, new BaseBoxTile("grass/grass14")); // bottled soul registeredTiles.put(32, new BottledSoulTile(1)); registeredTiles.put(33, new BottledSoulTile(2)); registeredTiles.put(34, new BottledSoulTile(3)); // bounce shroom registeredTiles.put(48, new TrampolinTile()); // crumbling stones registeredTiles.put(64, new CrumblingStoneTile()); // spike trap registeredTiles.put(80, new SpikeTile()); // water registeredTiles.put(96, new WaterTile(true)); registeredTiles.put(97, new WaterTile(false)); // snuvi start block registeredTiles.put(StartTile.ID, new StartTile()); // sky registeredTiles.put(128, new SkyTile()); // ice registeredTiles.put(144, new SlipperyTile("ice/ice")); // slippery slime registeredTiles.put(148, new SlipperyTile("slippery_slime/slippery_slime148")); registeredTiles.put(149, new SlipperyTile("slippery_slime/slippery_slime149")); registeredTiles.put(150, new SlipperyTile("slippery_slime/slippery_slime150")); registeredTiles.put(151, new SlipperyTile("slippery_slime/slippery_slime151")); registeredTiles.put(156, new SlipperyTile("slippery_slime/slippery_slime156")); registeredTiles.put(157, new SlipperyTile("slippery_slime/slippery_slime157")); registeredTiles.put(158, new SlipperyTile("slippery_slime/slippery_slime158")); registeredTiles.put(159, new SlipperyTile("slippery_slime/slippery_slime159")); // end level registeredTiles.put(160, new GoalTile("end/end")); registeredTiles.put(161, new GoalTile("end/end2")); // thorns registeredTiles.put(176, new KillTile("thorns/thorns_bottom", 2)); registeredTiles.put(177, new KillTile("thorns/thorns_mid", 2)); registeredTiles.put(178, new KillTile("thorns/thorns_top", 2)); registeredTiles.put(179, new KillTile("thorns/thorns_bottom", -3)); registeredTiles.put(180, new KillTile("thorns/thorns_mid", -3)); registeredTiles.put(181, new KillTile("thorns/thorns_top", -3)); // decoration shrooms registeredTiles.put(208, new DecoShroomTile("shrooms/shroom")); registeredTiles.put(209, new DecoShroomTile("shrooms/shroom2")); registeredTiles.put(210, new DecoShroomTile("shrooms/shroom3")); registeredTiles.put(211, new DecoShroomTile("shrooms/shroom4")); // fog, starting late to make length changes possible for(int i = 0; i < 16; i++) { registeredTiles.put(16000 + i, new FogTile((i + 1) / 16.0)); } // london stuff registeredTiles.put(224, new BaseBoxTile("london_background/london_background")); // london streets for(int i = 0; i < 16; i++) { registeredTiles.put(240 + i, new BaseBoxTile("london_streets/london_streets" + i)); } // london street light for(int i = 0; i < 25; i++) { registeredTiles.put(256 + i, new BaseBoxTile("street_light/street_light" + (i % 5) + "_" + (i / 5))); } } public Tile getTile(int id) { Tile tile = registeredTiles.get(id); if(tile == null) { return FALLBACK_TILE; } return tile; } public Tile getInteractionTile(int id) { return registeredTiles.getOrDefault(id, FALLBACK_TILE); } public void resetTiles() { registeredTiles.values().forEach(v -> v.reset()); } // ------------------------------------------------------------------------- // tick, rendering // ------------------------------------------------------------------------- public IRenderer getRenderer() { return renderer; } private static char[] getCharLine(char start, char mid, char end, char spacer) { char[] c = new char[MENU_WIDTH]; Arrays.fill(c, 1, c.length - 1, mid); c[0] = start; c[c.length - 1] = end; c[c.length - 3] = spacer; return c; } private static char[] getCharLine(char start, String s, char end) { char[] chars = new char[MENU_WIDTH]; chars[0] = start; chars[chars.length - 1] = end; int border = Math.min(MENU_WIDTH - 2, s.length()); for(int i = 0; i < border; i++) { chars[i + 1] = s.charAt(i); } return chars; } private static char[] getLevelCharLine(char start, char mid, char end, char spacer) { char[] c = new char[MENU_WIDTH]; Arrays.fill(c, 1, c.length - 1, mid); c[0] = start; c[c.length - 1] = end; c[c.length - 7] = spacer; c[c.length - 13] = spacer; return c; } private static char[] getMoreLevelCharLine(char start, char mid, char end, char spacer) { char[] c = new char[MENU_WIDTH]; Arrays.fill(c, 1, c.length - 1, mid); c[0] = start; c[1] = '.'; c[2] = '.'; c[3] = '.'; c[c.length - 1] = end; c[c.length - 7] = spacer; c[c.length - 13] = spacer; return c; } private static final int MENU_WIDTH = 36; private static final int MENU_MAX = 5; private static final char[] TABLE_TOP = getCharLine((char) 131, (char) 136, (char) 133, (char) 136); private static final char[] TABLE_HEADING = getCharLine((char) 134, "Choose a Level ...", (char) 134); private static final char[] TABLE_MID = getLevelCharLine((char) 130, (char) 136, (char) 132, (char) 129); private static final char[] TABLE_BOTTOM = getLevelCharLine((char) 137, (char) 136, (char) 138, (char) 135); private static final char[] TABLE_MORE = getMoreLevelCharLine((char) 134, ' ', (char) 134, (char) 134); private static final char[][] START_UP = new char[][] { getCharLine((char) 131, (char) 136, (char) 133, (char) 136), getCharLine((char) 134, "Super Snuvi", (char) 134), getCharLine((char) 130, (char) 136, (char) 132, (char) 136), getCharLine((char) 134, "Start Game", (char) 134), getCharLine((char) 134, "Options", (char) 134), getCharLine((char) 134, "Exit Game", (char) 134), getCharLine((char) 137, (char) 136, (char) 138, (char) 136) }; private static final char[][] SLOTS = new char[][] { getCharLine((char) 131, (char) 136, (char) 133, (char) 136), getCharLine((char) 134, "Choose a Savegame ...", (char) 134), getCharLine((char) 130, (char) 136, (char) 132, (char) 136), getCharLine((char) 134, "Slot 1", (char) 134), getCharLine((char) 134, "Slot 2", (char) 134), getCharLine((char) 134, "Slot 3", (char) 134), getCharLine((char) 134, "Back", (char) 134), getCharLine((char) 137, (char) 136, (char) 138, (char) 136) }; private static final char[][] OPTIONS = new char[][] { getCharLine((char) 131, (char) 136, (char) 133, (char) 136), getCharLine((char) 134, "Options", (char) 134), getCharLine((char) 130, (char) 136, (char) 132, (char) 136), getCharLine((char) 134, "Sound", (char) 134), getCharLine((char) 134, "K: Up", (char) 134), getCharLine((char) 134, "K: Down", (char) 134), getCharLine((char) 134, "K: Left", (char) 134), getCharLine((char) 134, "K: Rright", (char) 134), getCharLine((char) 134, "K: Jump", (char) 134), getCharLine((char) 134, "K: Run", (char) 134), getCharLine((char) 134, "K: Back", (char) 134), getCharLine((char) 134, "K: Enter", (char) 134), getCharLine((char) 134, "K: Combat", (char) 134), getCharLine((char) 134, "K: Switch Face", (char) 134), getCharLine((char) 134, "K: Dash/Dodge", (char) 134), getCharLine((char) 134, "K: Dash/Dodge", (char) 134), getCharLine((char) 134, "K: Block", (char) 134), getCharLine((char) 134, "K: Attack", (char) 134), getCharLine((char) 134, "", (char) 134), // save getCharLine((char) 134, "Back", (char) 134), getCharLine((char) 137, (char) 136, (char) 138, (char) 136) }; private static final char[] SAVE_OVERLAY = getCharLine(' ', "Save", ' '); private static final int OPTION_OFFSET = 10; static { int pos = OPTIONS[0].length - OPTION_OFFSET; OPTIONS[2][pos] = (char) 129; for(int i = 0; i < 17; i++) { OPTIONS[3 + i][pos] = (char) 134; } OPTIONS[20][pos] = (char) 135; } private void menuMove(Runnable esc, Runnable enter, Runnable down, Runnable up, Runnable end) { if(KeyHandler.ESCAPE.wasJustReleased()) { esc.run(); return; } if(KeyHandler.ENTER.wasJustReleased()) { enter.run(); return; } if(KeyHandler.DOWN.getDownTime() > 16) { KeyHandler.DOWN.resetTime(); down.run(); } else if(KeyHandler.UP.getDownTime() > 16) { KeyHandler.UP.resetTime(); up.run(); } else if(KeyHandler.DOWN.wasJustReleased()) { down.run(); } else if(KeyHandler.UP.wasJustReleased()) { up.run(); } end.run(); } public void update() { if(currentLevel != null) { SoundUtils.playSound(SoundUtils.Sound.SONG_1); SoundUtils.stopSound(SoundUtils.Sound.MENU_MUSIC); currentLevel.update(); // doing that here to prevent concurent modification if(currentLevel.shouldFinish()) { String base = "level." + currentLevel.getName(); SimpleConfig sp = SAVE_SLOTS[slotScreenIndex]; // save success sp.set(base, true); // update time, if a new highscore was scored double time = sp.getDouble(base + ".time", Integer.MAX_VALUE); if(currentLevel.getTime() < time) { sp.set(base + ".time", currentLevel.getTime()); } // update bottles, if a new highscore was scored int bottles = sp.getInt(base + ".bottles", 0); if(currentLevel.getCurrentBottles() > bottles) { sp.set(base + ".bottles", currentLevel.getCurrentBottles()); } // final save sp.save(); currentLevel.resetLevel(); currentLevel = null; return; } if(currentLevel.shouldReset()) { if(currentLevel.resetLevel()) { currentLevel = null; } } if(KeyHandler.ESCAPE.wasJustReleased()) { currentLevel = null; } } else { SoundUtils.playSound(SoundUtils.Sound.MENU_MUSIC); SoundUtils.stopSound(SoundUtils.Sound.SONG_1); switch(screen) { case 0: // start screen { menuMove(() -> { // do nothing on escape in start screen }, () -> { switch(startScreenIndex) { case 0: screen = 1; break; case 1: screen = 2; break; case 2: System.exit(0); break; } }, () -> startScreenIndex++, () -> startScreenIndex--, () -> { if(startScreenIndex < 0) { startScreenIndex = 0; } else if(startScreenIndex >= 3) { startScreenIndex = 2; } }); break; } case 1: // slot screen { menuMove(() -> { screen = 0; }, () -> { if(slotScreenIndex == 3) { screen = 0; return; } screen = 3; }, () -> slotScreenIndex++, () -> slotScreenIndex--, () -> { if(slotScreenIndex < 0) { slotScreenIndex = 0; } else if(slotScreenIndex >= 4) { slotScreenIndex = 3; } }); break; } case 2: // option screen { menuMove(() -> { screen = 0; }, () -> { switch(optionScreenIndex) { case 0: // toggle sound sound = !sound; if(!sound) { SoundUtils.turnSoundOff(); } optionsDirty = true; break; case 15: // save options for(IKeyBinding binding : KeyHandler.ARRAY) { CONFIG.set(binding.getName(), binding.getKeyName()); } CONFIG.set("sound", sound); CONFIG.save(); optionsDirty = false; break; case 16: // go back screen = 0; break; default: // rebind keys KeyHandler.rebindKey(KeyHandler.ARRAY[optionScreenIndex - 1]); optionsDirty = true; break; } }, () -> optionScreenIndex++, () -> optionScreenIndex--, () -> { if(optionScreenIndex < 0) { optionScreenIndex = 0; } else if(optionScreenIndex >= OPTIONS.length - 4) { optionScreenIndex = OPTIONS.length - 5; } }); break; } case 3: // level choose screen { menuMove(() -> { screen = 1; }, () -> currentLevel = levels[levelIndex], () -> levelIndex++, () -> levelIndex--, () -> { if(levelIndex < 0) { levelIndex = 0; } else if(levelIndex >= levels.length) { levelIndex = levels.length - 1; } }); break; } } } } public void render() { if(currentLevel != null) { currentLevel.render(); return; } switch(screen) { case 0: { renderer.prepareTextDrawing(255, 255, 255, 1.0, MENU_WIDTH * 5 / 4); double x = (renderer.getWidth() - renderer.getTextWidth(MENU_WIDTH)) * 0.5; double y = (renderer.getHeight() - renderer.getTextHeight(7)) * 0.5; double line = renderer.getTextHeight(1); renderer.save(); renderer.setFillColor(128, 128, 128, 1.0); renderer.fillRectangle( x + renderer.getTextWidth(1), y + renderer.getTextHeight(startScreenIndex + 3) - 1, renderer.getTextWidth(MENU_WIDTH - 2), line); renderer.restore(); for(char[] c : START_UP) { renderer.drawText(x, y, c); y += line; } break; } case 1: { renderer.prepareTextDrawing(255, 255, 255, 1.0, MENU_WIDTH * 5 / 4); double x = (renderer.getWidth() - renderer.getTextWidth(MENU_WIDTH)) * 0.5; double y = (renderer.getHeight() - renderer.getTextHeight(7)) * 0.5; double line = renderer.getTextHeight(1); renderer.save(); renderer.setFillColor(128, 128, 128, 1.0); renderer.fillRectangle( x + renderer.getTextWidth(1), y + renderer.getTextHeight(slotScreenIndex + 3) - 1, renderer.getTextWidth(MENU_WIDTH - 2), line); renderer.restore(); for(char[] c : SLOTS) { renderer.drawText(x, y, c); y += line; } break; } case 2: { renderer.prepareTextDrawing(255, 255, 255, 1.0, MENU_WIDTH * 5 / 4); double x = (renderer.getWidth() - renderer.getTextWidth(MENU_WIDTH)) * 0.5; double y = (renderer.getHeight() - renderer.getTextHeight(OPTIONS.length)) * 0.5; double line = renderer.getTextHeight(1); renderer.save(); renderer.setFillColor(128, 128, 128, 1.0); renderer.fillRectangle( x + renderer.getTextWidth(1), y + renderer.getTextHeight(optionScreenIndex + 3) - 1, renderer.getTextWidth(MENU_WIDTH - (OPTION_OFFSET + 1)), line); renderer.fillRectangle( x + renderer.getTextWidth(MENU_WIDTH - (OPTION_OFFSET - 1)), y + renderer.getTextHeight(optionScreenIndex + 3) - 1, renderer.getTextWidth(OPTION_OFFSET - 2), line); renderer.restore(); // option top for(int i = 0; i < 3; i++) { renderer.drawText(x, y, OPTIONS[i]); y += line; } // sound option if(sound) { int pos = OPTIONS[3].length - OPTION_OFFSET + 1; OPTIONS[3][pos] = 'y'; OPTIONS[3][pos + 1] = 'e'; OPTIONS[3][pos + 2] = 's'; } else { int pos = OPTIONS[3].length - OPTION_OFFSET + 1; OPTIONS[3][pos] = 'n'; OPTIONS[3][pos + 1] = 'o'; OPTIONS[3][pos + 2] = ' '; } renderer.drawText(x, y, OPTIONS[3]); y += line; // key binding options int end = 4 + KeyHandler.ARRAY.length; for(int i = 4; i < end; i++) { String name = KeyHandler.ARRAY[i - 4].toString(); System.arraycopy(name.toCharArray(), 0, OPTIONS[i], OPTIONS[i].length - OPTION_OFFSET + 1, Math.min(OPTION_OFFSET - 2, name.length())); renderer.drawText(x, y, OPTIONS[i]); y += line; } // save renderer.drawText(x, y, OPTIONS[end]); if(optionsDirty) { renderer.save(); renderer.setFillColor(180, 0, 0, 1); renderer.setStrokeColor(180, 0, 0, 1); renderer.drawText(x, y, SAVE_OVERLAY); renderer.restore(); } else { renderer.drawText(x, y, SAVE_OVERLAY); } y += line; // left over option table for(int i = end + 1; i < OPTIONS.length; i++) { renderer.drawText(x, y, OPTIONS[i]); y += line; } break; } case 3: { // level screen rendering renderer.prepareTextDrawing(255, 255, 255, 1.0, MENU_WIDTH * 5 / 4); int listLength = Math.min(levels.length, MENU_MAX); double x = (renderer.getWidth() - renderer.getTextWidth(MENU_WIDTH)) * 0.5; double y = (renderer.getHeight() - renderer.getTextHeight(listLength + 4)) * 0.5; double line = renderer.getTextHeight(1); renderer.drawText(x, y, TABLE_TOP); y += line; renderer.drawText(x, y, TABLE_HEADING); y += line; renderer.drawText(x, y, TABLE_MID); y += line; if(levels.length > MENU_MAX) { int upperHalf = MENU_MAX / 2; int downHalf = levels.length - upperHalf; if(levelIndex < upperHalf) { paintStringMarking(x, y, line, levelIndex); y = paintLevelName(x, y, line, 0, MENU_MAX - 1); renderer.drawText(x, y, TABLE_MORE); y += line; } else if(levelIndex >= downHalf) { paintStringMarking(x, y, line, levelIndex - downHalf + upperHalf + ((MENU_MAX & 1) != 0 ? 1 : 0)); renderer.drawText(x, y, TABLE_MORE); y += line; y = paintLevelName(x, y, line, levels.length - MENU_MAX + 1, MENU_MAX - 1); } else { paintStringMarking(x, y, line, upperHalf); renderer.drawText(x, y, TABLE_MORE); y += line; y = paintLevelName(x, y, line, levelIndex - upperHalf + 1, MENU_MAX - 2); renderer.drawText(x, y, TABLE_MORE); y += line; } } else { paintStringMarking(x, y, line, levelIndex); y = paintLevelName(x, y, line, 0, levels.length); } renderer.drawText(x, y, TABLE_BOTTOM); renderer.stopTextDrawing(); break; } } } private double paintLevelName(double x, double y, double line, int from, int length) { SimpleConfig sp = SAVE_SLOTS[slotScreenIndex]; char[] chars = new char[MENU_WIDTH]; chars[0] = 134; chars[MENU_WIDTH - 1] = 134; length += from; for(int j = from; j < length; j++) { String s = levels[j].getName(); int border = Math.min(MENU_WIDTH - 14, s.length()); for(int i = 0; i < border; i++) { chars[i + 1] = s.charAt(i); } Arrays.fill(chars, border + 1, MENU_WIDTH - 1, (char) 0); chars[chars.length - 13] = 134; chars[chars.length - 7] = 134; // bottles char[] tmp = levels[j].formatBottles(sp.getInt("level." + s + ".bottles", 0)); int l = Math.min(5, tmp.length); System.arraycopy(tmp, 0, chars, chars.length - 7 - l, l); // time tmp = levels[j].formatTime(sp.getDouble("level." + s + ".time", -1.0)); l = Math.min(5, tmp.length); System.arraycopy(tmp, 0, chars, chars.length - 1 - l, l); renderer.drawText(x, y, chars); y += line; } return y; } private void paintStringMarking(double x, double y, double line, int pos) { renderer.save(); renderer.setFillColor(128, 128, 128, 1.0); renderer.fillRectangle( x + renderer.getTextWidth(1), y + renderer.getTextHeight(pos) - 1, renderer.getTextWidth(MENU_WIDTH - 14), line); renderer.fillRectangle( x + renderer.getTextWidth(MENU_WIDTH - 12), y + renderer.getTextHeight(pos) - 1, renderer.getTextWidth(5), line); renderer.fillRectangle( x + renderer.getTextWidth(MENU_WIDTH - 6), y + renderer.getTextHeight(pos) - 1, renderer.getTextWidth(5), line); renderer.restore(); } public void tickTiles() { registeredTiles.values().forEach(tile -> tile.tick()); } }