From 806b762333863f8221ad3fe2e38e337b7fff50d5 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 20 Nov 2017 19:02:35 -0500 Subject: [PATCH] Added full block descriptions, improved shield shaders --- core/assets/shaders/shield.fragment | 3 +- core/src/io/anuke/mindustry/Renderer.java | 167 ++++++++++-------- core/src/io/anuke/mindustry/Shaders.java | 7 +- core/src/io/anuke/mindustry/UI.java | 28 ++- core/src/io/anuke/mindustry/Vars.java | 2 + .../mindustry/world/blocks/DefenseBlocks.java | 2 + desktop/mindustry-saves/3.mins | Bin 6776 -> 1539 bytes 7 files changed, 131 insertions(+), 78 deletions(-) diff --git a/core/assets/shaders/shield.fragment b/core/assets/shaders/shield.fragment index 98f0b5adba..e2cdb0dfc1 100644 --- a/core/assets/shaders/shield.fragment +++ b/core/assets/shaders/shield.fragment @@ -8,6 +8,7 @@ uniform sampler2D u_texture; uniform vec4 u_color; uniform vec2 u_texsize; uniform float u_time; +uniform float u_scaling; uniform vec2 u_offset; varying vec4 v_color; @@ -19,7 +20,7 @@ void main() { vec2 coords = (T * u_texsize) + u_offset; - T += vec2(sin(coords.y / 3.0 + u_time / 20.0) / 250.0, sin(coords.x / 3.0 + u_time / 20.0) / 250.0); + T += vec2(sin(coords.y / 3.0 + u_time / 20.0) / 250.0, sin(coords.x / 3.0 + u_time / 20.0) / 250.0) * u_scaling; float si = 1.0 + sin(u_time / 20.0 /*+ (coords.x + coords.y) / 30.0*/) / 8.0; diff --git a/core/src/io/anuke/mindustry/Renderer.java b/core/src/io/anuke/mindustry/Renderer.java index dc9db318ed..304e0e826e 100644 --- a/core/src/io/anuke/mindustry/Renderer.java +++ b/core/src/io/anuke/mindustry/Renderer.java @@ -45,17 +45,17 @@ public class Renderer extends RendererModule{ public Renderer() { Core.cameraScale = baseCameraScale; - + Graphics.addSurface("pixel", Core.cameraScale); } - + @Override public void init(){ pixelate = Settings.getBool("pixelate"); Graphics.addSurface("shadow", Settings.getBool("pixelate") ? Core.cameraScale : 1); Graphics.addSurface("shield", Settings.getBool("pixelate") ? Core.cameraScale : 1); } - + public void setPixelate(boolean pixelate){ this.pixelate = pixelate; } @@ -82,9 +82,9 @@ public class Renderer extends RendererModule{ clearScreen(); }else{ boolean smoothcam = Settings.getBool("smoothcam"); - + if(World.core.block() == ProductionBlocks.core){ - + if(!smoothcam){ setCamera(player.x, player.y); }else{ @@ -93,7 +93,7 @@ public class Renderer extends RendererModule{ }else{ smoothCamera(World.core.worldx(), World.core.worldy(), 0.4f); } - + if(Settings.getBool("pixelate")) limitCamera(4f, player.x, player.y); @@ -111,28 +111,26 @@ public class Renderer extends RendererModule{ } float lastx = camera.position.x, lasty = camera.position.y; - + if(Vars.snapCamera && smoothcam && Settings.getBool("pixelate")){ camera.position.set((int) camera.position.x, (int) camera.position.y, 0); } - + if(Gdx.graphics.getHeight() / Core.cameraScale % 2 == 1){ camera.position.add(0, -0.5f, 0); } - + if(Gdx.graphics.getWidth() / Core.cameraScale % 2 == 1){ camera.position.add(-0.5f, 0, 0); } - + long time = TimeUtils.nanoTime(); drawDefault(); - if(Timers.get("profiled", profileTime)) Profiler.draw = TimeUtils.timeSinceNanos(time); - + if(Timers.get("profiled", profileTime)) + Profiler.draw = TimeUtils.timeSinceNanos(time); + if(Vars.debug && Vars.debugGL && Timers.get("profile", 60)){ - UCore.log("shaders: " + GLProfiler.shaderSwitches, - "calls: " + GLProfiler.drawCalls, - "bindings: " + GLProfiler.textureBindings, - "vertices: " + GLProfiler.vertexCount.average); + UCore.log("shaders: " + GLProfiler.shaderSwitches, "calls: " + GLProfiler.drawCalls, "bindings: " + GLProfiler.textureBindings, "vertices: " + GLProfiler.vertexCount.average); } camera.position.set(lastx - deltax, lasty - deltay, 0); @@ -147,19 +145,21 @@ public class Renderer extends RendererModule{ public void draw(){ Graphics.surface("shield"); Graphics.surface(); - + long time = TimeUtils.nanoTime(); renderTiles(); - if(Timers.get("profilebd", profileTime)) Profiler.blockDraw = TimeUtils.timeSinceNanos(time); - + if(Timers.get("profilebd", profileTime)) + Profiler.blockDraw = TimeUtils.timeSinceNanos(time); + time = TimeUtils.nanoTime(); Entities.draw(); - if(Timers.get("profileed", profileTime)) Profiler.entityDraw = TimeUtils.timeSinceNanos(time); - + if(Timers.get("profileed", profileTime)) + Profiler.entityDraw = TimeUtils.timeSinceNanos(time); + drawShield(); - + renderPixelOverlay(); - + if(Settings.getBool("indicators")){ drawEnemyMarkers(); } @@ -173,20 +173,18 @@ public class Renderer extends RendererModule{ AndroidInput.mousey = Gdx.graphics.getHeight() / 2; camera.position.set(player.x, player.y, 0); } - + void drawEnemyMarkers(){ Draw.color(Color.RED); Draw.alpha(0.6f); for(Entity entity : Entities.all()){ if(entity instanceof Enemy){ - Enemy enemy = (Enemy)entity; - - if(Tmp.r1.setSize(camera.viewportWidth, camera.viewportHeight) - .setCenter(camera.position.x, camera.position.y) - .overlaps(enemy.hitbox.getRect(enemy.x, enemy.y))){ + Enemy enemy = (Enemy) entity; + + if(Tmp.r1.setSize(camera.viewportWidth, camera.viewportHeight).setCenter(camera.position.x, camera.position.y).overlaps(enemy.hitbox.getRect(enemy.x, enemy.y))){ continue; } - + float angle = Angles.angle(camera.position.x, camera.position.y, enemy.x, enemy.y); Angles.translation(angle, Unit.dp.inPixels(20f)); Draw.rect("enemyarrow", camera.position.x + Angles.x(), camera.position.y + Angles.y(), angle); @@ -194,20 +192,20 @@ public class Renderer extends RendererModule{ } Draw.color(); } - + void drawShield(){ Texture texture = Graphics.getSurface("shield").texture(); Shaders.shield.color.set(Color.SKY); - + Tmp.tr2.setRegion(texture); Shaders.shield.region = Tmp.tr2; - + Graphics.end(); Graphics.shader(Shaders.shield); Graphics.setScreen(); - + Core.batch.draw(texture, 0, Gdx.graphics.getHeight(), Gdx.graphics.getWidth(), -Gdx.graphics.getHeight()); - + Graphics.shader(); Graphics.end(); Graphics.beginCam(); @@ -219,9 +217,9 @@ public class Renderer extends RendererModule{ //render the entire map if(floorCache == null || floorCache.length != chunksx || floorCache[0].length != chunksy){ floorCache = new Cache[chunksx][chunksy]; - - for(int x = 0; x < chunksx; x ++){ - for(int y = 0; y < chunksy; y ++){ + + for(int x = 0; x < chunksx; x++){ + for(int y = 0; y < chunksy; y++){ renderCache(x, y); } } @@ -257,9 +255,9 @@ public class Renderer extends RendererModule{ int rangey = (int) (camera.viewportHeight * camera.zoom / tilesize / 2) + 2; boolean noshadows = Settings.getBool("noshadows"); - + boolean drawTiles = true; - + //0 = shadows //1 = normal blocks //2 = over blocks @@ -267,7 +265,7 @@ public class Renderer extends RendererModule{ if(l == 0){ Graphics.surface("shadow"); } - + for(int x = -rangex; x <= rangex; x++){ for(int y = -rangey; y <= rangey; y++){ int worldx = Mathf.scl(camera.position.x, tilesize) + x; @@ -294,6 +292,23 @@ public class Renderer extends RendererModule{ Draw.color(); } } + + if(Vars.debug && Vars.debugChunks){ + Draw.color(Color.YELLOW); + Draw.thick(1f); + for(int x = -crangex; x <= crangex; x++){ + for(int y = -crangey; y <= crangey; y++){ + int worldx = Mathf.scl(camera.position.x, chunksize * tilesize) + x; + int worldy = Mathf.scl(camera.position.y, chunksize * tilesize) + y; + + if(!Mathf.inBounds(worldx, worldy, floorCache)) + continue; + Draw.linerect(worldx * chunksize * tilesize, worldy * chunksize * tilesize, + chunksize * tilesize, chunksize * tilesize); + } + } + Draw.reset(); + } } void renderCache(int cx, int cy){ @@ -303,11 +318,11 @@ public class Renderer extends RendererModule{ for(int tiley = cy * chunksize; tiley < (cy + 1) * chunksize; tiley++){ Tile tile = World.tile(tilex, tiley); tile.floor().drawCache(tile); - + } } floorCache[cx][cy] = Caches.end(); - + } public void clearTiles(){ @@ -316,15 +331,16 @@ public class Renderer extends RendererModule{ void renderPixelOverlay(){ + //draw tutorial placement point if(Vars.control.tutorial.showBlock()){ int x = World.core.x + Vars.control.tutorial.getPlacePoint().x; int y = World.core.y + Vars.control.tutorial.getPlacePoint().y; int rot = Vars.control.tutorial.getPlaceRotation(); - + Draw.thick(1f); Draw.color(Color.YELLOW); - Draw.square(x * tilesize, y * tilesize, tilesize/2f + Mathf.sin(Timers.time(), 4f, 1f)); - + Draw.square(x * tilesize, y * tilesize, tilesize / 2f + Mathf.sin(Timers.time(), 4f, 1f)); + Draw.color(Color.ORANGE); Draw.thick(2f); if(rot != -1){ @@ -332,8 +348,10 @@ public class Renderer extends RendererModule{ } Draw.reset(); } - - if(player.recipe != null && Vars.control.hasItems(player.recipe.requirements) && (!ui.hasMouse() || android) && AndroidInput.mode == PlaceMode.cursor){ + + //draw placement box + if(player.recipe != null && Vars.control.hasItems(player.recipe.requirements) + && (!ui.hasMouse() || android) && AndroidInput.mode == PlaceMode.cursor){ float x = 0; float y = 0; @@ -348,22 +366,19 @@ public class Renderer extends RendererModule{ tilex = Input.tilex(); tiley = Input.tiley(); } - - x = tilex*tilesize; - y = tiley*tilesize; - boolean valid = World.validPlace(tilex, tiley, player.recipe.result) && (android || - Input.cursorNear()); - + x = tilex * tilesize; + y = tiley * tilesize; + + boolean valid = World.validPlace(tilex, tiley, player.recipe.result) && (android || Input.cursorNear()); + Vector2 offset = player.recipe.result.getPlaceOffset(); - + float si = MathUtils.sin(Timers.time() / 6f) + 1; - + Draw.color(valid ? Color.PURPLE : Color.SCARLET); Draw.thickness(2f); - Draw.linecrect(x + offset.x, y + offset.y, - tilesize * player.recipe.result.width + si, - tilesize * player.recipe.result.height + si); + Draw.linecrect(x + offset.x, y + offset.y, tilesize * player.recipe.result.width + si, tilesize * player.recipe.result.height + si); player.recipe.result.drawPlace(tilex, tiley, valid); @@ -390,25 +405,27 @@ public class Renderer extends RendererModule{ //block breaking if(Inputs.buttonDown(Buttons.RIGHT) && World.validBreak(Input.tilex(), Input.tiley())){ Tile tile = World.tile(Input.tilex(), Input.tiley()); - if(tile.isLinked()) tile = tile.getLinked(); + if(tile.isLinked()) + tile = tile.getLinked(); Vector2 offset = tile.block().getPlaceOffset(); - + Draw.color(Color.YELLOW, Color.SCARLET, player.breaktime / tile.getBreakTime()); Draw.linecrect(tile.worldx() + offset.x, tile.worldy() + offset.y, tile.block().width * Vars.tilesize, tile.block().height * Vars.tilesize); Draw.reset(); }else if(android && player.breaktime > 0){ //android block breaking Vector2 vec = Graphics.world(Gdx.input.getX(0), Gdx.input.getY(0)); - + if(World.validBreak(Mathf.scl2(vec.x, tilesize), Mathf.scl2(vec.y, tilesize))){ Tile tile = World.tile(Mathf.scl2(vec.x, tilesize), Mathf.scl2(vec.y, tilesize)); - + float fract = player.breaktime / tile.getBreakTime(); Draw.color(Color.YELLOW, Color.SCARLET, fract); Draw.circle(tile.worldx(), tile.worldy(), 4 + (1f - fract) * 26); Draw.reset(); } } - + + //draw selected block health if(player.recipe == null && !ui.hasMouse()){ Tile tile = World.tile(Input.tilex(), Input.tiley()); @@ -416,33 +433,33 @@ public class Renderer extends RendererModule{ Tile target = tile; if(tile.isLinked()) target = tile.getLinked(); - + Vector2 offset = target.block().getPlaceOffset(); - + if(target.entity != null) - drawHealth(target.entity.x + offset.x, target.entity.y - 3f - target.block().height/2f * Vars.tilesize + offset.y, - target.entity.health, target.entity.maxhealth); - + drawHealth(target.entity.x + offset.x, target.entity.y - 3f - target.block().height / 2f * Vars.tilesize + offset.y, target.entity.health, target.entity.maxhealth); + target.block().drawPixelOverlay(target); } } - - boolean smoothcam = Settings.getBool("smoothcam"); + boolean smoothcam = Settings.getBool("smoothcam"); + + //draw entity health bars for(Entity entity : Entities.all()){ if(entity instanceof DestructibleEntity && !(entity instanceof TileEntity)){ DestructibleEntity dest = ((DestructibleEntity) entity); - + if(dest instanceof Player && Vars.snapCamera && smoothcam && Settings.getBool("pixelate")){ - drawHealth((int)dest.x, (int)dest.y - 7f, dest.health, dest.maxhealth); + drawHealth((int) dest.x, (int) dest.y - 7f, dest.health, dest.maxhealth); }else{ drawHealth(dest.x, dest.y - 7f, dest.health, dest.maxhealth); } - + } } } - + void drawHealth(float x, float y, float health, float maxhealth){ drawBar(Color.RED, x, y, health / maxhealth); } diff --git a/core/src/io/anuke/mindustry/Shaders.java b/core/src/io/anuke/mindustry/Shaders.java index 5efc77295d..ec8a6b7da0 100644 --- a/core/src/io/anuke/mindustry/Shaders.java +++ b/core/src/io/anuke/mindustry/Shaders.java @@ -3,6 +3,7 @@ package io.anuke.mindustry; import com.badlogic.gdx.graphics.Color; import io.anuke.ucore.core.Core; +import io.anuke.ucore.core.Settings; import io.anuke.ucore.core.Timers; import io.anuke.ucore.graphics.Shader; import io.anuke.ucore.util.Tmp; @@ -35,10 +36,14 @@ public class Shaders{ @Override public void apply(){ + float scale = Settings.getBool("pixelate") ? 1 : Core.cameraScale / Core.camera.zoom; + float scaling = Core.cameraScale / 4f / Core.camera.zoom; shader.setUniformf("u_color", color); shader.setUniformf("u_time", Timers.time()); + shader.setUniformf("u_scaling", scaling); shader.setUniformf("u_offset", Tmp.v1.set(Core.camera.position.x, Core.camera.position.y)); - shader.setUniformf("u_texsize", Tmp.v1.set(region.getTexture().getWidth(), region.getTexture().getHeight())); + shader.setUniformf("u_texsize", Tmp.v1.set(region.getTexture().getWidth() / scale, + region.getTexture().getHeight() / scale)); } } diff --git a/core/src/io/anuke/mindustry/UI.java b/core/src/io/anuke/mindustry/UI.java index 224733fcbb..6de60ef5d2 100644 --- a/core/src/io/anuke/mindustry/UI.java +++ b/core/src/io/anuke/mindustry/UI.java @@ -665,7 +665,33 @@ public class UI extends SceneModule{ header.addImage(region).size(8*5).padTop(4).units(Unit.dp); Label nameLabel = new Label(recipe.result.formalName); nameLabel.setWrap(true); - header.add(nameLabel).padLeft(4).width(160f).units(Unit.dp); + header.add(nameLabel).padLeft(4).width(135f).units(Unit.dp); + + //extra info + if(recipe.result.fullDescription != null){ + header.addButton("?", ()->{ + Label desclabel = new Label(recipe.result.fullDescription); + desclabel.setWrap(true); + + boolean wasPaused = GameState.is(State.paused); + GameState.set(State.paused); + + FloatingDialog d = new FloatingDialog("Block Info"); + Table top = new Table(); + top.left(); + top.add(new Image(region)).size(8*5).units(Unit.dp); + top.add("[orange]"+recipe.result.formalName).padLeft(6f).units(Unit.dp); + d.content().add(top).fill().left(); + d.content().row(); + d.content().add(desclabel).width(600).units(Unit.dp); + d.buttons().addButton("OK", ()->{ + if(!wasPaused) GameState.set(State.playing); + d.hide(); + }).size(110, 50).pad(10f).units(Unit.dp); + d.show(); + }).fillX().top().right().size(36f, 40f).units(Unit.dp); + } + desctable.add().pad(2).units(Unit.dp); diff --git a/core/src/io/anuke/mindustry/Vars.java b/core/src/io/anuke/mindustry/Vars.java index 66e272d0ad..4810113676 100644 --- a/core/src/io/anuke/mindustry/Vars.java +++ b/core/src/io/anuke/mindustry/Vars.java @@ -31,6 +31,8 @@ public class Vars{ public static boolean debug = false; //whether to debug openGL info public static boolean debugGL = false; + //whether to draw chunk borders + public static boolean debugChunks = false; //whether turrets have infinite ammo (only with debug) public static boolean infiniteAmmo = true; //whether to show paths of enemies diff --git a/core/src/io/anuke/mindustry/world/blocks/DefenseBlocks.java b/core/src/io/anuke/mindustry/world/blocks/DefenseBlocks.java index e2d092e07a..59a835fb06 100644 --- a/core/src/io/anuke/mindustry/world/blocks/DefenseBlocks.java +++ b/core/src/io/anuke/mindustry/world/blocks/DefenseBlocks.java @@ -13,6 +13,8 @@ public class DefenseBlocks{ stonewall = new Wall("stonewall"){{ health = 50; formalName = "stone wall"; + fullDescription = + "A cheap defensive block. Useful for protecting the core and turrets in the first few waves."; }}, ironwall = new Wall("ironwall"){{ diff --git a/desktop/mindustry-saves/3.mins b/desktop/mindustry-saves/3.mins index 2554c77e80ac4eab10b843dcb443c87d6fe36195..3fe1e6e8ec005ea0d827c486a4820eebd48a10db 100644 GIT binary patch literal 1539 zcmZQzVBln6V2rjLU}s>EYuUlVAlDMX#30ww zzziZ-L0mQvixEUHFfcGOFt{*pE_0F4UBbw~z+l70z{q!mfq|izfkA#H0|@9dFet!T zIuMot1B0O^NRq*Tfk9pi#!`T@^x-T69hj`3E|{gyz+ePtiRppGK<2Fjv-BAl1oUAn zq4i*v0Rw{;gr(2Gpl$#b(`R7d-2`SCFffQgSRlQ|U@?6L2Jy`>mi%UrFBw8Wd<_Oi z`PbzP31_rquFfn~g7>j2Yj3o$X8Ct`{#BIPV0|o~1 zJusGx9gHPw2ew!W#MfY8V36I1%9lfvmxt@rgR^Aq!FrfM=>cR0$Sm3YXkv^E3|a3O z804U8L26)XC!tKw|PxHOS`4L)0MbU}Rv>b7EjnfT*iKv@b-GodV5XNXQ@_sAYb69YvVklP_@ zrC|Eyp=w}!kTvoUHK@^$z`&pYQHvQ3vMyi;Akx$dgR>-#fyMM0 z7&zU*ECU7x-s4~vC`Z7>l;A8?h^zqv1D^*>oiv=K3Sk*AFi4&R%Nj5+=zGFgLSA5& zJ_Cb3oW*k*CME`F$-!A>XTY)`+s?vR{BV|#FHB4p&eHG$i|I2kh@6M91TMf>LU5LJ u08C8aBA8{sz`zv*X6Z99a9swo3>X*$uYy_n3=C4CFqY6YFbh=LL0JGC&3{e+ literal 6776 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