package mindustry.ui.fragments; import arc.*; import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; import arc.scene.*; import arc.scene.actions.*; import arc.scene.event.*; import arc.scene.style.*; import arc.scene.ui.*; import arc.scene.ui.ImageButton.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.core.GameState.*; import mindustry.core.*; import mindustry.ctype.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.input.*; import mindustry.net.Packets.*; import mindustry.type.*; import mindustry.ui.*; import static mindustry.Vars.*; import static mindustry.gen.Tex.*; public class HudFragment{ private static final float dsize = 65f, pauseHeight = 36f; public final PlacementFragment blockfrag = new PlacementFragment(); public boolean shown = true; private ImageButton flip; private CoreItemsDisplay coreItems = new CoreItemsDisplay(); private String hudText = ""; private boolean showHudText; private Table lastUnlockTable; private Table lastUnlockLayout; private long lastToast; public void build(Group parent){ //warn about guardian/boss waves Events.on(WaveEvent.class, e -> { int max = 10; int winWave = state.isCampaign() && state.rules.winWave > 0 ? state.rules.winWave : Integer.MAX_VALUE; outer: for(int i = state.wave - 1; i <= Math.min(state.wave + max, winWave - 2); i++){ for(SpawnGroup group : state.rules.spawns){ if(group.effect == StatusEffects.boss && group.getSpawned(i) > 0){ int diff = (i + 2) - state.wave; //increments at which to warn about incoming guardian if(diff == 1 || diff == 2 || diff == 5 || diff == 10){ showToast(Icon.warning, group.type.emoji() + " " + Core.bundle.format("wave.guardianwarn" + (diff == 1 ? ".one" : ""), diff)); } break outer; } } } }); Events.on(SectorCaptureEvent.class, e -> { showToast(Core.bundle.format("sector.captured", e.sector.isBeingPlayed() ? "" : e.sector.name() + " ")); }); Events.on(SectorLoseEvent.class, e -> { showToast(Icon.warning, Core.bundle.format("sector.lost", e.sector.name())); }); Events.on(SectorInvasionEvent.class, e -> { showToast(Icon.warning, Core.bundle.format("sector.attacked", e.sector.name())); }); Events.on(ResetEvent.class, e -> { coreItems.resetUsed(); coreItems.clear(); }); //paused table parent.fill(t -> { t.name = "paused"; t.top().visible(() -> state.isPaused() && shown && !netServer.isWaitingForPlayers()).touchable = Touchable.disabled; t.table(Styles.black6, top -> top.label(() -> state.gameOver && state.isCampaign() ? "@sector.curlost" : "@paused") .style(Styles.outlineLabel).pad(8f)).height(pauseHeight).growX(); //.padLeft(dsize * 5 + 4f) to prevent alpha overlap on left }); //"waiting for players" parent.fill(t -> { t.name = "waiting"; t.visible(() -> netServer.isWaitingForPlayers() && state.isPaused() && shown).touchable = Touchable.disabled; t.table(Styles.black6, top -> top.add("@waiting.players").style(Styles.outlineLabel).pad(18f)); }); //minimap + position parent.fill(t -> { t.name = "minimap/position"; t.visible(() -> Core.settings.getBool("minimap") && shown); //minimap t.add(new Minimap()).name("minimap"); t.row(); //position t.label(() -> (Core.settings.getBool("position") ? player.tileX() + "," + player.tileY() + "\n" : "") + (Core.settings.getBool("mouseposition") ? "[lightgray]" + World.toTile(Core.input.mouseWorldX()) + "," + World.toTile(Core.input.mouseWorldY()) : "")) .visible(() -> Core.settings.getBool("position") || Core.settings.getBool("mouseposition")) .touchable(Touchable.disabled) .style(Styles.outlineLabel) .name("position"); t.top().right(); }); ui.hints.build(parent); //menu at top left parent.fill(cont -> { cont.name = "overlaymarker"; cont.top().left(); if(mobile){ //for better inset visuals cont.rect((x, y, w, h) -> { if(Core.scene.marginTop > 0){ Tex.paneRight.draw(x, y, w, Core.scene.marginTop); } }).fillX().row(); cont.table(select -> { select.name = "mobile buttons"; select.left(); select.defaults().size(dsize).left(); ImageButtonStyle style = Styles.cleari; select.button(Icon.menu, style, ui.paused::show).name("menu"); flip = select.button(Icon.upOpen, style, this::toggleMenus).get(); flip.name = "flip"; select.button(Icon.paste, style, ui.schematics::show) .name("schematics"); select.button(Icon.pause, style, () -> { if(net.active()){ ui.listfrag.toggle(); }else{ state.set(state.isPaused() ? State.playing : State.paused); } }).name("pause").update(i -> { if(net.active()){ i.getStyle().imageUp = Icon.players; }else{ i.setDisabled(false); i.getStyle().imageUp = state.isPaused() ? Icon.play : Icon.pause; } }); select.button(Icon.chat, style,() -> { if(net.active() && mobile){ if(ui.chatfrag.shown()){ ui.chatfrag.hide(); }else{ ui.chatfrag.toggle(); } }else if(state.isCampaign()){ ui.research.show(); }else{ ui.database.show(); } }).name("chat").update(i -> { if(net.active() && mobile){ i.getStyle().imageUp = Icon.chat; }else if(state.isCampaign()){ i.getStyle().imageUp = Icon.tree; }else{ i.getStyle().imageUp = Icon.book; } }); select.image().color(Pal.gray).width(4f).fillY(); }); cont.row(); cont.image().height(4f).color(Pal.gray).fillX(); cont.row(); } cont.update(() -> { if(Core.input.keyTap(Binding.toggle_menus) && !ui.chatfrag.shown() && !Core.scene.hasDialog() && !Core.scene.hasField()){ Core.settings.getBoolOnce("ui-hidden", () -> { ui.announce(Core.bundle.format("showui", Core.keybinds.get(Binding.toggle_menus).key.toString(), 11)); }); toggleMenus(); } }); Table wavesMain, editorMain; cont.stack(wavesMain = new Table(), editorMain = new Table()).height(wavesMain.getPrefHeight()) .name("waves/editor"); wavesMain.visible(() -> shown && !state.isEditor()); wavesMain.top().left().name = "waves"; wavesMain.table(s -> { //wave info button with text s.add(makeStatusTable()).grow().name("status"); var rightStyle = new ImageButtonStyle(){{ over = buttonRightOver; down = buttonRightDown; up = buttonRight; disabled = buttonRightDisabled; imageDisabledColor = Color.clear; imageUpColor = Color.white; }}; //table with button to skip wave s.button(Icon.play, rightStyle, 30f, () -> { if(net.client() && player.admin){ Call.adminRequest(player, AdminAction.wave); }else{ logic.skipWave(); } }).growY().fillX().right().width(40f).disabled(b -> !canSkipWave()).name("skip").get().toBack(); }).width(dsize * 5 + 4f).name("statustable"); wavesMain.row(); addInfoTable(wavesMain.table().width(dsize * 5f + 4f).left().get()); editorMain.name = "editor"; editorMain.table(Tex.buttonEdge4, t -> { //t.margin(0f); t.name = "teams"; t.add("@editor.teams").growX().left(); t.row(); t.table(teams -> { teams.left(); int i = 0; for(Team team : Team.baseTeams){ ImageButton button = teams.button(Tex.whiteui, Styles.clearNoneTogglei, 40f, () -> Call.setPlayerTeamEditor(player, team)) .size(50f).margin(6f).get(); button.getImageCell().grow(); button.getStyle().imageUpColor = team.color; button.update(() -> button.setChecked(player.team() == team)); if(++i % 3 == 0){ teams.row(); } } }).left(); }).width(dsize * 5 + 4f); editorMain.visible(() -> shown && state.isEditor()); //fps display cont.table(info -> { info.name = "fps/ping"; info.touchable = Touchable.disabled; info.top().left().margin(4).visible(() -> Core.settings.getBool("fps") && shown); IntFormat fps = new IntFormat("fps"); IntFormat ping = new IntFormat("ping"); IntFormat tps = new IntFormat("tps"); IntFormat mem = new IntFormat("memory"); IntFormat memnative = new IntFormat("memory2"); info.label(() -> fps.get(Core.graphics.getFramesPerSecond())).left().style(Styles.outlineLabel).name("fps"); info.row(); if(android){ info.label(() -> memnative.get((int)(Core.app.getJavaHeap() / 1024 / 1024), (int)(Core.app.getNativeHeap() / 1024 / 1024))).left().style(Styles.outlineLabel).name("memory2"); }else{ info.label(() -> mem.get((int)(Core.app.getJavaHeap() / 1024 / 1024))).left().style(Styles.outlineLabel).name("memory"); } info.row(); info.label(() -> ping.get(netClient.getPing())).visible(net::client).left().style(Styles.outlineLabel).name("ping").row(); info.label(() -> tps.get(state.serverTps == -1 ? 60 : state.serverTps)).visible(net::client).left().style(Styles.outlineLabel).name("tps").row(); }).top().left(); }); //core info parent.fill(t -> { t.top(); t.visible(() -> shown); t.name = "coreinfo"; t.collapser(v -> v.add().height(pauseHeight), () -> state.isPaused() && !netServer.isWaitingForPlayers()).row(); t.table(c -> { //core items c.top().collapser(coreItems, () -> Core.settings.getBool("coreitems") && !mobile && shown).fillX().row(); float notifDuration = 240f; float[] coreAttackTime = {0}; Events.run(Trigger.teamCoreDamage, () -> coreAttackTime[0] = notifDuration); //'core is under attack' table c.collapser(top -> top.background(Styles.black6).add("@coreattack").pad(8) .update(label -> label.color.set(Color.orange).lerp(Color.scarlet, Mathf.absin(Time.time, 2f, 1f))), true, () -> { if(!shown || state.isPaused()) return false; if(state.isMenu() || !player.team().data().hasCore()){ coreAttackTime[0] = 0f; return false; } return (coreAttackTime[0] -= Time.delta) > 0; }) .touchable(Touchable.disabled) .fillX().row(); }).row(); var bossb = new StringBuilder(); var bossText = Core.bundle.get("guardian"); int maxBosses = 6; t.table(v -> v.margin(10f) .add(new Bar(() -> { bossb.setLength(0); for(int i = 0; i < Math.min(state.teams.bosses.size, maxBosses); i++){ bossb.append(state.teams.bosses.get(i).type.emoji()); } if(state.teams.bosses.size > maxBosses){ bossb.append("[accent]+[]"); } bossb.append(" "); bossb.append(bossText); return bossb; }, () -> Pal.health, () -> { if(state.boss() == null) return 0f; float max = 0f, val = 0f; for(var boss : state.teams.bosses){ max += boss.maxHealth; val += boss.health; } return max == 0f ? 0f : val / max; }).blink(Color.white).outline(new Color(0, 0, 0, 0.6f), 7f)).grow()) .fillX().width(320f).height(60f).name("boss").visible(() -> state.rules.waves && state.boss() != null && !(mobile && Core.graphics.isPortrait())).padTop(7).row(); t.table(Styles.black3, p -> p.margin(4).label(() -> hudText).style(Styles.outlineLabel)).touchable(Touchable.disabled).with(p -> p.visible(() -> { p.color.a = Mathf.lerpDelta(p.color.a, Mathf.num(showHudText), 0.2f); if(state.isMenu()){ p.color.a = 0f; showHudText = false; } return p.color.a >= 0.001f; })); }); //spawner warning parent.fill(t -> { t.name = "nearpoint"; t.touchable = Touchable.disabled; t.table(Styles.black6, c -> c.add("@nearpoint") .update(l -> l.setColor(Tmp.c1.set(Color.white).lerp(Color.scarlet, Mathf.absin(Time.time, 10f, 1f)))) .labelAlign(Align.center, Align.center)) .margin(6).update(u -> u.color.a = Mathf.lerpDelta(u.color.a, Mathf.num(spawner.playerNear()), 0.1f)).get().color.a = 0f; }); //'saving' indicator parent.fill(t -> { t.name = "saving"; t.bottom().visible(() -> control.saves.isSaving()); t.add("@saving").style(Styles.outlineLabel); }); //TODO DEBUG: rate table if(false) parent.fill(t -> { t.bottom().left(); t.table(Styles.black6, c -> { Bits used = new Bits(content.items().size); Runnable rebuild = () -> { c.clearChildren(); for(Item item : content.items()){ if(state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1){ c.image(item.uiIcon); c.label(() -> (int)state.rules.sector.info.getExport(item) + " /s").color(Color.lightGray); c.row(); } } }; c.update(() -> { boolean wrong = false; for(Item item : content.items()){ boolean has = state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1; if(used.get(item.id) != has){ used.set(item.id, has); wrong = true; } } if(wrong){ rebuild.run(); } }); }).visible(() -> state.isCampaign() && content.items().contains(i -> state.rules.sector != null && state.rules.sector.info.getExport(i) > 0)); }); blockfrag.build(parent); } @Remote(targets = Loc.both, forward = true, called = Loc.both) public static void setPlayerTeamEditor(Player player, Team team){ if(state.isEditor() && player != null){ player.team(team); } } public void setHudText(String text){ showHudText = true; hudText = text; } public void toggleHudText(boolean shown){ showHudText = shown; } private void scheduleToast(Runnable run){ long duration = (int)(3.5 * 1000); long since = Time.timeSinceMillis(lastToast); if(since > duration){ lastToast = Time.millis(); run.run(); }else{ Time.runTask((duration - since) / 1000f * 60f, run); lastToast += duration; } } public boolean hasToast(){ return Time.timeSinceMillis(lastToast) < 3.5f * 1000f; } public void showToast(String text){ showToast(Icon.ok, text); } public void showToast(Drawable icon, String text){ showToast(icon, -1, text); } public void showToast(Drawable icon, float size, String text){ if(state.isMenu()) return; scheduleToast(() -> { Sounds.message.play(); Table table = new Table(Tex.button); table.update(() -> { if(state.isMenu() || !ui.hudfrag.shown){ table.remove(); } }); table.margin(12); var cell = table.image(icon).pad(3); if(size > 0) cell.size(size); table.add(text).wrap().width(280f).get().setAlignment(Align.center, Align.center); table.pack(); //create container table which will align and move Table container = Core.scene.table(); container.top().add(table); container.setTranslation(0, table.getPrefHeight()); container.actions(Actions.translateBy(0, -table.getPrefHeight(), 1f, Interp.fade), Actions.delay(2.5f), //nesting actions() calls is necessary so the right prefHeight() is used Actions.run(() -> container.actions(Actions.translateBy(0, table.getPrefHeight(), 1f, Interp.fade), Actions.remove()))); }); } /** Show unlock notification for a new recipe. */ public void showUnlock(UnlockableContent content){ //some content may not have icons... yet //also don't play in the tutorial to prevent confusion if(state.isMenu()) return; Sounds.message.play(); //if there's currently no unlock notification... if(lastUnlockTable == null){ scheduleToast(() -> { Table table = new Table(Tex.button); table.update(() -> { if(state.isMenu()){ table.remove(); lastUnlockLayout = null; lastUnlockTable = null; } }); table.margin(12); Table in = new Table(); //create texture stack for displaying Image image = new Image(content.uiIcon); image.setScaling(Scaling.fit); in.add(image).size(8 * 6).pad(2); //add to table table.add(in).padRight(8); table.add("@unlocked"); table.pack(); //create container table which will align and move Table container = Core.scene.table(); container.top().add(table); container.setTranslation(0, table.getPrefHeight()); container.actions(Actions.translateBy(0, -table.getPrefHeight(), 1f, Interp.fade), Actions.delay(2.5f), //nesting actions() calls is necessary so the right prefHeight() is used Actions.run(() -> container.actions(Actions.translateBy(0, table.getPrefHeight(), 1f, Interp.fade), Actions.run(() -> { lastUnlockTable = null; lastUnlockLayout = null; }), Actions.remove()))); lastUnlockTable = container; lastUnlockLayout = in; }); }else{ //max column size int col = 3; //max amount of elements minus extra 'plus' int cap = col * col - 1; //get old elements Seq elements = new Seq<>(lastUnlockLayout.getChildren()); int esize = elements.size; //...if it's already reached the cap, ignore everything if(esize > cap) return; //get size of each element float size = 48f / Math.min(elements.size + 1, col); lastUnlockLayout.clearChildren(); lastUnlockLayout.defaults().size(size).pad(2); for(int i = 0; i < esize; i++){ lastUnlockLayout.add(elements.get(i)); if(i % col == col - 1){ lastUnlockLayout.row(); } } //if there's space, add it if(esize < cap){ Image image = new Image(content.uiIcon); image.setScaling(Scaling.fit); lastUnlockLayout.add(image); }else{ //else, add a specific icon to denote no more space lastUnlockLayout.image(Icon.add); } lastUnlockLayout.pack(); } } public void showLaunch(){ float margin = 30f; Image image = new Image(); image.color.a = 0f; image.touchable = Touchable.disabled; image.setFillParent(true); image.actions(Actions.delay((coreLandDuration - margin) / 60f), Actions.fadeIn(margin / 60f, Interp.pow2In), Actions.delay(6f / 60f), Actions.remove()); image.update(() -> { image.toFront(); ui.loadfrag.toFront(); if(state.isMenu()){ image.remove(); } }); Core.scene.add(image); } public void showLand(){ Image image = new Image(); image.color.a = 1f; image.touchable = Touchable.disabled; image.setFillParent(true); image.actions(Actions.fadeOut(35f / 60f), Actions.remove()); image.update(() -> { image.toFront(); ui.loadfrag.toFront(); if(state.isMenu()){ image.remove(); } }); Core.scene.add(image); } private void toggleMenus(){ if(flip != null){ flip.getStyle().imageUp = shown ? Icon.downOpen : Icon.upOpen; } shown = !shown; } private Table makeStatusTable(){ Table table = new Table(Tex.wavepane); StringBuilder ibuild = new StringBuilder(); IntFormat wavef = new IntFormat("wave"), wavefc = new IntFormat("wave.cap"), enemyf = new IntFormat("wave.enemy"), enemiesf = new IntFormat("wave.enemies"), enemycf = new IntFormat("wave.enemycore"), enemycsf = new IntFormat("wave.enemycores"), waitingf = new IntFormat("wave.waiting", i -> { ibuild.setLength(0); int m = i/60; int s = i % 60; if(m > 0){ ibuild.append(m); ibuild.append(":"); if(s < 10){ ibuild.append("0"); } } ibuild.append(s); return ibuild.toString(); }); table.touchable = Touchable.enabled; StringBuilder builder = new StringBuilder(); table.name = "waves"; table.marginTop(0).marginBottom(4).marginLeft(4); class SideBar extends Element{ public final Floatp amount; public final boolean flip; public final Boolp flash; float last, blink, value; public SideBar(Floatp amount, Boolp flash, boolean flip){ this.amount = amount; this.flip = flip; this.flash = flash; setColor(Pal.health); } @Override public void draw(){ float next = amount.get(); if(Float.isNaN(next) || Float.isInfinite(next)) next = 1f; if(next < last && flash.get()){ blink = 1f; } blink = Mathf.lerpDelta(blink, 0f, 0.2f); value = Mathf.lerpDelta(value, next, 0.15f); last = next; if(Float.isNaN(value) || Float.isInfinite(value)) value = 1f; drawInner(Pal.darkishGray, 1f); drawInner(Tmp.c1.set(color).lerp(Color.white, blink), value); } void drawInner(Color color, float fract){ if(fract < 0) return; fract = Mathf.clamp(fract); if(flip){ x += width; width = -width; } float stroke = width * 0.35f; float bh = height/2f; Draw.color(color, parentAlpha); float f1 = Math.min(fract * 2f, 1f), f2 = (fract - 0.5f) * 2f; float bo = -(1f - f1) * (width - stroke); Fill.quad( x, y, x + stroke, y, x + width + bo, y + bh * f1, x + width - stroke + bo, y + bh * f1 ); if(f2 > 0){ float bx = x + (width - stroke) * (1f - f2); Fill.quad( x + width, y + bh, x + width - stroke, y + bh, bx, y + height * fract, bx + stroke, y + height * fract ); } Draw.reset(); if(flip){ width = -width; x -= width; } } } table.stack( new Element(){ @Override public void draw(){ Draw.color(Pal.darkerGray, parentAlpha); Fill.poly(x + width/2f, y + height/2f, 6, height / Mathf.sqrt3); Draw.reset(); Drawf.shadow(x + width/2f, y + height/2f, height * 1.13f, parentAlpha); } }, new Table(t -> { float bw = 40f; float pad = -20; t.margin(0); t.clicked(() -> { if(!player.dead() && mobile){ Call.unitClear(player); control.input.recentRespawnTimer = 1f; control.input.controlledType = null; } }); t.add(new SideBar(() -> player.unit().healthf(), () -> true, true)).width(bw).growY().padRight(pad); t.image(() -> player.icon()).scaling(Scaling.bounded).grow().maxWidth(54f); t.add(new SideBar(() -> player.dead() ? 0f : player.displayAmmo() ? player.unit().ammof() : player.unit().healthf(), () -> !player.displayAmmo(), false)).width(bw).growY().padLeft(pad).update(b -> { b.color.set(player.displayAmmo() ? player.dead() || player.unit() instanceof BlockUnitc ? Pal.ammo : player.unit().type.ammoType.color() : Pal.health); }); t.getChildren().get(1).toFront(); })).size(120f, 80).padRight(4); Cell[] lcell = {null}; boolean[] couldSkip = {true}; lcell[0] = table.labelWrap(() -> { //update padding depend on whether the button to the right is there boolean can = canSkipWave(); if(can != couldSkip[0]){ if(canSkipWave()){ lcell[0].padRight(8f); }else{ lcell[0].padRight(-42f); } table.invalidateHierarchy(); table.pack(); couldSkip[0] = can; } builder.setLength(0); //mission overrides everything if(state.rules.mission != null){ builder.append(state.rules.mission); return builder; } //objectives override mission? if(state.rules.objectives.any()){ boolean first = true; for(var obj : state.rules.objectives){ if(!obj.qualified()) continue; String text = obj.text(); if(text != null){ if(!first) builder.append("\n[white]"); builder.append(text); first = false; } } return builder; } if(!state.rules.waves && state.rules.attackMode){ int sum = Math.max(state.teams.present.sum(t -> t.team != player.team() ? t.cores.size : 0), 1); builder.append(sum > 1 ? enemycsf.get(sum) : enemycf.get(sum)); return builder; } if(!state.rules.waves && state.isCampaign()){ builder.append("[lightgray]").append(Core.bundle.get("sector.curcapture")); } if(!state.rules.waves){ return builder; } if(state.rules.winWave > 1 && state.rules.winWave >= state.wave && state.isCampaign()){ builder.append(wavefc.get(state.wave, state.rules.winWave)); }else{ builder.append(wavef.get(state.wave)); } builder.append("\n"); if(state.enemies > 0){ if(state.enemies == 1){ builder.append(enemyf.get(state.enemies)); }else{ builder.append(enemiesf.get(state.enemies)); } builder.append("\n"); } if(state.rules.waveTimer){ builder.append((logic.isWaitingWave() ? Core.bundle.get("wave.waveInProgress") : (waitingf.get((int)(state.wavetime/60))))); }else if(state.enemies == 0){ builder.append(Core.bundle.get("waiting")); } return builder; }).growX().pad(8f); table.row(); //TODO nobody reads details anyway. /* table.clicked(() -> { if(state.rules.objectives.any()){ StringBuilder text = new StringBuilder(); boolean first = true; for(var obj : state.rules.objectives){ if(!obj.qualified()) continue; String details = obj.details(); if(details != null){ if(!first) text.append('\n'); text.append(details); first = false; } } //TODO this, as said before, could be much better. ui.showInfo(text.toString()); } });*/ return table; } private void addInfoTable(Table table){ table.name = "infotable"; table.left(); var count = new float[]{-1}; table.table().update(t -> { if(player.unit() instanceof Payloadc payload){ if(count[0] != payload.payloadUsed()){ payload.contentInfo(t, 8 * 2, 275f); count[0] = payload.payloadUsed(); } }else{ count[0] = -1; t.clear(); } }).growX().visible(() -> player.unit() instanceof Payloadc p && p.payloadUsed() > 0).colspan(2); table.row(); Bits statuses = new Bits(); table.table().update(t -> { t.left(); Bits applied = player.unit().statusBits(); if(!statuses.equals(applied)){ t.clear(); if(applied != null){ for(StatusEffect effect : content.statusEffects()){ if(applied.get(effect.id) && !effect.isHidden()){ t.image(effect.uiIcon).size(iconMed).get() .addListener(new Tooltip(l -> l.label(() -> effect.localizedName + " [lightgray]" + UI.formatTime(player.unit().getDuration(effect))).style(Styles.outlineLabel))); } } statuses.set(applied); } } }).left(); } private boolean canSkipWave(){ return state.rules.waves && state.rules.waveSending && ((net.server() || player.admin) || !net.active()) && state.enemies == 0 && !spawner.isSpawning(); } }