From 6fba84959c3d34deeb55df74e425f1285b3a6285 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 21 Nov 2020 14:05:58 -0500 Subject: [PATCH] New hint tutorial system (unfinished!) --- .../annotations/impl/AssetsProcess.java | 4 +- core/assets/bundles/bundle.properties | 25 ++ core/src/mindustry/Vars.java | 2 +- core/src/mindustry/ai/types/BuilderAI.java | 4 - core/src/mindustry/content/Blocks.java | 1 + core/src/mindustry/content/UnitTypes.java | 1 + core/src/mindustry/core/Control.java | 10 - core/src/mindustry/core/Logic.java | 2 +- core/src/mindustry/core/NetServer.java | 6 +- core/src/mindustry/core/UI.java | 4 +- .../mindustry/entities/comp/BuildingComp.java | 4 +- core/src/mindustry/game/Tutorial.java | 308 ------------------ core/src/mindustry/game/Universe.java | 2 +- core/src/mindustry/input/DesktopInput.java | 3 + core/src/mindustry/mod/ContentParser.java | 21 +- core/src/mindustry/ui/ItemImage.java | 5 +- core/src/mindustry/ui/Styles.java | 9 +- .../ui/dialogs/LaunchLoadoutDialog.java | 2 +- .../mindustry/ui/dialogs/PlanetDialog.java | 5 + .../mindustry/ui/dialogs/ResearchDialog.java | 2 +- .../mindustry/ui/fragments/HintsFragment.java | 259 +++++++++++++++ .../mindustry/ui/fragments/HudFragment.java | 26 +- gradle.properties | 2 +- 23 files changed, 343 insertions(+), 364 deletions(-) delete mode 100644 core/src/mindustry/game/Tutorial.java create mode 100644 core/src/mindustry/ui/fragments/HintsFragment.java diff --git a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java index a4b175bd24..c38570a4d4 100644 --- a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java +++ b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java @@ -46,7 +46,7 @@ public class AssetsProcess extends BaseProcessor{ String name = Strings.kebabToCamel(split[1]).replace("Medium", "").replace("Icon", ""); if(SourceVersion.isKeyword(name) || name.equals("char")) name += "i"; - ichtype.addField(FieldSpec.builder(char.class, name, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).initializer("(char)" + key).build()); + ichtype.addField(FieldSpec.builder(char.class, name, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).addJavadoc(String.format("\\u%04x", Integer.parseInt(key))).initializer("'" + ((char)Integer.parseInt(key)) + "'").build()); }); ictype.addField(FieldSpec.builder(ParameterizedTypeName.get(ObjectMap.class, String.class, TextureRegionDrawable.class), @@ -64,7 +64,7 @@ public class AssetsProcess extends BaseProcessor{ int code = val.getInt("code", 0); iconcAll.append((char)code); - ichtype.addField(FieldSpec.builder(char.class, name, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).initializer("(char)" + code).build()); + ichtype.addField(FieldSpec.builder(char.class, name, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).addJavadoc(String.format("\\u%04x", code)).initializer("'" + ((char)code) + "'").build()); ichinit.addStatement("codes.put($S, $L)", name, code); ictype.addField(TextureRegionDrawable.class, name + "Small", Modifier.PUBLIC, Modifier.STATIC); diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 0a17beb754..db3c17c34c 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -497,6 +497,7 @@ requirement.produce = Produce {0} requirement.capture = Capture {0} launch.text = Launch research.multiplayer = Only the host can research items. +map.multiplayer = Only the host can view sectors. uncover = Uncover configure = Configure Loadout @@ -1235,6 +1236,30 @@ team.derelict.name = derelict team.green.name = green team.purple.name = purple +# DO NOT TRANSLATE ANY OF THESE YET! THEY ARE UNFINISHED! +hint.skip = Skip +hint.desktopMove = Use [accent][[WASD][] to move. +hint.zoom = [accent]Scroll[] to zoom in or out. +hint.mine = Move near the \uf8c4 copper ore and [accent]tap[] it to start mining. +hint.desktopShoot = [accent][[Left-click][] to shoot. +hint.depositItems = To transfer items, drag from your ship to the core. +hint.respawn = To respawn as a ship, press [accent][[V][]. +hint.respawn.mobile = You have switched control a unit/structure. To respawn as a ship, tap the avatar in the top left. +hint.desktopPause = Press [accent][[Space][] to pause and unpause the game. +hint.placeDrill = Select the \ue85e [accent]Drill[] tab in the menu at the bottom right, then select a \uf870 [accent]Drill[] and click on a copper patch to place it. +hint.placeDrill.mobile = Select the \ue85e[accent]Drill[] tab in the menu at the bottom right, then select a \uf870 [accent]Drill[] and tap on a copper patch to place it.\n\nPress the [accent]checkmark[] at the bottom left to confirm. +hint.placeConveyor = Conveyors move items from drills into other blocks. Select a \uf896 [accent]Conveyor[] from the \ue814 [accent]Distribution[] tab.\n\nClick and drag to place multiple conveyors.\n[accent]Scroll[] to rotate. +hint.placeConveyor.mobile = Conveyors move items from drills into other blocks. Select a \uf896 [accent]Conveyor[] from the \ue814 [accent]Distribution[] tab.\n\nHold down your finger for a second and drag to place multiple conveyors. +hint.placeTurret = Place \uf861 [accent]Turrets[] to defend your base from enemies.\n\nTurrets require ammo - in this case, \uf838copper.\nUse conveyors and drills to supply them. +hint.breaking = [accent]Right-click[] and drag to break blocks. +hint.breaking.mobile = Activate the \ue817 hammer in the bottom left and tap to break blocks.\nHold down your finger for a second and drag to break in a selection. +hint.research = Use the \ue875 [accent]Research[] button to research new technology. +hint.research.mobile = Use the \ue875 [accent]Research[] button in the \ue88c [accent]Menu[] to research new technology. +hint.unitControl = Hold [accent][[L-ctrl][] and [accent]click[] to control friendly units or turrets. +hint.unitControl.mobile = [accent][Double-tap[] to control friendly units or turrets. +hint.launch = Once you collect enough resources, you can [accent]Launch[] by selecting nearby sectors from the \ue827 [accent]Map[] in the bottom left. +hint.launch.mobile = Once you collect enough resources, you can [accent]Launch[] by selecting nearby sectors from the \ue827 [accent]Map[] in the \ue88c [accent]Menu[]. + tutorial.next = [lightgray] tutorial.intro = You have entered the[scarlet] Mindustry Tutorial.[]\nUse[accent] [[WASD][] to move.\n[accent]Scroll[] to zoom in and out.\nBegin by[accent] mining copper[]. Move close to it, then tap a copper ore vein near your core to do this.\n\n[accent]{0}/{1} copper tutorial.intro.mobile = You have entered the[scarlet] Mindustry Tutorial.[]\nSwipe the screen to move.\n[accent]Pinch with 2 fingers[] to zoom in and out.\nBegin by[accent] mining copper[]. Move close to it, then tap a copper ore vein near your core to do this.\n\n[accent]{0}/{1} copper diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index c62c621d45..e485c98788 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -88,7 +88,7 @@ public class Vars implements Loadable{ /** duration of time between turns in ticks */ public static final float turnDuration = 2 * Time.toMinutes; /** chance of an invasion per turn, 1 = 100% */ - public static final float baseInvasionChance = 1f / 30f; + public static final float baseInvasionChance = 1f / 45f; /** how many turns have to pass before invasions start */ public static final int invasionGracePeriod = 20; /** min armor fraction damage; e.g. 0.05 = at least 5% damage */ diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index acb7464b9d..621e269190 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -19,10 +19,6 @@ public class BuilderAI extends AIController{ @Override public void updateMovement(){ - if(unit.moving()){ - unit.lookAt(unit.vel.angle()); - } - if(target != null && shouldShoot()){ unit.lookAt(target); } diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index c267e3daa6..793cd765f0 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -2059,6 +2059,7 @@ public class Blocks implements ContentList{ size = 7; hasPower = true; consumes.power(10f); + buildCostMultiplier = 0.5f; }}; //endregion campaign diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index 7ecee44bc4..4de5cc2d1a 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -1250,6 +1250,7 @@ public class UnitTypes implements ContentList{ defaultController = RepairAI::new; mineTier = 3; + mineSpeed = 4f; health = 500; armor = 5f; speed = 2.5f; diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index d3ab614452..4d0fc99849 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -44,7 +44,6 @@ import static mindustry.Vars.*; public class Control implements ApplicationListener, Loadable{ public Saves saves; public SoundControl sound; - public Tutorial tutorial; public InputHandler input; private Interval timer = new Interval(2); @@ -53,7 +52,6 @@ public class Control implements ApplicationListener, Loadable{ public Control(){ saves = new Saves(); - tutorial = new Tutorial(); sound = new SoundControl(); Events.on(StateChangeEvent.class, event -> { @@ -87,7 +85,6 @@ public class Control implements ApplicationListener, Loadable{ Events.on(ResetEvent.class, event -> { player.reset(); - tutorial.reset(); hiscore = false; saves.resetSave(); @@ -408,13 +405,6 @@ public class Control implements ApplicationListener, Loadable{ public void init(){ platform.updateRPC(); - //just a regular reminder - if(!OS.prop("user.name").equals("anuke") && !OS.hasEnv("iknowwhatimdoing")){ - app.post(() -> app.post(() -> { - ui.showStartupInfo("@indev.popup"); - })); - } - //display UI scale changed dialog if(Core.settings.getBool("uiscalechanged", false)){ Core.app.post(() -> Core.app.post(() -> { diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 2a335c02e5..5f6bd2c1a9 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -264,7 +264,7 @@ public class Logic implements ApplicationListener{ Events.fire(new SectorCaptureEvent(state.rules.sector)); //save, just in case - if(!headless){ + if(!headless && !net.client()){ control.saves.saveSector(state.rules.sector); } } diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index 959874e82d..5c4c12dea1 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -41,7 +41,7 @@ public class NetServer implements ApplicationListener{ private static final Vec2 vector = new Vec2(); private static final Rect viewport = new Rect(); /** If a player goes away of their server-side coordinates by this distance, they get teleported back. */ - private static final float correctDist = 16f; + private static final float correctDist = tilesize * 8f; public final Administration admins = new Administration(); public final CommandHandler clientCommands = new CommandHandler("/"); @@ -638,12 +638,12 @@ public class NetServer implements ApplicationListener{ Unit unit = player.unit(); long elapsed = Time.timeSinceMillis(con.lastReceivedClientTime); - float maxSpeed = ((player.unit().type.canBoost && player.unit().isFlying()) ? player.unit().type.boostMultiplier : 1f) * player.unit().speed(); + float maxSpeed = unit.realSpeed(); if(unit.isGrounded()){ maxSpeed *= unit.floorSpeedMultiplier(); } - float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.1f; + float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.2f; //ignore the position if the player thinks they're dead, or the unit is wrong boolean ignorePosition = dead || unit.id != unitID; diff --git a/core/src/mindustry/core/UI.java b/core/src/mindustry/core/UI.java index 566d26feb0..4437bb4d48 100644 --- a/core/src/mindustry/core/UI.java +++ b/core/src/mindustry/core/UI.java @@ -42,6 +42,7 @@ public class UI implements ApplicationListener, Loadable{ public MinimapFragment minimapfrag; public PlayerListFragment listfrag; public LoadingFragment loadfrag; + public HintsFragment hints; public WidgetGroup menuGroup, hudGroup; @@ -150,6 +151,7 @@ public class UI implements ApplicationListener, Loadable{ menufrag = new MenuFragment(); hudfrag = new HudFragment(); + hints = new HintsFragment(); chatfrag = new ChatFragment(); minimapfrag = new MinimapFragment(); listfrag = new PlayerListFragment(); @@ -520,7 +522,7 @@ public class UI implements ApplicationListener, Loadable{ //TODO move? - public static String formatAmount(long number){ + public static String formatAmount(int number){ if(number >= 1_000_000_000){ return Strings.fixed(number / 1_000_000_000f, 1) + "[gray]" + Core.bundle.get("unit.billions") + "[]"; }else if(number >= 1_000_000){ diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 2f63bd1063..fca91f0417 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -519,7 +519,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, public void dumpLiquid(Liquid liquid){ int dump = this.cdump; - if(!net.client() && state.isCampaign()) liquid.unlock(); + if(!net.client() && state.isCampaign() && team == state.rules.defaultTeam) liquid.unlock(); for(int i = 0; i < proximity.size; i++){ incrementDump(proximity.size); @@ -620,7 +620,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, */ public void offload(Item item){ int dump = this.cdump; - if(!net.client() && state.isCampaign()) item.unlock(); + if(!net.client() && state.isCampaign() && team == state.rules.defaultTeam) item.unlock(); for(int i = 0; i < proximity.size; i++){ incrementDump(proximity.size); diff --git a/core/src/mindustry/game/Tutorial.java b/core/src/mindustry/game/Tutorial.java deleted file mode 100644 index d3e5bae56c..0000000000 --- a/core/src/mindustry/game/Tutorial.java +++ /dev/null @@ -1,308 +0,0 @@ -package mindustry.game; - -import arc.*; -import arc.func.*; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.scene.*; -import arc.scene.ui.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import arc.util.*; -import mindustry.content.*; -import mindustry.game.EventType.*; -import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.type.*; -import mindustry.world.*; - -import static mindustry.Vars.*; - -/** Handles tutorial state. */ -public class Tutorial{ - private static final int mineCopper = 18; - private static final int blocksToBreak = 3, blockOffset = -6; - - ObjectSet events = new ObjectSet<>(); - ObjectIntMap blocksPlaced = new ObjectIntMap<>(); - int sentence; - public TutorialStage stage = TutorialStage.values()[0]; - - public Tutorial(){ - Events.on(BlockBuildEndEvent.class, event -> { - if(!event.breaking){ - blocksPlaced.increment(event.tile.block(), 1); - } - }); - - Events.on(LineConfirmEvent.class, event -> events.add("lineconfirm")); - Events.on(TurretAmmoDeliverEvent.class, event -> events.add("ammo")); - Events.on(CoreItemDeliverEvent.class, event -> events.add("coreitem")); - Events.on(BlockInfoEvent.class, event -> events.add("blockinfo")); - Events.on(DepositEvent.class, event -> events.add("deposit")); - Events.on(WithdrawEvent.class, event -> events.add("withdraw")); - - Events.on(ClientLoadEvent.class, e -> { - for(TutorialStage stage : TutorialStage.values()){ - stage.load(); - } - }); - } - - /** update tutorial state, transition if needed */ - public void update(){ - if(stage.done.get() && !canNext()){ - next(); - }else{ - stage.update(); - } - } - - /** draw UI overlay */ - public void draw(){ - if(!Core.scene.hasDialog()){ - stage.draw(); - } - } - - /** Resets tutorial state. */ - public void reset(){ - stage = TutorialStage.values()[0]; - stage.begin(); - blocksPlaced.clear(); - events.clear(); - sentence = 0; - } - - /** Goes on to the next tutorial step. */ - public void next(){ - stage = TutorialStage.values()[Mathf.clamp(stage.ordinal() + 1, 0, TutorialStage.values().length)]; - stage.begin(); - blocksPlaced.clear(); - events.clear(); - sentence = 0; - } - - public boolean canNext(){ - return sentence + 1 < stage.sentences.size; - } - - public void nextSentence(){ - if(canNext()){ - sentence ++; - } - } - - public boolean canPrev(){ - return sentence > 0; - } - - public void prevSentence(){ - if(canPrev()){ - sentence --; - } - } - - public enum TutorialStage{ - intro( - line -> Core.bundle.format(line, item(Items.copper), mineCopper), - () -> item(Items.copper) >= mineCopper - ), - drill(() -> placed(Blocks.mechanicalDrill, 1)){ - void draw(){ - outline("category-production"); - outline("block-mechanical-drill"); - outline("confirmplace"); - } - }, - blockinfo(() -> event("blockinfo")){ - void draw(){ - outline("category-production"); - outline("block-mechanical-drill"); - outline("blockinfo"); - } - }, - conveyor(() -> placed(Blocks.conveyor, 2) && event("lineconfirm") && event("coreitem")){ - void draw(){ - outline("category-distribution"); - outline("block-conveyor"); - } - }, - turret(() -> placed(Blocks.duo, 1)){ - void draw(){ - outline("category-turret"); - outline("block-duo"); - } - }, - drillturret(() -> event("ammo")), - pause(() -> state.isPaused()){ - void draw(){ - if(mobile){ - outline("pause"); - } - } - }, - unpause(() -> !state.isPaused()){ - void draw(){ - if(mobile){ - outline("pause"); - } - } - }, - breaking(TutorialStage::blocksBroken){ - void begin(){ - placeBlocks(); - } - - void draw(){ - if(mobile){ - outline("breakmode"); - } - } - }, - withdraw(() -> event("withdraw")){ - void begin(){ - state.teams.playerCores().first().items.add(Items.copper, 10); - } - }, - deposit(() -> event("deposit")), - waves(() -> state.wave > 2 && state.enemies <= 0 && !spawner.isSpawning()){ - void begin(){ - state.rules.waveTimer = true; - logic.runWave(); - } - - void update(){ - if(state.wave > 2){ - state.rules.waveTimer = false; - } - } - }, - launch(() -> false){ - void begin(){ - state.rules.waveTimer = false; - state.wave = 5; - - //end tutorial, never show it again - Events.fire(Trigger.tutorialComplete); - Core.settings.put("playedtutorial", true); - } - - void draw(){ - outline("waves"); - } - },; - - protected String line = ""; - protected final Func text; - protected Seq sentences; - protected final Boolp done; - - TutorialStage(Func text, Boolp done){ - this.text = text; - this.done = done; - } - - TutorialStage(Boolp done){ - this(line -> line, done); - } - - /** displayed tutorial stage text.*/ - public String text(){ - if(sentences == null){ - load(); - } - String line = sentences.get(control.tutorial.sentence); - return line.contains("{") ? text.get(line) : line; - } - - void load(){ - this.line = Core.bundle.has("tutorial." + name() + ".mobile") && mobile ? "tutorial." + name() + ".mobile" : "tutorial." + name(); - this.sentences = Seq.select(Core.bundle.get(line).split("\n"), s -> !s.isEmpty()); - } - - /** called every frame when this stage is active.*/ - void update(){ - - } - - /** called when a stage begins.*/ - void begin(){ - - } - - /** called when a stage needs to draw itself, usually over highlighted UI elements. */ - void draw(){ - - } - - //utility - - static void placeBlocks(){ - Building core = state.teams.playerCores().first(); - for(int i = 0; i < blocksToBreak; i++){ - world.tile(core.tile().x + blockOffset, core.tile().y + i).remove(); - world.tile(core.tile().x + blockOffset, core.tile().y + i).setBlock(Blocks.scrapWall, state.rules.defaultTeam); - } - } - - static boolean blocksBroken(){ - Building core = state.teams.playerCores().first(); - - for(int i = 0; i < blocksToBreak; i++){ - if(world.tile(core.tile.x + blockOffset, core.tile.y + i).block() == Blocks.scrapWall){ - return false; - } - } - return true; - } - - static boolean event(String name){ - return control.tutorial.events.contains(name); - } - - static boolean placed(Block block, int amount){ - return placed(block) >= amount; - } - - static int placed(Block block){ - return control.tutorial.blocksPlaced.get(block, 0); - } - - static int item(Item item){ - return state.rules.defaultTeam.data().noCores() ? 0 : state.rules.defaultTeam.core().items.get(item); - } - - static boolean toggled(String name){ - Element element = Core.scene.findVisible(name); - if(element instanceof Button){ - return ((Button)element).isChecked(); - } - return false; - } - - static void outline(String name){ - Element element = Core.scene.findVisible(name); - if(element != null && !toggled(name)){ - element.localToStageCoordinates(Tmp.v1.setZero()); - float sin = Mathf.sin(11f, Scl.scl(4f)); - Lines.stroke(Scl.scl(7f), Pal.place); - Lines.rect(Tmp.v1.x - sin, Tmp.v1.y - sin, element.getWidth() + sin*2, element.getHeight() + sin*2); - - float size = Math.max(element.getWidth(), element.getHeight()) + Mathf.absin(11f/2f, Scl.scl(18f)); - float angle = Angles.angle(Core.graphics.getWidth()/2f, Core.graphics.getHeight()/2f, Tmp.v1.x + element.getWidth()/2f, Tmp.v1.y + element.getHeight()/2f); - Tmp.v2.trns(angle + 180f, size*1.4f); - float fs = Scl.scl(40f); - float fs2 = Scl.scl(56f); - - Draw.color(Pal.gray); - Drawf.tri(Tmp.v1.x + element.getWidth()/2f + Tmp.v2.x, Tmp.v1.y + element.getHeight()/2f + Tmp.v2.y, fs2, fs2, angle); - Draw.color(Pal.place); - Tmp.v2.setLength(Tmp.v2.len() - Scl.scl(4)); - Drawf.tri(Tmp.v1.x + element.getWidth()/2f + Tmp.v2.x, Tmp.v1.y + element.getHeight()/2f + Tmp.v2.y, fs, fs, angle); - Draw.reset(); - } - } - } - -} diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index c7e8536cf4..5d0e106e44 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -36,7 +36,7 @@ public class Universe{ //update base coverage on capture Events.on(SectorCaptureEvent.class, e -> { - if(state.isCampaign()){ + if(!net.client() && state.isCampaign()){ state.getSector().planet.updateBaseCoverage(); } }); diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index b35778c881..d7a25a2b92 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -47,6 +47,7 @@ public class DesktopInput extends InputHandler{ @Override public void buildUI(Group group){ + //respawn hints group.fill(t -> { t.visible(() -> Core.settings.getBool("hints") && ui.hudfrag.shown && !player.dead() && !player.unit().spawnedByCore() && !(Core.settings.getBool("hints") && lastSchematic != null && !selectRequests.isEmpty())); t.bottom(); @@ -56,6 +57,7 @@ public class DesktopInput extends InputHandler{ }).margin(6f); }); + //building hints group.fill(t -> { t.bottom(); t.visible(() -> { @@ -74,6 +76,7 @@ public class DesktopInput extends InputHandler{ }).margin(10f); }); + //schematic controls group.fill(t -> { t.visible(() -> ui.hudfrag.shown && lastSchematic != null && !selectRequests.isEmpty()); t.bottom(); diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index 153981ed98..5f2bbdb637 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -37,6 +37,7 @@ import mindustry.world.draw.*; import mindustry.world.meta.*; import java.lang.reflect.*; +import java.util.*; @SuppressWarnings("unchecked") public class ContentParser{ @@ -267,7 +268,13 @@ public class ContentParser{ UnitType unit; if(locate(ContentType.unit, name) == null){ unit = new UnitType(mod + "-" + name); - unit.constructor = Reflect.cons(resolve(Strings.capitalize(getType(value)), "mindustry.gen")); + var typeVal = value.get("type"); + + if(typeVal != null && !typeVal.isString()){ + throw new RuntimeException("Unit '" + name + "' has an incorrect type. Types must be strings."); + } + + unit.constructor = unitType(typeVal); }else{ unit = locate(ContentType.unit, name); } @@ -337,6 +344,18 @@ public class ContentParser{ //ContentType.sector, parser(ContentType.sector, SectorPreset::new) ); + private Prov unitType(JsonValue value){ + if(value == null) return UnitEntity::create; + return switch(value.asString()){ + case "flying" -> UnitEntity::create; + case "mech" -> MechUnit::create; + case "legs" -> LegsUnit::create; + case "naval" -> UnitWaterMove::create; + case "payload" -> PayloadUnit::create; + default -> throw new RuntimeException("Invalid unit type: '" + value + "'. Must be 'flying/mech/legs/naval/payload'."); + }; + } + private String getString(JsonValue value, String key){ if(value.has(key)){ return value.getString(key); diff --git a/core/src/mindustry/ui/ItemImage.java b/core/src/mindustry/ui/ItemImage.java index fb26769c98..437360829e 100644 --- a/core/src/mindustry/ui/ItemImage.java +++ b/core/src/mindustry/ui/ItemImage.java @@ -3,6 +3,7 @@ package mindustry.ui; import arc.graphics.g2d.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; +import mindustry.core.*; import mindustry.type.*; public class ItemImage extends Stack{ @@ -16,7 +17,7 @@ public class ItemImage extends Stack{ add(new Table(t -> { t.left().bottom(); - t.add(amount + ""); + t.add(amount > 1000 ? UI.formatAmount(amount) : amount + ""); t.pack(); })); } @@ -38,7 +39,7 @@ public class ItemImage extends Stack{ if(stack.amount != 0){ add(new Table(t -> { t.left().bottom(); - t.add(stack.amount + "").style(Styles.outlineLabel); + t.add(stack.amount > 1000 ? UI.formatAmount(stack.amount) : stack.amount + "").style(Styles.outlineLabel); t.pack(); })); } diff --git a/core/src/mindustry/ui/Styles.java b/core/src/mindustry/ui/Styles.java index 385d2aa81a..6b47d6d0b3 100644 --- a/core/src/mindustry/ui/Styles.java +++ b/core/src/mindustry/ui/Styles.java @@ -26,7 +26,7 @@ public class Styles{ //TODO all these names are inconsistent and not descriptive public static Drawable black, black9, black8, black6, black3, black5, none, flatDown, flatOver; public static ButtonStyle defaultb, waveb; - public static TextButtonStyle defaultt, squaret, nodet, cleart, discordt, infot, clearPartialt, clearTogglet, clearToggleMenut, togglet, transt, fullTogglet, logict; + public static TextButtonStyle defaultt, squaret, nodet, cleart, discordt, nonet, infot, clearPartialt, clearTogglet, clearToggleMenut, togglet, transt, fullTogglet, logict; public static ImageButtonStyle defaulti, nodei, righti, emptyi, emptytogglei, selecti, logici, geni, colori, accenti, cleari, clearFulli, clearPartiali, clearPartial2i, clearTogglei, clearTransi, clearToggleTransi, clearTogglePartiali; public static ScrollPaneStyle defaultPane, horizontalPane, smallPane; public static KeybindDialogStyle defaultKeybindDialog; @@ -86,6 +86,13 @@ public class Styles{ up = buttonOver; over = buttonDown; }}; + nonet = new TextButtonStyle(){{ + font = Fonts.outline; + fontColor = Color.lightGray; + overFontColor = Pal.accent; + disabledFontColor = Color.gray; + up = none; + }}; cleart = new TextButtonStyle(){{ over = flatOver; font = Fonts.def; diff --git a/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java b/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java index 8bad533d03..c8ea27ae97 100644 --- a/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java +++ b/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java @@ -116,7 +116,7 @@ public class LaunchLoadoutDialog extends BaseDialog{ selected = s; update.run(); rebuildItems.run(); - }).group(group).pad(4).disabled(!sitems.has(s.requirements())).checked(s == selected).size(200f); + }).group(group).pad(4).checked(s == selected).size(200f); if(++i % cols == 0){ t.row(); diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index 76745a97b0..14901c83e8 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -134,6 +134,11 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ /** show with no limitations, just as a map. */ @Override public Dialog show(){ + if(net.client()){ + ui.showInfo("@map.multiplayer"); + return this; + } + mode = look; selected = hovered = launchSector = null; launching = false; diff --git a/core/src/mindustry/ui/dialogs/ResearchDialog.java b/core/src/mindustry/ui/dialogs/ResearchDialog.java index ccefa203d0..04e9e987a0 100644 --- a/core/src/mindustry/ui/dialogs/ResearchDialog.java +++ b/core/src/mindustry/ui/dialogs/ResearchDialog.java @@ -169,7 +169,7 @@ public class ResearchDialog extends BaseDialog{ public Dialog show(){ if(net.client()){ ui.showInfo("@research.multiplayer"); - return null; + return this; } return super.show(); diff --git a/core/src/mindustry/ui/fragments/HintsFragment.java b/core/src/mindustry/ui/fragments/HintsFragment.java new file mode 100644 index 0000000000..d580fa754b --- /dev/null +++ b/core/src/mindustry/ui/fragments/HintsFragment.java @@ -0,0 +1,259 @@ +package mindustry.ui.fragments; + +import arc.*; +import arc.func.*; +import arc.input.*; +import arc.math.*; +import arc.scene.*; +import arc.scene.actions.*; +import arc.scene.event.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.content.*; +import mindustry.game.EventType.*; +import mindustry.input.*; +import mindustry.ui.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +public class HintsFragment extends Fragment{ + private static final Boolp isTutorial = () -> Vars.state.rules.sector == SectorPresets.groundZero.sector; + private static final float foutTime = 0.6f; + + /** All hints to be displayed in the game. */ + public Seq incomplete = Seq.with(DefaultHint.values()); + @Nullable + public Hint current; + + Group group = new WidgetGroup(); + ObjectSet events = new ObjectSet<>(); + ObjectSet placedBlocks = new ObjectSet<>(); + int checkIdx = 0; + Table last; + + @Override + public void build(Group parent){ + group.setFillParent(true); + group.touchable = Touchable.childrenOnly; + group.visibility = () -> Core.settings.getBool("hints", true); + group.update(() -> { + if(current != null){ + //current got completed + if(current.complete()){ + complete(); + }else if(!current.show()){ //current became hidden + hide(); + } + }else if(incomplete.size > 0){ + //check one hint each frame to see if it should be shown. + checkIdx = (checkIdx + 1) % incomplete.size; + Hint hint = incomplete.get(checkIdx); + if(hint.show() && !hint.finished() & !hint.complete()){ + display(hint); + } + } + }); + parent.addChild(group); + + checkNext(); + + Events.on(BlockBuildEndEvent.class, event -> { + if(!event.breaking && event.tile.team() == Vars.state.rules.defaultTeam){ + placedBlocks.add(event.tile.block()); + } + + if(event.breaking){ + events.add("break"); + } + }); + + Events.on(UnitControlEvent.class, e -> { + if(e.player == player){ + events.add("unitcontrol"); + } + }); + + Events.on(WorldLoadEvent.class, e -> Core.app.post(() -> { + checkNext(); + })); + } + + void checkNext(){ + if(current != null) return; + + incomplete.removeAll(h -> h == current || !h.valid() || h.finished() || (h.show() && h.complete())); + incomplete.sort(Hint::order); + + Hint first = incomplete.find(Hint::show); + if(first != null){ + incomplete.remove(first); + display(first); + } + } + + void display(Hint hint){ + if(current != null) return; + + group.fill(t -> { + last = t; + t.left(); + t.table(Styles.black5, cont -> { + cont.actions(Actions.alpha(0f), Actions.alpha(1f, 1f, Interp.smooth)); + cont.margin(6f).add(hint.text()).width(Vars.mobile ? 300f : 400f).left().labelAlign(Align.left).wrap(); + }); + t.row(); + t.button("@hint.skip", Styles.nonet, () -> { + if(current != null){ + complete(); + } + }).size(100f, 40f).left(); + }); + + this.current = hint; + } + + /** Completes and hides the current hint. */ + void complete(){ + if(current == null) return; + + current.finish(); + incomplete.remove(current); + + hide(); + } + + /** Hides the current hint, but does not complete it. */ + void hide(){ + //hide previous child if found + if(last != null){ + last.actions(Actions.parallel(Actions.alpha(0f, foutTime, Interp.smooth), Actions.translateBy(0f, Scl.scl(-200f), foutTime, Interp.smooth)), Actions.remove()); + } + //check for next hint to display immediately + current = null; + last = null; + checkNext(); + } + + public boolean shown(){ + return current != null; + } + + public enum DefaultHint implements Hint{ + desktopMove(visibleDesktop, () -> Core.input.axis(Binding.move_x) != 0 || Core.input.axis(Binding.move_y) != 0), + zoom(visibleDesktop, () -> Core.input.axis(KeyCode.scroll) != 0), + mine(isTutorial, () -> player.unit().mining()), + placeDrill(isTutorial, () -> ui.hints.placedBlocks.contains(Blocks.mechanicalDrill)), + placeConveyor(isTutorial, () -> ui.hints.placedBlocks.contains(Blocks.conveyor)), + placeTurret(isTutorial, () -> ui.hints.placedBlocks.contains(Blocks.duo)), + breaking(isTutorial, () -> ui.hints.events.contains("break")), + desktopShoot(visibleDesktop, () -> Vars.state.enemies > 0, () -> player.shooting), + depositItems(() -> player.unit().hasItem(), () -> !player.unit().hasItem()), + desktopPause(visibleDesktop, () -> isTutorial.get() && !Vars.net.active(), () -> Core.input.keyTap(Binding.pause)), + research(isTutorial, () -> ui.research.isShown()), + unitControl(() -> state.rules.defaultTeam.data().units.size > 1 && !net.active(), () -> !player.dead() && !player.unit().spawnedByCore), + respawn(visibleMobile, () -> !player.dead() && !player.unit().spawnedByCore, () -> !player.dead() && player.unit().spawnedByCore), + launch(() -> isTutorial.get() && state.rules.sector.isCaptured(), () -> ui.planet.isShown()), + ; + + @Nullable + String text; + int visibility = visibleAll; + Hint[] dependencies = {}; + boolean finished, cached; + Boolp complete, shown = () -> true; + + DefaultHint(Boolp complete){ + this.complete = complete; + } + + DefaultHint(int visiblity, Boolp complete){ + this(complete); + this.visibility = visiblity; + } + + DefaultHint(Boolp shown, Boolp complete){ + this(complete); + this.shown = shown; + } + + DefaultHint(int visiblity, Boolp shown, Boolp complete){ + this(complete); + this.shown = shown; + this.visibility = visiblity; + } + + @Override + public boolean finished(){ + if(!cached){ + cached = true; + finished = Core.settings.getBool(name() + "-hint-done", false); + } + return finished; + } + + @Override + public void finish(){ + Core.settings.put(name() + "-hint-done", finished = true); + } + + @Override + public String text(){ + if(text == null){ + text = Vars.mobile && Core.bundle.has("hint." + name() + ".mobile") ? Core.bundle.get("hint." + name() + ".mobile") : Core.bundle.get("hint." + name()); + if(!Vars.mobile) text = text.replace("tap", "click").replace("Tap", "Click"); + } + return text; + } + + @Override + public boolean complete(){ + return complete.get(); + } + + @Override + public boolean show(){ + return shown.get() && (dependencies.length == 0 || !Structs.contains(dependencies, d -> !d.finished())); + } + + @Override + public int order(){ + return ordinal(); + } + + @Override + public boolean valid(){ + return (Vars.mobile && (visibility & visibleMobile) != 0) || (!Vars.mobile && (visibility & visibleDesktop) != 0); + } + } + + /** Hint interface for defining any sort of message appearing at the left. */ + public interface Hint{ + int visibleDesktop = 1, visibleMobile = 2, visibleAll = visibleDesktop | visibleMobile; + + /** Hint name for preference storage. */ + String name(); + /** Displayed text. */ + String text(); + /** @return true if hint objective is complete */ + boolean complete(); + /** @return whether the hint is ready to be shown */ + boolean show(); + /** @return order integer, determines priority */ + int order(); + /** @return whether this hint should be processed, used for platform splits */ + boolean valid(); + + /** finishes the hint - it should not be shown again */ + default void finish(){ + Core.settings.put(name() + "-hint-done", true); + } + + /** @return whether the hint is finished - if true, it should not be shown again */ + default boolean finished(){ + return Core.settings.getBool(name() + "-hint-done", false); + } + } +} diff --git a/core/src/mindustry/ui/fragments/HudFragment.java b/core/src/mindustry/ui/fragments/HudFragment.java index 65c0892398..230a482fb4 100644 --- a/core/src/mindustry/ui/fragments/HudFragment.java +++ b/core/src/mindustry/ui/fragments/HudFragment.java @@ -108,7 +108,8 @@ public class HudFragment extends Fragment{ t.top().right(); }); - //TODO tear this all down + ui.hints.build(parent); + //menu at top left parent.fill(cont -> { cont.name = "overlaymarker"; @@ -319,29 +320,6 @@ public class HudFragment extends Fragment{ .update(label -> label.color.set(Color.orange).lerp(Color.scarlet, Mathf.absin(Time.time(), 2f, 1f)))).touchable(Touchable.disabled); }); - //TODO tutorial text - parent.fill(t -> { - t.name = "tutorial"; - Runnable resize = () -> { - t.clearChildren(); - t.top().right().visible(() -> false); - t.stack(new Button(){{ - marginLeft(48f); - labelWrap(() -> control.tutorial.stage.text() + (control.tutorial.canNext() ? "\n\n" + Core.bundle.get("tutorial.next") : "")).width(!Core.graphics.isPortrait() ? 400f : 160f).pad(2f); - clicked(() -> control.tutorial.nextSentence()); - setDisabled(() -> !control.tutorial.canNext()); - }}, - new Table(f -> { - f.left().button(Icon.left, Styles.emptyi, () -> { - control.tutorial.prevSentence(); - }).width(44f).growY().visible(() -> control.tutorial.canPrev()); - })); - }; - - resize.run(); - Events.on(ResizeEvent.class, e -> resize.run()); - }); - //'saving' indicator parent.fill(t -> { t.name = "saving"; diff --git a/gradle.properties b/gradle.properties index 0239b07be2..44e4a54d69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.daemon=true org.gradle.jvmargs=-Xms256m -Xmx1024m -archash=178412d54b62da44b56f7fde3b2fb9b81c5b0418 +archash=7a74424ff0d0399a21023c9c702243b1b18126ed