package mindustry.graphics; import arc.*; import arc.graphics.*; import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import arc.util.pooling.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.io.*; import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; public class MinimapRenderer{ private static final float baseSize = 16f, updateInterval = 2f; private final Seq units = new Seq<>(); private Pixmap pixmap; private Texture texture; private TextureRegion region; private Rect rect = new Rect(); private float zoom = 4; private float lastX, lastY, lastW, lastH, lastScl; private boolean worldSpace; private IntSet updates = new IntSet(); private float updateCounter = 0f; public MinimapRenderer(){ Events.on(WorldLoadEvent.class, event -> { reset(); updateAll(); }); Events.on(TileChangeEvent.class, event -> { if(!ui.editor.isShown()){ update(event.tile); //update floor below block. if(event.tile.block().solid && event.tile.y > 0 && event.tile.isCenter()){ event.tile.getLinkedTiles(t -> { Tile tile = world.tile(t.x, t.y - 1); if(tile != null && tile.block() == Blocks.air){ update(tile); } }); } } }); Events.on(TilePreChangeEvent.class, e -> { //update floor below a *recently removed* block. if(e.tile.block().solid && e.tile.y > 0){ Tile tile = world.tile(e.tile.x, e.tile.y - 1); if(tile.block() == Blocks.air){ Core.app.post(() -> update(tile)); } } }); Events.on(BuildTeamChangeEvent.class, event -> update(event.build.tile)); } public void update(){ //updates are batched to occur every 2 frames if((updateCounter += Time.delta) >= updateInterval){ updateCounter %= updateInterval; updates.each(pos -> { Tile tile = world.tile(pos); if(tile == null) return; int color = colorFor(tile); pixmap.set(tile.x, pixmap.height - 1 - tile.y, color); //yes, this calls glTexSubImage2D every time, with a 1x1 region Pixmaps.drawPixel(texture, tile.x, pixmap.height - 1 - tile.y, color); }); updates.clear(); } } public Pixmap getPixmap(){ return pixmap; } public @Nullable Texture getTexture(){ return texture; } public void zoomBy(float amount){ zoom += amount; setZoom(zoom); } public void setZoom(float amount){ zoom = Mathf.clamp(amount, 1f, Math.min(world.width(), world.height()) / baseSize / 2f); } public float getZoom(){ return zoom; } public void reset(){ updates.clear(); if(pixmap != null){ pixmap.dispose(); texture.dispose(); } setZoom(4f); pixmap = new Pixmap(world.width(), world.height()); texture = new Texture(pixmap); region = new TextureRegion(texture); } public void drawEntities(float x, float y, float w, float h, float scaling, boolean fullView){ lastX = x; lastY = y; lastW = w; lastH = h; lastScl = scaling; worldSpace = fullView; if(!fullView){ updateUnitArray(); }else{ units.clear(); Groups.unit.copy(units); } float sz = baseSize * zoom; float dx = (Core.camera.position.x / tilesize); float dy = (Core.camera.position.y / tilesize); dx = Mathf.clamp(dx, sz, world.width() - sz); dy = Mathf.clamp(dy, sz, world.height() - sz); rect.set((dx - sz) * tilesize, (dy - sz) * tilesize, sz * 2 * tilesize, sz * 2 * tilesize); for(Unit unit : units){ if(unit.inFogTo(player.team()) || !unit.type.drawMinimap) continue; float rx = !fullView ? (unit.x - rect.x) / rect.width * w : unit.x / (world.width() * tilesize) * w; float ry = !fullView ? (unit.y - rect.y) / rect.width * h : unit.y / (world.height() * tilesize) * h; Draw.mixcol(unit.team.color, 1f); float scale = Scl.scl(1f) / 2f * scaling * 32f; var region = unit.icon(); Draw.rect(region, x + rx, y + ry, scale, scale * (float)region.height / region.width, unit.rotation() - 90); Draw.reset(); } if(fullView && net.active()){ for(Player player : Groups.player){ if(!player.dead()){ float rx = player.x / (world.width() * tilesize) * w; float ry = player.y / (world.height() * tilesize) * h; drawLabel(x + rx, y + ry, player.name, player.color); } } } Draw.reset(); if(state.rules.fog){ if(fullView){ float z = zoom; //max zoom out fixes everything, somehow? setZoom(99999f); getRegion(); zoom = z; } Draw.shader(Shaders.fog); Texture staticTex = renderer.fog.getStaticTexture(), dynamicTex = renderer.fog.getDynamicTexture(); //crisp pixels dynamicTex.setFilter(TextureFilter.nearest); if(worldSpace){ region.set(0f, 0f, 1f, 1f); } Tmp.tr1.set(dynamicTex); Tmp.tr1.set(region.u, 1f - region.v, region.u2, 1f - region.v2); Draw.color(state.rules.dynamicColor); Draw.rect(Tmp.tr1, x + w/2f, y + h/2f, w, h); if(state.rules.staticFog){ staticTex.setFilter(TextureFilter.nearest); Tmp.tr1.texture = staticTex; //must be black to fit with borders Draw.color(0f, 0f, 0f, state.rules.staticColor.a); Draw.rect(Tmp.tr1, x + w/2f, y + h/2f, w, h); } Draw.color(); Draw.shader(); } //TODO might be useful in the standard minimap too if(fullView){ drawSpawns(x, y, w, h, scaling); if(!mobile){ //draw bounds for camera - not drawn on mobile because you can't shift it by tapping anyway Rect r = Core.camera.bounds(Tmp.r1); Vec2 bot = transform(Tmp.v1.set(r.x, r.y)); Vec2 top = transform(Tmp.v2.set(r.x + r.width, r.y + r.height)); Lines.stroke(Scl.scl(3f)); Draw.color(Pal.accent); Lines.rect(bot.x,bot.y, top.x - bot.x, top.y - bot.y); Draw.reset(); } } LongSeq indicators = control.indicators.list(); float fin = ((Time.globalTime / 30f) % 1f); float rad = scale(fin * 5f + tilesize - 2f); Lines.stroke(Scl.scl((1f - fin) * 4f + 0.5f)); for(int i = 0; i < indicators.size; i++){ long ind = indicators.items[i]; int pos = Indicator.pos(ind), ix = Point2.x(pos), iy = Point2.y(pos); float time = Indicator.time(ind), offset = 0f; //fix multiblock offset - this is suboptimal Building build = world.build(pos); if(build != null){ offset = build.block.offset / tilesize; } Vec2 v = transform(Tmp.v1.set((ix + 0.5f + offset) * tilesize, (iy + 0.5f + offset) * tilesize)); Draw.color(Color.orange, Color.scarlet, Mathf.clamp(time / 70f)); Lines.square(v.x, v.y, rad); } Draw.reset(); state.rules.objectives.eachRunning(obj -> { for(var marker : obj.markers){ marker.drawMinimap(this); } }); for(var marker : state.rules.markers.values()){ if(marker != null)marker.drawMinimap(this); } } public void drawSpawns(float x, float y, float w, float h, float scaling){ if(!state.rules.showSpawns || !state.hasSpawns() || !state.rules.waves) return; TextureRegion icon = Icon.units.getRegion(); Lines.stroke(Scl.scl(3f)); Draw.color(state.rules.waveTeam.color, Tmp.c2.set(state.rules.waveTeam.color).value(1.2f), Mathf.absin(Time.time, 16f, 1f)); float rad = scale(state.rules.dropZoneRadius); float curve = Mathf.curve(Time.time % 240f, 120f, 240f); for(Tile tile : spawner.getSpawns()){ float tx = ((tile.x + 0.5f) / world.width()) * w; float ty = ((tile.y + 0.5f) / world.height()) * h; Draw.rect(icon, x + tx, y + ty, icon.width, icon.height); Lines.circle(x + tx, y + ty, rad); if(curve > 0f) Lines.circle(x + tx, y + ty, rad * Interp.pow3Out.apply(curve)); } Draw.reset(); } //TODO horrible code, everywhere. public Vec2 transform(Vec2 position){ if(!worldSpace){ position.sub(rect.x, rect.y).scl(lastW / rect.width, lastH / rect.height); }else{ position.scl(lastW / world.unitWidth(), lastH / world.unitHeight()); } return position.add(lastX, lastY); } public float scale(float radius){ return worldSpace ? (radius / (baseSize / 2f)) * 5f * lastScl : lastW / rect.width * radius; } public @Nullable TextureRegion getRegion(){ if(texture == null) return null; float sz = Mathf.clamp(baseSize * zoom, baseSize, Math.min(world.width(), world.height())); float dx = (Core.camera.position.x / tilesize); float dy = (Core.camera.position.y / tilesize); dx = Mathf.clamp(dx, sz, world.width() - sz); dy = Mathf.clamp(dy, sz, world.height() - sz); float invTexWidth = 1f / texture.width; float invTexHeight = 1f / texture.height; float x = dx - sz, y = world.height() - dy - sz, width = sz * 2, height = sz * 2; region.set(x * invTexWidth, y * invTexHeight, (x + width) * invTexWidth, (y + height) * invTexHeight); return region; } public void updateAll(){ for(Tile tile : world.tiles){ pixmap.set(tile.x, pixmap.height - 1 - tile.y, colorFor(tile)); } texture.draw(pixmap); } public void update(Tile tile){ if(world.isGenerating() || !state.isGame()) return; if(tile.build != null && tile.isCenter()){ tile.getLinkedTiles(other -> { if(!other.isCenter()){ updatePixel(other); } if(tile.block().solid && other.y > 0){ Tile low = world.tile(other.x, other.y - 1); if(!low.solid()){ updatePixel(low); } } }); } updatePixel(tile); } void updatePixel(Tile tile){ updates.add(tile.pos()); } public void updateUnitArray(){ float sz = baseSize * zoom; float dx = (Core.camera.position.x / tilesize); float dy = (Core.camera.position.y / tilesize); dx = Mathf.clamp(dx, sz, world.width() - sz); dy = Mathf.clamp(dy, sz, world.height() - sz); units.clear(); Units.nearby((dx - sz) * tilesize, (dy - sz) * tilesize, sz * 2 * tilesize, sz * 2 * tilesize, units::add); } private Block realBlock(Tile tile){ //TODO doesn't work properly until player goes and looks at block return tile.build == null ? tile.block() : state.rules.fog && !tile.build.wasVisible ? Blocks.air : tile.block(); } private int colorFor(Tile tile){ if(tile == null) return 0; Block real = realBlock(tile); int bc = real.minimapColor(tile); Color color = Tmp.c1.set(bc == 0 ? MapIO.colorFor(real, tile.floor(), tile.overlay(), tile.team()) : bc); color.mul(1f - Mathf.clamp(world.getDarkness(tile.x, tile.y) / 4f)); if(real == Blocks.air && tile.y < world.height() - 1 && realBlock(world.tile(tile.x, tile.y + 1)).solid){ color.mul(0.7f); }else if(tile.floor().isLiquid && (tile.y >= world.height() - 1 || !world.tile(tile.x, tile.y + 1).floor().isLiquid)){ color.mul(0.84f, 0.84f, 0.9f, 1f); } return color.rgba(); } public void drawLabel(float x, float y, String text, Color color){ Font font = Fonts.outline; GlyphLayout l = Pools.obtain(GlyphLayout.class, GlyphLayout::new); boolean ints = font.usesIntegerPositions(); font.getData().setScale(1 / 1.5f / Scl.scl(1f)); font.setUseIntegerPositions(false); l.setText(font, text, color, 90f, Align.left, true); float yOffset = 20f; float margin = 3f; Draw.color(0f, 0f, 0f, 0.2f); Fill.rect(x, y + yOffset - l.height/2f, l.width + margin, l.height + margin); Draw.color(); font.setColor(color); font.draw(text, x - l.width/2f, y + yOffset, 90f, Align.left, true); font.setUseIntegerPositions(ints); font.getData().setScale(1f); Pools.free(l); } }