From 28b235ef07be92808cdba260168ff314db426376 Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 2 Jun 2021 11:08:08 -0400 Subject: [PATCH] Partial 7.0 merge - API preview --- android/AndroidManifest.xml | 3 +- android/build.gradle | 88 +-- android/proguard-rules.pro | 8 + android/res/values/strings.xml | 2 - android/res/values/styles.xml | 2 - .../mindustry/android/AndroidLauncher.java | 46 +- .../android/AndroidRhinoContext.java | 2 +- .../mindustry/android/GPGameService.java | 40 + .../mindustry/annotations/Annotations.java | 6 +- .../mindustry/annotations/BaseProcessor.java | 21 +- .../annotations/entity/EntityProcess.java | 66 +- .../annotations/impl/AssetsProcess.java | 2 +- .../annotations/impl/CallSuperProcess.java | 154 ---- .../annotations/misc/LoadRegionProcessor.java | 5 +- .../annotations/remote/CallGenerator.java | 363 +++++++++ .../annotations/remote/ClassEntry.java | 15 - .../annotations/remote/MethodEntry.java | 12 +- .../annotations/remote/RemoteProcess.java | 129 +--- .../remote/RemoteReadGenerator.java | 129 ---- .../remote/RemoteWriteGenerator.java | 228 ------ .../java/mindustry/annotations/util/Svar.java | 5 - .../src/main/resources/classids.properties | 1 + .../revisions/LargeLaunchPayloadComp/0.json | 1 + build.gradle | 59 +- .../blocks/campaign/launch-pad-large.png | Bin 4244 -> 0 bytes .../campaign/payload-launch-pad-light.png | Bin 0 -> 186 bytes .../blocks/campaign/payload-launch-pad.png | Bin 0 -> 3235 bytes .../distribution/ducts/duct-bottom-0.png | Bin 0 -> 119 bytes .../distribution/ducts/duct-bottom-1.png | Bin 0 -> 149 bytes .../distribution/ducts/duct-bottom-2.png | Bin 0 -> 121 bytes .../distribution/ducts/duct-bottom-3.png | Bin 0 -> 121 bytes .../distribution/ducts/duct-bottom-4.png | Bin 0 -> 121 bytes .../blocks/distribution/ducts/duct-bottom.png | Bin 0 -> 156 bytes .../distribution/ducts/duct-bridge-arrow.png | Bin 0 -> 139 bytes .../ducts/duct-bridge-bridge-bottom.png | Bin 0 -> 115 bytes .../distribution/ducts/duct-bridge-bridge.png | Bin 0 -> 142 bytes .../distribution/ducts/duct-bridge-dir.png | Bin 0 -> 174 bytes .../blocks/distribution/ducts/duct-bridge.png | Bin 0 -> 322 bytes .../distribution/ducts/duct-router-top.png | Bin 0 -> 165 bytes .../blocks/distribution/ducts/duct-router.png | Bin 0 -> 322 bytes .../blocks/distribution/ducts/duct-top-0.png | Bin 0 -> 329 bytes .../blocks/distribution/ducts/duct-top-1.png | Bin 0 -> 364 bytes .../blocks/distribution/ducts/duct-top-2.png | Bin 0 -> 442 bytes .../blocks/distribution/ducts/duct-top-3.png | Bin 0 -> 560 bytes .../blocks/distribution/ducts/duct-top-4.png | Bin 0 -> 413 bytes .../blocks/drills/oil-extractor-liquid.png | Bin 329 -> 478 bytes .../sprites/blocks/environment/char1.png | Bin 236 -> 364 bytes .../sprites/blocks/environment/char2.png | Bin 219 -> 321 bytes .../sprites/blocks/environment/char3.png | Bin 243 -> 380 bytes .../blocks/environment/dune-wall-large.png | Bin 631 -> 1001 bytes .../sprites/blocks/environment/dune-wall1.png | Bin 389 -> 393 bytes .../sprites/blocks/environment/edgier.png | Bin 137 -> 0 bytes .../sprites/blocks/environment/env-error.png | Bin 0 -> 287 bytes .../blocks/environment/metal-floor.png | Bin 232 -> 254 bytes .../sprites/blocks/environment/spawn.png | Bin 221 -> 343 bytes core/assets-raw/sprites/blocks/fire/fire0.png | Bin 0 -> 1193 bytes core/assets-raw/sprites/blocks/fire/fire1.png | Bin 0 -> 1200 bytes .../assets-raw/sprites/blocks/fire/fire10.png | Bin 0 -> 1111 bytes .../assets-raw/sprites/blocks/fire/fire11.png | Bin 0 -> 1131 bytes .../assets-raw/sprites/blocks/fire/fire12.png | Bin 0 -> 1138 bytes .../assets-raw/sprites/blocks/fire/fire13.png | Bin 0 -> 1195 bytes .../assets-raw/sprites/blocks/fire/fire14.png | Bin 0 -> 1217 bytes .../assets-raw/sprites/blocks/fire/fire15.png | Bin 0 -> 1227 bytes .../assets-raw/sprites/blocks/fire/fire16.png | Bin 0 -> 1236 bytes .../assets-raw/sprites/blocks/fire/fire17.png | Bin 0 -> 1244 bytes .../assets-raw/sprites/blocks/fire/fire18.png | Bin 0 -> 1249 bytes .../assets-raw/sprites/blocks/fire/fire19.png | Bin 0 -> 1310 bytes core/assets-raw/sprites/blocks/fire/fire2.png | Bin 0 -> 1178 bytes .../assets-raw/sprites/blocks/fire/fire20.png | Bin 0 -> 1317 bytes .../assets-raw/sprites/blocks/fire/fire21.png | Bin 0 -> 1337 bytes .../assets-raw/sprites/blocks/fire/fire22.png | Bin 0 -> 1356 bytes .../assets-raw/sprites/blocks/fire/fire23.png | Bin 0 -> 1348 bytes .../assets-raw/sprites/blocks/fire/fire24.png | Bin 0 -> 1331 bytes .../assets-raw/sprites/blocks/fire/fire25.png | Bin 0 -> 1351 bytes .../assets-raw/sprites/blocks/fire/fire26.png | Bin 0 -> 1322 bytes .../assets-raw/sprites/blocks/fire/fire27.png | Bin 0 -> 1281 bytes .../assets-raw/sprites/blocks/fire/fire28.png | Bin 0 -> 1291 bytes .../assets-raw/sprites/blocks/fire/fire29.png | Bin 0 -> 1318 bytes core/assets-raw/sprites/blocks/fire/fire3.png | Bin 0 -> 1112 bytes .../assets-raw/sprites/blocks/fire/fire30.png | Bin 0 -> 1355 bytes .../assets-raw/sprites/blocks/fire/fire31.png | Bin 0 -> 1270 bytes .../assets-raw/sprites/blocks/fire/fire32.png | Bin 0 -> 1320 bytes .../assets-raw/sprites/blocks/fire/fire33.png | Bin 0 -> 1370 bytes .../assets-raw/sprites/blocks/fire/fire34.png | Bin 0 -> 1297 bytes .../assets-raw/sprites/blocks/fire/fire35.png | Bin 0 -> 1237 bytes .../assets-raw/sprites/blocks/fire/fire36.png | Bin 0 -> 1239 bytes .../assets-raw/sprites/blocks/fire/fire37.png | Bin 0 -> 1221 bytes .../assets-raw/sprites/blocks/fire/fire38.png | Bin 0 -> 1230 bytes .../assets-raw/sprites/blocks/fire/fire39.png | Bin 0 -> 1225 bytes core/assets-raw/sprites/blocks/fire/fire4.png | Bin 0 -> 1089 bytes core/assets-raw/sprites/blocks/fire/fire5.png | Bin 0 -> 1126 bytes core/assets-raw/sprites/blocks/fire/fire6.png | Bin 0 -> 1125 bytes core/assets-raw/sprites/blocks/fire/fire7.png | Bin 0 -> 1116 bytes core/assets-raw/sprites/blocks/fire/fire8.png | Bin 0 -> 1109 bytes core/assets-raw/sprites/blocks/fire/fire9.png | Bin 0 -> 1145 bytes .../sprites/blocks/liquid/conduit-bottom.png | Bin 162 -> 156 bytes .../sprites/blocks/liquid/phase-conduit.png | Bin 292 -> 248 bytes .../blocks/liquid/plated-conduit-top-4.png | Bin 1823 -> 279 bytes .../{production => payload}/block-forge.png | Bin .../block-loader.png | Bin .../block-unloader.png | Bin .../payload-conveyor-edge.png | Bin .../payload-conveyor-icon.png | Bin .../payload-conveyor-top.png | Bin .../payload-conveyor.png | Bin .../payload/payload-propulsion-tower-base.png | Bin 0 -> 2470 bytes .../payload/payload-propulsion-tower-cap.png | Bin 0 -> 1889 bytes .../payload/payload-propulsion-tower-left.png | Bin 0 -> 1881 bytes .../payload-propulsion-tower-right.png | Bin 0 -> 1833 bytes .../payload/payload-propulsion-tower-top.png | Bin 0 -> 3519 bytes .../payload/payload-propulsion-tower.png | Bin 0 -> 3031 bytes .../payload-router-edge.png | Bin .../payload-router-icon.png | Bin .../payload-router-over.png | Bin .../payload-router-top.png | Bin .../payload-router.png | Bin .../blocks/payload/payload-source-top.png | Bin 0 -> 1124 bytes .../sprites/blocks/payload/payload-source.png | Bin 0 -> 1263 bytes .../blocks/payload/payload-void-top.png | Bin 0 -> 1105 bytes .../sprites/blocks/payload/payload-void.png | Bin 0 -> 1263 bytes .../sprites/blocks/storage/container.png | Bin 435 -> 421 bytes .../sprites/blocks/storage/vault.png | Bin 671 -> 675 bytes .../sprites/blocks/units/factory-in-5.png | Bin 2172 -> 1206 bytes .../sprites/blocks/units/factory-out-5.png | Bin 1160 -> 1243 bytes .../sprites/blocks/units/factory-top-5.png | Bin 0 -> 1565 bytes .../sprites/blocks/units/repair-point.png | Bin 243 -> 343 bytes .../sprites/blocks/units/repair-turret.png | Bin 0 -> 993 bytes .../sprites/effects/circle-bullet-back.png | Bin 0 -> 293 bytes .../sprites/effects/circle-bullet.png | Bin 0 -> 243 bytes .../sprites/effects/laser-top-end.png | Bin 0 -> 206 bytes core/assets-raw/sprites/effects/laser-top.png | Bin 0 -> 100 bytes .../sprites/effects/laser-white-end.png | Bin 0 -> 325 bytes .../sprites/effects/laser-white.png | Bin 0 -> 99 bytes .../sprites/effects/mine-bullet-back.png | Bin 0 -> 349 bytes .../sprites/effects/mine-bullet.png | Bin 0 -> 271 bytes .../sprites/effects/missile-large-back.png | Bin 0 -> 390 bytes .../sprites/effects/missile-large.png | Bin 0 -> 327 bytes .../sprites/items/item-graphite.png | Bin 354 -> 371 bytes core/assets-raw/sprites/shapes/hcircle.png | Bin 0 -> 1490 bytes .../sprites/statuses/status-electrified.png | Bin 0 -> 232 bytes .../assets-raw/sprites/units/aegires-cell.png | Bin 0 -> 2064 bytes core/assets-raw/sprites/units/aegires.png | Bin 0 -> 8017 bytes core/assets-raw/sprites/units/bryde.png | Bin 2755 -> 2233 bytes core/assets-raw/sprites/units/cyerce-cell.png | Bin 0 -> 1441 bytes core/assets-raw/sprites/units/cyerce.png | Bin 0 -> 3720 bytes .../assets-raw/sprites/units/eclipse.aseprite | Bin 12883 -> 0 bytes core/assets-raw/sprites/units/eclipse.png | Bin 10257 -> 6823 bytes .../assets-raw/sprites/units/navanax-cell.png | Bin 0 -> 2396 bytes core/assets-raw/sprites/units/navanax.png | Bin 0 -> 13748 bytes core/assets-raw/sprites/units/oxynoe-cell.png | Bin 0 -> 816 bytes core/assets-raw/sprites/units/oxynoe.png | Bin 0 -> 2376 bytes core/assets-raw/sprites/units/quad.png | Bin 5100 -> 3555 bytes core/assets-raw/sprites/units/retusa-cell.png | Bin 0 -> 749 bytes core/assets-raw/sprites/units/retusa.png | Bin 0 -> 1683 bytes .../units/weapons/emp-cannon-mount-heat.png | Bin 0 -> 569 bytes .../units/weapons/emp-cannon-mount.png | Bin 0 -> 2014 bytes .../units/weapons/plasma-laser-mount-heat.png | Bin 0 -> 562 bytes .../units/weapons/plasma-laser-mount.png | Bin 0 -> 1206 bytes .../units/weapons/plasma-missile-mount.png | Bin 0 -> 833 bytes .../units/weapons/plasma-mount-weapon.png | Bin 0 -> 415 bytes .../units/weapons/point-defense-mount.png | Bin 0 -> 505 bytes .../weapons/repair-beam-weapon-center.png | Bin 0 -> 377 bytes .../units/weapons/repair-beam-weapon.png | Bin 0 -> 344 bytes core/assets/bloomshaders/alpha_bloom.frag | 6 +- core/assets/bloomshaders/maskedtreshold.frag | 9 - core/assets/bundles/bundle.properties | 35 +- core/assets/bundles/bundle_ru.properties | 2 +- core/assets/icons/icons.properties | 687 +++++++++--------- core/assets/maps/shoreline.msav | Bin 15212 -> 0 bytes core/assets/scripts/global.js | 14 +- core/assets/shaders/atmosphere.frag | 2 + core/assets/shaders/blockbuild.frag | 2 + core/assets/shaders/caustics.frag | 24 + core/assets/shaders/water.frag | 1 - core/assets/sprites/clouds.png | Bin 0 -> 9675 bytes core/src/mindustry/ClientLauncher.java | 15 +- core/src/mindustry/Vars.java | 26 +- core/src/mindustry/ai/BaseAI.java | 5 +- core/src/mindustry/ai/BlockIndexer.java | 410 ++++------- core/src/mindustry/ai/types/BuilderAI.java | 4 +- core/src/mindustry/ai/types/FormationAI.java | 9 +- core/src/mindustry/ai/types/GroundAI.java | 14 +- core/src/mindustry/ai/types/HugAI.java | 81 +++ core/src/mindustry/ai/types/LogicAI.java | 2 +- core/src/mindustry/ai/types/MinerAI.java | 4 +- core/src/mindustry/ai/types/SuicideAI.java | 25 +- core/src/mindustry/async/AsyncCore.java | 19 +- core/src/mindustry/audio/SoundControl.java | 2 + core/src/mindustry/content/Blocks.java | 236 ++++-- core/src/mindustry/content/Bullets.java | 13 +- core/src/mindustry/content/Fx.java | 325 ++++++++- core/src/mindustry/content/Items.java | 5 +- core/src/mindustry/content/Liquids.java | 2 +- core/src/mindustry/content/Planets.java | 13 +- core/src/mindustry/content/SectorPresets.java | 4 + core/src/mindustry/content/StatusEffects.java | 29 +- core/src/mindustry/content/TechTree.java | 4 +- core/src/mindustry/content/UnitTypes.java | 655 ++++++++++++++--- core/src/mindustry/content/Weathers.java | 17 +- core/src/mindustry/core/ContentLoader.java | 9 +- core/src/mindustry/core/Control.java | 54 +- core/src/mindustry/core/GameState.java | 7 +- core/src/mindustry/core/Logic.java | 11 +- core/src/mindustry/core/NetClient.java | 63 +- core/src/mindustry/core/NetServer.java | 56 +- core/src/mindustry/core/Platform.java | 13 +- core/src/mindustry/core/Renderer.java | 106 ++- core/src/mindustry/core/UI.java | 39 +- core/src/mindustry/core/World.java | 76 +- core/src/mindustry/ctype/Content.java | 5 +- .../mindustry/ctype/UnlockableContent.java | 56 +- core/src/mindustry/editor/DrawOperation.java | 5 - core/src/mindustry/editor/EditorTile.java | 8 +- core/src/mindustry/editor/EditorTool.java | 25 +- core/src/mindustry/editor/MapEditor.java | 6 +- .../src/mindustry/editor/MapEditorDialog.java | 17 +- .../mindustry/editor/MapGenerateDialog.java | 135 ++-- core/src/mindustry/editor/MapInfoDialog.java | 10 +- core/src/mindustry/editor/MapRenderer.java | 9 +- .../src/mindustry/editor/MapResizeDialog.java | 3 +- core/src/mindustry/editor/MapView.java | 10 +- core/src/mindustry/editor/WaveGraph.java | 2 +- core/src/mindustry/editor/WaveInfoDialog.java | 6 +- core/src/mindustry/entities/Damage.java | 17 +- core/src/mindustry/entities/Effect.java | 13 +- core/src/mindustry/entities/Sized.java | 4 +- core/src/mindustry/entities/Units.java | 59 +- .../abilities/EnergyFieldAbility.java | 145 ++++ .../abilities/ShieldRegenFieldAbility.java | 4 +- .../abilities/StatusFieldAbility.java | 6 + .../entities/abilities/UnitSpawnAbility.java | 2 +- .../entities/bullet/ArtilleryBulletType.java | 28 +- .../entities/bullet/BasicBulletType.java | 1 + .../mindustry/entities/bullet/BulletType.java | 142 +++- .../entities/bullet/EmpBulletType.java | 70 ++ .../entities/bullet/FlakBulletType.java | 20 +- .../entities/bullet/LaserBoltBulletType.java | 4 + .../entities/bullet/LiquidBulletType.java | 3 +- .../entities/bullet/RailBulletType.java | 6 +- .../entities/bullet/SapBulletType.java | 4 +- .../entities/bullet/ShrapnelBulletType.java | 7 +- .../entities/comp/BlockUnitComp.java | 3 +- .../mindustry/entities/comp/BoundedComp.java | 15 +- .../mindustry/entities/comp/BuilderComp.java | 5 +- .../mindustry/entities/comp/BuildingComp.java | 106 ++- .../mindustry/entities/comp/BulletComp.java | 37 +- .../entities/comp/CommanderComp.java | 2 +- .../mindustry/entities/comp/EntityComp.java | 6 +- .../src/mindustry/entities/comp/FireComp.java | 78 +- .../entities/comp/LaunchCoreComp.java | 2 +- .../mindustry/entities/comp/PayloadComp.java | 8 +- .../mindustry/entities/comp/PhysicsComp.java | 2 +- .../mindustry/entities/comp/PlayerComp.java | 28 +- .../mindustry/entities/comp/PuddleComp.java | 6 +- .../mindustry/entities/comp/ShieldComp.java | 6 +- .../mindustry/entities/comp/StatusComp.java | 25 +- .../src/mindustry/entities/comp/TeamComp.java | 7 +- .../src/mindustry/entities/comp/UnitComp.java | 38 +- core/src/mindustry/entities/comp/VelComp.java | 25 + .../mindustry/entities/comp/WeaponsComp.java | 158 +--- .../entities/effect/ExplosionEffect.java | 47 ++ .../entities/effect/ParticleEffect.java | 19 +- .../mindustry/entities/effect/WaveEffect.java | 12 +- .../entities/units/AIController.java | 28 +- .../mindustry/entities/units/StatusEntry.java | 2 - .../mindustry/entities/units/WeaponMount.java | 4 + core/src/mindustry/game/EventType.java | 38 +- core/src/mindustry/game/Gamemode.java | 3 +- core/src/mindustry/game/Rules.java | 8 + core/src/mindustry/game/Saves.java | 2 +- core/src/mindustry/game/Schematic.java | 5 +- core/src/mindustry/game/Schematics.java | 27 +- core/src/mindustry/game/SectorInfo.java | 10 + core/src/mindustry/game/Teams.java | 30 +- core/src/mindustry/game/Universe.java | 5 + .../src/mindustry/graphics/BlockRenderer.java | 207 ++++-- core/src/mindustry/graphics/CacheLayer.java | 120 ++- core/src/mindustry/graphics/Drawf.java | 14 +- core/src/mindustry/graphics/EnvRenderers.java | 8 + .../src/mindustry/graphics/FloorRenderer.java | 364 ++++++++-- .../src/mindustry/graphics/LightRenderer.java | 41 +- core/src/mindustry/graphics/LoadRenderer.java | 4 +- core/src/mindustry/graphics/MenuRenderer.java | 15 +- .../mindustry/graphics/MinimapRenderer.java | 14 +- core/src/mindustry/graphics/MultiPacker.java | 51 +- .../mindustry/graphics/OverlayRenderer.java | 26 +- core/src/mindustry/graphics/Pal.java | 4 +- .../mindustry/graphics/ScorchGenerator.java | 31 - core/src/mindustry/graphics/Shaders.java | 22 +- core/src/mindustry/graphics/Trail.java | 88 ++- .../graphics/g3d/PlanetRenderer.java | 9 +- core/src/mindustry/input/DesktopInput.java | 15 +- core/src/mindustry/input/InputHandler.java | 93 ++- core/src/mindustry/input/MobileInput.java | 21 +- core/src/mindustry/input/Placement.java | 56 ++ core/src/mindustry/io/JsonIO.java | 3 +- core/src/mindustry/io/MapIO.java | 25 +- core/src/mindustry/io/SaveFileReader.java | 4 +- core/src/mindustry/io/SaveIO.java | 6 +- core/src/mindustry/io/SavePreviewLoader.java | 4 +- core/src/mindustry/io/SaveVersion.java | 56 +- core/src/mindustry/io/TypeIO.java | 9 +- core/src/mindustry/io/versions/Save4.java | 16 + core/src/mindustry/io/versions/Save5.java | 10 + core/src/mindustry/logic/LAccess.java | 4 +- core/src/mindustry/logic/LExecutor.java | 4 +- core/src/mindustry/logic/LParser.java | 11 +- core/src/mindustry/logic/LStatement.java | 43 +- core/src/mindustry/logic/LStatements.java | 13 +- core/src/mindustry/maps/Map.java | 6 +- core/src/mindustry/maps/MapPreviewLoader.java | 4 +- core/src/mindustry/maps/Maps.java | 4 +- .../mindustry/maps/filters/BlendFilter.java | 6 + .../mindustry/maps/filters/ClearFilter.java | 6 + .../maps/filters/CoreSpawnFilter.java | 6 + .../mindustry/maps/filters/DistortFilter.java | 6 + .../maps/filters/EnemySpawnFilter.java | 6 + .../mindustry/maps/filters/FilterOption.java | 20 +- .../maps/filters/GenerateFilter.java | 4 + .../mindustry/maps/filters/MedianFilter.java | 6 + .../mindustry/maps/filters/MirrorFilter.java | 6 + .../mindustry/maps/filters/NoiseFilter.java | 6 + .../src/mindustry/maps/filters/OreFilter.java | 6 + .../maps/filters/OreMedianFilter.java | 6 + .../maps/filters/RiverNoiseFilter.java | 6 + .../mindustry/maps/filters/ScatterFilter.java | 6 + .../maps/filters/SpawnPathFilter.java | 6 + .../mindustry/maps/filters/TerrainFilter.java | 6 + .../maps/generators/BaseGenerator.java | 3 +- .../maps/generators/BasicGenerator.java | 15 +- .../maps/generators/PlanetGenerator.java | 71 ++ .../maps/planet/SerpuloPlanetGenerator.java | 13 +- .../maps/planet/TantrosPlanetGenerator.java | 32 - core/src/mindustry/mod/ClassMap.java | 82 ++- core/src/mindustry/mod/ContentParser.java | 31 +- core/src/mindustry/mod/ModClassLoader.java | 35 + core/src/mindustry/mod/Mods.java | 138 +++- core/src/mindustry/mod/Scripts.java | 9 +- core/src/mindustry/net/Administration.java | 8 +- core/src/mindustry/net/ArcNetProvider.java | 155 +++- core/src/mindustry/net/Net.java | 110 +-- core/src/mindustry/net/NetConnection.java | 13 +- core/src/mindustry/net/NetworkIO.java | 2 + core/src/mindustry/net/Packet.java | 27 +- core/src/mindustry/net/Packets.java | 130 ++-- core/src/mindustry/net/Registrator.java | 45 -- core/src/mindustry/net/Streamable.java | 8 +- .../src/mindustry/service/Achievement.java | 23 +- core/src/mindustry/service/GameService.java | 358 +++++++++ .../src/mindustry/service}/SStat.java | 12 +- core/src/mindustry/type/Item.java | 6 +- core/src/mindustry/type/ItemStack.java | 4 +- core/src/mindustry/type/Planet.java | 14 +- core/src/mindustry/type/Satellite.java | 3 +- core/src/mindustry/type/Sector.java | 5 +- core/src/mindustry/type/SectorPreset.java | 9 +- core/src/mindustry/type/StatusEffect.java | 57 +- core/src/mindustry/type/UnitType.java | 167 +++-- core/src/mindustry/type/Weapon.java | 257 ++++++- core/src/mindustry/type/Weather.java | 30 +- .../type/weapons/PointDefenseWeapon.java | 62 ++ .../type/weapons/RepairBeamWeapon.java | 121 +++ .../mindustry/type/weather/MagneticStorm.java | 11 + .../mindustry/type/weather/SolarFlare.java | 11 + core/src/mindustry/ui/Bar.java | 20 +- core/src/mindustry/ui/Cicon.java | 23 +- core/src/mindustry/ui/CoreItemsDisplay.java | 4 +- core/src/mindustry/ui/Fonts.java | 21 +- core/src/mindustry/ui/ItemDisplay.java | 14 + core/src/mindustry/ui/ItemImage.java | 2 +- core/src/mindustry/ui/ItemsDisplay.java | 2 +- core/src/mindustry/ui/LiquidDisplay.java | 6 +- core/src/mindustry/ui/Styles.java | 19 +- core/src/mindustry/ui/dialogs/BaseDialog.java | 8 +- .../ui/dialogs/ContentInfoDialog.java | 6 +- .../mindustry/ui/dialogs/ControlsDialog.java | 26 - .../ui/dialogs/CustomGameDialog.java | 15 +- .../ui/dialogs/CustomRulesDialog.java | 9 +- .../mindustry/ui/dialogs/DatabaseDialog.java | 2 +- .../mindustry/ui/dialogs/GameOverDialog.java | 2 +- core/src/mindustry/ui/dialogs/HostDialog.java | 2 +- core/src/mindustry/ui/dialogs/JoinDialog.java | 13 +- .../mindustry/ui/dialogs/KeybindDialog.java | 262 +++++++ .../mindustry/ui/dialogs/LanguageDialog.java | 40 +- .../ui/dialogs/LaunchLoadoutDialog.java | 2 +- .../mindustry/ui/dialogs/LoadoutDialog.java | 2 +- core/src/mindustry/ui/dialogs/MapsDialog.java | 2 +- core/src/mindustry/ui/dialogs/ModsDialog.java | 7 +- .../mindustry/ui/dialogs/PausedDialog.java | 11 +- .../mindustry/ui/dialogs/PlanetDialog.java | 146 +++- .../mindustry/ui/dialogs/ResearchDialog.java | 9 +- .../ui/dialogs/SchematicsDialog.java | 409 ++++++++++- .../ui/dialogs/SettingsMenuDialog.java | 233 +++++- .../ui/fragments/BlockInventoryFragment.java | 3 +- .../mindustry/ui/fragments/HintsFragment.java | 1 + .../mindustry/ui/fragments/HudFragment.java | 167 +++-- .../ui/fragments/PlacementFragment.java | 14 +- core/src/mindustry/world/Block.java | 137 ++-- core/src/mindustry/world/Build.java | 17 +- core/src/mindustry/world/Tile.java | 65 +- .../mindustry/world/blocks/Attributes.java | 22 +- .../world/blocks/ConstructBlock.java | 165 ++--- .../mindustry/world/blocks/ItemSelection.java | 2 +- .../world/blocks/campaign/Accelerator.java | 5 +- .../world/blocks/campaign/LaunchPad.java | 41 +- .../blocks/campaign/PayloadLaunchPad.java | 280 +++++++ .../world/blocks/defense/ForceProjector.java | 45 +- .../world/blocks/defense/MendProjector.java | 9 +- .../blocks/defense/OverdriveProjector.java | 1 + .../blocks/defense/turrets/ItemTurret.java | 21 +- .../blocks/defense/turrets/LaserTurret.java | 15 +- .../blocks/defense/turrets/LiquidTurret.java | 17 +- .../blocks/defense/turrets/PowerTurret.java | 7 +- .../blocks/defense/turrets/ReloadTurret.java | 20 +- .../defense/turrets/TractorBeamTurret.java | 8 +- .../world/blocks/defense/turrets/Turret.java | 33 +- .../distribution/BufferedItemBridge.java | 5 + .../world/blocks/distribution/Conveyor.java | 5 +- .../world/blocks/distribution/Duct.java | 187 +++++ .../world/blocks/distribution/DuctBridge.java | 149 ++++ .../world/blocks/distribution/DuctRouter.java | 125 ++++ .../world/blocks/distribution/ItemBridge.java | 45 +- .../world/blocks/distribution/MassDriver.java | 63 +- .../blocks/distribution/PayloadConveyor.java | 25 +- .../blocks/distribution/PayloadRouter.java | 15 +- .../blocks/distribution/StackConveyor.java | 2 +- .../world/blocks/environment/Bush.java | 46 ++ .../world/blocks/environment/Floor.java | 15 +- .../world/blocks/environment/OreBlock.java | 27 +- .../environment/{Boulder.java => Prop.java} | 13 +- .../blocks/environment/StaticClusterWall.java | 25 + .../world/blocks/environment/StaticWall.java | 7 +- .../world/blocks/environment/TreeBlock.java | 26 +- .../blocks/environment/WallOreBlock.java | 16 + .../world/blocks/environment/WavingProp.java | 30 + .../world/blocks/environment/WobbleProp.java | 24 + .../world/blocks/experimental/BlockForge.java | 169 ----- .../blocks/experimental/BlockLoader.java | 6 +- .../world/blocks/logic/LogicBlock.java | 31 +- .../world/blocks/payloads/BallisticSilo.java | 29 + .../world/blocks/payloads/BlockForge.java | 89 +++ .../world/blocks/payloads/BlockProducer.java | 143 ++++ .../world/blocks/payloads/BuildPayload.java | 17 +- .../world/blocks/payloads/NuclearWarhead.java | 18 + .../world/blocks/payloads/Payload.java | 19 +- .../world/blocks/payloads/PayloadBlock.java | 233 ++++++ .../blocks/payloads/PayloadMassDriver.java | 481 ++++++++++++ .../world/blocks/payloads/PayloadSource.java | 135 ++++ .../world/blocks/payloads/PayloadVoid.java | 57 ++ .../world/blocks/payloads/UnitPayload.java | 25 +- .../world/blocks/power/LightBlock.java | 9 +- .../world/blocks/power/NuclearReactor.java | 20 +- .../world/blocks/power/PowerDiode.java | 2 +- .../blocks/production/AttributeCrafter.java | 58 ++ .../blocks/production/AttributeSmelter.java | 59 +- .../world/blocks/production/BeamDrill.java | 254 +++++++ .../world/blocks/production/Cultivator.java | 89 +-- .../world/blocks/production/Drill.java | 48 +- .../world/blocks/production/Fracker.java | 2 + .../blocks/production/GenericCrafter.java | 29 +- .../blocks/production/GenericSmelter.java | 8 +- .../blocks/production/LiquidConverter.java | 12 +- .../blocks/production/PayloadAcceptor.java | 180 +---- .../world/blocks/production/Pump.java | 7 +- .../world/blocks/production/Separator.java | 11 +- .../production/SingleBlockProducer.java | 23 + .../world/blocks/sandbox/ItemSource.java | 1 + .../world/blocks/sandbox/ItemVoid.java | 1 + .../world/blocks/sandbox/PowerSource.java | 3 + .../world/blocks/sandbox/PowerVoid.java | 1 + .../world/blocks/storage/CoreBlock.java | 80 +- .../world/blocks/storage/Unloader.java | 19 +- .../world/blocks/units/Reconstructor.java | 18 +- .../world/blocks/units/RepairPoint.java | 138 +++- .../world/blocks/units/UnitBlock.java | 12 +- .../world/blocks/units/UnitFactory.java | 5 +- .../mindustry/world/consumers/Consume.java | 6 +- .../world/consumers/ConsumeItemDynamic.java | 2 +- .../world/consumers/ConsumeItemFilter.java | 5 +- .../world/consumers/ConsumeItems.java | 13 +- .../world/consumers/ConsumeLiquid.java | 4 +- .../world/consumers/ConsumeLiquidFilter.java | 5 +- .../mindustry/world/draw/DrawAnimation.java | 22 +- .../mindustry/world/draw/DrawArcSmelter.java | 69 ++ core/src/mindustry/world/draw/DrawBlock.java | 11 +- core/src/mindustry/world/draw/DrawCells.java | 57 ++ .../mindustry/world/draw/DrawCultivator.java | 56 ++ core/src/mindustry/world/draw/DrawGlow.java | 8 +- core/src/mindustry/world/draw/DrawMixer.java | 16 +- .../src/mindustry/world/draw/DrawRotator.java | 6 +- .../src/mindustry/world/draw/DrawSmelter.java | 57 ++ core/src/mindustry/world/draw/DrawWeave.java | 16 +- core/src/mindustry/world/meta/Attribute.java | 48 +- core/src/mindustry/world/meta/BlockFlag.java | 7 +- core/src/mindustry/world/meta/Env.java | 14 + core/src/mindustry/world/meta/Stat.java | 1 + core/src/mindustry/world/meta/StatValues.java | 310 ++++++++ core/src/mindustry/world/meta/Stats.java | 19 +- .../world/meta/values/AmmoListValue.java | 115 --- .../world/meta/values/BlockFilterValue.java | 37 - .../world/meta/values/BlockListValue.java | 33 - .../world/meta/values/BooleanValue.java | 17 - .../world/meta/values/BoosterListValue.java | 56 -- .../meta/values/FloorEfficiencyValue.java | 27 - .../world/meta/values/FloorValue.java | 21 - .../world/meta/values/ItemFilterValue.java | 33 - .../world/meta/values/ItemListValue.java | 27 - .../world/meta/values/LiquidFilterValue.java | 39 - .../world/meta/values/LiquidValue.java | 23 - .../world/meta/values/NumberValue.java | 27 - .../world/meta/values/StringValue.java | 18 - .../world/meta/values/WeaponListValue.java | 55 -- .../world/modules/ConsumeModule.java | 4 + desktop/build.gradle | 5 +- .../mindustry/desktop/DesktopLauncher.java | 61 +- desktop/src/mindustry/desktop/steam/SNet.java | 95 +-- .../src/mindustry/desktop/steam/SStats.java | 346 +-------- .../src/mindustry/desktop/steam/SUser.java | 16 - .../mindustry/desktop/steam/SWorkshop.java | 52 +- .../android/en-US/changelogs/29791.txt | 3 + gradle.properties | 4 +- ios/robovm.xml | 7 +- ios/src/mindustry/ios/IOSLauncher.java | 1 - settings.gradle | 3 + tools/build.gradle | 245 ++----- tools/src/mindustry/tools/Edgifier.java | 88 --- tools/src/mindustry/tools/Generators.java | 486 +++++++------ tools/src/mindustry/tools/IconConverter.java | 21 +- tools/src/mindustry/tools/Image.java | 229 ------ tools/src/mindustry/tools/ImagePacker.java | 127 ++-- .../mindustry/tools/ScriptMainGenerator.java | 5 +- 531 files changed, 12356 insertions(+), 6286 deletions(-) create mode 100644 android/proguard-rules.pro create mode 100644 android/srcgp/mindustry/android/GPGameService.java delete mode 100644 annotations/src/main/java/mindustry/annotations/impl/CallSuperProcess.java create mode 100644 annotations/src/main/java/mindustry/annotations/remote/CallGenerator.java delete mode 100644 annotations/src/main/java/mindustry/annotations/remote/ClassEntry.java delete mode 100644 annotations/src/main/java/mindustry/annotations/remote/RemoteReadGenerator.java delete mode 100644 annotations/src/main/java/mindustry/annotations/remote/RemoteWriteGenerator.java create mode 100644 annotations/src/main/resources/revisions/LargeLaunchPayloadComp/0.json delete mode 100644 core/assets-raw/sprites/blocks/campaign/launch-pad-large.png create mode 100644 core/assets-raw/sprites/blocks/campaign/payload-launch-pad-light.png create mode 100644 core/assets-raw/sprites/blocks/campaign/payload-launch-pad.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-0.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-1.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-2.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-3.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-4.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-arrow.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge-bottom.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-dir.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-router-top.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-router.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-top-0.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-top-1.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-top-2.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-top-3.png create mode 100644 core/assets-raw/sprites/blocks/distribution/ducts/duct-top-4.png delete mode 100644 core/assets-raw/sprites/blocks/environment/edgier.png create mode 100644 core/assets-raw/sprites/blocks/environment/env-error.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire0.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire1.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire10.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire11.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire12.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire13.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire14.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire15.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire16.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire17.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire18.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire19.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire2.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire20.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire21.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire22.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire23.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire24.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire25.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire26.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire27.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire28.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire29.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire3.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire30.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire31.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire32.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire33.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire34.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire35.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire36.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire37.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire38.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire39.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire4.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire5.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire6.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire7.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire8.png create mode 100644 core/assets-raw/sprites/blocks/fire/fire9.png rename core/assets-raw/sprites/blocks/{production => payload}/block-forge.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/block-loader.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/block-unloader.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-conveyor-edge.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-conveyor-icon.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-conveyor-top.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-conveyor.png (100%) create mode 100644 core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-base.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-cap.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-left.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-right.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-top.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-propulsion-tower.png rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-router-edge.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-router-icon.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-router-over.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-router-top.png (100%) rename core/assets-raw/sprites/blocks/{distribution => payload}/payload-router.png (100%) create mode 100644 core/assets-raw/sprites/blocks/payload/payload-source-top.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-source.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-void-top.png create mode 100644 core/assets-raw/sprites/blocks/payload/payload-void.png create mode 100644 core/assets-raw/sprites/blocks/units/factory-top-5.png create mode 100644 core/assets-raw/sprites/blocks/units/repair-turret.png create mode 100644 core/assets-raw/sprites/effects/circle-bullet-back.png create mode 100644 core/assets-raw/sprites/effects/circle-bullet.png create mode 100644 core/assets-raw/sprites/effects/laser-top-end.png create mode 100644 core/assets-raw/sprites/effects/laser-top.png create mode 100644 core/assets-raw/sprites/effects/laser-white-end.png create mode 100644 core/assets-raw/sprites/effects/laser-white.png create mode 100644 core/assets-raw/sprites/effects/mine-bullet-back.png create mode 100644 core/assets-raw/sprites/effects/mine-bullet.png create mode 100644 core/assets-raw/sprites/effects/missile-large-back.png create mode 100644 core/assets-raw/sprites/effects/missile-large.png create mode 100644 core/assets-raw/sprites/shapes/hcircle.png create mode 100644 core/assets-raw/sprites/statuses/status-electrified.png create mode 100644 core/assets-raw/sprites/units/aegires-cell.png create mode 100644 core/assets-raw/sprites/units/aegires.png create mode 100644 core/assets-raw/sprites/units/cyerce-cell.png create mode 100644 core/assets-raw/sprites/units/cyerce.png delete mode 100644 core/assets-raw/sprites/units/eclipse.aseprite create mode 100644 core/assets-raw/sprites/units/navanax-cell.png create mode 100644 core/assets-raw/sprites/units/navanax.png create mode 100644 core/assets-raw/sprites/units/oxynoe-cell.png create mode 100644 core/assets-raw/sprites/units/oxynoe.png create mode 100644 core/assets-raw/sprites/units/retusa-cell.png create mode 100644 core/assets-raw/sprites/units/retusa.png create mode 100644 core/assets-raw/sprites/units/weapons/emp-cannon-mount-heat.png create mode 100644 core/assets-raw/sprites/units/weapons/emp-cannon-mount.png create mode 100644 core/assets-raw/sprites/units/weapons/plasma-laser-mount-heat.png create mode 100644 core/assets-raw/sprites/units/weapons/plasma-laser-mount.png create mode 100644 core/assets-raw/sprites/units/weapons/plasma-missile-mount.png create mode 100644 core/assets-raw/sprites/units/weapons/plasma-mount-weapon.png create mode 100644 core/assets-raw/sprites/units/weapons/point-defense-mount.png create mode 100644 core/assets-raw/sprites/units/weapons/repair-beam-weapon-center.png create mode 100644 core/assets-raw/sprites/units/weapons/repair-beam-weapon.png delete mode 100644 core/assets/bloomshaders/maskedtreshold.frag delete mode 100644 core/assets/maps/shoreline.msav create mode 100644 core/assets/shaders/caustics.frag create mode 100644 core/assets/sprites/clouds.png create mode 100644 core/src/mindustry/ai/types/HugAI.java create mode 100644 core/src/mindustry/entities/abilities/EnergyFieldAbility.java create mode 100644 core/src/mindustry/entities/bullet/EmpBulletType.java create mode 100644 core/src/mindustry/entities/effect/ExplosionEffect.java create mode 100644 core/src/mindustry/graphics/EnvRenderers.java delete mode 100644 core/src/mindustry/graphics/ScorchGenerator.java create mode 100644 core/src/mindustry/io/versions/Save5.java delete mode 100644 core/src/mindustry/maps/planet/TantrosPlanetGenerator.java create mode 100644 core/src/mindustry/mod/ModClassLoader.java delete mode 100644 core/src/mindustry/net/Registrator.java rename desktop/src/mindustry/desktop/steam/SAchievement.java => core/src/mindustry/service/Achievement.java (83%) create mode 100644 core/src/mindustry/service/GameService.java rename {desktop/src/mindustry/desktop/steam => core/src/mindustry/service}/SStat.java (76%) create mode 100644 core/src/mindustry/type/weapons/PointDefenseWeapon.java create mode 100644 core/src/mindustry/type/weapons/RepairBeamWeapon.java create mode 100644 core/src/mindustry/type/weather/MagneticStorm.java create mode 100644 core/src/mindustry/type/weather/SolarFlare.java delete mode 100644 core/src/mindustry/ui/dialogs/ControlsDialog.java create mode 100644 core/src/mindustry/ui/dialogs/KeybindDialog.java create mode 100644 core/src/mindustry/world/blocks/campaign/PayloadLaunchPad.java create mode 100644 core/src/mindustry/world/blocks/distribution/Duct.java create mode 100644 core/src/mindustry/world/blocks/distribution/DuctBridge.java create mode 100644 core/src/mindustry/world/blocks/distribution/DuctRouter.java create mode 100644 core/src/mindustry/world/blocks/environment/Bush.java rename core/src/mindustry/world/blocks/environment/{Boulder.java => Prop.java} (71%) create mode 100644 core/src/mindustry/world/blocks/environment/StaticClusterWall.java create mode 100644 core/src/mindustry/world/blocks/environment/WallOreBlock.java create mode 100644 core/src/mindustry/world/blocks/environment/WavingProp.java create mode 100644 core/src/mindustry/world/blocks/environment/WobbleProp.java delete mode 100644 core/src/mindustry/world/blocks/experimental/BlockForge.java create mode 100644 core/src/mindustry/world/blocks/payloads/BallisticSilo.java create mode 100644 core/src/mindustry/world/blocks/payloads/BlockForge.java create mode 100644 core/src/mindustry/world/blocks/payloads/BlockProducer.java create mode 100644 core/src/mindustry/world/blocks/payloads/NuclearWarhead.java create mode 100644 core/src/mindustry/world/blocks/payloads/PayloadBlock.java create mode 100644 core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java create mode 100644 core/src/mindustry/world/blocks/payloads/PayloadSource.java create mode 100644 core/src/mindustry/world/blocks/payloads/PayloadVoid.java create mode 100644 core/src/mindustry/world/blocks/production/AttributeCrafter.java create mode 100644 core/src/mindustry/world/blocks/production/BeamDrill.java create mode 100644 core/src/mindustry/world/blocks/production/SingleBlockProducer.java create mode 100644 core/src/mindustry/world/draw/DrawArcSmelter.java create mode 100644 core/src/mindustry/world/draw/DrawCells.java create mode 100644 core/src/mindustry/world/draw/DrawCultivator.java create mode 100644 core/src/mindustry/world/draw/DrawSmelter.java create mode 100644 core/src/mindustry/world/meta/Env.java create mode 100644 core/src/mindustry/world/meta/StatValues.java delete mode 100644 core/src/mindustry/world/meta/values/AmmoListValue.java delete mode 100644 core/src/mindustry/world/meta/values/BlockFilterValue.java delete mode 100644 core/src/mindustry/world/meta/values/BlockListValue.java delete mode 100644 core/src/mindustry/world/meta/values/BooleanValue.java delete mode 100644 core/src/mindustry/world/meta/values/BoosterListValue.java delete mode 100644 core/src/mindustry/world/meta/values/FloorEfficiencyValue.java delete mode 100644 core/src/mindustry/world/meta/values/FloorValue.java delete mode 100644 core/src/mindustry/world/meta/values/ItemFilterValue.java delete mode 100644 core/src/mindustry/world/meta/values/ItemListValue.java delete mode 100644 core/src/mindustry/world/meta/values/LiquidFilterValue.java delete mode 100644 core/src/mindustry/world/meta/values/LiquidValue.java delete mode 100644 core/src/mindustry/world/meta/values/NumberValue.java delete mode 100644 core/src/mindustry/world/meta/values/StringValue.java delete mode 100644 core/src/mindustry/world/meta/values/WeaponListValue.java create mode 100644 fastlane/metadata/android/en-US/changelogs/29791.txt delete mode 100644 tools/src/mindustry/tools/Edgifier.java delete mode 100644 tools/src/mindustry/tools/Image.java diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 7d4d731df1..9af4f50c90 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -17,7 +17,8 @@ android:usesCleartextTraffic="true" android:appCategory="game" android:label="@string/app_name" - android:theme="@style/ArcTheme" android:fullBackupContent="@xml/backup_rules"> + android:theme="@style/ArcTheme" + android:fullBackupContent="@xml/backup_rules"> copy{ @@ -131,25 +155,7 @@ task copyAndroidNatives(){ } task run(type: Exec){ - def path - def localProperties = project.file("../local.properties") - if(localProperties.exists()){ - Properties properties = new Properties() - localProperties.withInputStream{ instr -> - properties.load(instr) - } - def sdkDir = properties.getProperty('sdk.dir') - if(sdkDir){ - path = sdkDir - }else{ - path = "$System.env.ANDROID_HOME" - } - }else{ - path = "$System.env.ANDROID_HOME" - } - - def adb = path + "/platform-tools/adb" - commandLine "$adb", 'shell', 'am', 'start', '-n', 'io.anuke.mindustry/mindustry.android.AndroidLauncher' + commandLine "${findSdkDir()}/platform-tools/adb", 'shell', 'am', 'start', '-n', 'io.anuke.mindustry/mindustry.android.AndroidLauncher' } if(!project.ext.hasSprites()){ diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 0000000000..f25e5e79c5 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,8 @@ +-dontobfuscate + +#these are essential packages that should not be "optimized" in any way +#the main purpose of d8 here is to shrink the absurdly-large google play games libraries +-keep class mindustry.** { *; } +-keep class arc.** { *; } +-keep class net.jpountz.** { *; } +-keep class rhino.** { *; } diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index d40cf3accd..d42ffe6d4a 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -1,6 +1,4 @@ - Mindustry - diff --git a/android/res/values/styles.xml b/android/res/values/styles.xml index 3fe4d7f6a8..c37d2c6ed9 100644 --- a/android/res/values/styles.xml +++ b/android/res/values/styles.xml @@ -1,5 +1,4 @@ - - diff --git a/android/src/mindustry/android/AndroidLauncher.java b/android/src/mindustry/android/AndroidLauncher.java index b46d09c9c0..69543be82e 100644 --- a/android/src/mindustry/android/AndroidLauncher.java +++ b/android/src/mindustry/android/AndroidLauncher.java @@ -15,9 +15,11 @@ import arc.func.*; import arc.scene.ui.layout.*; import arc.util.*; import dalvik.system.*; +import io.anuke.mindustry.*; import mindustry.*; import mindustry.game.Saves.*; import mindustry.io.*; +import mindustry.mod.*; import mindustry.net.*; import mindustry.ui.dialogs.*; @@ -33,6 +35,9 @@ public class AndroidLauncher extends AndroidApplication{ FileChooser chooser; Runnable permCallback; + Object gpService; + Class serviceClass; + @Override protected void onCreate(Bundle savedInstanceState){ UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); @@ -50,7 +55,7 @@ public class AndroidLauncher extends AndroidApplication{ }); super.onCreate(savedInstanceState); - if(doubleScaleTablets && isTablet(this.getContext())){ + if(doubleScaleTablets && isTablet(this)){ Scl.setAddition(0.5f); } @@ -63,7 +68,9 @@ public class AndroidLauncher extends AndroidApplication{ @Override public rhino.Context getScriptContext(){ - return AndroidRhinoContext.enter(getContext().getCacheDir()); + rhino.Context result = AndroidRhinoContext.enter(((Context)AndroidLauncher.this).getCacheDir()); + result.setClassShutter(Scripts::allowClass); + return result; } @Override @@ -71,8 +78,8 @@ public class AndroidLauncher extends AndroidApplication{ } @Override - public ClassLoader loadJar(Fi jar, String mainClass) throws Exception{ - return new DexClassLoader(jar.file().getPath(), getFilesDir().getPath(), null, getClassLoader()); + public ClassLoader loadJar(Fi jar, ClassLoader parent) throws Exception{ + return new DexClassLoader(jar.file().getPath(), getFilesDir().getPath(), null, parent); } @Override @@ -165,9 +172,20 @@ public class AndroidLauncher extends AndroidApplication{ try{ //new external folder - Fi data = Core.files.absolute(getContext().getExternalFilesDir(null).getAbsolutePath()); + Fi data = Core.files.absolute(((Context)this).getExternalFilesDir(null).getAbsolutePath()); Core.settings.setDataDirectory(data); + //delete unused cache folder to free up space + try{ + Fi cache = Core.settings.getDataDirectory().child("cache"); + if(cache.exists()){ + cache.deleteDirectory(); + } + }catch(Throwable t){ + Log.err("Failed to delete cached folder", t); + } + + //move to internal storage if there's no file indicating that it moved if(!Core.files.local("files_moved").exists()){ Log.info("Moving files to external storage..."); @@ -209,6 +227,24 @@ public class AndroidLauncher extends AndroidApplication{ } } + @Override + public void onResume(){ + super.onResume(); + + //TODO enable once GPGS is set up on the GP console + if(false && BuildConfig.FLAVOR.equals("gp")){ + try{ + if(gpService == null){ + serviceClass = Class.forName("mindustry.android.GPGameService"); + gpService = serviceClass.getConstructor().newInstance(); + } + serviceClass.getMethod("onResume", Context.class).invoke(gpService, this); + }catch(Exception e){ + Log.err("Failed to update Google Play Services", e); + } + } + } + private void checkFiles(Intent intent){ try{ Uri uri = intent.getData(); diff --git a/android/src/mindustry/android/AndroidRhinoContext.java b/android/src/mindustry/android/AndroidRhinoContext.java index d3e80c7427..0147f118b8 100644 --- a/android/src/mindustry/android/AndroidRhinoContext.java +++ b/android/src/mindustry/android/AndroidRhinoContext.java @@ -175,7 +175,7 @@ public class AndroidRhinoContext{ }catch(IOException e){ e.printStackTrace(); } - android.content.Context context = ((AndroidApplication) Core.app).getContext(); + android.content.Context context = (android.content.Context)((AndroidApplication)Core.app); return new DexClassLoader(dexFile.getPath(), VERSION.SDK_INT >= 21 ? context.getCodeCacheDir().getPath() : context.getCacheDir().getAbsolutePath(), null, getParent()).loadClass(name); } diff --git a/android/srcgp/mindustry/android/GPGameService.java b/android/srcgp/mindustry/android/GPGameService.java new file mode 100644 index 0000000000..9a64b6f3d4 --- /dev/null +++ b/android/srcgp/mindustry/android/GPGameService.java @@ -0,0 +1,40 @@ +package mindustry.android; + +import android.content.*; +import arc.util.*; +import com.google.android.gms.auth.api.signin.*; +import com.google.android.gms.games.*; +import mindustry.service.*; + +public class GPGameService extends GameService{ + private GoogleSignInAccount account; + + public void onResume(Context context){ + Log.info("[GooglePlayService] Resuming."); + + GoogleSignInAccount current = GoogleSignIn.getLastSignedInAccount(context); + + GoogleSignInOptions options = + new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN) + .requestScopes(Games.SCOPE_GAMES_SNAPSHOTS) + .build(); + + if(GoogleSignIn.hasPermissions(current, options.getScopeArray())){ + this.account = current; + Log.info("Already signed in to Google Play Games."); + }else{ + GoogleSignIn.getClient(context, options).silentSignIn().addOnCompleteListener(complete -> { + if(!complete.isSuccessful()){ + if(complete.getException() != null){ + Log.err("Failed to sign in to Google Play Games.", complete.getException()); + }else{ + Log.warn("Failed to sign in to Google Play Games."); + } + }else{ + this.account = complete.getResult(); + Log.info("Signed in to Google Play Games."); + } + }); + } + } +} diff --git a/annotations/src/main/java/mindustry/annotations/Annotations.java b/annotations/src/main/java/mindustry/annotations/Annotations.java index 88bd457e05..9960f673eb 100644 --- a/annotations/src/main/java/mindustry/annotations/Annotations.java +++ b/annotations/src/main/java/mindustry/annotations/Annotations.java @@ -118,7 +118,7 @@ public class Annotations{ /** * The region name to load. Variables can be used: * "@" -> block name - * "$size" -> block size + * "@size" -> block size * "#" "#1" "#2" -> index number, for arrays * */ String value(); @@ -177,12 +177,12 @@ public class Annotations{ //region remote public enum PacketPriority{ + /** Does not get handled unless client is connected. */ + low, /** Gets put in a queue and processed if not connected. */ normal, /** Gets handled immediately, regardless of connection status. */ high, - /** Does not get handled unless client is connected. */ - low } /** A set of two booleans, one specifying server and one specifying client. */ diff --git a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java index 74cd4e8bde..310f8e2906 100644 --- a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java @@ -2,15 +2,10 @@ package mindustry.annotations; import arc.files.*; import arc.struct.*; -import arc.util.Log; -import arc.util.Log.*; import arc.util.*; +import arc.util.Log.*; import com.squareup.javapoet.*; import com.sun.source.util.*; -import com.sun.tools.javac.model.*; -import com.sun.tools.javac.processing.*; -import com.sun.tools.javac.tree.*; -import com.sun.tools.javac.util.*; import mindustry.annotations.util.*; import javax.annotation.processing.*; @@ -22,7 +17,6 @@ import javax.tools.Diagnostic.*; import javax.tools.*; import java.io.*; import java.lang.annotation.*; -import java.util.List; import java.util.*; @SupportedSourceVersion(SourceVersion.RELEASE_8) @@ -31,19 +25,16 @@ public abstract class BaseProcessor extends AbstractProcessor{ public static final String packageName = "mindustry.gen"; public static Types typeu; - public static JavacElements elementu; + public static Elements elementu; public static Filer filer; public static Messager messager; public static Trees trees; - public static TreeMaker maker; protected int round; protected int rounds = 1; protected RoundEnvironment env; protected Fi rootDirectory; - protected Context context; - public static String getMethodName(Element element){ return ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString() + "." + element.getSimpleName(); } @@ -186,7 +177,7 @@ public abstract class BaseProcessor extends AbstractProcessor{ Log.err("[CODEGEN ERROR] " + message + ": " + elem); } - public void err(String message, Selement elem){ + public static void err(String message, Selement elem){ err(message, elem.e); } @@ -194,15 +185,11 @@ public abstract class BaseProcessor extends AbstractProcessor{ public synchronized void init(ProcessingEnvironment env){ super.init(env); - JavacProcessingEnvironment javacProcessingEnv = (JavacProcessingEnvironment)env; - trees = Trees.instance(env); typeu = env.getTypeUtils(); - elementu = javacProcessingEnv.getElementUtils(); + elementu = env.getElementUtils(); filer = env.getFiler(); messager = env.getMessager(); - context = ((JavacProcessingEnvironment)env).getContext(); - maker = TreeMaker.instance(javacProcessingEnv.getContext()); Log.level = LogLevel.info; diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java index 6067b986b5..9612018e7b 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java @@ -132,6 +132,7 @@ public class EntityProcess extends BaseProcessor{ .build())).addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).build()); } + //generate interface getters and setters for all "standard" fields for(Svar field : component.fields().select(e -> !e.is(Modifier.STATIC) && !e.is(Modifier.PRIVATE) && !e.has(Import.class))){ String cname = field.name(); @@ -674,11 +675,28 @@ public class EntityProcess extends BaseProcessor{ //build mapping class for sync IDs TypeSpec.Builder idBuilder = TypeSpec.classBuilder("EntityMapping").addModifiers(Modifier.PUBLIC) .addField(FieldSpec.builder(TypeName.get(Prov[].class), "idMap", Modifier.PUBLIC, Modifier.STATIC).initializer("new Prov[256]").build()) + .addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(ObjectMap.class), tname(String.class), tname(Prov.class)), "nameMap", Modifier.PUBLIC, Modifier.STATIC).initializer("new ObjectMap<>()").build()) + + .addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(IntMap.class), tname(String.class)), + "customIdMap", Modifier.PUBLIC, Modifier.STATIC).initializer("new IntMap<>()").build()) + + .addMethod(MethodSpec.methodBuilder("register").addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(TypeName.get(int.class)) + .addParameter(String.class, "name").addParameter(Prov.class, "constructor") + .addStatement("int next = arc.util.Structs.indexOf(idMap, v -> v == null)") + .addStatement("idMap[next] = constructor") + .addStatement("nameMap.put(name, constructor)") + .addStatement("customIdMap.put(next, name)") + .addStatement("return next") + .addJavadoc("Use this method for obtaining a classId for custom modded unit types. Only call this once for each type. Modded types should return this id in their overridden classId method.") + .build()) + .addMethod(MethodSpec.methodBuilder("map").addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(TypeName.get(Prov.class)).addParameter(int.class, "id").addStatement("return idMap[id]").build()) + .addMethod(MethodSpec.methodBuilder("map").addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(TypeName.get(Prov.class)).addParameter(String.class, "name").addStatement("return nameMap.get(name)").build()); @@ -707,11 +725,6 @@ public class EntityProcess extends BaseProcessor{ }else{ //round 3: generate actual classes and implement interfaces - //write base classes - for(TypeSpec.Builder b : baseClasses){ - write(b, imports.asArray()); - } - //implement each definition for(EntityDefinition def : definitions){ @@ -736,6 +749,12 @@ public class EntityProcess extends BaseProcessor{ if(def.legacy) continue; + @Nullable TypeSpec.Builder superclass = null; + + if(def.extend != null){ + superclass = baseClasses.find(b -> (packageName + "." + Reflect.get(b, "name")).equals(def.extend.toString())); + } + //generate getter/setter for each method for(Smethod method : inter.methods()){ String var = method.name(); @@ -743,14 +762,36 @@ public class EntityProcess extends BaseProcessor{ //make sure it's a real variable AND that the component doesn't already implement it somewhere with custom logic if(field == null || methodNames.contains(method.simpleString())) continue; + MethodSpec result = null; + //getter if(!method.isVoid()){ - def.builder.addMethod(MethodSpec.overriding(method.e).addStatement("return " + var).build()); + result = MethodSpec.overriding(method.e).addStatement("return " + var).build(); } //setter if(method.isVoid() && !Seq.with(field.annotations).contains(f -> f.type.toString().equals("@mindustry.annotations.Annotations.ReadOnly"))){ - def.builder.addMethod(MethodSpec.overriding(method.e).addStatement("this." + var + " = " + var).build()); + result = MethodSpec.overriding(method.e).addStatement("this." + var + " = " + var).build(); + } + + //add getter/setter to parent class, if possible. when this happens, skip adding getters setters *here* because they are defined in the superclass. + if(result != null && superclass != null){ + FieldSpec superField = Seq.with(superclass.fieldSpecs).find(f -> f.name.equals(var)); + + //found the right field, try to check for the method already existing now + if(superField != null){ + MethodSpec fr = result; + MethodSpec targetMethod = Seq.with(superclass.methodSpecs).find(m -> m.name.equals(var) && m.returnType.equals(fr.returnType)); + //if the method isn't added yet, add it. in any case, skip. + if(targetMethod == null){ + superclass.addMethod(result); + } + continue; + } + } + + if(result != null){ + def.builder.addMethod(result); } } } @@ -758,9 +799,16 @@ public class EntityProcess extends BaseProcessor{ write(def.builder, imports.asArray()); } + //write base classes last + for(TypeSpec.Builder b : baseClasses){ + write(b, imports.asArray()); + } + + //TODO nulls were an awful idea //store nulls TypeSpec.Builder nullsBuilder = TypeSpec.classBuilder("Nulls").addModifiers(Modifier.PUBLIC).addModifiers(Modifier.FINAL); - ObjectSet nullList = ObjectSet.with("unit", "blockUnit"); + //TODO should be dynamic + ObjectSet nullList = ObjectSet.with("unit"); //create mock types of all components for(Stype interf : allInterfaces){ @@ -918,7 +966,7 @@ public class EntityProcess extends BaseProcessor{ } String createName(Selement elem){ - Seq comps = types(elem.annotation(EntityDef.class), EntityDef::value).map(this::interfaceToComp);; + Seq comps = types(elem.annotation(EntityDef.class), EntityDef::value).map(this::interfaceToComp); comps.sortComparing(Selement::name); return comps.toString("", s -> s.name().replace("Comp", "")); } diff --git a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java index 0b5620c11d..c144abc534 100644 --- a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java +++ b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java @@ -43,7 +43,7 @@ public class AssetsProcess extends BaseProcessor{ texIcons.each((key, val) -> { String[] split = val.split("\\|"); - String name = Strings.kebabToCamel(split[1]).replace("Medium", "").replace("Icon", ""); + String name = Strings.kebabToCamel(split[1]).replace("Medium", "").replace("Icon", "").replace("Ui", ""); if(SourceVersion.isKeyword(name) || name.equals("char")) name += "i"; 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()); diff --git a/annotations/src/main/java/mindustry/annotations/impl/CallSuperProcess.java b/annotations/src/main/java/mindustry/annotations/impl/CallSuperProcess.java deleted file mode 100644 index e28ecf13a0..0000000000 --- a/annotations/src/main/java/mindustry/annotations/impl/CallSuperProcess.java +++ /dev/null @@ -1,154 +0,0 @@ -package mindustry.annotations.impl; - -import com.sun.source.tree.*; -import com.sun.source.util.*; -import com.sun.tools.javac.code.Scope; -import com.sun.tools.javac.code.*; -import com.sun.tools.javac.code.Symbol.*; -import com.sun.tools.javac.code.Type.*; -import com.sun.tools.javac.tree.*; -import com.sun.tools.javac.tree.JCTree.*; -import mindustry.annotations.Annotations.*; - -import javax.annotation.processing.*; -import javax.lang.model.*; -import javax.lang.model.element.*; -import javax.tools.Diagnostic.*; -import java.lang.annotation.*; -import java.util.*; - -@SupportedAnnotationTypes({"java.lang.Override"}) -public class CallSuperProcess extends AbstractProcessor{ - private Trees trees; - - @Override - public void init(ProcessingEnvironment pe){ - super.init(pe); - trees = Trees.instance(pe); - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv){ - for(Element e : roundEnv.getElementsAnnotatedWith(Override.class)){ - if(e.getAnnotation(OverrideCallSuper.class) != null) return false; - - CodeAnalyzerTreeScanner codeScanner = new CodeAnalyzerTreeScanner(); - codeScanner.methodName = e.getSimpleName().toString(); - - TreePath tp = trees.getPath(e.getEnclosingElement()); - codeScanner.scan(tp, trees); - - if(codeScanner.callSuperUsed){ - List list = codeScanner.method.getBody().getStatements(); - - if(!doesCallSuper(list, codeScanner.methodName)){ - processingEnv.getMessager().printMessage(Kind.ERROR, "Overriding method '" + codeScanner.methodName + "' must explicitly call super method from its parent class.", e); - } - } - } - - return false; - } - - private boolean doesCallSuper(List list, String methodName){ - for(Object object : list){ - if(object instanceof JCTree.JCExpressionStatement){ - JCTree.JCExpressionStatement expr = (JCExpressionStatement)object; - String exprString = expr.toString(); - if(exprString.startsWith("super." + methodName) && exprString.endsWith(");")) return true; - } - } - - return false; - } - - @Override - public SourceVersion getSupportedSourceVersion(){ - return SourceVersion.RELEASE_8; - } - - static class CodeAnalyzerTreeScanner extends TreePathScanner{ - String methodName; - MethodTree method; - boolean callSuperUsed; - - @Override - public Object visitClass(ClassTree classTree, Trees trees){ - Tree extendTree = classTree.getExtendsClause(); - - if(extendTree instanceof JCTypeApply){ //generic classes case - JCTypeApply generic = (JCTypeApply)extendTree; - extendTree = generic.clazz; - } - - if(extendTree instanceof JCIdent){ - JCIdent tree = (JCIdent)extendTree; - - if(tree == null || tree.sym == null) return super.visitClass(classTree, trees); - - com.sun.tools.javac.code.Scope members = tree.sym.members(); - - if(checkScope(members)) - return super.visitClass(classTree, trees); - - if(checkSuperTypes((ClassType)tree.type)) - return super.visitClass(classTree, trees); - - } - callSuperUsed = false; - - return super.visitClass(classTree, trees); - } - - public boolean checkSuperTypes(ClassType type){ - if(type.supertype_field != null && type.supertype_field.tsym != null){ - if(checkScope(type.supertype_field.tsym.members())) - return true; - else - return checkSuperTypes((ClassType)type.supertype_field); - } - - return false; - } - - @SuppressWarnings("unchecked") - public boolean checkScope(Scope members){ - Iterable it; - try{ - it = (Iterable)members.getClass().getMethod("getElements").invoke(members); - }catch(Throwable t){ - try{ - it = (Iterable)members.getClass().getMethod("getSymbols").invoke(members); - }catch(Exception e){ - throw new RuntimeException(e); - } - } - - for(Symbol s : it){ - - if(s instanceof MethodSymbol){ - MethodSymbol ms = (MethodSymbol)s; - - if(ms.getSimpleName().toString().equals(methodName)){ - Annotation annotation = ms.getAnnotation(CallSuper.class); - if(annotation != null){ - callSuperUsed = true; - return true; - } - } - } - } - - return false; - } - - @Override - public Object visitMethod(MethodTree methodTree, Trees trees){ - if(methodTree.getName().toString().equals(methodName)) - method = methodTree; - - return super.visitMethod(methodTree, trees); - } - - } -} diff --git a/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java b/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java index d2e3e99c17..7002972345 100644 --- a/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java @@ -18,6 +18,7 @@ public class LoadRegionProcessor extends BaseProcessor{ @Override public void process(RoundEnvironment env) throws Exception{ TypeSpec.Builder regionClass = TypeSpec.classBuilder("ContentRegions") + .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "\"deprecation\"").build()) .addModifiers(Modifier.PUBLIC); MethodSpec.Builder method = MethodSpec.methodBuilder("loadRegions") .addParameter(tname("mindustry.ctype.MappableContent"), "content") @@ -34,7 +35,7 @@ public class LoadRegionProcessor extends BaseProcessor{ } for(Entry> entry : fieldMap){ - method.beginControlFlow("if(content instanceof $T)", entry.key.tname()); + method.beginControlFlow("if(content instanceof $L)", entry.key.fullName()); for(Svar field : entry.value){ Load an = field.annotation(Load.class); @@ -45,7 +46,7 @@ public class LoadRegionProcessor extends BaseProcessor{ //not an array if(dims == 0){ - method.addStatement("(($T)content).$L = $T.atlas.find($L$L)", entry.key.tname(), field.name(), Core.class, parse(an.value()), fallbackString); + method.addStatement("(($L)content).$L = $T.atlas.find($L$L)", entry.key.fullName(), field.name(), Core.class, parse(an.value()), fallbackString); }else{ //is an array, create length string int[] lengths = an.lengths(); diff --git a/annotations/src/main/java/mindustry/annotations/remote/CallGenerator.java b/annotations/src/main/java/mindustry/annotations/remote/CallGenerator.java new file mode 100644 index 0000000000..00ac5fabac --- /dev/null +++ b/annotations/src/main/java/mindustry/annotations/remote/CallGenerator.java @@ -0,0 +1,363 @@ +package mindustry.annotations.remote; + +import arc.struct.*; +import arc.util.io.*; +import com.squareup.javapoet.*; +import mindustry.annotations.Annotations.*; +import mindustry.annotations.*; +import mindustry.annotations.util.*; +import mindustry.annotations.util.TypeIOResolver.*; + +import javax.lang.model.element.*; +import java.io.*; + +import static mindustry.annotations.BaseProcessor.*; + +/** Generates code for writing remote invoke packets on the client and server. */ +public class CallGenerator{ + + /** Generates all classes in this list. */ + public static void generate(ClassSerializer serializer, Seq methods) throws IOException{ + //create builder + TypeSpec.Builder callBuilder = TypeSpec.classBuilder(RemoteProcess.callLocation).addModifiers(Modifier.PUBLIC); + + MethodSpec.Builder register = MethodSpec.methodBuilder("registerPackets") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC); + + //go through each method entry in this class + for(MethodEntry ent : methods){ + //builder for the packet type + TypeSpec.Builder packet = TypeSpec.classBuilder(ent.packetClassName) + .addModifiers(Modifier.PUBLIC); + + packet.superclass(tname("mindustry.net.Packet")); + + //return the correct priority + if(ent.priority != PacketPriority.normal){ + packet.addMethod(MethodSpec.methodBuilder("getPriority") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class).returns(int.class).addStatement("return $L", ent.priority.ordinal()) + .build()); + } + + //implement read & write methods + packet.addMethod(makeWriter(ent, serializer)); + packet.addMethod(makeReader(ent, serializer)); + + //generate handlers + if(ent.where.isClient){ + packet.addMethod(writeHandleMethod(ent, false)); + } + + if(ent.where.isServer){ + packet.addMethod(writeHandleMethod(ent, true)); + } + + //register packet + register.addStatement("mindustry.net.Net.registerPacket($L.$L::new)", packageName, ent.packetClassName); + + //add fields to the type + for(Svar param : ent.element.params()){ + packet.addField(param.tname(), param.name(), Modifier.PUBLIC); + } + + //write the 'send event to all players' variant: always happens for clients, but only happens if 'all' is enabled on the server method + if(ent.where.isClient || ent.target.isAll){ + writeCallMethod(callBuilder, ent, true, false); + } + + //write the 'send event to one player' variant, which is only applicable on the server + if(ent.where.isServer && ent.target.isOne){ + writeCallMethod(callBuilder, ent, false, false); + } + + //write the forwarded method version + if(ent.where.isServer && ent.forward){ + writeCallMethod(callBuilder, ent, true, true); + } + + //write the completed packet class + JavaFile.builder(packageName, packet.build()).build().writeTo(BaseProcessor.filer); + } + + callBuilder.addMethod(register.build()); + + //build and write resulting class + TypeSpec spec = callBuilder.build(); + JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); + } + + private static MethodSpec makeWriter(MethodEntry ent, ClassSerializer serializer){ + MethodSpec.Builder builder = MethodSpec.methodBuilder("write") + .addParameter(Writes.class, "WRITE") + .addModifiers(Modifier.PUBLIC).addAnnotation(Override.class); + Seq params = ent.element.params(); + + for(int i = 0; i < params.size; i++){ + //first argument is skipped as it is always the player caller + if(!ent.where.isServer && i == 0){ + continue; + } + + Svar var = params.get(i); + + //name of parameter + String varName = var.name(); + //name of parameter type + String typeName = var.mirror().toString(); + //special case: method can be called from anywhere to anywhere + //thus, only write the player when the SERVER is writing data, since the client is the only one who reads the player anyway + boolean writePlayerSkipCheck = ent.where == Loc.both && i == 0; + + if(writePlayerSkipCheck){ //write begin check + builder.beginControlFlow("if(mindustry.Vars.net.server())"); + } + + if(BaseProcessor.isPrimitive(typeName)){ //check if it's a primitive, and if so write it + builder.addStatement("WRITE.$L($L)", typeName.equals("boolean") ? "bool" : typeName.charAt(0) + "", varName); + }else{ + //else, try and find a serializer + String ser = serializer.writers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(ent.element.e, var.mirror(), true)); + + if(ser == null){ //make sure a serializer exists! + BaseProcessor.err("No method to write class type: '" + typeName + "'", var); + } + + //add statement for writing it + builder.addStatement(ser + "(WRITE, " + varName + ")"); + } + + if(writePlayerSkipCheck){ //write end check + builder.endControlFlow(); + } + } + + return builder.build(); + } + + private static MethodSpec makeReader(MethodEntry ent, ClassSerializer serializer){ + MethodSpec.Builder builder = MethodSpec.methodBuilder("read") + .addParameter(Reads.class, "READ") + .addModifiers(Modifier.PUBLIC).addAnnotation(Override.class); + Seq params = ent.element.params(); + + //go through each parameter + for(int i = 0; i < params.size; i++){ + Svar var = params.get(i); + + //first argument is skipped as it is always the player caller + if(!ent.where.isServer && i == 0){ + continue; + } + + //special case: method can be called from anywhere to anywhere + //thus, only read the player when the CLIENT is receiving data, since the client is the only one who cares about the player anyway + boolean writePlayerSkipCheck = ent.where == Loc.both && i == 0; + + if(writePlayerSkipCheck){ //write begin check + builder.beginControlFlow("if(mindustry.Vars.net.client())"); + } + + //full type name of parameter + String typeName = var.mirror().toString(); + //name of parameter + String varName = var.name(); + //capitalized version of type name for reading primitives + String pname = typeName.equals("boolean") ? "bool" : typeName.charAt(0) + ""; + + //write primitives automatically + if(BaseProcessor.isPrimitive(typeName)){ + builder.addStatement("$L = READ.$L()", varName, pname); + }else{ + //else, try and find a serializer + String ser = serializer.readers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(ent.element.e, var.mirror(), false)); + + if(ser == null){ //make sure a serializer exists! + BaseProcessor.err("No read method to read class type '" + typeName + "' in method " + ent.targetMethod + "; " + serializer.readers, var); + } + + //add statement for reading it + builder.addStatement("$L = $L(READ)", varName, ser); + } + + if(writePlayerSkipCheck){ //write end check + builder.endControlFlow(); + } + } + + return builder.build(); + } + + /** Creates a specific variant for a method entry. */ + private static void writeCallMethod(TypeSpec.Builder classBuilder, MethodEntry ent, boolean toAll, boolean forwarded){ + Smethod elem = ent.element; + Seq params = elem.params(); + + //create builder + MethodSpec.Builder method = MethodSpec.methodBuilder(elem.name() + (forwarded ? "__forward" : "")) //add except suffix when forwarding + .addModifiers(Modifier.STATIC) + .returns(void.class); + + //forwarded methods aren't intended for use, and are not public + if(!forwarded){ + method.addModifiers(Modifier.PUBLIC); + } + + //validate client methods to make sure + if(ent.where.isClient){ + if(params.isEmpty()){ + BaseProcessor.err("Client invoke methods must have a first parameter of type Player", elem); + return; + } + + if(!params.get(0).mirror().toString().contains("Player")){ + BaseProcessor.err("Client invoke methods should have a first parameter of type Player", elem); + return; + } + } + + //if toAll is false, it's a 'send to one player' variant, so add the player as a parameter + if(!toAll){ + method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "playerConnection"); + } + + //add sender to ignore + if(forwarded){ + method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "exceptConnection"); + } + + //call local method if applicable, shouldn't happen when forwarding method as that already happens by default + if(!forwarded && ent.local != Loc.none){ + //add in local checks + if(ent.local != Loc.both){ + method.beginControlFlow("if(" + getCheckString(ent.local) + " || !mindustry.Vars.net.active())"); + } + + //concatenate parameters + int index = 0; + StringBuilder results = new StringBuilder(); + for(Svar var : params){ + //special case: calling local-only methods uses the local player + if(index == 0 && ent.where == Loc.client){ + results.append("mindustry.Vars.player"); + }else{ + results.append(var.name()); + } + if(index != params.size - 1) results.append(", "); + index++; + } + + //add the statement to call it + method.addStatement("$N." + elem.name() + "(" + results + ")", + ((TypeElement)elem.up()).getQualifiedName().toString()); + + if(ent.local != Loc.both){ + method.endControlFlow(); + } + } + + //start control flow to check if it's actually client/server so no netcode is called + method.beginControlFlow("if(" + getCheckString(ent.where) + ")"); + + //add statement to create packet from pool + method.addStatement("$1T packet = new $1T()", tname("mindustry.gen." + ent.packetClassName)); + + method.addTypeVariables(Seq.with(elem.e.getTypeParameters()).map(BaseProcessor::getTVN)); + + for(int i = 0; i < params.size; i++){ + //first argument is skipped as it is always the player caller + if((!ent.where.isServer) && i == 0){ + continue; + } + + Svar var = params.get(i); + + method.addParameter(var.tname(), var.name()); + + //name of parameter + String varName = var.name(); + //special case: method can be called from anywhere to anywhere + //thus, only write the player when the SERVER is writing data, since the client is the only one who reads it + boolean writePlayerSkipCheck = ent.where == Loc.both && i == 0; + + if(writePlayerSkipCheck){ //write begin check + method.beginControlFlow("if(mindustry.Vars.net.server())"); + } + + method.addStatement("packet.$L = $L", varName, varName); + + if(writePlayerSkipCheck){ //write end check + method.endControlFlow(); + } + } + + String sendString; + + if(forwarded){ //forward packet + if(!ent.local.isClient){ //if the client doesn't get it called locally, forward it back after validation + sendString = "mindustry.Vars.net.send("; + }else{ + sendString = "mindustry.Vars.net.sendExcept(exceptConnection, "; + } + }else if(toAll){ //send to all players / to server + sendString = "mindustry.Vars.net.send("; + }else{ //send to specific client from server + sendString = "playerConnection.send("; + } + + //send the actual packet + method.addStatement(sendString + "packet, " + (!ent.unreliable) + ")"); + + + //end check for server/client + method.endControlFlow(); + + //add method to class, finally + classBuilder.addMethod(method.build()); + } + + private static String getCheckString(Loc loc){ + return + loc.isClient && loc.isServer ? "mindustry.Vars.net.server() || mindustry.Vars.net.client()" : + loc.isClient ? "mindustry.Vars.net.client()" : + loc.isServer ? "mindustry.Vars.net.server()" : "false"; + } + + /** Generates handleServer / handleClient methods. */ + public static MethodSpec writeHandleMethod(MethodEntry ent, boolean isClient){ + + //create main method builder + MethodSpec.Builder builder = MethodSpec.methodBuilder(isClient ? "handleClient" : "handleServer") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(void.class); + + Smethod elem = ent.element; + Seq params = elem.params(); + + if(!isClient){ + //add player parameter + builder.addParameter(ClassName.get("mindustry.net", "NetConnection"), "con"); + + //skip if player is invalid + builder.beginControlFlow("if(con.player == null || con.kicked)"); + builder.addStatement("return"); + builder.endControlFlow(); + + //make sure to use the actual player who sent the packet + builder.addStatement("mindustry.gen.Player player = con.player"); + } + + //execute the relevant method before the forward + //if it throws a ValidateException, the method won't be forwarded + builder.addStatement("$N." + elem.name() + "(" + params.toString(", ", s -> s.name()) + ")", ((TypeElement)elem.up()).getQualifiedName().toString()); + + //call forwarded method, don't forward on the client reader + if(ent.forward && ent.where.isServer && !isClient){ + //call forwarded method + builder.addStatement("$L.$L.$L__forward(con, $L)", packageName, ent.className, elem.name(), params.toString(", ", s -> s.name())); + } + + return builder.build(); + } +} diff --git a/annotations/src/main/java/mindustry/annotations/remote/ClassEntry.java b/annotations/src/main/java/mindustry/annotations/remote/ClassEntry.java deleted file mode 100644 index 3474eff468..0000000000 --- a/annotations/src/main/java/mindustry/annotations/remote/ClassEntry.java +++ /dev/null @@ -1,15 +0,0 @@ -package mindustry.annotations.remote; - -import java.util.ArrayList; - -/** Represents a class witha list method entries to include in it. */ -public class ClassEntry{ - /** All methods in this generated class. */ - public final ArrayList methods = new ArrayList<>(); - /** Simple class name. */ - public final String name; - - public ClassEntry(String name){ - this.name = name; - } -} diff --git a/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java b/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java index 68ea81dec0..768b373387 100644 --- a/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java +++ b/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java @@ -1,8 +1,7 @@ package mindustry.annotations.remote; import mindustry.annotations.Annotations.*; - -import javax.lang.model.element.ExecutableElement; +import mindustry.annotations.util.*; /** Class that repesents a remote method to be constructed and put into a class. */ public class MethodEntry{ @@ -10,6 +9,8 @@ public class MethodEntry{ public final String className; /** Fully qualified target method to call. */ public final String targetMethod; + /** Simple name of the generated packet class. */ + public final String packetClassName; /** Whether this method can be called on a client/server. */ public final Loc where; /** @@ -26,12 +27,13 @@ public class MethodEntry{ /** Unique method ID. */ public final int id; /** The element method associated with this entry. */ - public final ExecutableElement element; + public final Smethod element; /** The assigned packet priority. Only used in clients. */ public final PacketPriority priority; - public MethodEntry(String className, String targetMethod, Loc where, Variant target, - Loc local, boolean unreliable, boolean forward, int id, ExecutableElement element, PacketPriority priority){ + public MethodEntry(String className, String targetMethod, String packetClassName, Loc where, Variant target, + Loc local, boolean unreliable, boolean forward, int id, Smethod element, PacketPriority priority){ + this.packetClassName = packetClassName; this.className = className; this.forward = forward; this.targetMethod = targetMethod; diff --git a/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java b/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java index 916bb3f278..ceceddb8a8 100644 --- a/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java +++ b/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java @@ -1,7 +1,7 @@ package mindustry.annotations.remote; import arc.struct.*; -import com.squareup.javapoet.*; +import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.annotations.*; import mindustry.annotations.util.*; @@ -9,7 +9,6 @@ import mindustry.annotations.util.TypeIOResolver.*; import javax.annotation.processing.*; import javax.lang.model.element.*; -import java.util.*; /** The annotation processor for generating remote method call code. */ @@ -18,106 +17,58 @@ import java.util.*; "mindustry.annotations.Annotations.TypeIOHandler" }) public class RemoteProcess extends BaseProcessor{ - /** Maximum size of each event packet. */ - public static final int maxPacketSize = 8192; - /** Warning on top of each autogenerated file. */ - public static final String autogenWarning = "Autogenerated file. Do not modify!\n"; - - /** Name of class that handles reading and invoking packets on the server. */ - private static final String readServerName = "RemoteReadServer"; - /** Name of class that handles reading and invoking packets on the client. */ - private static final String readClientName = "RemoteReadClient"; /** Simple class name of generated class name. */ - private static final String callLocation = "Call"; - - //class serializers - private ClassSerializer serializer; - //all elements with the Remote annotation - private Seq elements; - //map of all classes to generate by name - private HashMap classMap; - //list of all method entries - private Seq methods; - //list of all method entries - private Seq classes; - - { - rounds = 2; - } + public static final String callLocation = "Call"; @Override public void process(RoundEnvironment roundEnv) throws Exception{ - //round 1: find all annotations, generate *writers* - if(round == 1){ - //get serializers - serializer = TypeIOResolver.resolve(this); - //last method ID used - int lastMethodID = 0; - //find all elements with the Remote annotation - elements = methods(Remote.class); - //map of all classes to generate by name - classMap = new HashMap<>(); - //list of all method entries - methods = new Seq<>(); - //list of all method entries - classes = new Seq<>(); + //get serializers + //class serializers + ClassSerializer serializer = TypeIOResolver.resolve(this); + //last method ID used + int lastMethodID = 0; + //find all elements with the Remote annotation + //all elements with the Remote annotation + Seq elements = methods(Remote.class); + //list of all method entries + Seq methods = new Seq<>(); - Seq orderedElements = elements.copy(); - orderedElements.sort((a, b) -> -a.toString().compareTo(b.toString())); + Seq orderedElements = elements.copy(); + orderedElements.sortComparing(Selement::toString); - //create methods - for(Smethod element : orderedElements){ - Remote annotation = element.annotation(Remote.class); + //create methods + for(Smethod element : orderedElements){ + Remote annotation = element.annotation(Remote.class); - //check for static - if(!element.is(Modifier.STATIC) || !element.is(Modifier.PUBLIC)){ - err("All @Remote methods must be public and static", element); - } - - //can't generate none methods - if(annotation.targets() == Loc.none){ - err("A @Remote method's targets() cannot be equal to 'none'", element); - } - - //get and create class entry if needed - if(!classMap.containsKey(callLocation)){ - ClassEntry clas = new ClassEntry(callLocation); - classMap.put(callLocation, clas); - classes.add(clas); - } - - ClassEntry entry = classMap.get(callLocation); - - //create and add entry - MethodEntry method = new MethodEntry(entry.name, BaseProcessor.getMethodName(element.e), annotation.targets(), annotation.variants(), - annotation.called(), annotation.unreliable(), annotation.forward(), lastMethodID++, element.e, annotation.priority()); - - entry.methods.add(method); - methods.add(method); + //check for static + if(!element.is(Modifier.STATIC) || !element.is(Modifier.PUBLIC)){ + err("All @Remote methods must be public and static", element); } - //create read/write generators - RemoteWriteGenerator writegen = new RemoteWriteGenerator(serializer); + //can't generate none methods + if(annotation.targets() == Loc.none){ + err("A @Remote method's targets() cannot be equal to 'none'", element); + } - //generate the methods to invoke (write) - writegen.generateFor(classes, packageName); - }else if(round == 2){ //round 2: generate all *readers* - RemoteReadGenerator readgen = new RemoteReadGenerator(serializer); + String packetName = Strings.capitalize(element.name()) + "CallPacket"; + int[] index = {1}; - //generate server readers - readgen.generateFor(methods.select(method -> method.where.isClient), readServerName, packageName, true); - //generate client readers - readgen.generateFor(methods.select(method -> method.where.isServer), readClientName, packageName, false); + while(methods.contains(m -> m.packetClassName.equals(packetName + (index[0] == 1 ? "" : index[0])))){ + index[0] ++; + } - //create class for storing unique method hash - TypeSpec.Builder hashBuilder = TypeSpec.classBuilder("MethodHash").addModifiers(Modifier.PUBLIC); - hashBuilder.addJavadoc(autogenWarning); - hashBuilder.addField(FieldSpec.builder(int.class, "HASH", Modifier.STATIC, Modifier.PUBLIC, Modifier.FINAL) - .initializer("$1L", Arrays.hashCode(methods.map(m -> m.element).toArray())).build()); + //create and add entry + MethodEntry method = new MethodEntry( + callLocation, BaseProcessor.getMethodName(element.e), packetName + (index[0] == 1 ? "" : index[0]), + annotation.targets(), annotation.variants(), + annotation.called(), annotation.unreliable(), annotation.forward(), lastMethodID++, + element, annotation.priority() + ); - //build and write resulting hash class - TypeSpec spec = hashBuilder.build(); - JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); + methods.add(method); } + + //generate the methods to invoke, as well as the packet classes + CallGenerator.generate(serializer, methods); } } diff --git a/annotations/src/main/java/mindustry/annotations/remote/RemoteReadGenerator.java b/annotations/src/main/java/mindustry/annotations/remote/RemoteReadGenerator.java deleted file mode 100644 index 3442843669..0000000000 --- a/annotations/src/main/java/mindustry/annotations/remote/RemoteReadGenerator.java +++ /dev/null @@ -1,129 +0,0 @@ -package mindustry.annotations.remote; - -import arc.struct.*; -import arc.util.io.*; -import com.squareup.javapoet.*; -import mindustry.annotations.*; -import mindustry.annotations.util.TypeIOResolver.*; - -import javax.lang.model.element.*; - -/** Generates code for reading remote invoke packets on the client and server. */ -public class RemoteReadGenerator{ - private final ClassSerializer serializers; - - /** Creates a read generator that uses the supplied serializer setup. */ - public RemoteReadGenerator(ClassSerializer serializers){ - this.serializers = serializers; - } - - /** - * Generates a class for reading remote invoke packets. - * @param entries List of methods to use. - * @param className Simple target class name. - * @param packageName Full target package name. - * @param needsPlayer Whether this read method requires a reference to the player sender. - */ - public void generateFor(Seq entries, String className, String packageName, boolean needsPlayer) throws Exception{ - - TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC); - classBuilder.addJavadoc(RemoteProcess.autogenWarning); - - //create main method builder - MethodSpec.Builder readMethod = MethodSpec.methodBuilder("readPacket") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addParameter(Reads.class, "read") //buffer to read form - .addParameter(int.class, "id") //ID of method type to read - .returns(void.class); - - if(needsPlayer){ - //add player parameter - readMethod.addParameter(ClassName.get(packageName, "Player"), "player"); - } - - CodeBlock.Builder readBlock = CodeBlock.builder(); //start building block of code inside read method - boolean started = false; //whether an if() statement has been written yet - - for(MethodEntry entry : entries){ - //write if check for this entry ID - if(!started){ - started = true; - readBlock.beginControlFlow("if(id == " + entry.id + ")"); - }else{ - readBlock.nextControlFlow("else if(id == " + entry.id + ")"); - } - - readBlock.beginControlFlow("try"); - - //concatenated list of variable names for method invocation - StringBuilder varResult = new StringBuilder(); - - //go through each parameter - for(int i = 0; i < entry.element.getParameters().size(); i++){ - VariableElement var = entry.element.getParameters().get(i); - - if(!needsPlayer || i != 0){ //if client, skip first parameter since it's always of type player and doesn't need to be read - //full type name of parameter - String typeName = var.asType().toString(); - //name of parameter - String varName = var.getSimpleName().toString(); - //captialized version of type name for reading primitives - String pname = typeName.equals("boolean") ? "bool" : typeName.charAt(0) + ""; - - //write primitives automatically - if(BaseProcessor.isPrimitive(typeName)){ - readBlock.addStatement("$L $L = read.$L()", typeName, varName, pname); - }else{ - //else, try and find a serializer - String ser = serializers.readers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(entry.element, var.asType(), false)); - - if(ser == null){ //make sure a serializer exists! - BaseProcessor.err("No read method to read class type '" + typeName + "' in method " + entry.targetMethod + "; " + serializers.readers, var); - return; - } - - //add statement for reading it - readBlock.addStatement(typeName + " " + varName + " = " + ser + "(read)"); - } - - //append variable name to string builder - varResult.append(var.getSimpleName()); - if(i != entry.element.getParameters().size() - 1) varResult.append(", "); - }else{ - varResult.append("player"); - if(i != entry.element.getParameters().size() - 1) varResult.append(", "); - } - } - - //execute the relevant method before the forward - //if it throws a ValidateException, the method won't be forwarded - readBlock.addStatement("$N." + entry.element.getSimpleName() + "(" + varResult.toString() + ")", ((TypeElement)entry.element.getEnclosingElement()).getQualifiedName().toString()); - - //call forwarded method, don't forward on the client reader - if(entry.forward && entry.where.isServer && needsPlayer){ - //call forwarded method - readBlock.addStatement(packageName + "." + entry.className + "." + entry.element.getSimpleName() + - "__forward(player.con" + (varResult.length() == 0 ? "" : ", ") + varResult.toString() + ")"); - } - - readBlock.nextControlFlow("catch (java.lang.Exception e)"); - readBlock.addStatement("throw new java.lang.RuntimeException(\"Failed to read remote method '" + entry.element.getSimpleName() + "'!\", e)"); - readBlock.endControlFlow(); - } - - //end control flow if necessary - if(started){ - readBlock.nextControlFlow("else"); - readBlock.addStatement("throw new $1N(\"Invalid read method ID: \" + id + \"\")", RuntimeException.class.getName()); //handle invalid method IDs - readBlock.endControlFlow(); - } - - //add block and method to class - readMethod.addCode(readBlock.build()); - classBuilder.addMethod(readMethod.build()); - - //build and write resulting class - TypeSpec spec = classBuilder.build(); - JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); - } -} diff --git a/annotations/src/main/java/mindustry/annotations/remote/RemoteWriteGenerator.java b/annotations/src/main/java/mindustry/annotations/remote/RemoteWriteGenerator.java deleted file mode 100644 index 606fe513bd..0000000000 --- a/annotations/src/main/java/mindustry/annotations/remote/RemoteWriteGenerator.java +++ /dev/null @@ -1,228 +0,0 @@ -package mindustry.annotations.remote; - -import arc.struct.*; -import arc.util.io.*; -import com.squareup.javapoet.*; -import mindustry.annotations.Annotations.*; -import mindustry.annotations.*; -import mindustry.annotations.util.TypeIOResolver.*; - -import javax.lang.model.element.*; -import java.io.*; - -/** Generates code for writing remote invoke packets on the client and server. */ -public class RemoteWriteGenerator{ - private final ClassSerializer serializers; - - /** Creates a write generator that uses the supplied serializer setup. */ - public RemoteWriteGenerator(ClassSerializer serializers){ - this.serializers = serializers; - } - - /** Generates all classes in this list. */ - public void generateFor(Seq entries, String packageName) throws IOException{ - - for(ClassEntry entry : entries){ - //create builder - TypeSpec.Builder classBuilder = TypeSpec.classBuilder(entry.name).addModifiers(Modifier.PUBLIC); - classBuilder.addJavadoc(RemoteProcess.autogenWarning); - - //add temporary write buffer - classBuilder.addField(FieldSpec.builder(ReusableByteOutStream.class, "OUT", Modifier.STATIC, Modifier.PRIVATE, Modifier.FINAL) - .initializer("new ReusableByteOutStream($L)", RemoteProcess.maxPacketSize).build()); - - //add writer for that buffer - classBuilder.addField(FieldSpec.builder(Writes.class, "WRITE", Modifier.STATIC, Modifier.PRIVATE, Modifier.FINAL) - .initializer("new Writes(new $T(OUT))", DataOutputStream.class).build()); - - //go through each method entry in this class - for(MethodEntry methodEntry : entry.methods){ - //write the 'send event to all players' variant: always happens for clients, but only happens if 'all' is enabled on the server method - if(methodEntry.where.isClient || methodEntry.target.isAll){ - writeMethodVariant(classBuilder, methodEntry, true, false); - } - - //write the 'send event to one player' variant, which is only applicable on the server - if(methodEntry.where.isServer && methodEntry.target.isOne){ - writeMethodVariant(classBuilder, methodEntry, false, false); - } - - //write the forwarded method version - if(methodEntry.where.isServer && methodEntry.forward){ - writeMethodVariant(classBuilder, methodEntry, true, true); - } - } - - //build and write resulting class - TypeSpec spec = classBuilder.build(); - JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); - } - } - - /** Creates a specific variant for a method entry. */ - private void writeMethodVariant(TypeSpec.Builder classBuilder, MethodEntry methodEntry, boolean toAll, boolean forwarded){ - ExecutableElement elem = methodEntry.element; - - //create builder - MethodSpec.Builder method = MethodSpec.methodBuilder(elem.getSimpleName().toString() + (forwarded ? "__forward" : "")) //add except suffix when forwarding - .addModifiers(Modifier.STATIC) - .returns(void.class); - - //forwarded methods aren't intended for use, and are not public - if(!forwarded){ - method.addModifiers(Modifier.PUBLIC); - } - - //validate client methods to make sure - if(methodEntry.where.isClient){ - if(elem.getParameters().isEmpty()){ - BaseProcessor.err("Client invoke methods must have a first parameter of type Player", elem); - return; - } - - if(!elem.getParameters().get(0).asType().toString().contains("Player")){ - BaseProcessor.err("Client invoke methods should have a first parameter of type Player", elem); - return; - } - } - - //if toAll is false, it's a 'send to one player' variant, so add the player as a parameter - if(!toAll){ - method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "playerConnection"); - } - - //add sender to ignore - if(forwarded){ - method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "exceptConnection"); - } - - //call local method if applicable, shouldn't happen when forwarding method as that already happens by default - if(!forwarded && methodEntry.local != Loc.none){ - //add in local checks - if(methodEntry.local != Loc.both){ - method.beginControlFlow("if(" + getCheckString(methodEntry.local) + " || !mindustry.Vars.net.active())"); - } - - //concatenate parameters - int index = 0; - StringBuilder results = new StringBuilder(); - for(VariableElement var : elem.getParameters()){ - //special case: calling local-only methods uses the local player - if(index == 0 && methodEntry.where == Loc.client){ - results.append("mindustry.Vars.player"); - }else{ - results.append(var.getSimpleName()); - } - if(index != elem.getParameters().size() - 1) results.append(", "); - index++; - } - - //add the statement to call it - method.addStatement("$N." + elem.getSimpleName() + "(" + results.toString() + ")", - ((TypeElement)elem.getEnclosingElement()).getQualifiedName().toString()); - - if(methodEntry.local != Loc.both){ - method.endControlFlow(); - } - } - - //start control flow to check if it's actually client/server so no netcode is called - method.beginControlFlow("if(" + getCheckString(methodEntry.where) + ")"); - - //add statement to create packet from pool - method.addStatement("$1N packet = $2N.obtain($1N.class, $1N::new)", "mindustry.net.Packets.InvokePacket", "arc.util.pooling.Pools"); - //assign priority - method.addStatement("packet.priority = (byte)" + methodEntry.priority.ordinal()); - //assign method ID - method.addStatement("packet.type = (byte)" + methodEntry.id); - //reset stream - method.addStatement("OUT.reset()"); - - method.addTypeVariables(Seq.with(elem.getTypeParameters()).map(BaseProcessor::getTVN)); - - for(int i = 0; i < elem.getParameters().size(); i++){ - //first argument is skipped as it is always the player caller - if((!methodEntry.where.isServer/* || methodEntry.mode == Loc.both*/) && i == 0){ - continue; - } - - VariableElement var = elem.getParameters().get(i); - - try{ - //add parameter to method - method.addParameter(TypeName.get(var.asType()), var.getSimpleName().toString()); - }catch(Throwable t){ - throw new RuntimeException("Error parsing method " + methodEntry.targetMethod); - } - - //name of parameter - String varName = var.getSimpleName().toString(); - //name of parameter type - String typeName = var.asType().toString(); - //captialized version of type name for writing primitives - String capName = typeName.equals("byte") ? "" : Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1); - //special case: method can be called from anywhere to anywhere - //thus, only write the player when the SERVER is writing data, since the client is the only one who reads it - boolean writePlayerSkipCheck = methodEntry.where == Loc.both && i == 0; - - if(writePlayerSkipCheck){ //write begin check - method.beginControlFlow("if(mindustry.Vars.net.server())"); - } - - if(BaseProcessor.isPrimitive(typeName)){ //check if it's a primitive, and if so write it - method.addStatement("WRITE.$L($L)", typeName.equals("boolean") ? "bool" : typeName.charAt(0) + "", varName); - }else{ - //else, try and find a serializer - String ser = serializers.writers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(elem, var.asType(), true)); - - if(ser == null){ //make sure a serializer exists! - BaseProcessor.err("No @WriteClass method to write class type: '" + typeName + "'", var); - return; - } - - //add statement for writing it - method.addStatement(ser + "(WRITE, " + varName + ")"); - } - - if(writePlayerSkipCheck){ //write end check - method.endControlFlow(); - } - } - - //assign packet bytes - method.addStatement("packet.bytes = OUT.getBytes()"); - //assign packet length - method.addStatement("packet.length = OUT.size()"); - - String sendString; - - if(forwarded){ //forward packet - if(!methodEntry.local.isClient){ //if the client doesn't get it called locally, forward it back after validation - sendString = "mindustry.Vars.net.send("; - }else{ - sendString = "mindustry.Vars.net.sendExcept(exceptConnection, "; - } - }else if(toAll){ //send to all players / to server - sendString = "mindustry.Vars.net.send("; - }else{ //send to specific client from server - sendString = "playerConnection.send("; - } - - //send the actual packet - method.addStatement(sendString + "packet, " + - (methodEntry.unreliable ? "mindustry.net.Net.SendMode.udp" : "mindustry.net.Net.SendMode.tcp") + ")"); - - - //end check for server/client - method.endControlFlow(); - - //add method to class, finally - classBuilder.addMethod(method.build()); - } - - private String getCheckString(Loc loc){ - return loc.isClient && loc.isServer ? "mindustry.Vars.net.server() || mindustry.Vars.net.client()" : - loc.isClient ? "mindustry.Vars.net.client()" : - loc.isServer ? "mindustry.Vars.net.server()" : "false"; - } -} diff --git a/annotations/src/main/java/mindustry/annotations/util/Svar.java b/annotations/src/main/java/mindustry/annotations/util/Svar.java index 2b74ced930..b160ec6ca0 100644 --- a/annotations/src/main/java/mindustry/annotations/util/Svar.java +++ b/annotations/src/main/java/mindustry/annotations/util/Svar.java @@ -1,7 +1,6 @@ package mindustry.annotations.util; import com.sun.source.tree.*; -import com.sun.tools.javac.tree.JCTree.*; import mindustry.annotations.*; import javax.lang.model.element.*; @@ -16,10 +15,6 @@ public class Svar extends Selement{ return up().asType().toString() + "#" + super.toString().replace("mindustry.gen.", ""); } - public JCVariableDecl jtree(){ - return (JCVariableDecl)BaseProcessor.elementu.getTree(e); - } - public Stype enclosingType(){ return new Stype((TypeElement)up()); } diff --git a/annotations/src/main/resources/classids.properties b/annotations/src/main/resources/classids.properties index 293373a9df..2dc5b73284 100644 --- a/annotations/src/main/resources/classids.properties +++ b/annotations/src/main/resources/classids.properties @@ -22,6 +22,7 @@ mindustry.entities.comp.PosTeamDef=28 mindustry.entities.comp.PuddleComp=13 mindustry.type.Weather.WeatherStateComp=14 mindustry.world.blocks.campaign.LaunchPad.LaunchPayloadComp=15 +mindustry.world.blocks.campaign.PayloadLaunchPad.LargeLaunchPayloadComp=34 mindustry.world.blocks.defense.ForceProjector.ForceDrawComp=22 mono=16 nova=17 diff --git a/annotations/src/main/resources/revisions/LargeLaunchPayloadComp/0.json b/annotations/src/main/resources/revisions/LargeLaunchPayloadComp/0.json new file mode 100644 index 0000000000..677d3f93da --- /dev/null +++ b/annotations/src/main/resources/revisions/LargeLaunchPayloadComp/0.json @@ -0,0 +1 @@ +{fields:[{name:lifetime,type:float},{name:payload,type:mindustry.world.blocks.payloads.Payload},{name:team,type:mindustry.game.Team},{name:time,type:float},{name:x,type:float},{name:y,type:float}]} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0017d58727..b3e911106a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,18 +29,18 @@ plugins{ } allprojects{ - apply plugin: 'maven' + apply plugin: 'maven-publish' version = 'release' group = 'com.github.Anuken' ext{ - versionNumber = '6' - if(!project.hasProperty("versionModifier")) versionModifier = 'release' + versionNumber = '7' + if(!project.hasProperty("versionModifier")) versionModifier = 'pre-alpha' if(!project.hasProperty("versionType")) versionType = 'official' appName = 'Mindustry' - steamworksVersion = '891ed912791e01fe9ee6237a6497e5212b85c256' - rhinoVersion = '378626d8abc552bba57864358358045d2f2dbe9b' + steamworksVersion = '0b86023401880bb5e586bc404bedbaae9b1f1c94' + rhinoVersion = '099aed6c82f8094b3ba39a273b8d2ba7bdcc6443' loadVersionProps = { return new Properties().with{p -> p.load(file('../core/assets/version.properties').newReader()); return p } @@ -88,13 +88,11 @@ allprojects{ } hasSprites = { - return new File(rootDir, "core/assets/sprites/sprites.atlas").exists() + return new File(rootDir, "core/assets/sprites/sprites.aatls").exists() } getModifierString = { - if(versionModifier != "release"){ - return "[${versionModifier.toUpperCase()}]" - } + if(versionModifier != "release") return "[${versionModifier.toUpperCase()}]" return "" } @@ -112,8 +110,7 @@ allprojects{ def v = System.getenv("ANDROID_HOME") if(v != null) return v //rootDir is null here, amazing. brilliant. - def file = new File("local.properties") - if(!file.exists()) file = new File("../local.properties") + def file = new File(rootDir, "local.properties") def props = new Properties().with{p -> p.load(file.newReader()); return p } return props.get("sdk.dir") } @@ -200,10 +197,20 @@ allprojects{ tasks.withType(JavaCompile){ targetCompatibility = 8 - sourceCompatibility = 14 + //TODO fix dynamically, this is a hack + if(System.getProperty("user.name") == "anuke"){ + sourceCompatibility = JavaVersion.VERSION_15 + }else{ + sourceCompatibility = JavaVersion.VERSION_14 + } options.encoding = "UTF-8" options.compilerArgs += ["-Xlint:deprecation"] dependsOn clearCache + + options.forkOptions.jvmArgs.addAll([ + '--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED' + ]) } } @@ -211,15 +218,14 @@ configure(project(":annotations")){ tasks.withType(JavaCompile){ targetCompatibility = 8 sourceCompatibility = 8 + options.fork = true } } //compile with java 8 compatibility for everything except the annotation project configure(subprojects - project(":annotations")){ tasks.withType(JavaCompile){ - if(JavaVersion.current() != JavaVersion.VERSION_1_8){ - options.compilerArgs.addAll(['--release', '8', '--enable-preview']) - } + options.compilerArgs.addAll(['--release', '8', '--enable-preview']) doFirst{ options.compilerArgs = options.compilerArgs.findAll{it != '--enable-preview' } @@ -242,9 +248,9 @@ project(":desktop"){ dependencies{ implementation project(":core") + implementation arcModule("extensions:discord") implementation arcModule("natives:natives-desktop") implementation arcModule("natives:natives-freetype-desktop") - implementation 'com.github.MinnDevelopment:java-discord-rpc:v2.0.1' if(debugged()) implementation project(":debug") @@ -360,14 +366,16 @@ project(":core"){ dependencies{ compileJava.dependsOn(preGen) - api "org.lz4:lz4-java:1.4.1" + api "org.lz4:lz4-java:1.7.1" api arcModule("arc-core") + api arcModule("extensions:flabel") api arcModule("extensions:freetype") api arcModule("extensions:g3d") api arcModule("extensions:fx") api arcModule("extensions:arcnet") api "com.github.Anuken:rhino:$rhinoVersion" if(localArc() && debugged()) api arcModule("extensions:recorder") + if(localArc()) api arcModule(":extensions:packer") annotationProcessor 'com.github.Anuken:jabel:34e4c172e65b3928cd9eabe1993654ea79c409cd' compileOnly project(":annotations") @@ -425,7 +433,7 @@ project(":tests"){ test{ useJUnitPlatform() workingDir = new File("../core/assets") - testLogging { + testLogging{ exceptionFormat = 'full' showStandardStreams = true } @@ -453,6 +461,21 @@ project(":annotations"){ } } +configure([":core", ":desktop", ":server", ":tools"].collect{project(it)}){ + java{ + withJavadocJar() + withSourcesJar() + } + + publishing{ + publications{ + maven(MavenPublication){ + from components.java + } + } + } +} + task deployAll{ task cleanDeployOutput{ doFirst{ diff --git a/core/assets-raw/sprites/blocks/campaign/launch-pad-large.png b/core/assets-raw/sprites/blocks/campaign/launch-pad-large.png deleted file mode 100644 index 5eee3591911e0fb069dd4bf8b925677de7f5a1b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4244 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 UdHoa`{M!H+8JOu;Tp`B%53G4J!2kdN diff --git a/core/assets-raw/sprites/blocks/campaign/payload-launch-pad-light.png b/core/assets-raw/sprites/blocks/campaign/payload-launch-pad-light.png new file mode 100644 index 0000000000000000000000000000000000000000..bf9ab108631c21b7c03ff2349bf8b3e835e84aab GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|N<3X0Ln`LH zz3RwyK!JzB@$&!wmV3LMxO>HBT0T>FB$UGoR0ISK!AIY5|M5*rv+wTMRK9B!pWR7~ zQ{gNXE3}#z^g~<(5{wzGrpmwTU|`l(<}lb;B@EGqg^YO5mY*&lkTCJ}IgqTUtDnm{ Hr-UW|t1dD) literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/campaign/payload-launch-pad.png b/core/assets-raw/sprites/blocks/campaign/payload-launch-pad.png new file mode 100644 index 0000000000000000000000000000000000000000..ec96499d67a4039579bd7e92d704186fc3e820e6 GIT binary patch literal 3235 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 PK+O!Eu6{1-oD!M<03#xm literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-1.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-1.png new file mode 100644 index 0000000000000000000000000000000000000000..6e793c7cac4204318ea11be49c265eb6912edb81 GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}{+=$5ArY-_ zFBu9k2=K557TS1io%H*DgLF!ghso}>hi0Fgz9b|o`quC6P2bsp>Y-pALvhJZt`3J8 neh)eg@(*wqnjGrz6$q@5KK+XQS?ZY&wjgz$u6{1-oD!M8|4!T(WRJkz=xWA0wJ@1A?s5~vCZI85T%%gv;&vry*)NYvBS&t;uc GLK6TX86a)| literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-3.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-3.png new file mode 100644 index 0000000000000000000000000000000000000000..a2dd7b5e0cfcac960eaafb2f8ecec1335a10aa05 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}#-1*YArY-_ z&lxf@7;qdpu>8|4!T(WRJkz=xWA0wJ@1A?s5~vCZI85T%%gv;&vry*)NYvBS&t;uc GLK6TX86a)| literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-4.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-4.png new file mode 100644 index 0000000000000000000000000000000000000000..a2dd7b5e0cfcac960eaafb2f8ecec1335a10aa05 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}#-1*YArY-_ z&lxf@7;qdpu>8|4!T(WRJkz=xWA0wJ@1A?s5~vCZI85T%%gv;&vry*)NYvBS&t;uc GLK6TX86a)| literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..04cecf2c9676d005fed00973681efe39c13c0911 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}VV*9IArY-_ zFKiTa2#{cTnEWq@NGb1 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-arrow.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..f47060e98c3cba9ef5b63e9bc72b3d236ebe1fe0 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}uAVNAArY-_ zFFSGpRk;Md{Z~HE@4_L+&KLVjPqdpbF?`s)#%9`*#|zF}($P*(I<)Ft&juzd3TR(#g*Tz{NF9Qu^@O1TaS?83{1OQ`GFt7js literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge-bottom.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..16256794a0ef75a830e543dc54657be9bd4aee21 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}x}GkMArY-_ z&lxf@DDWIPp#SMbN4>7XV&-zTNh}Tbuf8i*XTO$mr-6}~VS)<(rVPgKlWrKr05vjr My85}Sb4q9e0F`edQUCw| literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge.png new file mode 100644 index 0000000000000000000000000000000000000000..ff853cecf62e044a5ea3735ca8cdefcc9716862f GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}9-c0aArY-_ zuWl4%5D;*=n7U5HsaHTN)W5BJIrU#aoIop*Np3&C(b=)P|@_t4Qr@nWGMW$ jiRleDP=7-_7`-dtn$wOspkUyb}PNftHK zRVp1>k@x1ElWl7Mg5Wr(x?Le0l`MHb-!NzvF)XjR``v5Co;_Mu7?)32Uw@RLLHG}c WiqQT02P1%XGI+ZBxvXPx#{7FPXR9J=WmN5>)APhwTaTK~B^&-ibdsIC_?}(HI?m~wO2_>ce~e*D#f5ig3>eCVQ8(_{A$f$hese<^-QY)2ZE!gAJ12Y4tejIO|jVb z1nwDVL&}`ex&%lupwyBOCxG&s{0hRR7{H<_Fy1>hOv@)sF?=$BHJjc{Q128^b6hya zNF#zEYA*wAd@Ima{EwIb6JP>N0CFaj4)#nq(w?Ckkv$XkKv1`6@wBaJ?}bnb&%~(+ zrR$*1#8pNtL?(GPV_M*ED47XNOE2q|rR#vBN6v&?bFLma6LQVDdgM$vQj9HnZ{jGA Uy;z7RN&o-=07*qoM6N<$g4!~J?f?J) literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-router-top.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-router-top.png new file mode 100644 index 0000000000000000000000000000000000000000..f93d8639c9c3b3f885a1da93e4485f2c8874a929 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}@t!V@ArY-_ zuSRnLRk;M_{;%F9%Cbz^+2s4~M-KYNORv1IEK6`XQYiTSi&g3Ykw;ZRQh`Crf=~B4 zE1qHyS;Xp~aH~NtNs(dhy?acr6qm#~zjAh|?OHki;+66>Q-Y*>4Q+(dsu%JBtzz(W L^>bP0l+XkK78*Ev literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-router.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-router.png new file mode 100644 index 0000000000000000000000000000000000000000..428785c66c5180ade5ddfe4a080f034604f6bd1a GIT binary patch literal 322 zcmV-I0lof-P)Px#{7FPXR9J=WmN5>)APhwTaTK~B^&-ibdsIC_?}(HI?m~wO2_>ce~e*D#f5ig3>eCVQ8(_{A$f$hese<^-QY)2ZE!gAJ12Y4tejIO|jVb z1nwDVL&}`ex&%lupwyBOCxG&s{0hRR7{H<_Fy1>hOv@)sF?=$BHJjc{Q128^b6hya zNF#zEYA*wAd@Ima{EwIb6JP>N0CFaj4)#nq(w?Ckkv$XkKv1`6@wBaJ?}bnb&%~(+ zrR$*1#8pNtL?(GPV_M*ED47XNOE2q|rR#vBN6v&?bFLma6LQVDdgM$vQj9HnZ{jGA Uy;z7RN&o-=07*qoM6N<$g4!~J?f?J) literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-0.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-0.png new file mode 100644 index 0000000000000000000000000000000000000000..04cd2abceb6eecdd02b35ac78e0f74612565667b GIT binary patch literal 329 zcmV-P0k-~$P)Px$14%?dR9J=WmoXB;AP_}&9g0jYV3|e>3ooLj_i#LdcXY-ST9`vPmTO}HNq_}* zvB~ce2>-FblEuUEMBve8yZ1E(^?cmeeOFQg*-8Nq0@Q$r0%${++;x_MU`0Sv3jn@9 zZW?GC07RU0BqDFVz_|}Ry1(3E*bxYsgy3RIS^^D-dDFRY6w>1ISb-L!`V~^*{HTx=+(BJ=v768L7I<)FURs=MQ)F^;Px$CP_p=R9J=WmrV|WFc5`j2uFb>XV93q!G#O&LAZh3z@L-2a^b>o2bX%6aY4X# zT1u&uZg>d^rBnKmnKuCDy90rsB+bMjT+bDXVRq5CDMx8?^HE zdR72JLP!-F0z$FK+z?ui_Cbt=v1x3*O=2zxV>_J#Vz8h4hWG+-t#hb-7AHvn0000< KMNUMnLSTZo8Px$bV)=(R9J=Wm%UEIFcgK4i-#&xH%1m18N!TIRH;iD9Etbf=LrIjipV?*%JI>&-=%D|ZV2Vh^{?KY510mfKoqEd?N7tFMP(A!5ljB^HTCN{V}d6i%c z2=6lx8-gd(_KWvVHwem1DWzmSLSJ&0`Sa0EOTAX0ArW~0K2_^+y!2& zu9#TP$;nd6-he`g-nE&}zwlQz#=1)u}rVr&y--T`PJ#By#-X_jSP#`{&lW2ecA| kUcW-5drhc;Lzn}=53S6-K?v@=ZU6uP07*qoM6N<$f;%(6A^-pY literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-3.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-3.png new file mode 100644 index 0000000000000000000000000000000000000000..491efae7c960c6ffb81fbca1d8c11e8609f9ea9b GIT binary patch literal 560 zcmV-00?+-4P)Px$>PbXFR9J<@m%DDlKoCV|oKz_yYS&2_DMT()AWBmhiIRWFqe{odbbJUs+oVK% z07WFaKuwKOP-arVqJ4Xf?c_?9ybt?a&#diPOnUIrK#7>HIAN$qLU{{9;68wBjB|dBgt;b+q!N)e*F&+eCCQ2Y@jIppA`nNY>NKH5;6F^1S_4g(Pz;oKn`$#r`DarE z(hcfUE2W?;+@?wf>i9aIZJ@4C^H~P!f^*L8^+3~bxu&@IYk0feR@z1iT#UNHZd@zZ zgmJ>~ZCHGf970*XVnx1YzW6*bl?ZH?IZdYL!F^*cxM4t;C=0L^r#9(u08d#i(xF3( zVER(woC937VE(bNaxm=;$h9$v6NdXQ=G|)9B-ecpk6kBxvtAuipl=w3eCY_44-O!$ y0hbAY!xQP%Y!%6@$TeV-T)w$VxuOz4XZ!*ISrI>l`gcSC00004%P)Px$S4l)cR9J<@m%VMnFc5{`2^mEyHG_iyZk)nJ_6Rb9M+h>BD;F*d?cmbhJB2Sd7;m*gEioOyISpCbaVr` zFZgf(7h*Ml40$U20PwbZ^>#84FR4F}IX8wm#?Dx}oF;bL`(aE2bePl2ZSM{5jeHLn8DFfRtsGAE+-lTgE4c3Twz ztv8pl09tIv%sgMNj_2t=1u^rKNsi=jb5^*VroRZJ%xmQ{H|m~g4rENilJ5*(&VA-U z#$J$m#SC&{VEa1b`A00000NkvXX Hu0mjf3*N2p literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/drills/oil-extractor-liquid.png b/core/assets-raw/sprites/blocks/drills/oil-extractor-liquid.png index 9a058e3fc91f6c5da870497cc6aee2dfe13cf4dc..6639f0889b36675ad43f435fa418fc5a4a084cb5 100644 GIT binary patch delta 453 zcmX@fbdPz0ay8nw4p zEqV|jb^gtv|F%z4bFmj5Pod9`KqbDqQ(kJ4w$U+c9DD&BTZ^w5UZ|F>({-8r1a*53HDfbZd3?y$VN zfVK}m&mFB=WwPJ&ZKNt=NLF3I+{y|D(b@7M>C_9uOnlsuN3ATgcc@D3Jf?49|9MG3%Y$*k9X>qbCHeZq^R{ zli#}Y^|`&5S~e&()O(3KYhB!2;OQb$4nuFf3k0003dNkli6_Ce0Y&ILXP1AcDey9i6nDbKI$2fKexNFU+XRSgb(Ay}4 z=l8mlwQ3_HfQedyoEH-MXpY^VB7jZAt^Kk8er?GWF#?{%p}7h` z10jtoWkt&vcz=_LtF9Zr8B_0LhfiOitGpTjgkN_=p-;zgc1SANqUHby= zLZmT(eXud?alF^<5MJaywn9LW5s(g%UP1vVvnwD@0A3TWNT~rNb1Dz8BiRK&DW!Qx zX%@{r5F!9&EJ{fWL2g5@F@%t|VFlnZjd5)Hh*cj(J<6^K(6j8i0N_M1j;4uWf6Z7l j30r@`x_*{ltv|pAzzd7Oq$f2J00000NkvXXu0mjf>FA6` delta 208 zcmV;>05AXS0_*{hB!9C>L_t(|+U?dc4uBvG1yJtA>j?%P(yOgF`2U z{}P}OQd&2zVq9)j`(Ii?5f@c?_nu+;kR7nb+2f&4Q2T&VH^KgLx z+{D^b!UB*9T>ush2So&n8_gXc?Reh?AkS>=GhK8UdDjo<4%h*Fx6u39`3Wij0000< KMNUMnLSTZ4Wm9Yb diff --git a/core/assets-raw/sprites/blocks/environment/char2.png b/core/assets-raw/sprites/blocks/environment/char2.png index c37787ba4f1e4d41069ecd256b79fd076fb1ff76..c418289e485ffd4219d9d01235f1a3a89f473590 100644 GIT binary patch delta 294 zcmV+>0onfB0l@;0B!2;OQb$4nuFf3k0002{Nkld#e% z83*%`KG1H&4S-azq(<-XI_{me3rksCO-M@}Q~;HY=5(Ypx>CSEqyktFBS7jIVC^3z zsF5rx7!7b&a1ns5=QMx_^9-*SfQa3^NFyx$K=g#Z8m07*qoM6N<$g1}XS@Bjb+ delta 191 zcmV;w06_o20^0$QB!8kwL_t(|+TGVZ4uBvG2GH+b%-(>_`=3MOVvvxQO8<1|A~bzi zH5Ik)M;oPVkLcI61028_@eP0_L9a`-yoM4)7C6rd@GNcykmSIs_-t+t tfOg+SE%5#k>7j@!d{0E_0G`FKI05O=y2_jB@FoBN002ovPDHLkV1h9KS2zFw diff --git a/core/assets-raw/sprites/blocks/environment/char3.png b/core/assets-raw/sprites/blocks/environment/char3.png index c45e69823a39363de36b594480a077270d790ea9..cfb54e0ea0aae605ce87ea6e664971bc51828184 100644 GIT binary patch delta 353 zcmV-n0iOQz0sI1xB!2;OQb$4nuFf3k0003tNklT94|qRB1I}iOj2AtC46LyCk!AeH@@?RpRpmVJM2MNXu6fUmuVaeNeyfdqumvJc6L zetwZER&*4j;9BIN2rf9+X=MfQ3^ei_G!algQ1klbX!>8kAyuJRK+5fw&d5{&Nrbg6 zEA9z35mK+nA$h5)2U2F$zL_gv5h3BPyFkJhD0Gc01)v}T00000NkvXXu0mjf))AQA delta 215 zcmV;|04V?b0`mcoB!9X|L_t(|+U=Gx4uBvGMY$KVH(+z;{x{IX1QVJ<`=x+HhfV_T zrL6@Q!hZA&@3$=IcQ+FL9RUu^!7l-6fdq&#W>JEx1T-Ratwx}B-a7@PjU@>bem5a1 zAT=8-fc!S7#99LM(BUPp8aUkrG+^yuTTj3WTop)z1r>ObO;A^Ya}t)|_X=<&uuePN zge5r5R=SW(1+-x9Mw~97dJnWd!J40BXwys=kR;G4xu!r(WJn1t;}@7Na01?q&WfOo RCocd1002ovPDHLkV1iz=Sfl^| diff --git a/core/assets-raw/sprites/blocks/environment/dune-wall-large.png b/core/assets-raw/sprites/blocks/environment/dune-wall-large.png index 23082592c761bb9bae574a1a3b5977806fb8d50a..9966c564e987655cbc7de08326a5e3d732d794d9 100644 GIT binary patch delta 980 zcmV;_11tRZ1nCEmBYyw^b5ch_0Itp)=>Px&qe(%vjFbswf&mMs;bc7V{ zQ|u*tb{9E<3hNHpCd_bQJbr+r6p#OzOcI&Tk01z<^xO4%{i1J`Qs~umYX8}`?WlGf zrW1egY7kEMb$xjFN)S#&L=uFEwrzV_Xg^bcLqI#!rh7&R|9|-a34+xr>9c2p@ZbbB zpTI{t3g)-!0RWNi6SR)s*R|V^MCu!vPXs`OJH|F$3!>Kwvc4r^sYC_TZmJbzH!60P!iSyGxX0IPu-h*6GFQ&0IR3-Htn!Gc^(UMt@!YseHpDbKV7 z3xed2N+~MT5P(`S8ff%D@O*&=fyj2sTrhyP!nPA$M|vS-rUO!T6$C))d^_i6LLiF! zgw|3$bveNZ1S|gt2<=ZqEU6vBu&VaBHIkY(_AVFmB2Ox^n8L|LKd0xsx|kYxqdGt@lM?XjLvO{27kEZlOXThraD*(2cnJWOUJ8I@1^)ocMKq-kUJr@@0000TrR3+l7TG%Lp6hH-`47yVQW+3!Nf^-U4hw=;n@1F#%0-kj~ z5u=A*25vp**HAN3J|kP0yBRFyVQ1+eD-UIgTG z`jP&p?Fj3!Di633e2K@*}AA;D2bIVI2+76d0}3Xk~}p>q`Of z3@ZgfRE^M@6rCDCaWI0;11xjea;^xi2SlG(3qK{ME~=yeAk)DU_|&UQuP^D&&};x? zM9O!Ekxo_h09E=+oe%IFs8nxzpAw*#F-AiuromJIrV?~200o5VOpx~(3UWHc3!$nV zB3UF2z*uf#L~VN`+j;2#s?5?G8{|i2nV0u2m+XFlw|piIg398Gx}WI)-oqAI2oFFe tfHJ12Af^D!0Sp4 diff --git a/core/assets-raw/sprites/blocks/environment/dune-wall1.png b/core/assets-raw/sprites/blocks/environment/dune-wall1.png index 64d13b3f24a5a68d4d3ca6c766d47803ff961859..fbd9cb8090263a3fb5b479ddd651d4a85a3d0a8c 100644 GIT binary patch delta 353 zcmV-n0iOPa1BnBWF@HlzL_t(og}s)$5yK!1hGkzyzy+J6$P%7CMMiL8?ch}8FzAVL z=*~qPBtA<&NXx$O7gUx~f|<6qFA#Amz$4<^!j5FG%K#PFx&{&58C=ifa5-?Bd;Y%x z005lF0WC;W3s(PI2`G>#ga$%tpn!1BUa?6KBtQ+qSxV{mM1M>K008*B-)ayG2$uyF zzzSgn83OP?6cEa&ERX=UeI3|yS^$I+)JoTc~$;gC%@bO~nX&=EK^JJ3^5n)q*o zr5r%9{qQeg+P3Y4Mp8;(l~RgZf98x?oqO~LfQWOCpa2@f+#U8Y!h~MMP}F&wsv)Ml>K)6jXCS1EB?R z0azd^2qst*P=F<85<=Tv+DHrl)j0*hb%zInO~D-yA%Hs|+9+TVd2tGo18j0GIS-Dw zR15%=qIN{reeNQpK&Vtu5ZW4HBhW*%4m1$G9ww$MSv9ugd@TvWd>Tqh{TFFc<2ntW zf*|h$9*7>Ke^$oqiA7jSAq%O#Zw*_z1%OFGTj^3ES&rb|mUtu5q$2{M8bs0^F#<8Y v*Yy_lbF=tPz6#Z00000NkvXXu0mjfL)43Z diff --git a/core/assets-raw/sprites/blocks/environment/edgier.png b/core/assets-raw/sprites/blocks/environment/edgier.png deleted file mode 100644 index 0e4e66c84c12389dce14fc6a4f388fe55f197392..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ08bakkcwMx&pGlo7znUl%x94Q zv4W{&0fYPrN55K~`7LU^OlACMyr0i-+vry~3 iOYqWkC!k>rAF9op%)LYv7CdSPaXnrAT-G@yGywp^zAa?{ diff --git a/core/assets-raw/sprites/blocks/environment/env-error.png b/core/assets-raw/sprites/blocks/environment/env-error.png new file mode 100644 index 0000000000000000000000000000000000000000..b72971ab8a2b0ae2df236e66a24e586d1436ecf7 GIT binary patch literal 287 zcmV+)0pR|LP)Px#*-1n}R9J=WmfH@*AP7WZ|No!aCmTYTfoN+tS$U~e3&#sn)XYp?w9lKt4*&&F z)%3Xy09gd_ebz<`m^uQ#D`o}*`#H!cKo&qmsKabNGYT*Qp@1V;9QXYSNOuVlI*Q*- z!#j;##4P~rESM3SRe`zyGR3RfOf8Xu#{LBmQ!Z-2X#4Z5(lmpKY1tB@Rr5ZgPYB?5 z!hgtwCEF$DQ)HtMA^Q&@ng)2P2x$Sti^TrODI`7B%PK|i(tF=XrW!V%dEbSt(( zkY<4ef;^105oCZif(6h-umM^K5kLbW21pSq00{y-J|De+UlN5#PY{L3av%axB%wE= cS_Phn53qM)=ATD(L;wH)07*qoM6N<$f*;#gIsgCw delta 204 zcmV;-05kvo0q6mcB!90-L_t(|+Qrwg0e~h4w}m^mOy1hZF6g<$2BDiEyR^Hl_jP_cqQm274aDC5-(0!4Mkt0Hg&%?bin+RY+x zM(zv(ckYi6bd-j_~mhrm@?dKFi3;W#2qVk&9$FM;-b(%&`!KUSDOK*ZO}@OX{K9nQbY26dN&1pu&O z2N=Soc~}F8Xa<)|um<3?H-&Rr=fL~x&D=m*0Y^A(z!9!8P=68*z-2&C6C=5w)e7&! zKUSdGZtUQMR27(-uvYR%=c+<8U^^u92h9?g1y6C9u^I3(A%@Vy1bH3zb-m~wU^CPX zL6w1}+l{RpQ3-YlikZ+F@G>E`WM4ri`Ju8fZ_QMI zu-POcsX6N+)H!`WHheCCM>q=Da)w6-a0sr24+;o4b`zkxC&1C&{vY57xjZPBhP-TS zCvYI5AWc97#WCTN< v5DkVpAtSie3+e0ET1-O#=0@}>KyPR4E(DpA1VGSO00000NkvXXu0mjfocm6c diff --git a/core/assets-raw/sprites/blocks/fire/fire0.png b/core/assets-raw/sprites/blocks/fire/fire0.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e1db18e6c92e1003dc20297c38524ba79ba854 GIT binary patch literal 1193 zcmeAS@N?(olHy`uVBq!ia0vp^DImK)AnyR-@oU3TKe{&qy;NXezVPRi{OyZVc`*D zV&i6PZf!W|bRa=7VS|7HvBDa2CcK=@o?ZC+mfn7?-+QL(hy_BDzdx(CZ*BEaWf6Pr^eE+ts+XJK$2jLrE+?}lI9VRci{q%0^B*ia;@NV<^Q85gxwBV1o3zM{ZIMX2=IJe;Coek8@}y)5a|*AglCGDJ zWA~&7Eh2LtwA|?7b4h5~!oNrN_9|sHg{9g|ZX6TUTEErrE!7B&N)7hTeE56L-`Ss8 zrU)qcapf*AJH2%J%AhTgdrn=sdUVmfeqq%DCyt9h3nz$VJwB4*+F7DDNhI6p*!j0{ z|2pTE-;e%Zn7n*mY)xk)6{EvRw+??1akZtjWj-}kbA*P@gYzhk@` z#ho}bH}OoKIBliK)RVUlf8F-?(Y!O)FQ{%7SS6cesg-+E?@d<9j7i~L`fFS`O831E zoYu4TxqxqtOWT2{Q$@N5*)juzfvO7KSq->V-F+V;9S*J^xGbzpTm;qkuj zSAFvN3+m73I>`F-HzY;g_5Yla8EmfCDDX%+w8d(s=K9~$m=%w>ryQC1jqh0b5eE)K z#q(cw&ro$!vrsg%<@OMK~_b$b4 zN(pDLZ9RSZbNs&>JC+!>3QRioV&&5Bns@ud&PkjWTC{C@Y~|ix^PFRpQaVy2PkrfS zUiq@C@1y3@4^EFROjMMA?d=uZ((_$+uk}lr68ltt!G+6oge95cgZ6L&G+a?OHRJ-z~xFvip<@;YDulLQZWFZj%FVdQ&MBb@08mg8vH$=8 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire1.png b/core/assets-raw/sprites/blocks/fire/fire1.png new file mode 100644 index 0000000000000000000000000000000000000000..d3026cedd2dc4781cc18e0a652ad0913118a934e GIT binary patch literal 1200 zcmeAS@N?(olHy`uVBq!ia0vp^DImaSW-L^LCc4kBFnl@%NLO z72J-x7>K(*J=}Dp$*Fg0kOY^Z*tT1lnaQfHVNnUG$tznsw~4u#D2g39wW76kqSHQ+ zuSzo*>J*rseOYU_X}kGu>+0>szwbZ&JIlJP?E221Kkv<+dDqFWVbO#V_7*P}4hba| z9zmuy4o2se21Ta>0*VP80tUnipO`b@W_J6jRgZJ0$3MyazEsRe`G|kZcG*}TwU(BX zALe!Is0ur6a(UJ2(#iMTbVcB#D#b{kJ&7(S&itIW%H&dAw$3(@k6-iV7yn&vo9W$k^V)WL#>M34`@QvK+6?pQ;*F?>EevORr&WBQ-aQ}?CqTx6< zQSgJ)o)l9fiPcUem#-e@X216Tbba+V)$LDD^d0!T*yZH=_p5haO32@7@rBFCD@oj` zIqtm9Y2D^TO~;E1m0ME`Rs{99aPZe%QCc_Y;Djm1?w)PDK3OCwH_`R=edj4jCaFhN znM~>ExZf>atkRgjnh!e(=7~d#26Rhj;dWJ{WSf*7MY(xyg+$wLRD#PyLj1qLLn&JIqR;~#ZIBQ zE7yyBW%wi4gt5HBm)v9FMy}#=|XLDB@qy=-eOi;O4dHbl^N)aJbuck%Jy&+tR z6%(3|Eb(NS9HrP(eZkW&gwskt{D#U`r#mycI(W@bdr3)sxVf5Ldr`{Q&!4JhpWTctyt=%Y z9y>*_Nd+!eG3Ive;+k}5{k&y@(MBBFn%tpm{3QWPZcK4Jp`)E%f1#PQ+zVG-&v0<= W_t?bE|I>ga5re0zpUXO@geCx({vG82 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire10.png b/core/assets-raw/sprites/blocks/fire/fire10.png new file mode 100644 index 0000000000000000000000000000000000000000..b55606ede5189b1105adddf766a1ccef4fc0d218 GIT binary patch literal 1111 zcmeAS@N?(olHy`uVBq!ia0vp^DImkb@sGT| zxuPydSFvu@I$Hl=X;*-#F ziD_=V>bzRi#WDJF+0#y&mnlZGb0(cUmw)YfHRtRlFE`CUcjlehO@}6}*H!FG9k?CQQNv%f1?vDrV@?z*m^#&iF(luBmG*yhSikqvE{=OGk6t(V9c*-QT(N$&#^mX{ zyzZSpzcpU|8y_2=ieFFCp$A3r>7NW*jLKFS|KF$kcku^@0EMY;?NK6Zjs@CPx0>(U z?rr_v%j($C$HW}O>lUKrcKhX7)%DxsZ?N?t64PJuHzkVl^uLWM%E1 zt9q!-<~+3Xk>t8c-)yab1cy34U-r|xZ?CDn_hoPYvC@>66B%!OZEj+ae0w+LX;!qn zYKxN~j};3$W03c<%kh1TU05O^+jPnBOV+^!`|jQlzva}( zyzocdRKCOK{bTRN7CrD`xU(xeUrTOHl%Ykm#sq~qb?MU=Gv_#UzB+$lV}a-Q#XP?L z%L0S++0vO=rIy4L28MlId){7%%eIZX+kVk{`K+r+1z#6ca65erNoiVmUBYp#43kq_ zb3tG>i*K-+$dftEXZG99v`_He{m%LSQ?;qZ0(}XK0*Vf+A9}WqeWw5Oa$6A+hIf(Skot)|JIe7XIGy>CR@K zD=YIa9rTF|d>K4dZf{!hlouMlr#G3-{q=AAb;Yem;>#BJ{6FR@p(&u?IVW?a#2(q! zMR}K7O|Z)D`>TqFKcDU8Un?_X z=JMK~=W}fTEcsBWCNWz;hQ(u!kFw*@6(YX=Q?CiMUD)nx(0fW+Kw7b0zWcLO%Q6i) z)oshPuUGOqEoE&gu6y0Gs$}Uqqd5C3`Nu@|F<)pr`~595)Te%~j_jDJ zAZ4j_WloI5uZBlj9h%&UyPQsPb}+cG8#*mHik4FhO$yVh{~g-;_?^eE{J9Fp1%8!H zGGJnw)^Mc5%`0fJ3ov(0kT@yzNW<#GuS4@nm=-;K6cX-Ba-RFgWO{8=@1HB5R{#qP N22WQ%mvv4FO#sA#>kI$@ literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire11.png b/core/assets-raw/sprites/blocks/fire/fire11.png new file mode 100644 index 0000000000000000000000000000000000000000..4602c384edb9e3efc3fb712b506c75c76bdc8626 GIT binary patch literal 1131 zcmeAS@N?(olHy`uVBq!ia0vp^DImrYN_n3S~QkXw7-#nP(w_l1nFEPhFpO*zo8Bv7V}OLFN#2Ps8u z$2(Kos$M_jWO7+HVdr!Eth#yA)@}Q=#kBf=@{294Ru>+vS6TS@XW{ebXYJ1a z^A(R<#>K<9=#|ZmofE{m-tcEXyqWNF$u@bZk8{r`aAZC`Uv%)uge&{DBqTVbXiW?h zYFEnV*dKfAxus#Cdc42mi`!OS0&NZ|nz|Aa0-7)X=mk&c6e`WR@|bI>7>{k^N>)~` zC6gvCIAPfRc=h5#U(e?hL~<*+E}S7T>GBhI&*G95S(2(@%^p`{92L`TJ(jY@Mk-FV zKJn@H!b>HAcdU787j4oipRi+E+}xts*3n$r#onzmqbDz|z5RZVf6Mi2_KzanQ+E65 z8*epYkCd=}80nCjvLaCE+VpJUd2junmrd{UXPV67c(1X2ui}|qK8`*-Tl+6x&H4~L zZPv1;NfRfP{OUR_qk8srS7`jNbtlc+zIp^aS>3dfb?Gf5)=c?|7eB5|;9Wa0Jj#ga z$}GVz5vu>|F8R;bkkEY5mK1XB?&(W`Pk!%UwOw1%;}xWC!FTM{?!8QbCqHJX%m@#! zkDsG6f5|mw{-E6kGvsaK>hAR&?)N$`&33t9KmW=nT-sS(LUS}kW-gl4BP8pbt1Y6L z%dq*5<0hSV!QE`opAGqMfy8K~R9mkyb9Q zdkgfoxVq+uZ~SKViOsLd$kKS`NuTpJ>NfQgRE#fMoLQfKE`{e=G0&=+Yt{!=DRV@y zFm@{>2(sujHMk0F;9wGSJfK9J;Iz*Qi<_lwzwUW|Z22^u@2b6X*e)7g$-W*qht2S* zmDlCQ9dSTxW2Lg(@yUbYAa-xX8vFfMC)N~^yz6ueCqD6;Q$f0?+>gVS2;SlHZpcbUwG-2xMS94g|A#vvRYRHnHYn#N>g=S^;%3;tA^ zOp7kuV1BXg@4j8jU!4t~SH15=hk!!1UXG^yG0WVQO#vxCkDRDjw|W2awAj5DAGUwc zpRa#5Y+5PrULR3TCYP->_gC%GDB5Xlwq@Rwcf4Ns+{aX?g98~PPqNY8uWnFjA ztVu<~)8Gq>8YdS|hY0(Edm0nJ1}pCqG@PICT!)oa$h?+CLuTt5Hs`Ffl7W|`WLK#y zK9VW0S7W)l_$iT;XID?2%1^u97d=zv!iJy9-4EPP+}R!cwAs|$GPv&UzTag^7JpZ9 z8NODimzU3dvAyukt@clkzjO=7iA?GHmvnZC-f5rPr_>9k1SW|t5#*7SeY+SVH9>HB;? zA2jgGOkOc*kwx)q7eDW1m&5jL-dprDd)Jq#%=(KD9v3bW%jnh(le|ZTzJ?mIdxkOX_tK0i8x6ZUAaXUYf zNjjm{VBgd*_2pXOuxPdIet*7i&Uw#VG;_|T4gs0x==tf$g%>Y*x&I%&aQ1wd{zcA5 z-p2#h=PnX}<-JGVXT`TY=M!~gI>qEnWF~}U*Z+)0Yp>*6zZF{xEK3+XUHx3vIVCg! E04MqW_y7O^ literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire13.png b/core/assets-raw/sprites/blocks/fire/fire13.png new file mode 100644 index 0000000000000000000000000000000000000000..27e48033ade441130c9d90a829ec638fab6ae608 GIT binary patch literal 1195 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 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire16.png b/core/assets-raw/sprites/blocks/fire/fire16.png new file mode 100644 index 0000000000000000000000000000000000000000..7e61b28a69e06cb02532e908e3e066342c805a23 GIT binary patch literal 1236 zcmeAS@N?(olHy`uVBq!ia0vp^DImzP9%$xNLOG!!=0EYj%e46|a}Qi7hwP z^!EAY|559kS3SqFc#m=Vv+B0{>Bj3H&)xaFc>iRrfPx!u{x3oP14V;=%InUsnvMI7x5!Kq;ZU5oQZvVsv-H`b$fC|GtsP$4`{tSc zlfGXvL8SZ4J2{mi=T%d#H8xBCHg=w`Yuc!FW!lNdMAMa&wN`R1T;?R3dD7@e;oX%JZ8`;>KdDu8S9f+`3V9}1 zIz2^m+1yHrsmoLf6}ull-(K_ZYP|QY{S#STnpPcuJ~>6xB&o~WOI1ZFC2xoKZ`SOx z_Z(A&TGnwE3%+(rkqouYWXQCM+)Lci~y-u=p@$*`^@m9u**H?~ASR&xmW5MUF;%E}TD}}>Q%Vd_8cKQUKP@fr7 zR-9J9)?)It_jze;hk)bLrc)QV)xO3|5e%KO?Bung-CQN|Lh7&Q|MF^eKK$Wu@@l6; zK~86<*#!2i*8g~zRkG!43ybH+*LOVoR!;djar3=&vv2#hL|r`EKHJ;+Z1~&#<^D@f zetNC`XIg-dfYO!6JC2@Ex>9U*bjhXmBdX4O#ioncOTH?N4hj-CaDEzxN4>KkN`Vzl?cCPy~mB4hxSM z6B{>Ub8EvvrvnL!2^$0qNE0^FPD$Eud~xpm7b$vgx7|?L?6k_aMFB{Pf(>50s>O%z z?S~_E=T98>c{XE}fKvB_)0;GwYPEz22RJR$d#knc^6vb)j?(sYL$8FE6-G}sDHsNe zmsi~jy1A6KrRV(d?N!p}zyJF8?bpKT2A2|ALby5&R*I`N|FA4N;XIK^(M4Q#>V)hQ zS62ioEmHPjlM-+`q>z~Svt?O}<|6}u-vP7@ND(TzR{sUHOC-=Vo^zBgD zuVd=gyq67XbK?UGu3t6xd$Lr^<<7y^d^xK)Ckd5(=aM}7_hI4evpRwcr`;5rT-Czy zXGy?LKB;Lw!qp)qN`GG65Slzo;O>ui*MBbBC#CA?!I_l$Fu(Bs@2Pw5J$s#>?A_Qo zVTFj$4ilHNcWobCbeCEGxJJcU);@+k}WLIYjJ70YB({e_0E60??JPtNIned&~X-*)taJxC2{r7 z2g(&wWyUBsw>69hvZS)~>tXi%!4ymtJdn>DWwxU7ru9MVZu`TwNBc zw~^Je@atW(LMcVZpb}%fNsoiPLelbO<7y|CJ#78F%~F0-iKSCtm)Few_6zoI`+H@^ z*KK-V90%N5!DI^-RVSi0Byi@i2^SI=PnWhR5VN)}({ zol99yzaL)Yyf)j~OGs2nsAkG>jqFyJ`|T%=ubQ%B^XjAdmDz!l)H*pn&i-|3b=fN6 zEm_N3WT&}rnq{8ypy)&t`=j24nstFm#k@|Qw@fy!f0?=dz)_}vqb^;mk0vTw8l?OOFKgBJP@^tYCYhP>Pq2uJjvLeaC$jh_lL=xk{15X6fAo&j^ xV|y?NGvAH7ej*Eh7 ztu7tkezhAbt@!hKa8v6A(x1<%LqN@0AiOJFarJq9)0l_1yQS@B zC#byr+J0c?gimi~u4*|n;UCv~nRk6tlDyaxC-XVF`zvnW9hZ6II*-}Q4A$jNEj=0@ zT)f`9618)7bUANks(I|wp>uc&pJ-^;TbC&oApQo2Gbdh0bVItvF$)pcv&N94pLRYSp!5?HVOE#VA?bR$=pGd+2iF zz?0p3&iZt>8B~7F3-(Uh&bj8XuZ6eA6p>h4MN7S0@|6P+5>-91<@3meixRT?QlTyN)!4Q5>;}~(N7l-6?5IzY zF!Gt$qEcx3_p4`GT4bQ6rx%;(i?mlB&(b3k17`Ug7E7FTiSML<1Lwwr?$2*8Tk=-C zE%D`t-WNHOXPmibwa{-{Wc`9iztR?Q`8Z9uG*PWCPtR>hOX{EF(M~SQPbWpVDBo+> onI^#J!<+bzlI#sHBR;ar-(`DwebqfTV9~|k>FVdQ&MBb@0N-;c)c^nh literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire19.png b/core/assets-raw/sprites/blocks/fire/fire19.png new file mode 100644 index 0000000000000000000000000000000000000000..e79a1538e95bbd01f4e4db87650bdb465cc34d84 GIT binary patch literal 1310 zcmb`H>sOKo0LHP9anUm{Q`1a94ROv@W_Fnl5rP**1#!u|ZooWdmNn9vOS~XQhzv+k zySWgIOf5a;1?NiBQuDgJ4NHr3dMr05Vwqucf5Z0Sxt!bFcS5U}?ry7b_AP{&4AO*6{TwN$_IX+CV6}5ItWZ=3iB-&Dp4wPl(e1eui8I15O zu))UB+u1*i5cXJu$pXRPa8F@V0LaodMgm5`kT%EAvI~k5e-ZF~D<|XFz?pS=|JeAU z>#-TL#si6uyfdt9d1Lku_O=rrBw!NmB6meW9lXF+cAFpwi`=}dw*N3o$?n|cF1z?Q zZ=ajPPo4do&E8AoZZrJw8EvE3AvRtdFr0^GYkcC=%v54gyq+1!xf;b#6tr zyI(L^kg<%BoAEiBH5vk9i1)C0Jz;HisilGg?13sUZ`1Le&9c9aq^)iF;=boa)0?>2 zv#x!)Uas7Hv%Wd=K4&24_U8Ai?KOlY(fK>lvd6cOioj^ccOLlRMAPrZbiq|-#KLO! zcuPF)ARlGJO4-@De+!li<>CXm!uOSG+dP_Rz6rLwKSF6Gj+$)5j+Z%_M^Z!F^OzC) zX-8dft>P=WnFHdR{fQy$RIJ-}XCV@`zTM7foD7(GPQ!AklJPRL&lMAsQ_e{{Rq@jJ zZuMH=)MW4-pDC+wltM^*v^<6%D3iD>uJo_OFj+!gm#n0U3Af*~njqMveHy>s+n84# zBr0GO7LM^fF5m94nq?~Q>vsX}4P7bp6CFzrGN>rhnU25E4 zZd1Y(;JLxc*al^T6ahFn?_q5EdPimqv+pj=)aF~%Q0WIT_W&lndvIZWuUByKiAG9m zbyz^($6FqF-Ma&q3Vwm(4m7C1MC#i%$~WnkXp-ZF`~@v|V(LUC+q>3c#8vTS@~5Rl zVef~Mf@?{dY_(QDQ`8(XyCRVrcYaHRFrtK7b$Im5u#c;NWO^f+-$WMX7wlKfQ-*9W z*HH=bK{NsnnBh7#Ns72}C@#H%jB?$fnH!Z4j~U}>&rUkc_DPDMfI?M?+t-;NSlp@J z`u8jS{o>f$p%bw~(ZTE_ELqe+0!o{F)E`=}mxt9u@VAB9eNLyUMv;+ic5<20W1FEw z#>}W)Y_H^?*IG^&Sw*@$WuiwhKu5Q1jlZ1cTQ9%r#pH9l&e3T}FifzRn8%{jeKlLx z`D`L6bE09b`Onl#3}!FF6X*!J_bZ{QgnjP{+RK%$tFwsd>G4^pzVLBn`k>;bwxwN% zjJ&M!fQ5RK;rAj%M*qV@*VeJG_7a5-7TpoRjqK6Vq|&UT*-z?ukyJVP8ISMPopi@x zF;zW|l<%Y|r`y9TwA4ZQZ$DlPoj0NB`kv@v`W9U0XwW0&d6}x#%=eR?#^@M1cFr{( zZbXIpuGA79%wGDLQtjtJ@fGq&56RB|10)0%<#_w9t!~To4eo&eaww^Wn2_@iB&A39 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire2.png b/core/assets-raw/sprites/blocks/fire/fire2.png new file mode 100644 index 0000000000000000000000000000000000000000..0b6ffb4c5007bf4c449ccbdd1ccc0360b305bb50 GIT binary patch literal 1178 zcmeAS@N?(olHy`uVBq!ia0vp^DImHw#Ug@YA7~YSQHd0T$+~E6~dqU zQa@Mnr=I`kHo$PvNA zB-Y$;lw@Wr^Gqolp;i5FWom?{0=ROegn`axUdo zH7BblEJ_tkNqXRL!}1GbS*61nE)K^P-+d39;gww~npJk<-^(^u$l*{+ch6aDly4wr7FNltZR}7rNA)^tNPnwA@j@x60y9?Q7j5 z+gws*TFUlxK0fhes`D*xQD4r`?4Y{y>%;OU2Cnhx=$TYzl+rPO{W9(Cw}0K*`Y%VM z$05Mkb>p^_)W}7TmNg}m1g@}PF?A7{(Of0drlH%l{$NAF|0ID8dzf$D*|&6y)!v*Z z)eB|qH6$kV-G1M+;iG^Tg}X$>h11{3aA*5^ z>WUnBxnO~V#>UT+vv&V6QV`iC&+hCk{WnVLXW&;0triYG7xP9ZYulC2n%DMiJK=M$ zDd5wR6|7HvrRAb)?4vCuL*)Bs&uXn+zIxv6qK6y5zBFPos@s@ed-(f3S<`(Nlb`Q8 z^p1VL^8WlQef?bqkCc{d+gz>nefvDAb$?_;o$Jo1`J_#_Xgqgn@8ag=G7o3CDDo6a zw_d7x>H6XCvx{HnUf;61^6+<$Ljq@Q{sbvaS*rf`;r!OEzsz>^y_z*)p1W|PjqJL~ ziZ0ss#jd{>?&EX2l@XNV>zO8S$Y7^p;ZfbtK#44)zPw65_R#N!Ez{I*9~Ur{nJcjh9hVYraD7_vE_he)q%T~jLwOk&O|H>XQd8hM_HAcJxWcw+t9J-H zi>o(f#WlX!@y+T)mr~iTs%3p;xu>-Sgs(D9Sa0?^=7otY%ZU@6eH*^tT(Wh)jqkK` zpRaiTfAn21QNiI-!j;WAs#Cu_lc+r_%Ei)gwq!Z8s=i~%a+TLXQ`6&s0eUX1@RFbF z+V7uZpXqkY$v+(w)XK0i_VC_QP9AScN?#V$>K#(}KYN$l$M~W<1^@f2rI=FI?ALxH z*H^b#o6G6%Ebkd@3=0t8h6iTM@!Z~d!r+{XE-@(--Og` z@OwVPF*va0<%vm~IVT2|Z0dDfKjYG#OImj-J68A4TjnXgv>^Rehs2^NjdknO9rvFq zntJ11ZQA{8^1FF6HN-4dbVb%PtTSX|>~=hmB(On( zMTZ*9)k_`j@#lX(a{ad4zUX6c7bKOWGi)G63z)PDHDvvB!;9xiB&P#vm z==b{!t!4-xFTb*U<-gzgf9H2fNlw20jm0n5Ry^K`!Ks0R(K4;WyvkG3S>)xF0ra-m3F`Yb@o@;@ z=~B3|p4I1PK|J%RFMFBq{n@_L>G`pBzs~>pwWG(Pw0i&lUt4#ws?U#~&O7DIbpfwq z0o#SWo}Lu{U){uU%m3Jf*Ds=eJy&hzl$PB(^Vs!&C7BoQm!H1+F~VSG^9uu?MS+h( z#g)G+ONcLaJvu>ohK0t;rav_mD;}EX{>wJ~lzv1*^IOTfigPT=e!A7NK|42|XV;h( z&T4o6W^`8#Z)s!y?6-kJW>zkRZk@scj1TuWvN>}z zd-_^W{&Wzz{nFsY8O>K)%r$Z!L?y!FjZl$<2k^)OOYy z=|JwPsMK9;0t$EiHX3>{#s7crD5g<)(#qq8#>)LK_rLVMb9>VJ!kfno9Svn=@2`LV z)htG$*lN<8o(Tq1R+*T+aw@W2oGgkj_LVn2&N-ztWx?B5vhkNo zBpz8d2FYgrxt#a%z@_`wAFmPAYI|2X+hFFsI&)nf=X=YGKfe;a&FQhgBv_*_H=Qn62~Crb<&X$@6&!X&!_zAEXyk#3 zMZe8U&%RI)`F~bbVxz8S;C1CnjmxfgSsM8^^&0D5-})tU{VdL|vlkuN%~uECk7)>a z)Ueus<8nye`YX9p#2PG41S%i>_E$$^!l%fd{hlXYF4GS$ubta;fzj`?)sfJ3`8!Wr z$}29@=vb`W9W-;(q5z5HomZ|2*u6+|)O{t>eBh`;f+0r)50hAP1L@3qh87{qhO<`c Rn!w_Z!PC{xWt~$(69BK_QM~{F literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire21.png b/core/assets-raw/sprites/blocks/fire/fire21.png new file mode 100644 index 0000000000000000000000000000000000000000..409d597aa75b8fc11aa14948781b61f4b4b045db GIT binary patch literal 1337 zcmeAS@N?(olHy`uVBq!ia0vp^DImKDQ&W9D+jT(z6Q+Z!{lG{Jx?7q+FSPos3M32ZuSQpwk`Kf`b!y zBl(VM>`+zkKlXb6wxzr7Pyb)X?xfT+VL5w!4)2w=&;3R0 z?k5E9r)sK&NGk6RD}A#%il;>&vh7CMM5Tz@nq;GiDq)(-QeNB&)ns+*&{uK#XIEU4 zY_;dR*YSi|DL(frR@&xG4bl>K`v2o)^!hFPFE7tbNRc!$4*$N@v(T4~Ybuvuv+1Vf zR3-0)xjTJD!&krDqyO*5BGEn1&R0ABikKUe81}K9HQ=*Y&5Ga3;*@-Y0jpMs{yO$3aD2USGkw{ zNp;VgI%|zUkk;HyIYueV*8QDbJBg=szLv9F$4`sR53brvMq7#9tF!%Q?|yu9*=s+$ z4?@Aa1nw){mXOW4y=+R&oKoo*jSs5?G_8Q{S2^bHRiZdqecSTh)aIYO+>f*tU+++A zQQ6{`FwrqwH+FAofQhP}wso)-i=8H?p3Ag{PBVjcHFj-X+t;GBLLjv5bx7)@B&{18 zmum-Gg=l_VTAJ;pkln$*ZK|-LLCE(>LBGG0uYCGGX4%Rco}6k+UW6>2q@Gw;v@UsR z=oHo2vrq2+9lSPolJlglMfb8hmag+TuTx#SaCLaO{G(GJDvWMxr#@v!+Of1$_LxeJ9uPyCr}`TWqj!zRnQ4O@0dZ&$qJcp_L#>7wu?^{2a? zukY@%wq0-J=~?~i;P3P%PD$Ic-Dz5HR8*~Js<{0TW4h~ncZtt%dSW*@>1wb zop=A(i}g?KB+UdvI&@WY)&EYK^8ei1yzJX=zg)dG-Je-!xr)g8|M#Zp{nx$6z4-y> z$;nBp)m2WYrM^0IuJ5r~{mzxS`f*8T?Y7>(FHreXfpgj03q^~3B0q(wJh_mcrBw1_ z$qSbizgr}}=03X6cK&@#Nm5sr!B$R9)hgbPD}^_S%;u5IO84@<*571$$hGZ`*%Xlu z>B9jx?rmNDUs}~<;SvY#Lf>}%&*k-}`r@NaCgff}uAjynr6Rjf)AhuYlkengb{T!I zQIm|ZUUPT)bUww!(oRcM{%ObXODay^ZzdJ3e^oTRUXesUpFL zvnEY2T4C^e&6ZzvH?>bpZko`O|L#}-&n_LcWgPdXlz5vcE_Tnqo^+>v%1kzKP1}zJ z7sV$#^Nl|j_|FS7|H7Crz-3Atk44$rjF6*2UngA^nQy>5U literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire22.png b/core/assets-raw/sprites/blocks/fire/fire22.png new file mode 100644 index 0000000000000000000000000000000000000000..5bbd4b93d70cfe0d2eddc082186f0c69c62653f1 GIT binary patch literal 1356 zcmb`H{Xf$Q0LOO@ZL%!MsU4Yz`c8VudSr?3Jj4<+qzz#m(N4I^JWSYFB&xgViOQy< zoDna1%Iqb1I!tV~Zj7irKctz+aP=?T>wb8D-k-nVok{RG0oU581%W`|Zmv$mZ~o;w zP|a@}J(}wSfoN=Va{_(h@@Hk;S$Zc7+7|AI-ciD(fhYjvYMLV^CBaf6>j1Yc)KGp-@jYyg+VQFE(XC+W`ISB;r3ZM%*ZdY1Q)z5V5ZfxdF(bccf9 zRVq?YlB-MV6RZ1g6x`KvGEq1!fZ^Zfmb$;y?rb=VD~&5Mj2C$pDZ8tpGZiOa96laNEE5iF>0=6Lst)}&A;u- z3kFDqg-15l?Tidpo(LvK?LC0p)MJ7M+N8ZGeyQY>!uN(y&m{_*{oppLm2ZC2;1@s(UFDRFQyZ&k74kOrj}%6 zWf2ddAxB!UB1r(9txUEs$jG^U#8&JMR9N>3lNP-&mG!x{ksuFM*1{_I?BS$rCX@H# z_%sCK?lZiV4i_uBShc&gVd$K)Oc`}kQiQ3Ask_=@ni>+I6OXRl`g{NK$aHtz$spU2 z^1^Aw02lQ8H?*-fyQCn!Vf>Gi!dR_+Go(oo zl=Fs{kZHEp$neTj@m316aw(76T}AbeSFF6Q7|yE$_+NC2Px-oP{BU2j9(Euyqz8x6 zu2-&@WDq7|cer;O*Wx7z92(2wgzt+^$ZntBv|Zbb%C<}t(M(<(<&dfrM8DN`tv44Je37! z)ki|vKoH@dFec|>-r=N}gZM@JZBy$p(F=)r51GT#pPC4d-)E`C6d9G6r1*>rQ~#WW zvAhEI<9uQ5i*>){#Nj0Ki;RYxc>ulLDSbl7w*!B!5wMAiHS*n}phK9~6J>w(_JBY; zI!phuA3`=+lrB#Lp$8*5!Gw=%x0xXiEHL=+)FrS&t`B3D(9=F}mh*YzhN1l&z+0bV z7PY>Gm2AYiko14BHJ#ya^l$(J8JP~Z?W`!Re9oxm#d^aGw@8%_^jo2V-&@Y7fK`^pYwGHfvvpN^fc uu!HKR229`jr=7x{^4Jrk=12dzFiOqwPHK;ER9*b7k`Om%52rF*Fymiugn%0W literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire23.png b/core/assets-raw/sprites/blocks/fire/fire23.png new file mode 100644 index 0000000000000000000000000000000000000000..85ede17d2f9dad737029b8f5be885e8a3e259b81 GIT binary patch literal 1348 zcmbu9{W}u~0LSIj?FH>EcPiFqR+iMQ(;idGY}s};*~~~rGp-jdE^lXTOU(=+j3IT; z(K_rXQq+~Z&cv##FqxGhy)0ohb_H~|v6_1f=dRfRlu0oTq3-tB8VUd^X-*`}P7{-j)FNMB&M0Sy z6Eo&&d_Az;m$N-c^y)U3Ggx#cr>@ir-Zpv#vMmzurMH8li~TC_iqp9#?~s0vL&g1{ zJAPdH%$CNs*@i}b(v5X1bKnDJ$J(>vVrTxiwDa*&IuSXV4y4D$f)^&QZdnj=XR{fb zv=A=FOSD-kkgL@QRFK&A;a7>FgiyQW*SH>lk}yNlv4`HO{3GC&zaCtFIJr=rJoA1s zuce|`lyW3gXc?ZCs0XnE`7oPK^C4FryubAFwBaiq50i_H-dX8svZ~6;4)opiV8=ZA z(XIL(7uoe)tz-EayukJPCpMLadohyxnSlnIq3}aYbsCOr=R?d(qtZXyI5&E=0|ySl zYA`{2Ltbb$n$rw5^RI-~Ee|s~c3BgTb*K z3#g!&_=5g04&Abr-~FsZl1TxEVS;3ub{C$*qR%DMhW+6CGjFH+S6djbCG%x4Mql?J zGQBG+p5OlV+7rpvwh2OTt?`(xBkywW)|-rX@AOm`R)I4rXneD0=UFrqkI7A0Gl{Er z4tKRjdIjd{?_E(fh&y38dP4w0)!gI&N8eAmvr#mZdsw@UO^2YG9NSIuoJd+})kLdo zM{=MJf+Q;tv`F!@2WyY23$OQND1El3HYUn@wGmNWjdWG9Jo4j&bvpL&wZuQ>`!w8B zj%2AGztB^JYMnTqkhCaRq_gC?(V=Zm=7uTazl%zfM&D->N{MxVs0&AlL#3+4N8c>U zW4FxQ5~%>xRe>;p`}vV`KUWTX0cCyEamc~DAq;u4LP5!6%Gio_9z~-s)-nYf*in5} z_-?0akg!^jUq6sb3m|36jx$LDwe z2Pg~Si6_bAH8v!NqJf zr@Nm>whgCtb0=(5PBm@I+!*|%f?Lc294VRGV-m9$nf=prxXuh)9$V z{s8n%KDo-;NzNm-6evt2l3RdbAB8`sd~+f#(<(es4{Bt~PR?p^O&*qA(F-tRhN|-1 z`h8b!%q!n9xR$K3c22ZYs^5xBYa%K>cwl+Oi1Ut8gbGFh(9`sLrJzqY)ns5=m@=${ zSz!Hp{N^+Tqiy4^sW0DCzV7&&Li&J+8ufK273k*Hb@*r;L1w?stCdxuM(FF|Ps?Km zBd;+oqzvZpy^~aXkUn=YlnIyeW;onpIxoj+Hqz7jbI2y`@CrD}gr7n;oo(Cwi%uNE tfbwD8=w^$+C1SziDE literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire24.png b/core/assets-raw/sprites/blocks/fire/fire24.png new file mode 100644 index 0000000000000000000000000000000000000000..5544ee65e84929adc96fedce5ca65db484b00438 GIT binary patch literal 1331 zcmbu9|2xwO0LSH4A?}v>l5UkTWO=OZ>3ANdvM=#j+hm&}Dvwq@Z6#%i5wWR_x*OZH zi5PPU4QuKuQcb?CN0Yt?bLGaGFPqZ&TDk6DxclMtem&3o4|oaoP{JJS*4tTGSvdgV zc5h1_e~m+(5Aa@%--yl462C()}j@!T2NaQa*y>tMne$cW^<{zhR1F$`*erxL0M#$aBq zuu!xiKqY>g4Vj(nFzGDb$CJylVp;doYl3J+Yb9HKD~&10#UApxVedj4)Foahnm(xY zq&@7&c+m2EmWUD%FZN%@6UVR>3S8nuU>T^(9WlC+HFETZo+Gc{vt{c%PuI|^`_@3A z08)F|HpP4ux0>`kR&(RrK&;u}cxm4sLgbs^p-r*OE$e zVLac@(_icqMQk>O6VKDS`lCpE30;>_flGiLbn|{8FGMZhnq)JJ@Umcomr@!C3W5Z; zA@Zr{zyQ%=TJfBEzlaMd7#JU_no!m;+<6SZ$v4eq9mEzupJR~w%r0{wfGl%HU-LzI zuxa#w$?LKY<6Fmx$uxL(l2=f^+4pni+pO?ak*yUR6zX)PCb6Zzm94cz0-Vuq(@viP z;n}N;6Lihq5;3nzvxVF3-bkf3>hVt5FC85T-Q&0GOM3Q7+PPF1lO|6<ym`u6?8x1RJyr*KAz^S(beMYRJDkk zQp69Q*c__r18=)VDulqtCT{u9L0Qk|5QDu4fuhp`q;@!v*T@q(aU2{WM=E?!DyNtn zk`OAfPb(`EmwQ>}n_9A6;|53H>?+c>U-E;s=VLIrFvWb=Go-I&*_0 z4_t3wYM11Qx26NOoY_DN8!W8#qnT{dr`LyGxcZF8C@oazgq6W3LhDtGCklSCt3GbL zPRh36gq<0kROj`Py#N49v_D&38$uy6VH-eoU@ZHLP^u=h0RZ{6ivF~#xjwVEH#310 z#SzeXZ3|h7aOq^~j&jG5efJL$72fbfVs!fsDmh%7wRSXxv^d9@;?meOf=o62e j?BbVhF4n96Lq9&Q9)Crm^njd$zsJxDAW-m1Y<$i?kw|NA literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire25.png b/core/assets-raw/sprites/blocks/fire/fire25.png new file mode 100644 index 0000000000000000000000000000000000000000..989beef8e71b1524e9e0d49e764c4ee30b813e1b GIT binary patch literal 1351 zcmbu9{Xf$Q0LNFaVlg!%4^!?qIdh4vj?i(wR=)GFt?$jNFi#`8Y}`e5p}JTxP0VIo zi)?v{Owm{#TAiHgQ0HM1$0hQ#t*Dj8>hv$%{qXsGUa$AhpVymBCi)o}pbcO!m=OWQ zLu(uRIdHwT{%Nv^0)uVbOu+k4ez~l=M51rqzpZ;BK~&GG^0}0DuyY;C{VN6f34XJ6 zj^AV+%Vb;NJ_{L=bl>ogEVenxehls>c7r<@FI>6GW?Lg=PfVqlecI3!#cITIM~|?t zr%&0VJRP~RLQS1esz+5BGJ=v614gI70ImK`{b%HmG@ZO^nz7`Ti#vB=b9~bvk&F^x>?^%i8D9=J(!m=_2o-L z$=TYKf$GFWLu{Tw$n7PfsHOMv9fs;~Fl+8wGw z-IK;nE3PW~D~Geo;?=xJv`JVSvKV15Nts>MO^4}{88i^azTwj?ELOa{hsQN zXy%vnV`WJ*@<;Fgw2l3+WYx9Y!_vqaO4N}g{p~;!Y4D!t^|zwVU`sxXXDaylRaTZR zMql#LlFzRIzzvLW90lZ%LTR4KAKtiK7|B0FuNH_+cnJI73dok62EOAQc`jomVsUD? z``+;F+*YR@SUcn2aS+fsM(`60@c(YLCAV7BX(8G~RBE!0hmvD{rw&3#(nCde~LI8I{+MKYJ;N z3EH(2^<=k(qIBOEL;pgEKHVn#PX2UxNe9B8M>2BxWLyCog-d^4-5lWuWY;%7cT9uN zZ5>AmyoJt}hy(#^*?)xjj`%6xt+$+E>#*aN#@|0_YTSMDTnPm53DK7LBGbKHoN~B5 z0iZ(knYy~WEJugg@yA^*dz7y~PMz%t>w7hx4o$mCx%3B(gufo!7`GxaN2xUHGdEpi ziJ$zKrR#%mgQ*lOApw43FXIGuED=Z#PxX3pYowV%%^;nxhhQ?MhfJ*9|ZQ;!2- zXz~21+T%E#6}g>LkeQg}S=RMB-l|y~@wL=c(_56MSng4KBp$hMFAjtaoo&soe>UC- zR8)X|yzk{luJH+?7ft479dWbvK-@yjpw|`=(K*Y*mEyn4Qq-wvF1?lwJ}gBe+^mZoY0Z zbR`*zXklCuvx!X1l+9E%NJ$lfHIpj2RwrCk$5ezTat$C?Xo#p5v}jgyqcKmotT(dX`UC| zj6YLaQurru$M6UcYD^`g%CAXaVsf;k1CDj4D%mv08@Q zWX&yN*0 zdbYVxL(b}a!6+ox$7jCyb6$74M}mET*{IUGKfFEhZt9(2pn`EFbWFpGD0@pxxLhk! z`2lqB)5==AeNto_$BQkq4>dd*c~)PcY^-ZipSe%wI)`ck&;$W%|8d*_^i~>R-v;i{ ztZ$ckgq#09p(1j0-qKRiG%*an$WU!&PSS_N=iBF*M3HG>OG?Q=J^$Wpf@O$9>T_gG zjtu%mwR3hIqB2;%062Pxa{!pyTj6%c8Ttx!{-$7shjXPyzP@OFNci=%1czyoAQf2X z^JNdrbwJiep;UMl^k+5BL9KwEVY%i0ZjM#eEQO+t{UmcZIyATZat5I;pA==vRO-08 znWc)`GC5H`wX9CAQfd>G0s7ruKpwySy(0L)Q7*%<^*NJp z(5h^Z_hED*!vux16RBDtJOWt5wTj!4bUA~bBQB$(V+r^phAA~FNAw13`Yei_#n9Dc zUhC4jANlE5Q9fP@7k2c$yTztCq4iE&;=-v2pA^F-|MY~a67VFNY!t{t<(+R?EyFHh z9nb}Pus+MpzMacjY39l5jIRSCr;Qng z`^q1$bycix;Q6d3*yY#**8{P7DSijvH6UdzkItI+WEy3` WyGgaR_3rI&Horgf=FP=Wfdwy4{bp&o+96=z z!Xcr=!XwDk#=+>^(xB*cKtM5pLg9_eT6AnIBCnpV>bw8?#N(KpHIj;Ik}-WMsb@Jd zMT>+#Jw7(E&AROLPIuq;S8mRBm;QFUd&#raPaQgvRt*-ujDKeBQNw$Gbikt>iOy4 zu5J6-HPdSI?tkWd_hwD=_ z{{3rjcilbOX5{6WnI8YP*jTcse@eg-r52SLry_YCYPGj*&axL$UG&fPX;+|^%%*8V zF&(~bd?!Ep_H_6B`WU`sO4--7TJvWYc)JT}efDnMzP#+py2Iyozu%L>Ci+OLxpPub zoqhOz>#tKRW-{%D){@5*f5cuuW=!*QW58qe;Z5v%&1)3vwWyLXe5Y3Ny(4~L^PHhFl) zED)N+wss2NqPML#G^SZ`H!l{JUH0Xv<@~EcPCuB=_Ppo6RdTkcH^kHHM6%ja)fR*1 z>GR4fJXe~8&8`1ieAQ~!6c;BpegBez50W6*!>qVw56s{^+++ci?UDddG~fN@9X_? z=UaZAQecr!eaR#-eP+Ts;UFJlGda1INs~@)xLzJL@73lh`r-T$-|tp^oEbRB+p0vs zs)XsArHWTe)NwUc-(~LKP3PRcaO1SX^NA;|zgqkjt+wKx+PdHO^-eF%`dQnTEqVW- z;dAB8lod}sj~-Av(yrpUQcrJxXv~iNf0%d86aFTD{$|RH(n%hf+=?w)N57q#9&~7p zyq09YbCOR`OiIgw+!lq2Dyl2FPCkm=-MhoSN8!tql&O45rizuP5*~SVzH+~`?CtC| zE9##;?33vXX*n`8=SrdEC$-i|3%pMn%evfFUm~f;w4yn0b>OsouSZLi->R3%YwCWK zIkb8Iy33#Rg4KQGE~}eGz4M%V)TCK&Q}+~|)DK6usj42S3Da`=A|6)wusc_1a=Mn2 zuga{3msfJ%2XZV?)65MLel&TKhGceci14EQ*Lqhe>rD2Xyfm(4lCsd7lzZ(RJsAmB hrHPW{l?xyFi*EY-Q=gSP7g)40c)I$ztaD0e0sv5IK{WsX literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire28.png b/core/assets-raw/sprites/blocks/fire/fire28.png new file mode 100644 index 0000000000000000000000000000000000000000..464e0c37978f7a97b7239ffde9d124249dc37b60 GIT binary patch literal 1291 zcmbu9{W}u~0LRBI&1*BOhly)5HJX=)lyr%;&E87ZcG3$=E{%s?<)uzF<7OhZHgi^( zMKh@>S6!o!3(2u0m9cbgwWT#JbEBO8g}WcVFVFX6<_{i+*H6Km!1PDk3(LmL;C5+(!okZL*7Hrlq3+i6F_5q7zyoDo8hc{REEn`;H;Eo(~XB`?;` zG*7$HyeJt!SuS`ReQ1N@N$uaBr>~{U6P42Ho>gM|LYRl{XqtNJ$NqB+Hp`T6ZM_XQ)MS(#y0%i12VNnJg-2t?>^D3xo zwxA!A*<-@klxXwiF-yrL!!2$m&(>yLL%wl1sp!ihrQF-d`uRM2&lZRhmdk^~?B9jP`wn&&8LBdFr;$D6y<;{mPT-CaocayzK`2r5mWRMm6U1W-@W%eIF7MbmG&taOVDmsyJg>?3wm;-6j~+ zr@;XusLos6wY!(=;=L!}es|@u(EX$}b*8Qk2@C110Edty1h|`tl-ESI-% zFS9sJM~3GCm&!pcello1W&$5Yqm)S`QPxEqR+U3bG{k4X8IuXgQwi-+yXPfH4w71( zkA#2dbY9(o{l#&y7dyS8svcB;catJ_3AGEw>$t9OL5jlX_Zx>Q1SFoN47&IYPlR=e zZQ{!ld6BrV;;;!UXKJaIML5>=0Rhw0x5m`6hZXZeN;86TF(TeINBz>Daq>t3TF#Z~UE`S|={7tL!p$q1@umOismJJb$Go zO$loGvt!@th0W7st7XAcW8Ub4tvi-E|L0kH#FcIFHrah!lFRC! zzD&RNMM1QL$xbm-qSi>rmnU5M+(emRuL~&xj;%kIG<#0&Q;F!R`}h5-h99Kfk|t{n2;x_3-%W$-A5* z+1~&5;M7Y=tI?WuUG&-cKe95b^TofFOW!Wx`x5cqQR$I_rCrb_p(V@R*1cKtKggqP zmuk$@3nte>+#_7>6h^04*UnaHGB}xVYQm(|0%D2HPk$_N6Bl$}Yb9Hn8!_qEJ+(_7 z7u#2S|NZ;*-;Mw7E!ceJPRouH0ULQ!|18P#?(F?ozF%mm%F@L;MS?~pS>AcUS6(f@ z*cPwqUFgX!l{x8PikOp;)wFZpf32A(H0j`rAg%Q6%O<^>B;=y7Z0{E4i#ox4jJQ z{CAFPrQbpBk1us5xktbIvCz18YhT}Qy?sp+7XZz>|1>-|WIU$l` zdCRxhr4x8=6}7)ky2KM)XjJ0Cv@zlAqW|j_^2YB}X<2e%TKoE6y}U^gZC>|WLw(s# zUU2>J@yK-hudo033su&!#~10o(qU`0kkl&aU7`^9L+A9jFbh4~eIE>-a(Q`i2|xN& zHSg9F&83o>E*`VC3QeAJ>SylOd&T-qr<^=EpEN6aD{{W7{CY#JJotXoffO!jZz{P50=xqG+1Yvvbv|MTsUXE)w&fA#9M4`;Iv$HjYB z<$U}7)wdN(p1Lwq9A6}eVN6L3NDd?%{ z8Ep~@;ShbMv}8t`Zm?I*;n27r%T!iOu=y{2RD9mCRY_{vSDlvp5b)t>;t~D(v#Cub zx_bJ|X}l+-oPMmEy5r0SiTNw_OLus7d9sxjy|7}kJnf~d%v~6?NljV1mBVxLq#f!d z(vk7Ymxw-?G-+7{pJ$Z1;tyG|(oNk{F1+97b7P6g-N{zn7cX89+OgR;=@c*ly{!;F z*s{WVw_dLA^;7SDENawwwbmy<(Nohg`je|dh0_(kbW5c}J`0_vb$GaCky~IqmOqf4 WH>LCM(;8s;$KdJe=d#Wzp$P!_pG*V* literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire3.png b/core/assets-raw/sprites/blocks/fire/fire3.png new file mode 100644 index 0000000000000000000000000000000000000000..0d99250e4150102c7e787d006ef8d4051618d91c GIT binary patch literal 1112 zcmeAS@N?(olHy`uVBq!ia0vp^DImb6l zZcFdKcT%|aboI;Mag)41J^6mm_Wsd0y*OvPJ@02Px&G2Kpj51uA*hy#X<9>rszL%M zM+C{tt3?Xh+x@P7xn8?H{->*~xA9gU4+*6`cSUDRW7wG3^1YDpXypV8iC_C(Xq;=_ z62-(6mZ#7i-p3kz^1$Km0l_T4woJ9`Xu1%;+%>Yc;NC93Dfa*TzC@mo5IH?%D%(;) zH*L1PZ7zv_BBP#LiEK-7e3bM`#HAXE(^c(y!m`;v;FIB3!PRh za9F{hu!#Bk{NGkwOAoKe667-=7 zwHN*NX)bqo@x*NN2KAF)%x7BoFW7uaQ$UN;Ym&6{sc-B?x6ZXblJU4RDQUqIl@6<$ z+a^w8XS=RZDCF|=l}5&t%L;Q&N(DJ=%inh*G_0{T(85rI(eOy`0(FJ|(K{=Pj)z<= zf43zB0Hh?L5>v=D*pbX0DItH$jT$6amIO=)zV_#7mQ+z~j$XDYn%&Ffj|3jFa}w$De$Ae) zqwpm*;qU9^ff60TQ)9jvwOHhCY*47Nyd5UO_5JbWZE}iQrI$))D9uX_WLgv|(ln3c c-1nE&ocR#nudnirz!HPO)78&qol`;+0Gr?Bj{pDw literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire30.png b/core/assets-raw/sprites/blocks/fire/fire30.png new file mode 100644 index 0000000000000000000000000000000000000000..88cb4a0bc2ca2b95d9f6b8a3c51fef1fe53e13b5 GIT binary patch literal 1355 zcmeAS@N?(olHy`uVBq!ia0vp^DImuNVVuV!-lTF&LdYbpL;XL)J2uqUJ$nRZ^T;_* zzO-Y4^zr+;tQmg~#z%8=&6;woWroX(t5E~|;Y9yw)Kex1u-KjqwA%lge0etFU1QqGed z9nwy8-Qy0t-kblTblSg2lRibZ`z%W)$XqZo`~7m}ySKu-*Uq%AeXv~RlHU93y+8Wa z-v2B1=et{+NB-+q2cO?o|7J5avCL`HE3ZV!jXi4wLR}=getz0rtH~`2u)kw8BRwdh&1;QB z)G`h4q-okpI|U{sN+)}UCgs?6c(&MGIplBExwBk44Ti1?%NtVXBAty7MRh-tz-I})Mc|iW`6+tmK8+|;=1h%$L=-IgS z@xkjaBRBo{J-Okvw~Nm*otQmWXI2#Fe#m!PFl~a!%!5iHUz)sv*46H*U6Chr?9!`k z*HspKE-PEpzFK)Lm*o7nrPH3B*>2S_L)K5?s^|3*`%9|?ruSIw`SWwZ4@bU zbsyVX)y`W^8s0Bn%+}OtUeIuR$r6nfI>~3(&AfEu(){vn<CjxJe3eQ-?4G&FNe}YZBh0C4Qmv2 zTtyFlu#`8{i#T@bW~WuXn&r*bmX!7`&o;Yv%B{@jXFgI+`Zs&;-Q2V zZ_%E6W^zW(HHDrR=bNPB z*^AFfn^?j+{L6k!aQ5=s#O3+3DnRzizNROC?6))Q&Pf$MWRx^->XVkI0T;{8uD+ET zY8d2do3gX(t-Hp9&>)|H1+}j|c!0jCtq8r^yrgNfgXrVQlA%Gbz9{vkdYm|{{cHXA zi9y?=)03vD&z`a{i^ucSgYNmK78d_Hth>m|OG~nJg>;SZ&6W?`0^15h`*>RxnESqV zy*owrym4e}NNVQ^jmfsoR~BrR5lmcuQPtZe0S;523O3KTiU_3R-Rj+Vau3oIlV NJYD@<);T3K0RV)*Z?^yd literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire31.png b/core/assets-raw/sprites/blocks/fire/fire31.png new file mode 100644 index 0000000000000000000000000000000000000000..ea83d1566b453c3ae4b9235a4dca0028a410b476 GIT binary patch literal 1270 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 Yr(0PS&G#b-?e9N) za$Kb5wy4AHh+ClIq69^amn^IQFgHd02)xl1mbCN+kg;MxeZ$m8lkAHY zsb#6mpR-T=Ua`^4n|E){o%{FhFFS#6d1ae-&wO6@`Q4k`EK$a;sEg_fI_@nEicSXv z6caiG3|u%QlvsEKnc6rQokg&UfASt?lvoVb1ftgS@nls5!^BYb=}Mv?%4p z?S-D2b0;h@3ecP@5Pl&@$mPUx@84_Z-|C$nf1reoX(v~w(84@n^{1z&GN1kXCB7yu zt8pi%r;_vCYbvgy3%76OFEPDswM}R5JDI90pEfCEcX+m(yL03Hu}MeH^y%-NS6Q^h z?)Ss#mp4v$#XhlHV0y`l{mz;|?X5=&qC9v?3$)`i9o1C0gZbNDUDn?hwu}Ax(Y)?y zd`w;)*;W#!s%IXZZ#}ij^qYPEHDxB%BjR&!UpJX{=jAGi&?DPlv%ju=KF@KfinwA3 zcg$;#S-%pKKK{OQYFmfN0#D6QCm*9p4nY~63TozxdYX58*^kmy!{{HpqEnmyS((e5)ut+xE|IbgW^WtOcSs>m>)S7>OD_C>M+-Abh9XihKPg*8T^5i&KaC6?INh+#5-IDgZt-o2sy32+C z)LJ?9q&XO*(&-XPd6t+9_9mUzdLE z;VtsD@wRfPyk=nPhwAHm3W-&-t@;ZlF81csZ04xa_tt;0i)U->dyaP{rkZMkXRiER zDEd2YPS1Rsb+%g;ehj|-d&5_?wmFkj4o^FMHB=-}h+{HGT4;WCa;C8J%BTvdh$ zhkGyAf4d_pswIk76%AcIHDfp)Iy+o?*p+$zPUy_pKXPN^r-*PU9RAN=I0Pg;blb|DJPk@{Eth-sbk!+t$Wyyr*`!RQkRHZ{)5ioyFDDJnz)}-FiFr z!E9w-%`f^~t3;;v&D&GF<6gDV9p{^lEk~9*yNfT}zPn?i=f|@zI_I8#Z+544wwF?f z)2F5-AF}twKXr=nDv`W1&5di0K(y7D+?aO#W96|1Cr>cVnXqS}zg6v{ZaaSIo!pYv zy*~|FxR+|a(n(It+86ZtU}ek+^$_(MzUYH(YgB@MOfp~Bw`8rnPeVdMUqEq|+&%7giNcMb^i;Ky@HXXr})@3C3ugxR|Sky6iy85}S Ib4q9e0FBy2V*mgE literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire35.png b/core/assets-raw/sprites/blocks/fire/fire35.png new file mode 100644 index 0000000000000000000000000000000000000000..4a9f15335d5dc6f8180d6404a357e0a732e4826b GIT binary patch literal 1237 zcmeAS@N?(olHy`uVBq!ia0vp^DIm1_6Tz4hbC= z9x*01ZpP-;hJ#KA5)>0i6MhnP;?xC!Pp1%^DpiH?T-Z%f&vqq=p%r^P#!e51Ec&ANVhn#R1AmodJZO5SG8UoqeFMseDPwS6heR?HR% zy^t#TXk#A3BDT2HrynhIjan9{B(<&YH|^W|`241=ZcbMSv|>QRYfPBg?R#(-(0z@V?6mphw_(J*F(Riq$)MA#ZDKxUc7D2q+7SbHpk|; z9Q~)xGBYCe=tMQyO&p>^!d_kligw`!YAut5VmK~)>9z30)CFz{yb|iPL`B@`lS^L9 zj3rBcRo*Qv51K5k=-D~xqV<;5Kfgb_Bp-h_Q+dV&lke`2766^4rMZeHvT2=di^O`T zE3te2+*l@~xXkG1f3Cv&AFus>A86!req*n>V&L~XPoL+iyQMvT92MMk(m~(f{|dj) z&)>^)Zb)sac$+f&z(jWWGkRywpV_A3IxVTmUo)=d$Wmv!{-8yZ9Rqf+6mjZM%qZ}U zpWo8zJuAD&|IB))18OQ=TY_R1O`a%bp&0n`gp$fB6V(EzZPw>nL)P_O%eG(LI`8kt z?{}@Q@<#_0mW8^9JjmiWX;901m3>R5c3!J1^ZIKOO^i75gTSXE0ygy z4PVSHm?toiMN!SuXq)l9WjbOLdG1Lxk(14Vg~C6^hEh@6Yc~C-fyEVrr>mdKI;Vst E08OPx# literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire36.png b/core/assets-raw/sprites/blocks/fire/fire36.png new file mode 100644 index 0000000000000000000000000000000000000000..bec029e19a5db89dbcf1e95deba6e077f2be0ac7 GIT binary patch literal 1239 zcmeAS@N?(olHy`uVBq!ia0vp^DIm2zMr%wUv5AD6qEOt)N1F~)$iYPN0ggK#PzcHft0Q2pB|gNa(Qeh%vEoGd2@1-1gOJTK?RUqu-gu=Uc0-SLF-i`sg*GSGjwmpT`WI zn}!`pYlMW{*{s4i16x@ZEj=QoXtt#Jgv*KNtKFj~Z~cBtd&Pf`EmM|g9nn5y-=@aa zdZpz=%8&T>Azn|vRlJtTyW7>3x~<*CGjf(3_nA)@zRY4_ zy*ts~`*`i{x-)lcpQ=s~dF(W4&)>EB|1K zWIKJoTQPTbNrj=Ns-a?>;6FoA!zn_yeoYS2UC-Y4Rc(pIh0BHVjti0%pB6993$Op; z|D*E(-` z`J>F&-`CmOJ^fd;c~R9Y1{J3b1pl`O1}5^l+TB` zE{bg(+RO8E3WDWNZrWHrML<#Y)s7WydQ$2a-}Qgz{-8AZVf&LK6E`K-_?Pi~1#1?z3i0N{Z=V@N7HKA|j!<>x;i_amgZLbbRd3VLP@K`-NepdM1I`7RtD|W2t`d>MHCBMs&iHTij zT3T#td?sJITj@ScC{!_W8OyTvAKo{PuRP4yc-T09s(R~^itm|4U)LGPai{a|Qs%#x zefqRZN0wEJ(UBc}Q&c)83r%`>_f^Q`njxg HN@xNA_dO+8 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire37.png b/core/assets-raw/sprites/blocks/fire/fire37.png new file mode 100644 index 0000000000000000000000000000000000000000..c94dd0d01ed03fd3d60847e161a5c0c163ba8ffd GIT binary patch literal 1221 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 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire4.png b/core/assets-raw/sprites/blocks/fire/fire4.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b83d2d55ebef9ed0ecead81bec75b8daa12691 GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^DImTvf}{G|L4V74yMa74M)-M7l{pOe$nByC&%M-}NmDq71aE(voHaFJPImPZPvE3H}A9Uzl44&opPX|$LDN#$!m{0i|%o9 zy*eRqupyvjQ|{v{HD6|1L{4$%6m565MY0cKm^WFkk=eJ(p zsA0wNOR98sm*O^4^H;B)g|Vb^AB`3Woc7YA-SKE*N6x|&wx0$63SQ5)`t&CGoqj=L zl7-Cdze{T44811JbME2URi~OCU%gR?XO8TvA6suaG|DTg^<2HJzIg8Lt)HrI*Y0(; zj^6Gov1pA(B|HBj?}QmjOAcD+M@>-U5ft)D6Og_f65}cH>2Zok{HLszB}WTHpDdc^ ze)ed0V(AyoGycYJGcH?pyj5_>N&Itui>e=+tEWbd+$6IV{@&oY#tZ&?)++0kmYjHhHsik7hAH6+AyHHO z&O4UmPr7Ipu5qt1z*ED_k0W+r1*g-`3bA==`Ho#u%}aW&PtZ&K^`(2Ygv;d9hht13 zX?sF_?arNl>su@e&i}O+N}b@)ub5xX8O$}I)%ftk8{U80RedC0CC4nB!lc68_Igh9 zjej!J*T(ct3hEJ<-^3C*N2b&5;)^qz%#}6P?_!%|5<69}xrlF4+TDUy$84utt#kV~ zc}2Z+ywoFPXIL`l#zQThc&nF1cNqebNzGqt_AhkUkj@sGYS zTV2;4RnO4K&|Dkz;bD}&k{pYE*WR1r3LJ7G2d67^h&#UFUa`F^j)h-o-kYoJ8n=Sh z1Vjl=P13z(cJ|boxfV0$rflE6*?9lWKl{ZupWGJvee%tdDj#jRwC;an4Coc)h+tt7 zYiw{;NFbRR_Rv8re=g6DxAS)Adz@@P4!=`!bMM5U~Pm@dW_MWxEo1^AUw3nOHH|Iy~^|-_Kw=XXdbvg3jc4R<^z^CNZ z#oC$mwkaE?uL+aLEK2;B;Mk;caK+kdhw8U~FR6Ony}#IAEjW`kar$zbX9AXe2Upeo zeLQvB)IAA{cRoD!X_t9@sm7g!a_ooj_4xcxc zND(>V9X8Fvr!-Ty>7cV)A=i?dFFb^`xI|7ozj!dA@>6te_4yBs-R{Zi5&~_C_m3Cc z;$>w^S^cx!$|!c5mU){^eva2;!Ivi^C+zt1PWIuEby{<*84M%*8mE*`lUDn-BXE(^ zOPO1|Cj}p&$?vu{Q7i!2h$9}!7D*w(kR7FBS%sF#Ma*frdfGO^r-3vcR z^zAtNSmO73f2Y+BjVprop8fatUx7f#RqkJ55`}-Os`UOW-L1cG#s356-P4uoSG+#8 z`cA+@$B>QbmD(a3RU~Ab?yXxJGWB=y5}SlMEDt|UUfUGtXmH}z8IJ(D87u5Hl0VE! z>@bwLt8uRBL*Su2bxGEmcNbbGhR<|N)0g4=AN=?HvmXCxN?v#B{%m`<{b6apkh=Sc zM{k2mTei0Tm-lGbNM%Nr>LBl5&e&^?!ch29Kp)u3Z zCvH_#;)&m#8lPTVko0|J*<#|grum9p4Bz~{Pm}u-!!A6Hty>fx)f&^T(&|yQQ|P-l zlTVyHC+u)eQb1O*Zl&Wn%?V2ur)a+xn3t7ue1tv_Db5|jh(PYm}8c_`E>>N%WEA=loq6js_42*EdDep^a+Q{M4%FrmC@%!8f4&k zV$;n-XN6B)t$%vYdT}vlW5rp+AIWNgx8#sPgg&ebxsLQ0O+Ce#Q*>R literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/fire/fire6.png b/core/assets-raw/sprites/blocks/fire/fire6.png new file mode 100644 index 0000000000000000000000000000000000000000..0e2a45ff80007a49af93738d13ff6cc7534f3a68 GIT binary patch literal 1125 zcmeAS@N?(olHy`uVBq!ia0vp^DImquy~l(vtP)t>LSOn zgeJ%2AB9(H-q&QU|GuU6-7UNG&tKNwKP?;6>lG&O&CoF5G*Q@^3e&8kQlF;$oGvJZIig}mL z-C5``Z&QfDtdrBeHhYP-s42bF^cFoFyFNsIcYD>`-lz5pZ(n?EHuYD_l8t3YR1I8Y zTm6o&%`wl}{`^wL*N>;C8uC}(e_MXxGE?cPtfwJcf7b5(<)N|YVE57sZ#mf|S-D(P z|Cey|BxskPmOHgQ`n`0VyW2)S7gNa=UdP5Yx@#)V|5jt;eb>1&!Qs?%{;O-{TPCwC zwYcy$XKkL!J^i}}8@2>pzc=qfsft#Vy;lD6yN7-#?y$C7aWhf3Nz&=3Sxn!Ww)P0>_#K>d!SLIJhvS zeLv$AtPvvXQ1H8+_ncjRrd8cN%a?Dx|36Ki=_?`AVece(o|RAb!c=MXI|ftQMAVbt z9@NzQ_wD`Kw)dQKbP^Ug{`pzB=TU&sfe-EmnTsQsADy2qw5oG!;`JvT!82`Miyf1z zV&53b6L?D_tZ7S7>Dw8hx5Gr0yM>x-EcV{F{qbb;x6>XQEvhFk+K{_p!^@0qCd*Z( za$fA+RP%O5xq|uhEP20D-qZFSTuB}0-CpGFeShb2-kMgB+?qZ2#X@-E`}hpRH$!t7zG+u3tH^G6j-8VeiwovT}&F zD&49v46WV!`bW*(gXcb-J(6&(;lk9(rw+-Lwu+kaG950t))25t!z^8tL$vMEnZ506 z9%OcD#sB!tbJJzo^?)@Kn{KTAl^+9J9`eY9M zpXDdQ7U-ewb-UTcx4m;q^9B8@7Z-QlFWa(k+kAm7Tbe(lxLp4x?9^>~$?DQKg%BC8 r_-|4gJgxaWyvu$+I8AacT(kd@(jxbiS<@E+iw_1*S3j3^P6%(W>{l5ABv)%iQi%J0^pZ_tWD2p($ zwKg~^CIoOuki#@-nQ-&;@3{-U@6NB+c=1ms(=g6s22bf36U}4XE88-Idc=gL@&y}4 zc_>d>Ibmh+?rZfM_nuD+n%>zu+ud3CLgm-3hN&8y6F57C9@?j8J`4YAKf&#ll8Ud@ zqZ1xYp2E8(tel{8DkNskQql6;x2~p5-LJzoFW74T?OE@$ioLEM@0rLHx+%-oePM*< zy}8e;?!3L$sJGh2;7QG1>xcX9Rn{{_@lBbqV@1i8L(93VWm7Z_oE|MZte;%@&;L=i zqIaWUkblTdBSsBH`}niFZ|ffSTDgAfhlE?3_D>CtcQ-e1n!>Yne(My2fYVbM6jQ2} zY=5@Z-8(b*g-y_%Yp?S5dOA*1bl`X?Ialk*R3+VY$?Bi4Z(CmLoSn1Z{%zcXiU)>s zWH=NvJH0w(xSfwI@N5)R(hU54HR{2FDK5Nw>K}Hf9pvs2(Ep@j9^zFzId$!tV|uZU ztA7~O+1LaY9{jdhpX>g6S^hnN$B#~*+1)ClwBq>dz|56uNA(wfzBT&*Bae{cntyQ{ z52bJv>11nea1wE>{~ocUFr{|FB$Wel|IO3C9M=w4Qs#^FZWI@4`EzM+(4z~6H{Ry` z4LWpbVvEjF2~8IdCD(IX+5d0fWhf+MF8lRBgksYy;YCMQ6hE4_D(a+`>dF?Md3PT2 zx!nJJ{?@G%JGK9cHpkD4-m>k!RC{jfv`b@^Qkd3Hn`Dr3;HQqa@ISxo%A6Cj zKc*e_H{TQA-^hPhLh+Jq=<6+uZr#huT{mgrx4%Ipi9G^}UR>gBmVxUVr@K@HKT7@O zv;Oku46p8j$1PWc>t_lH-m8@jDiTiGFCF)P)6r&$+fCwx1W9GWfUqQZM` z)71;Ar}yq;T!D3j_=_Sa`S?!A$E89eCgt{Qc1NQtzpa)A zbcpI5u8x^5!o}#kdxFis+ZR=~a4+o0h}ZXQOs)oe<@^)(7E+*Z=q3{ac|nIW_6uw~Hq@U+nvLXl1H~?*w5XcEP&Xgj3scQn@A_ zO;Jf~vERP$$LZ)_Uz9(UUNb=2@A1-0i`S%ZV?o`<8vRV0)?$T19wTxh|mwAAsX z%$v?y-8`$VQ;`BE*<9_Go84e8z2IxftMszE`4DG3!!5nwRa;I^)#^??Z8>4*YG&U1 zZ*7$)A7m_e=w+k1+~JVKuVO2oSx>B#SJxhS>04X*=)P$2?}tBDKb`tr{B>^i`|w2u zU!HtbnKEavaip!Oe*vG_nP}~ z*wS~=FK6b>UEx*u`pB02mdSe;h6YcXB9kNRb`M7VKX#T~nRoF`{*fiU!oX}G zbg5o>+11liL+-RboS*77O?S%EQz3E!tDMx2u=ebEJc$)8r(~SvdmsJ#e_rpt(_gR5 zQK;`e6mb4|r;|YUCl|#IaeG-;7hv{!v^MXKMleut$4c>lJ5xSv*?!=Y-H$*ba~dp9 bewFUFoWaWSRP z+axdGn$_0S7Qq=S66O?Q9xKUq(f3|y;EjHJ#Scy|{5Lf1da*8VQNXU(YDyO-YR4=?M#j?; zC%*g)EC2PP!Zf?)UfhoALz#O%-`n+d!JMAP8EpzJ3x4-Fcb8qTN_yPnchEO7uEXiP z;*6Bf-y&Y!zCY*q%NyShKK8qOf&F#B0|&28KdZY!4N+BvC-=_Xee>y-JM(+a$Gj|< zfBjpDlpF__OL|GcxldX54BH-j`?~%3v|qP3ueaR%FZqQ=fWp)#?GJPRuXX;QVW2Rz zuP=&|&E?dW_{KB$XE(*)NOY*{*WI;!ca>Dno_qc3@pq%+ykDK)*V6Vt!C~K*t)Y87 zBd!O`-@YN3XS2bTwnX9-~Rj6>p92k`R14Pzu)_Jo7Q4pCYRoA&L#Ft z>bdNx#<4TGm|8Bm&fI-*>fheHN`sZ9P27$bO4%-HG%{$iZ2a_bQuU^1?aM#OCCkK0 z+}-xh$HmM`iP5w7>(e;h?>%n2j<=Y2DY$uT{NvUx)hjYL@X-CW!7FZm_@L0a`26En zy8`pH*zSi1R-Es0_p-bkb~Z-hQj_2Isdbl(0xzT`ZCrEDO0)Frf|nJc~Y-mAmzKDldort$P!lB&;r01?B{X z8~(6c+%nZvVb1rVJ>`GBird>QLru!NU83wwn2ud?-B>DZu<+!AXN{)$OJ z;t0n?Ghu-ZEG#;VjNT3hgn-N=%Wr1S{Lgpsm&Tv}Y>EnAJP-HEtMxSGJUQB-#dxt} z!yzeEXugxaI7MVj--$(hAvfe}SzR={Ui0OXoF%{V`vlLN)9F-x5Ll!zc)I$ztaD0e F0ssav3ugcT literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/liquid/conduit-bottom.png b/core/assets-raw/sprites/blocks/liquid/conduit-bottom.png index f2f7dfdd9978cf4771a5cca896f55c2d10d40909..04cecf2c9676d005fed00973681efe39c13c0911 100644 GIT binary patch delta 127 zcmV-_0D%9Z0h|GlBzOUHQb$4nuFf3k00011NklV0k$fEN%TK!5-N0tD1FFDL*n hAfVQ*p#Z#qfFFIz32~QvgzEqR002ovPDHLkV1jr2F&zK^ delta 133 zcmV;00DAwN0ipqrBz|d0L_t(|+GE(T>%jkk#0;Yj7;p#V6*T-GNIdF*!Rdg2fB>3c zq8vag%W*k^fu5FQa|8puEk}0*!$4S$>9D>0004VQb$4nuFf3kkv>0vc1c7*R5*>*(@72h zAqYg#vgH0}?!loELDv1ZypR|blyg8LkO^c0UDPcNzU}oXkrghb7(7*O7r?V?XzwMAnIV_Vt_v z9+AZi419+{nDKc2iWH!#T2B|p5RLO&FI?npP~dUB7$h!G$k23`K|SExe9r|OOY46B zv)I|fvcQBtpxMtseZpkxZF6}feoORpE1gVO>!W3{LVAOt&R&myzHvJYSo9TJ_pH=- z@N}yEgU>mh8Cs{-Fh`u#tiufScl>1K7cQKf9igwsXQDo4NXQDd0Z=)~|s+`(so z=nSQXYYHvR59Rxp3b7qy5{U3o47;C@&MC?8F2tDnm{r-UW| D+_G5( delta 1773 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 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-left.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-left.png new file mode 100644 index 0000000000000000000000000000000000000000..ac79f7b59b3c9fe4c46f7cfd7accd938e49c932c GIT binary patch literal 1881 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 X+MCpcyoF-b00000NkvXXu0mjf9#A}c literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-top.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-top.png new file mode 100644 index 0000000000000000000000000000000000000000..2e40b34e971b4174aabda528e571160d6e504058 GIT binary patch literal 3519 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 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower.png new file mode 100644 index 0000000000000000000000000000000000000000..d66a166412279f3dcb176c17745f59bbd19b2f47 GIT binary patch literal 3031 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 Z{0m9D%eOU+nTG%X002ovPDHLkV1kMFn;ZZD literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-edge.png b/core/assets-raw/sprites/blocks/payload/payload-router-edge.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-edge.png rename to core/assets-raw/sprites/blocks/payload/payload-router-edge.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-icon.png b/core/assets-raw/sprites/blocks/payload/payload-router-icon.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-icon.png rename to core/assets-raw/sprites/blocks/payload/payload-router-icon.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-over.png b/core/assets-raw/sprites/blocks/payload/payload-router-over.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-over.png rename to core/assets-raw/sprites/blocks/payload/payload-router-over.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-top.png b/core/assets-raw/sprites/blocks/payload/payload-router-top.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-top.png rename to core/assets-raw/sprites/blocks/payload/payload-router-top.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router.png b/core/assets-raw/sprites/blocks/payload/payload-router.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router.png rename to core/assets-raw/sprites/blocks/payload/payload-router.png diff --git a/core/assets-raw/sprites/blocks/payload/payload-source-top.png b/core/assets-raw/sprites/blocks/payload/payload-source-top.png new file mode 100644 index 0000000000000000000000000000000000000000..2de4c170be3cb5e4f40f5fbc0f5bcc07f00d8520 GIT binary patch literal 1124 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I8A$FCoGuTf7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`211o(uw0_p#k>#sCs0~zd6u|GlTN`m}?|NsBb;Gnhg3y{xQ;1OBOz`%DH zgc*^p{6G`1$;^ z{&uGHwFe@#3$Ag^*^18mfB!)J_8ivZ39rqhUTpWjCc9icd;LKN``V597JF`3_p<(a zI_)(7=V{*a7dqJADL%a8`L$G)3-7(w@h_W{-Cx{zum14yc%J+pJKlyYVAp^CbD44D zyxooRTjp?lxuN@K(ZkvW%kv(om*z9umPqekStb5rru5Iiv$GxgRcZu4)_VU~ut@er zXAA$lHP(Lq*IM|C&N}d`)O<*~Ur@18y=HgjtV=)GxTL(En!V(p}7R+bYwJYhIOpVRXrnMWGW-nolPGmLnes%Dw~K+^x2e!M2UlMPL0&^ebhJ3(djJ10QUOM?& z`>lL;;G~mhb>7N<2Ua?{vL3(U@W5r^f=Ss~DhB!`hj%Q0e!$XHWX9inPunZK&rPh( z()sYC`ghym?pZDTz~K7a3QEp~obpd@HKudtvL07Zd)VA%-q?G{FRSm*if6G8-nP~S ze>MB?%*lRbwR}bP!g!N89D6oi;Ey`Taxd(~&t<`l>fE(|jutG@0VeR62lM7njgj|J zy)LKR`1Ac;gLgYiCArSjz5jH0&(llkFuV6&PDuD!{kre+#!rv005u}0{{R3yb+fl00001b5ch_0Itp) z=>Px#3{Xr|MVOkTu)4r*aDYlnQ@!c)J^%m&kx4{BRCt{2-0OOyKnMn4GT#3^@3u*F z4h(!S2WeA&^@#%St~#D2dc+P&!G)B93;zRS3%`J&h2Ox)!Y5D_9Ug1sm`Zo{LB>4+xb4RXYv%7G5inQePEn`Bgg= z_!Pd1BS1x{rdREB;Fs_$j{rrXp8Y7>0l<_uK!9UW3{iF5lJX9q45^4%Q3MZ!)q^E$X0Xx@r91I^>$W*{(7?;B|l z9|znwRs%j3xNmQ#`${06xX#yW)C2N~wvI&=n=t#82l9zlpIcb`$^rRAyU!`CerxI@ho6RPq7HANjV~N@~o6RI74hRd&5l;u& zM@Qm;pfLG?ppb@ubx#uz6Al5JmNp=0Aw0kfq;+!qMPwXV13f$Uc__f zKY{IOr*k8jJ^u^How&P(nfdb{*M7JHqK7orerDg1Uqvc!Xs_vKUc!$q(6}9AK5Gt1 zKvn4R*|JCmYC@OKl13U37Fv9G8tK60_cvAFQsChE+;X6O+3y8<)=mS^b=9}^jR>g` zQseUjeIpPDWcS513d~oQQ6O_PLo!froDwLM=AcGMjgJNDjZ*@J;vCcnsqt|@y>IP9 z=9GclNmCE#@%e;4pwH(Lx`1AvN9Y0ieGcKT1cqP4@i&tA!%T#MI{dCK{=q{0!>#xS ztnm-|?7^TGP+| zFX~=AGW)jP$N@*dlx!WX^FM)|i$@OMwwsCIKBVop>2@r-liLXVrH=%}f2bnWfc=kPy=UK8dn7$_|=QjvTpe>t)T|bEvVPOHZW;1b>nN)BEFcp6c z#u8wfaCPAwYk>914@+K|VOZZG$sLvR1S&=>j!6pUz zlm?#@=wl>1ee*u&vdZWAAWryC3dR2@Q~b|bRl+F;$5JHOr|pa99KDOcWCy0q2Vaxt z0|DmkphbM8Bm^k=0AbY**hns6LV&Ulya9c+YlIzOIuizc;4S>KT_fxP&lvS-LgxQw zA`tN@LT>4qfZ|h#S4}3EiIA@p>OkQ+)4XQiCkZ<(I}ev(LYB}9I};&aE<}I{@YTZZ zM97zau@(U)z*h=|iIDFSV!#CWa)dY$@|{8uct=ol;OncucPpfz=)mvq65nl*gQ5cy Z{R5$L+}_^(W5ECb002ovPDHLkV1oNaMR@=K literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/payload/payload-void-top.png b/core/assets-raw/sprites/blocks/payload/payload-void-top.png new file mode 100644 index 0000000000000000000000000000000000000000..f334d2303233ab2dc9043f8fc0c7342c522d9b47 GIT binary patch literal 1105 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I8A$FCoGuTf7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`211o(uw0_lI78}@{Nv{&h7vVhc;1o;L3|No!CL2Km~AfL0qBeIx*f$uN~ zGak=hk;1^hywB6cF{C2y?YY3BCki~S7iXsYubx-(x{Q5;&V-g&&)##-nIHdt{<(ei zfi+xbR-qEV`kDThNqpYMD)jBxoNd|@*08Zh&SvU-RWL<_Z%LBm{GG1MKi4wLSFtkx zT**D(>L}CCrNZ*RTbO<>l%Buah3RK31AoOD)=&S^Z1SSHo*b0St52Nqa9gJ8gy(5H z{vI(ooO{|yq5o`wbdT{Jvop+P%_WlV#dR7DS}t6m}9(dN=)pQn6JG{8+2mZ&CPFG-#*Zp zu&aIbuWMZXOqs$t;tPB?@M>LfnUx?NvY^xCpxFus;moGofJU`TEVmah`7Yty=EyqB zLpY~7Udm%;!fuw_rnSr2g4Ay4In7RZozXajbKB_)YB!?16jGbsI&IErgaN`KX7Tq0oEh78%(qc7z_Dzdbwg8?l5~VW)c?wO8t1+xLxGZwutg>mm z&GoV>Icz7Bx#*;?y|GWZ?*w^-HZxB(E^t;1)b$Iwcc4VkSGF(Y-GLNEPup`L{|HHXd+MbX4{Wop_)?(Ux6iwNv9kr=mWK@S{x5tyeU5EHGl;E+(}2LDM005u}0{{R3yb+fl00001b5ch_0Itp) z=>Px#3{Xr|MVOkTu)4r*aDYlnQ@!c)J^%m&kx4{BRCt{2-0OOyKnMn4GT#3^@3u*F z4h(!S2WeA&^@#%St~#D2dc+P&!G)B93;zRS3%`J&h2Ox)!Y5D_9Ug1sm`Zo{LB>4+xb4RXYv%7G5inQePEn`Bgg= z_!Pd1BS1x{rdREB;Fs_$j{rrXp8Y7>0l<_uK!9UW3{iF5lJX9q45^4%Q3MZ!)q^E$X0Xx@r91I^>$W*{(7?;B|l z9|znwRs%j3xNmQ#`${06xX#yW)C2N~wvI&=n=t#82l9zlpIcb`$^rRAyU!`CerxI@ho6RPq7HANjV~N@~o6RI74hRd&5l;u& zM@Qm;pfLG?ppb@ubx#uz6Al5JmNp=0Aw0kfq;+!qMPwXV13f$Uc__f zKY{IOr*k8jJ^u^How&P(nfdb{*M7JHqK7orerDg1Uqvc!Xs_vKUc!$q(6}9AK5Gt1 zKvn4R*|JCmYC@OKl13U37Fv9G8tK60_cvAFQsChE+;X6O+3y8<)=mS^b=9}^jR>g` zQseUjeIpPDWcS513d~oQQ6O_PLo!froDwLM=AcGMjgJNDjZ*@J;vCcnsqt|@y>IP9 z=9GclNmCE#@%e;4pwH(Lx`1AvN9Y0ieGcKT1cqP4@i&tA!%T#MI{dCK{=q{0!>#xS ztnm-|?7^TGP+| zFX~=AGW)jP$N@*dlx!WX^FM)|i$@OMwwsCIKBVop>2@r-liLXVrH=%}f2bnWfc=kPy=UK8dn7$_|=QjvTpe>t)T|bEvVPOHZW;1b>nN)BEFcp6c z#u8wfaCPAwYk>914@+K|VOZZG$sLvR1S&=>j!6pUz zlm?#@=wl>1ee*u&vdZWAAWryC3dR2@Q~b|bRl+F;$5JHOr|pa99KDOcWCy0q2Vaxt z0|DmkphbM8Bm^k=0AbY**hns6LV&Ulya9c+YlIzOIuizc;4S>KT_fxP&lvS-LgxQw zA`tN@LT>4qfZ|h#S4}3EiIA@p>OkQ+)4XQiCkZ<(I}ev(LYB}9I};&aE<}I{@YTZZ zM97zau@(U)z*h=|iIDFSV!#CWa)dY$@|{8uct=ol;OncucPpfz=)mvq65nl*gQ5cy Z{R5$L+}_^(W5ECb002ovPDHLkV1oNaMR@=K literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/blocks/storage/container.png b/core/assets-raw/sprites/blocks/storage/container.png index 959e2b6edf6ff5d4c74e66e9c7f390d8f7f55b15..038adbcd82a12e3ba2f494eb837ec6eb5273f58c 100644 GIT binary patch delta 348 zcmV-i0i*u21Em9y7#0Wv0001;w}I>c0004VQb$4nuFf3kkv=|u0V7F7K~z|U?bp!` z!Y~L0(98b+qm9+AK%Fi7S-sAPhLG3KoM;l? zpCE;38Am-hA?$;HPklvLfOBK8!VW;VsVYFcz1x5khOmh$u-tPISm(V41S?kpqP716 zf8h5(yOsN!slT1qpn>)yEqkgGKp?311p|cP4VGd009h1L_t(Y$L*ENfyE#Q0HFi11MI)Z=LZcU z%3-g|DKV2#Awi(>4xk(W7$_i8KvOWekq1x}bO{0Q_Pi^gDeC1hOCR|RrJVYq5J9mE zm5~V)4=5iXcXkK7fPS;(i{&NBgIw#7w-&&EHsL%**A#S=Y>tjaG9_Cw?x~vFBmz7Y zDi4$ZPK5?q#yy1w;44`60o>B~2C@s7O1MCt%fA6@`4=Fs3|xS%l>fjVfH2izvEp*S zKbY{7gAiDXgg&4mTOb(#?XTx3NCEBlw~FoeZ}VA&ZXmtc7zOGUG?sh{bdRzM+}dC{ z+(^S#95nOrI0z4v_n9P{_5(?Piz2lg!ZWJk4O9h0iU0um0ouux+SqpRuK)l507*qo IM6N<$f~yIW0ssI2 diff --git a/core/assets-raw/sprites/blocks/storage/vault.png b/core/assets-raw/sprites/blocks/storage/vault.png index 36a4ee5ea5ba3489da2f37ac46c778f6dc8cf400..150f9acf2479873fc4eaad3fcc0adfd58e778cf0 100644 GIT binary patch delta 604 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 F^*?Oz13v%& literal 1160 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuB%fjKx9jP7LeL$-HD>U~%$vaSW-L z^Y*S`p0J}x`^Wu)y;Jo(3L2N3ab564F0gL`ci-qO)x><r-+ccJr6z|5hwfj99 zP8f+soop9na@ltIffxVvJ$ki9H`G=)U(I2>D9X4}dMPka=nFf0ctil}BKbl&0 z?^I*rNm~MRCr|1Y5Se@gL?&JVk|p_1KxFP05Sh&l42Tz(!hoTBVXp2`^^c)*!e@5? z!=uEl;YFA6qPe?w?w$ST?=0gbNCC141z))s2P+`mVf;B*W3iA$aoaNeZu!0jT zd8J6Q_10z%fmp!=hd_QSf5ig{Me$FT-d)rV)Y8$OuqItFK_ISa<2=^}#tN=uRbq@* z&vhkUN3y(_aU3N1K%q)LAkMm*fnC597;}faWk3<(3Q9}Yb;6f_vFcu(Z29^3tB9C6 z92t!az~K6|W#)Fb*+1QtZ(a%w;=HhOt&2xPU#_APQ%N4sD+Uc9uPC?yy}~SM=mZSJ zEkMBq6O~jBEO6_bu>MiguTMXN-!D&S0+|H~`i{`Ny!Srq*0?sDD3glIY+>op2|hR# zXsk!tPOoQzpy*79Q4|e*(sH!wpuyI()yIHT;HQU8Uz-AtJd%)q&6;!=E{BvN{Lt zzWd=~VYyGkkHzjld+WFKOg4))u2@Hm-U4f#mTa{HDVnU3am|P}B0i`Bg z?U=wY@u(G$Z+4o4Q($$3t4Bk`StTHU;w2#esF#w8!;A=NMaDyCW&!!jih%lOobH^! z5a^l(G%qU%C?8U)2~=O|4K#1ojP3~x7te$MrIwuoDm5vWQe<4zJ(+_Ml;>}B#Pe%D U7np2mwgANSboFyt=akR{09$0*X8-^I diff --git a/core/assets-raw/sprites/blocks/units/factory-top-5.png b/core/assets-raw/sprites/blocks/units/factory-top-5.png new file mode 100644 index 0000000000000000000000000000000000000000..d48314ce89a25ed6afb4d7b18d86870f961b26bb GIT binary patch literal 1565 zcmai!>s!)y7{jg(L`Lt1GEORi8#Lmm8L- zmr=TEN+qdexIEOd5mMT<<|ZA*$_dg*Ekox4E4ShH2kgaj-=FJ#^}KkVTgHozB$M7J z0RYI+QDF&oubv$^$F7IFGyMP{Jc$kq;paYl)fcztae!l?sOs_yYTl94!yeVc7xJ21 z64RwCDz;|H@;#?X*1*q;@=V5KvGo4@cj)xQ+jWDMYQL5t%bV=1Yv{}*!nM{Q4vTDe za8e372tuGXN5XIrvN#l}Ki%}o8ufrY*@VZ~w%?zyS)Dvd=6s+JMmNLHz_@<3t2st~ zk)pzncdjOz>zsnxG7qpQfsCM>>1x%58ElstR=Jdt8-~^3#+;d~@d|7EByX zn{I=c68vn93%~rRp9HUiJj;nfLw!{*3#K{7Tq|i?9sK4QgQuOv7cEhl>nt zpSr_7kYO1v45;_K4by<2y@w#mc~=I1!wIgRlLs(IKEzZ&U|cZ0Z*^x32{X!!J7; z=2!JZ#`lf1@JHerJi_9Dp!3{#bKPXlW~3TnADY@-|Jo-J=WA96TdLWi{#2g zeD6IaP>ePVRBX=JN>nc6SPQJa+#rbwZzge_XU$i?O|l>R7aGR0GM%gih8i}*#(8n7 zACEG+y8gW82|QuY*i64u4q_x%A>49p4#(&!+=i*X=0zCk!f_NrM~HyV-cVt|N<^ges^TvzQNd#;BRvBIq~Sx@)cGp{sBS)-4!` z_3ftJzm$+%eA5aOMiYUT247gdz*q)PF3$7jpO7loy>&HVA#^p<8D?M zKjkJ5?zrlwmBXYh`6Z15Ol7N$wdk>IM&3Af;1@eH_{E<%DDNm1=e-gsLU$CZ{IqqD z`Q$M{Id*rk?FRMB+^RY3_1LKl7oAU`_~B8SZ49UNI?2+Iw3|>3^jSCZjsJqGx%PMO zb~Ww3Lc~m|?fWyPjdv$9r|_7mt?2c^3SjRCfwgkBCN2yF5h2%_*8Uvf(M2xu<#50C zCmcWAt(Z$UFJ~u7cEYBBTCZl^^Y?fJ(JEvmCa`gVAToKIbpvJNz#}AgUIbm#5~7h? zyIyv3v4jyx--%Q2Swpg4m z}s2B6A*;0savz2ul>lr(0NRp@KtLqnquBdNXD7R zyNUIdy~GNcI6?M@J+;ikRTPL5%EoHZuT1msZZh$mRb<*s*Np|F0L0u}yz|SqCyM<) zsP@sQ;torm#gT<9T@-e+RE23AbP&7Q zsFG?NIqR^S29-qPqHBWf$GXZk zgf=)dNONNkAihC9Li7-gA&o9cgL}|p)aGtWK}y&AopTclJgc|+|8dBOsGWZqKL~R_ z2y@-JC{FSyPPFG<(?#Rxh3}BB03>k^0@^^G(>S2V{lNOLDSxbW-yZ-5Q( zEJyilxF6IHP$497F`$f)#vwp9Q@_fdqDw z`Rum3bFY)V1)mU-I0UfH9W6o?AEy&@fDU02hXGXxlQ;{2A~948X|5S2G!5*Gnf2uZx1pO^=EqhL&TG5<3F O0000BRnwxs4cH;)&6g?Hb zGZKZHivB;SbQt>j%UZYH(4_7=uiMb5#7h7$1do>jAczj|0pl?_Ge3(n?E~ncF@U|U z4&VqLuL`gT9#;WSL<&ei!a)EWkqfY|1_rQ*9030?p#Yi`z)=4!;Q${Y#nk{DA;lqp z(C)_(QXIMu#1K*(z7gdD+B%F<2=#ahQ6(-Rs>O3oNA-9JQ9bS>K8g2*TrY+xy|yKb RSN;G1002ovPDHLkV1g$`TkQY< diff --git a/core/assets-raw/sprites/blocks/units/repair-turret.png b/core/assets-raw/sprites/blocks/units/repair-turret.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc4ffc5bdad5ccb66eb6f6ec3eebdf01d0f9cb9 GIT binary patch literal 993 zcmV<710MW|P)Px&n@L1LRCt{2n;~x$K@`Vl336pcMFj?T$vFh#(jX8havFsoDBuH-&`bIO+A4xL zg(g)MCz{KN6F3qaNmEhLkfv(=A;@dq4sT}Pd-L92fc>lXZgy^eznz_(eR~(ekRe0< z=Tx4Zzj=4C!T+Be9k4#{bg^u0_x$O;+kW2j>NtikNi4)NE&?p!Azl*91$e+iJS4cC z0)I;e0o&ezZD$Z**_r3%3i9#OXVcFx6VL_UO!)i#{qeW#`<4<+1X#jD`jSvAzyluA zhXmJ2umm2`C80EK0xaMmo)TOHSi(cRB;40*?IN`;2{zTECD6wJV$~hJcKJj zH3^0g!jotF?6&o)BCiaG&GF`9aZ7H)g}=VM3L%8B`(Tf~KRBw(padS08VOW1M;=5- zBNm{6hj1j6410|Pk$@6-NDlDP&BNmUr4rb4Kn#C=wrsr)E48L+pX=07AFaoKtn5pV~>B)Up_l1TH+z*PS1&|6^e8ND$8f5_m{Xf(9Pa zC4m!>01V$uHlmvz%ZgCk7Pps5U<-)hHQ{Whz3bCTUc#0#gLoA%{xcfCgf zb0w%tV%)WTs@?ng?cE#m>5Oc|T^5%tpH%`RH#Ssz3=fGVNTIY6!~&%7c)NO4k?@fD zbXJYlV>OIsJCOh_e4mj@U<$BRpAi)C6EHV;UTAs<%aW!+f#z+!%qM3y{EDKNql&(A)O}0X{CO;y@Lr5G)}z?6HJY{SHfz!b7?wBn4>UA@k|1is9o@`uW+i1s|8V zL-;L03lGUjAO)1dQ~h9Y>Qafp@!wWImr3}UXM4s&%y^M>z P00000NkvXXu0mjfI0MEB literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/circle-bullet-back.png b/core/assets-raw/sprites/effects/circle-bullet-back.png new file mode 100644 index 0000000000000000000000000000000000000000..e08bfeae8565204e17607bce12b74b83eca8195e GIT binary patch literal 293 zcmV+=0owkFP)Px#-$_J4RA_Zv2HD^iC$5=39+X60W<;il+*>usA zmR)!HMAp#F0FyGq2hRq-1Q`_RK4^9Dl3tOn#s3ZudL+yX-wF?`5~f9OhX)pk#|5nL z;7Q_kCp$d2lW6Yn5+1Z9svEt92Q`VE5o9G7Bd>QKlvVgv1-##VaIS)U1>^;eQm8yW z0FRM;1ww#L2(SqOHX* literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/circle-bullet.png b/core/assets-raw/sprites/effects/circle-bullet.png new file mode 100644 index 0000000000000000000000000000000000000000..3039f67c858394bff7b7897f8a2624e46b1e4012 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Ydu{YLoyoQ z-Z;qDqQKLVc=LOGx=ruRHJ#p>3p4-h-+Z7)Mx>9Kq2W-}mzKTX|LsV~UYOnH`%V8x z?!yI2?>yHW%x9eA8q>V@;d+5Sq07vR55+sGdHe8o@#!0@o?#Aa70>3pe0cTY?xjc4 zQzyu_&3;$)+h`Z}`x}jS^W)vh7XIN2P>^Qb6sRB_|0w5AZ(8;BMmH~c&81%!#(BlB q_ITO7Ha~T}SpD05cUTz~Y!SAT^wHYRY19LBID@CFpUXO@geCy4!D6!j literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/laser-top-end.png b/core/assets-raw/sprites/effects/laser-top-end.png new file mode 100644 index 0000000000000000000000000000000000000000..5f4b05e6a9d7b7bf140da0ba973509beac9d80ed GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!jKx9jP7LeL$-D$|Iy_w*Ln`LH zy>XDMK|#dz;G6&dZ-zN-x!ARf%XoL#`$CRO+jxP>K;TD^r~ZX2QLlgQoMLn|@1yFv z#C4YzE;?LYI4fnzjRzMke6=pUcYfby>Fsjszub;lOPTgo{+<`570~1$%T%f(dLlf> wiLYz#&d!CstB)~Ezv{U8-#duG3=I298RE8SNUU;OJqsk{>FVdQ&MBb@00~Y|E&u=k literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/laser-top.png b/core/assets-raw/sprites/effects/laser-top.png new file mode 100644 index 0000000000000000000000000000000000000000..c50f6d69a605dab03b97c307494cc3ec9f501fd4 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^EI@3)!3HGFq;4MtQjEnx?oJHr&dIz4a^yT+978lF vCMQU+USaF`4+Q->2U^xO*)c6{_{G3b70<$xboGlfP$h$>tDnm{r-UW|rN$ia literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/laser-white-end.png b/core/assets-raw/sprites/effects/laser-white-end.png new file mode 100644 index 0000000000000000000000000000000000000000..1e6aeffcc5e01ee9c34e659ad2e0cb65c2237c54 GIT binary patch literal 325 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!jKx9jP7LeL$-D$|{(HJOhE&XX zd&`lp$w9#7qTFN49shUk;Ls}xWApY57d%s+ZFE+Im3yuhP%#)7xqWZCz3pW7wbHe# zR;5OMIqDyO|GC88=v~{_smXAp&APtbK0x*Eg81f}o2&2N<=wS)-L}V9=d3H(b^Vvd zRfX$q_ul>Q;w`evt)6^`Q8kYBHBY+3#`i77VNaB6Q`a?2db{!562~Ux1;)3MXZtp@ zFbQ#ZC@3^GAQFpewbG#d}2tAFzLCgo0k9*@^tlcS?83{ F1OQa9gVq25 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/laser-white.png b/core/assets-raw/sprites/effects/laser-white.png new file mode 100644 index 0000000000000000000000000000000000000000..13ac448bbe87309f205e06a79181ca41ae586be3 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^EI@3)!3HGFq;4MtQjEnx?oJHr&dIz4a%4SS978lF uCMQVPHGKRJ1QS_jTnO#q;%PZhz{GI+DT~gcn08m7LIzJ)KbLh*2~7Z%W*nse literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/mine-bullet-back.png b/core/assets-raw/sprites/effects/mine-bullet-back.png new file mode 100644 index 0000000000000000000000000000000000000000..33738a0c4d7fa9b9e693765e57846c261a576e1a GIT binary patch literal 349 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%z#XVgdLn`LH zopqa!Ns-4T{qO(!W6Nh{EV?vpYpmt-n{jDOI!C;Kniv?Oi$X-@l6#iB^(|g9ebS=! zk9xU;r-_*)&TrWx{68Z@scdQBX`!<^FY_PSZd~mXF)1a@^L*F$BkK$l=O(0iRv)cS z+N`#uY4M@3$%cwDH(d7!nktH1@!cb6_^`}PIlM`Cg66HGEt8xttT0>n#W*8mW^F)b=beG1r1MDr_r3;yodU2!=1(6{hJo)viur1%bcOkAh-=ZSGfiE-oe z0u!_7J-1lq9lj}ZlO=k6L)J&O^NAm2rXBguqp=fS?83{1OPBnj8gyr literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/mine-bullet.png b/core/assets-raw/sprites/effects/mine-bullet.png new file mode 100644 index 0000000000000000000000000000000000000000..9525c22bfcab6525e5dfd7ea8276950da4c4aa63 GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|PI|gHhE&XX zd()Ax*?`9-@X+7)F=iFbB2&eTg){$IH_Y`@ES;6h3{=U$AnoNS48$FB3+9ow+x z=%tcxYptConE&~9Vzu)Gv)#Y971)u#oS8!T$B^GX#KHMTGJPYX~*<~-PT~k{)9P2;(Fe< QQy`amy85}Sb4q9e0RQT4VE_OC literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/missile-large-back.png b/core/assets-raw/sprites/effects/missile-large-back.png new file mode 100644 index 0000000000000000000000000000000000000000..bc3d3c2ef4dd4169ce8a275d0d2ba19abc01ff56 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^79h;Q1|(OsS<3+_#^NA%Cx&(BWL^T<4xTQKAsLNt zXPxG2G2n20{pY`ZdD!(7wOi~lS|R6xq8}{jJ=hXt_B>+hifd8zLh{Ea-SM9E=k(l@ z;vTcqcTBeCHkf9@dvCIJx4{$>ZX2y~u_TpDp^`AZ2#-t5UQZdKCrSBev(K2c#X-mN zz@#NCiKiLcmRL1RmAT-x$}IXcWABsp$PV`)11n95xgtG&TyE*rj)mM?R{1htD)Zfv zxm9L@hhKh$7vC8#Ki#@0zB3+t`d^gT-BjDfDkn25PrAz*_te3E)6Yx0WimYP?dwot zf2X1^?>3n^eA4ahSuiuzSr>mk7sl%{QN;B2f!#}9m2*~1mHGLw_Psk>g{|!c>!Z&& z&z%TNk5)_inzbY*P{c*@x56>4)&ke{T#4saWga&SjlURH@NCP%v;|=`*WH_ppSvzq hzIfaJkJ`nbOgZ6mVxB)dsR#^M22WQ%mvv4FO#rSYnxg;! literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/effects/missile-large.png b/core/assets-raw/sprites/effects/missile-large.png new file mode 100644 index 0000000000000000000000000000000000000000..f1353b6b2ee234f2a93eae5653d60572c4c80896 GIT binary patch literal 327 zcmV-N0l5B&P)Px$0ZBwbRA_1F&1KsAOnzR$m#Bd?6RIBN99v37(DwPe+2MGu97dc~8a%j+Qa>j^c`7$BF(! zSS?BIWQ@|is8RZD!ss1Mdk;VvN8RvG_xHtkkFe1{D?krlo)+W1GQD^n%Kt_pgb+gf Z;REptj4mgs@hAWQ002ovPDHLkV1lKag69AL literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/items/item-graphite.png b/core/assets-raw/sprites/items/item-graphite.png index 3d802be63f225d850ec6d9b09701cf15fb048f25..a6cbb3454a61dc51e1ee47c162b87585ad5f4d6b 100644 GIT binary patch delta 331 zcmV-R0kr<&0`mfpF@G&dL_t(oh3%HT4Z<)Gh0h2fDAHtu%s_>N=$XSaSeXGa19MOl zMG6J9vGj;>KD)RaKKA#=i0Dt|unBE|ze0>Yfq1cvEjbH0((0{q! zSPkyS9orucZ$mb;oMK6Wa{vHnvz08Y!@E^%ODQ;K001gTuYZXtvCUS}6bpec#YO;v zbE~;pa##6vh`zE`0H2vJ*Bg?#3gHCMrEj{}ass4=@B&B);SG=qf&`EPf(#HFA_yQR z1bP&!QvY||^jcS5=Yb8DQ~}J)RS8#|yaXW-Q30qBt|bcIgj5LA{Sgd6g)k|}WkktI zb{Szgx666dHbgoH@~&ann;-^6-Zi5)C|UumG@} delta 314 zcmV-A0mc6F0^$OYF@GFML_t(oh3%HH6@wrUhL5~743GhAtu5?QI|noHoFOkWI7ezb z3v1g9-~cH}WrBEs$2*$Dzw&XM|L1`m0Ehlv!W}Tie4e#ZP&BQvU8vw0T(A+M0to$y- zQrRHDo-Z$}nq+Q6L;+m6n~qyifZPzf0CGa?2FL|L0>}YD2H=KB0^o!|k78T)|L&Sj z>?qU23Th4A%{WB@9JPg$A~B_~@m!g8>&%{d_F zUSM|u4hT8-n=?BoS^;b#gV1++mw*#O&b^RxkH&&~fCC~^H4pue-Zh|f+7rxLh5!Hn M07*qoM6N<$g8J=-+yDRo diff --git a/core/assets-raw/sprites/shapes/hcircle.png b/core/assets-raw/sprites/shapes/hcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..5eb12e968fd7c3b6f9ef7efa52718566a41e4fc1 GIT binary patch literal 1490 zcmb7^`#V&59LJ}j?6ibKnzcRAai(3Vi3&Ys+?uH~#vr1EM5Wfq<(VerI+Gqhk}@F z4o>D*hdy!SQF^xP=z6BF`op}T{lscb&Ze?BHz z9AzwE#;?01+rn7PIpVc#FKjq-q8}wQlLOv`C03=&1|y2L#GxOzYBmNGmHB6(sooX> z(|YHW!fcrD%rfGksiydr{laMr1$^F_AJWj& z?kJ?duL%|eo`G84TpoI1u35%NFu9~W3TpYy3x@Ja6Q-I7ra>o!$AT*kG!go}tztZV zfjyR5Y{l~Qg`Sn)Dj5*yceW66HYRwI-JO&9NDD*o#B;Yr)y+PS`1UeakZ?GKv$5Ij z(LIm0(!ib9e8f)oJjm`mG~R5|1=xOT+eJb4d%^mAH^oSP3TN{{OMQO%>XP#?^yjfF zZsdk%HY5Nju38i%MBeGBi}RVuRN*+MkFx@Qdt|I$NU$S~siik569*Lr^Y(?bhQpg? zY;|!lchH~VfUuy1hiM#=XnOs4|6yM8d`M8t!y05v9#TO=#=19EwDxRl?hz>(D_RKw zb!TB26?L=k?p2@I?d_oj*4)>61$d0b>ML@E)3;nDGg0;UM|7k(4gp0oEpg$)g zQzIh`7&^Wt}hYid^Hz1t9t2QoN?leRsvg3oIyh- zdUIQjhps*vDmT9djN4PxA7|In$k^e}@xrJK{Xx3BOi7HO{_22)*DHy-INuqQ4;xHn zxrJE)_!UBRLo!wiX0PDi*8o>5QhPSgre}1?{nQvbsiR8w&0(K}f+YVNE4%y`HunX7 z^0{BBbdBv>sVW9i_1)PGL&}G=fx4SS*~?J5g=XXByU0@n+A(gvV2_0Nwkp-a1c851 zgYhp-xHK#sX3wtb%u7-4jmcSfkXwik6HL>W6!9u?oiP+O`f6&;*9~(ieL!H}iu5qk z#W~;0J;arIrb{cYGd;{As7Fqp2`0eoj_zAmvVC4<`;6Y*UI_`+1=_`4FofD>AL62Y z=9)<~tdVje6@l}^+RX$hoLb8=6o%$l^3_zbW1XZL28Z4$KY-}s;;QFVHalxSJ~-v! z;w5)aECtHL5@q7VHIt&DN$^uuE$jieD7NOO%fQty$+6s8m1z<~zlEne*YoQZZNc^i$0ZFhQVtS8HzAF|AX$E`ry5w=sS=e??W7Jy z(JE626?~G;SY!0c{+<4%x5@T)ls4auk`v8()N|w99aRr;MZHGD**>?6hUN#?3OSa% z=LNCGTv`M3)a$d(QpBdUSR%`$HT-QAO)B<+;qed*FWEn)IUx^uB|#)`TCPCKg6;%u^6rSfN$Ha@VQpK%-`P@o!OD<$I%T$7j(S dQlFzE87}*2njGG$VhVIEgQu&X%Q~loCICOWRl5KH literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/aegires-cell.png b/core/assets-raw/sprites/units/aegires-cell.png new file mode 100644 index 0000000000000000000000000000000000000000..f85cfc8e176982b243d989e18aeb04dc5540ff09 GIT binary patch literal 2064 zcmb_d`#%#38y|DtT#j-%iM^wkavG^zLb9}E9KCaiaxJBu(U4ghwz-sQdkc{k$E{j! zVbaD7yQFm{%YBk-(g|5J9cG%;nNOe3`xm^==kq+z_j!JJe)xW#=lf9nkO*CEkTw7S z(DnB62vFt44cGjaD#C(3=Btt#E&u@s(0jli0f0?i-X2E+lS`x%VfpW&MnCzr(a-^S zgOH^y1cQ($Yrs-Y{IbgS+;r zs@E-@`oEf3NA=J$4EUs&v&w% z1Rcf2e@gh_`l zdkKPo5Go}-hP=aq4l-^${`FHo#s|M^?n!%L2ordL66NCl-$a!tg%BL&H3}B}*`NyQ@ zU$Ur!JlpkVJ=XC0gu~8zE};`aOotbk0(LQtYM(e(Rv}R_4MGVpMgAQGwg4)!#Ercc z$7Kv32!C`v$GFf7)ivJSw9ZOW%h@*^*&vMNEhtWOF+3mLZ*M)_Fif^~EnrF%(v>+Z zzO_xi#i1RSj@g}KdN&8f6>9h$aLhyoRyVIyXLI7+;-kVxBw%O1r<bAB5kIz;A2W)A_=SbiX!xKf-_}&x}8Cz8hEO=D6cK zsBz1sM*T3efrSdSFL7en?KI%Zt0#R&fp(X-dqu2PT(>UDdv*uPzI#Zg-67@dj9%5A zOr3 zJo+KJ;PjQ$$x^d#Hq6BZR;8hqtbG7xgtCk`i+opK^(aUD{^@b^_|Za0LmiT6qC_w0 zCsP&&e!I@yc93rZz0Ex)yV|Ye#40x6OdI=%)1a0VF_F1P8I@Sj@8`V%pKm}ftqmwr zK`oNO&1PTWd4e6*Xj?iSE^ESBLl8oq2bBr#8 zeHbbX%6stXU8yG470S&!-QlV~eIPIu$hn5M5X|C?lz{qp<@Y4swo*6Pyhg!@druYI z+FJLbRL||q>!P)Ve1&**-8ZYn_Ff*SCdE`PZ<{3HUlJ~?!)EoTJYV0+C|9>rj4r+P znPki~ouX^8Abj0et&1P z&A1VGzU23fuDFbPd}y4mj4|qTLOfo2x#}bdy1WB(G#t`?XY^+AkY?ke+0hEKY6pSi zbJrS9`&^lxjtmP1Zm<5U@H2Uln}(3eieo2W^PkH)SD7Bre3XkG{^neNxM`lGtyd^KV|D&JFn)(QVqZK(&*uuTpW8d@uB`fiRPFwrHd{oc$^-v zeb$e%btp~yS64aq(AJO;X;sG46Wf1?Swib~wOC7QnJ~KowbAiDhV-DrBd`1ezv$>D literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/aegires.png b/core/assets-raw/sprites/units/aegires.png new file mode 100644 index 0000000000000000000000000000000000000000..21f7743fd54e79dc5ba9e2ed8b4900feebb8f7a8 GIT binary patch literal 8017 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 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/bryde.png b/core/assets-raw/sprites/units/bryde.png index b4a49bbeeeb29688673c3d15a4f807b789867e87..97990553b7ad64d4e0ebebedd9138d6fdabd3e13 100644 GIT binary patch delta 2171 zcmV->2!!{;6}b_RB!7fyLqkwWLqi}?a&Km7Y-IodD3N`UJxIeq9K~N#MJ<&MRuFN> zP@P&36>-!m6rn<>6nNgNw7S4z7YA_yOYL>ZIr*CH}7| zw21NGxF7HCJ?`ECLcPp1t7{z4blXfN;$kMdD)zi0fDi@|MSq{nEMrcRlJFc~_we!c zF2=LG&;2?2)ttoupGZ8*4AUmwAfDc|4bJ<-VOEq?;&bAtNf#u3?RNSu0mr>z@3D!JNLb%ypV0NMI35kRU=q z6(y8mBSx!EihqR^?Z-X*!%e?LE`?krFmf!Q0u6H05B>+gyS4I@6K+y44s^db&c`Uw zvkNq8j`Mx&IE@p){|sE|ZGWW>%zTnwYip4sAhZo!T(>o454hX`hMo-Bl$(;DrjXAA z?`QN)SzzE62&}oiwf1rP0Hmp_-w$6!a-TSi-gT3& z0XBaIyh%hsRCt{2-0ha)APffJg;4MRl2?I*Hz5>k(b=G?{-N~e zrvaFLF{AX08Kqy$DE)Vskkf{WHef>7rMZs@VVmYQCWL*OyO{Vw#AnKnKa0tV7$ZOa zBqnz0=P;qi+!{A`Fu}vz(cHiUA9F)<9TR^>nCqI$m@vj%*1V13wh&{U29@!H7-=jt zP4`q~{2Zpu`#}hyd#W;i3=_K6f>_|`Szlf2RV{25s6uyj+Nsiiys5D=|5L#lhyiVXWEGK_H ziXpH>usl!T6^g!tDP2I6+eJ)$!F1S%F;o#oBZu1f35x6`5uEpD#8n9#R_^9@?uTstv#QUyy;@jT464+9PN76&65cx@L(HQZS| zOyDsMFYUpY;2BwZjR~(E7&QR;R%3tkHQjY|K{LhZ&=|Hk(CE;*i{X)2r(E0Q^C(=tX~3QO$N=6QEek0gJ-1hX95 zS1~z=a_a|YIBGw&0Q(ZA43%tYm?O!{-@wFXsjVJ}IhxTOGMh1VJ1dw&V-CTz?d}IL zrS2L;*`hESbnq~t#5m@z#pf-(u^@-)vmyAHK=>lPvvpd)$Yk2a zPHA<3$uC3Zu&kM5U}BeXY%rzYG^S-<*Gw_}S|D0XeHRS|)~TzS8Ae&;slsp;i|GnZ zH4_Zig4b+=2|;Nrrd+qzG;0{bt?G021g!>0@N&ZZt0A?7!3fQZr`CUJPzf-wz=GoU zF{CjGFl|`Gsu4`mn3cOm3m7wbCBT4%H4T%Lg+Fw-hcQ!W0*tb>LBg1%EDLw{CKyr) zJLc#tqB*c@bPXFYGM%;&!tV``0nlXmw6~e{r^TD@x8p12L zSi*SR3^7u;z5!hA>y7d(y~Y&dRo2NU9${o&sTE;Rz}N)~9AJ-@f~0T6zK4-W#~-4cCN*(tx5Z-_J-4{%9rnNG=NS-!!b1kw9@C->{Cpw)?2TL#(pW!wcv{VR5M-A_^T&py|25-vCK8Im0lcR05mvs zFf+|`4Lr8k+xYgd*GQ%BVW7*S*|r+Z$suUgzLvYNRZ&|bt2sRkSaRV*<)wX-bddL} z2NU+j(%;C~fRP=uZ}Bv`+)fPE?pb<`Eq57nw%dOu-(od=xtV2UM~Q$Qb3O|N7c**e zzN37g=3z18IWd<-%VTDlUz_Afwv;ropa!y3!M5MMyt?r xC*A%h6&o!Z{(Z+j%m4Q;U$&)R%qaa+n7{PwPz-uG%^d&$002ovPDHLkV1oaVB-Q`` delta 2697 zcmV;43U>9m5yKUbB!7ZwLqkwWLqi}?a&Km7Y-IodD9@FVO(;ZB9L0ZT2-C>ID9Pe2 zu%d)y$xu?Gra_I5h46X>TKfSsZot`PNi%lfd=pw8a=N4YQcz|forc#F z`KB3Yx`xnPvA3Unj_+Vfx{6)F-ad3@BzwNXyX(q(`wsJr-yhL$Uqy1C1vwPVld%Cd ze*g(^NLh0L01m?d01m?e$8V@)000QHNklV z1=jb2z19$0wvRSxS{VNK??1$!%#1&o8GkY}{$yr+hs=jy@($l16E5Fj^S%r!28H{) zErW=lbenf&K3(F|(3Rp{-jtDryKtp^f0OrQLZgR;d%PvXJ?VMtxp_w>U!b11p5ixT zcydRq$np*D%joU1!>eq#&TSd*I+3|;b6F;PscUD@4Qqq07q}`@%`^yw-Ei1=fr~O~ zp(cz@da+m_!qr@G{|zR{HBcX&T8H2RT5GE6&W@5&eVOOYJxh) z7q~8iP4uTQc5`iw4|? zoaQoce1UbD;RT6i@oLG%Y_Y=#942AnP!PV0k`Eo?4mg(~%CO*lf7hr4ltfiH zY!Eo-RE8iyi|7`pV3oW!C~#B|Ips`-7@n1#jLn`=&|tG;@!{_tKSnve>TyfdZ@k0_&PFa!^YUD0Ef^S%QCW|gZHqmwCCNMvsg+7 zm!FccTfn{eV*T-@X|BO<$f&DizM70|YBBghnY3wLzj_(BX{(M7v2GAL)8X`Er*Aaur68Nogj=fOZ6C$Ua%E-o^MWtmp@91hTYUa|{P-wZU=1j#!W(to=9T z@flqwGAehQb=V;8e}HotqWl9azK>*Z`^YV*3ynG7-E zQp2vR2BKzu>31*NoX8N8e|!-(>bjOooBCzgo`1qQvsshT{bp?G`k7=SE=5=Fe!Yhyl+|my|(7 zP||YBa1@B0uq0C-rxuW(+W{%IFYiiSZp74`2k>WOSsk6-M`Dws}tf z;j0cs&a|QbuM>1(G#WXR*|Yta(N*L`%l@=|}cgGM83GHMWs(PnRxS1qu~YyH^{8jZY`+1Ny0B+@4e zSag@Dv&r{IVQWL14HKEchB(6}^r7JWdnmJMd$yp-mYGavLzsv^%^1u#HWZP0ZqFvP z*fEtU8=^pev@&eio|{AkEjdsA{#p6E|keU->5D>DAjWiB$_#~W`SQDkaUoXG?tr!2^j6v_JYw_!EO3^rM- ze@(b#29Z66Dj24O} z)Anj1A$G#C%#dWk&Bp&3TIfibwsW*Oe|~HZWk`xwUXM%HLpE(<*d$z-#8RH7K=ezF zWR#_+){^{dMQg=vdKce2xNsm(vzPE!9LVTsq$NZ*o595tMi&kwtP%f$xeP}MYIvXV zhU|$w9B8mggFRCj4ib|@7hnss?No3qVGAvG%w#yWv7rUT1}4UK?r<=O4F*lNe@tXJ zs>CXVKt^Y4;NPVtM%|mC{V=VT+|3rG!E26W8e@jdqT|M; zE9?$1I%Z8~z@o!u=TaqYVfzKH%czmtq>EiePVynSZgX44KjyPN&(KWjY%Q_jzCD8~Kx&@h9_|fB*gu970f_SdG4T00000NkvXXu0mjf DR^}?@ diff --git a/core/assets-raw/sprites/units/cyerce-cell.png b/core/assets-raw/sprites/units/cyerce-cell.png new file mode 100644 index 0000000000000000000000000000000000000000..6dba8c92850515daeea9d8fb033e17e2f2b2f561 GIT binary patch literal 1441 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%0A|f))cGY*xmU+B_GwN}KL){o36Z zbx{*0#Oa*4mY*8PXw|0^hGJRnx$yMjUU|+AJ@URcr@-@CTGIApLKP!XlhunIM5;d4 Ru=jiE^MQ+IT7BMu{{f)f672v0 diff --git a/core/assets-raw/sprites/units/eclipse.png b/core/assets-raw/sprites/units/eclipse.png index 1e071d6331d33b93ae8e3c9edd5c47cd6f9ecc5b..5d49e2132e2eb0d1fc129c25e12778453f9b2303 100644 GIT binary patch literal 6823 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 literal 10257 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 diff --git a/core/assets-raw/sprites/units/navanax-cell.png b/core/assets-raw/sprites/units/navanax-cell.png new file mode 100644 index 0000000000000000000000000000000000000000..79e2b847b77ff878ce3043bd7ad5318a5cbbcf57 GIT binary patch literal 2396 zcmd5;c{JPG7LGI`#H{&wJ8PYF&idBg-yi#&@7p`W)7@E4 zMneV!0?E0$;Jra02{mA6!Jq&V7nOto4hf34vjeDhR8s^3Nvpcz?a#&+uFev#Kf>;Z zdj45EdbkL0wl`1E(}Hi8v5&~(dh9>KR(B^2d^xe##J(h1qy#|nf4MB)ou`7Hh-11G zCc5@$RC7}g&w+S?T?p&mC34J4c_tc}=ydw8`<&N6%l^)vI8JD;^>kHm7(UW27<`LYVjNQA3r{aRQ6=DY@Min)@#ly#%$Uz6MfsVE zOyj9msk^ktoCVfPv7gW^97EQdY(>atyEja~&VH1*R&Nmu^BBT=pO!O%>_@T_j=icI^8?Ympi2x-Lr_G80c{ z{4|Xu2l=Che2!sd@ADf!Y#X@wwdb~GWh8tt8Vwyk-f;b+nOwn)W7%b1&1|p_PdtE8 zvsK@BX-t~`In=N97G_ZiMf*J5juW6SWill0DxvJqVCd2Q94+$(@z+dE+*?6(XdvX( zsVMI@0qqr8>RDOiz>PU9C8nW4u@3vT5te`8!j)2U)x^`n4QBN=?}l`~WQT?O3s6f! z3o}8K8w$%;AG0J>!+(9=Lak2CDtJX!c`&(gMh=r{Yn^t9V6$`9(Jju6wfk~>!npkE z9r3_8N2;kfyt1G+0^3Y2j`m%G1{f=&M1hVr0FB47IUnjb()ac(UADMgNt-_Ni4qua zcIQSZ^TdhzN%O{}bbgyaaOe%l){SPI@Ig&J+j35Dd%AimZf=;pNL&Ugk9cxME<3lN^>a7+kk z;tOb0f>vP}g+kkpbmRo-?p9c?qL30CqAP#L!TV8DeikX#^b9W8=#NS_YFpP&R?5x9 z@3zO7PJAgeQ7vNokZAWnLKMQhqqp$jtv-@6IG;LI1TPTuhFBoXD9v^I%{B`dgVRmo zi7X|P5;w=ci3BZ3=>mS?ZRtpV>z?`Qrv?#12RgWd%emkq6wgO#DNCzQ zhsmqX7hh2_Gx{9lT&gCqZ@)h986mQsOolYCFLI~KmjE`%=fUUaH{&)lq#B)SiFFWy zPBwyP;gcfU3Lz+FBl`VIVd}B4e2|{$okkrcvq&XWu2TREBcVt3(I@@D1r470&;}tK z(b%&IC$bn-cD*GYBg={?!-}VGv?VCX?6k~85q)`j z=|}RYE=k4@p4W<_Fblr{@`VuWF+%QEBR{(N?+Y9hC2=8=3WN`A#i}jUrp6!1<@fx@_#NlZ@eKeX zm-hghgg~;B2Kq6&@4v&4^Bu;viTT{ctFEryPagQ^_1Jzp64;#Xaz^7;xv1(bWcZLq za`{Jg5@a}~|6RNIW$0aW@S2+62Nc`+ziB^=<@PCUP>5VF%835ydV7)Co>IS^mce$< zwCYx+7|cRJm(WiUxzFmI*{bm=XwO$kjZ$`WYI5m9mn#(_i)07yvu{BuqrS)CQ7}XW zM7D%xsFMO`L1Y7MRQC6zV*YYbuwzR-zPtXYp>+y80wP;I4aT?BJ9XDzHgQRTV<5v$ zz-MbpB|rEJ&El zD05MSA-t7QcJ_dEp6mwo!PNdEA{<J+^DxM62|KoAeWNd$T*Ln`6yZ^ms3Wt@oqy z>cXA}FIKyKb@lz@$;rpPu;dgtrEqQ)|3p&jveAPhB&>2!kh9u8z^03;TDwzECqW45 z;YQblOp5_jQ@P#DcG*bSoe`f?(^B&b%6QkAjMP2v@#Qqo2;u9kTFD zfwv}3YJ*l4jaEXW{w?src|&%?UT>D)Z@2M4m&qu1IN5?4htW1>fl3iBLp$Rw0Ya7q zo{vfefcp!i32y)Ze1vNQMEs8fiq6>HufA*JaM}nkJApv1 MPVV?x2U7As0s0u)e*gdg literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/navanax.png b/core/assets-raw/sprites/units/navanax.png new file mode 100644 index 0000000000000000000000000000000000000000..051b01e9082fc74cd7ee5bc385eaa6555134f712 GIT binary patch literal 13748 zcmb_@i9eLz8}<+tqm68R$vU<%lcfbkp%{i}Fc^c0LAEd|*&;<*vJ7U7UH0rT7?GWZ zL3W`OhV022;eGnO|H1otKObWr&htET=A8SS>%On+dcuwG>Yn1fzzG6@PU#_Tqd*`Q z0pJyT@;LCH$L>L@z#EG>Y1+AFjG0*Qk3Zflx8ODpB%#KE+ZrJcIe)sj9J2&Tbgla&gMp-bPFXoIXNxvX)EDucV2}GQyvn$SkcU5nQk85x zBSQ+4Sc-Tbf#<&u{Rsl@n5A8qWObjv≶aFk2T1ztQ2&6-iH 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 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/oxynoe-cell.png b/core/assets-raw/sprites/units/oxynoe-cell.png new file mode 100644 index 0000000000000000000000000000000000000000..92120a6bd412b764136cdb4645fd4aed7806c744 GIT binary patch literal 816 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%0D2fu5JAwLXAqOC??|@Ig{W(e zB_xCx#LFe{yM{#DBkfevlKIC&IKHx!>S>b?KETHk6W&NY=HpGTxTRibiR;zb?WQG#Nh*r}Jjx~Td%d!4w zk5B5|36U+;GXpLsR`!nm_#J!Q{fwzd@O+}{85uV8vb8D3n%4GUttP{cYrK#9SH|bq zEMZO_jceTZDk&e=M;eKxtr_x~NLwej^AKmqOjnHMZCk2@p6*T^(HA47Il*ur`#Ano zrIbrb!Rtzv&`e<;@^uhHsa`U11CQwnY`ZvfUO2jiEpv_<8yBEil6xdEpz zku>iTP|`2yj6|a+-K^IcujXmxr-3*=M?D)^=o_E}ik#2>S*H-~Vk4s3gtQ#J?z+d| zTvo${z5`GTM~hlshKj9N?r{tVW6cp7_7EzHN9$3FlHc%s4E%xalh`LZMwQ*;Oc!Hz zpmEHSZwjxx{XWc%Ny$@jEk~*am!iIL0fJm#BA_WE=G?c<(By&$H@w8R%A9f#&Q@0} zQf1gyK5N0hwhOsFs@F0>4EzFHl(DNe(#~VL$jJ&Vv06Woq9rV4egxvMBh(;i&N6;! zJbj4%RaIA(hc=7T#iiqphW1>`qz5%bV= z<_2>8VPn&K;*ZKS?rOcrv#*}$p!j)3Uoi`#JAKw~AqcI%_Zo%%phyhP;!5el=FEEy z^5W#ty2Tz!Y%5=1avaupou>!H34Ut49_nz0Q{#KH8YZNv?e!;fZT2RcV88QR8+t~6 zvaS9eVqQo34o+Sfb9bV7o^%&^@WL0sP`;uL&KnsS{QNaU@x|!Nof@g`8KG5%+k>Zy zkJGvab8H^3l>hx|2b%*n*99T_j)e+h=U05tU!BxSJ9(jpjdMn=S_Kg1lnOfuwg-GE zoWK56e*JLx5m2pndh<^u6)xu}qGLb55KZi-dTKvM_d=!>!yA?oi8IuIzp^KNppxrc z-kjo^X3>rk1pa#~bDvCUc^o^W?b;<|Kfh8;d>3MN^p~x^CW^}1{D#b`6m4mx4Yzkh zw~$63W8+$m^tJbQ*oV#;9q|pF+lnKKIN0ogz&gm!0`qW81gi|XJ3%Q+e@O}Wo7lPl zovAp3!?o^BS0D+zOkj7jDIFUbCqkH*Tg{h1`In33E=UB_l|-_;$hm zu#$r~7QC!Gh~A1Oo*o_N9@YFG&i^lYaeTCS%EZnNlQ$eT*Jbz{7ADrlHHNOZ{{a(q BoV@@5 diff --git a/core/assets-raw/sprites/units/retusa-cell.png b/core/assets-raw/sprites/units/retusa-cell.png new file mode 100644 index 0000000000000000000000000000000000000000..bb044aa5a358755c2c7fdf62427d757961888b0e GIT binary patch literal 749 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 literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/emp-cannon-mount-heat.png b/core/assets-raw/sprites/units/weapons/emp-cannon-mount-heat.png new file mode 100644 index 0000000000000000000000000000000000000000..b23f9257be489d98ea7f20b16e1516092e3728b5 GIT binary patch literal 569 zcmeAS@N?(olHy`uVBq!ia0vp^0YKcu!3HERKKJhgQjEnx?oJHr&dIz4vOjyeIEGZr zd3(#&@34Wyv5&`BygJevv4i#a1Cuu~4bdCE2<>I$c0LjiYUJJI^u)w(&iAd_f9r)+ z{?(*pJ&9rh8ppuUFh5}NWr@e_OP2q;?(XUC*)DL%aPvRcOQDxsgRa}#wF<<2lY2dh z{XO6B56|xXO}R2(w|I4~_BuvCwI5S8icVyxg+5!a`(EHKQ;*?3Ke@T(Isf9LJNKR0 z!ScdV@x9$y<~4%z((f;0yvB1rnqAz^wGeN{drQNZ?i48n7ju z?pA!AD|9rdcE-xROze+?pQdzlepofng+GcjU{=eFiu*AYLHB$a{I@q{s%_agE$l&T zmg)gr>&&Zn%L9}T?2Ik)RVYr4El*f^Fa5#tCC`t1{`}(amES&d3unK}`p0?R_eYpO z^s5<0yB9_k%~fjo@a$@u=4R0h(+9db$CK*ZtKP1Pob~43+o?r6Q|4K}ymRv1-8jze q(Z9SW$64Rpdz%>?8~BL{Du0=t#VluWe;UaPQt9dH=d#Wzp$Py)a`{dG literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/emp-cannon-mount.png b/core/assets-raw/sprites/units/weapons/emp-cannon-mount.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcaf470b1e4d6f613c241bfc14b9b516d0e107d GIT binary patch literal 2014 zcmV<42O;>0P)Px+m`OxIRCt{2ojq?JMG(iwh|&!aLM~ibQD`hAItq%&rf@+KMSKv%2S^blJ^-RZ z6jXcwl)^;{$4Ws#hlHL63%PJ%MTj)+O@RiD=b71g*||L|&TR#Ud zH8nLgH8nLgH8nLgH8nLgHBLKg1pWEt`K@&P>B*7HPjCK84G*YQ9%Vu3&0no|Mh3Gg z|9^==H4Na$ij9o*?uZ~(g|08J+-1?Udk6c?m?LiI#amz{ylCDLEpbtROTzP(u{6ZR z09wY+PS49?S!FE=acD1$!U}IT7u{uH8I>0#qTv8}MJdEqry!-_7hnyDLjh_P+-xpf z@KAdP`)cbQ4RIL2R>eO1{3u*rE6+%Xt2_g#GVm($R%um$9Drw7!_%w{aW#M$cv{*u zA+7?j2cDK^Er`PbR>0Hpt^siM0xJ-;(7ks#7zo6Uv#oEr@>3~&i} zD1<1D3Go;OeIFo(0A}H7k^r;@)Y7@^bg&Ad8{i0dS*;1t1uzQFnwDj6y;d3vqUNa= zV!9Vz1D=((8lp!jfNR3D@{EQU2B;Z~tfo+{@lr|int7In0FHw9;^vj>SgQ<6RS?|( zBk*3_yizNSiomnZSr9cp6%btjqwu7^sWR{?%3K0sx)-+KX~(pR{{HE2?$}ydUbgp9 zkbMLJBmsKiaar&lo;)ziz%*Wnk}4sF0ZIm2@Sf}*H_PK7S`hg;tJSt*OP(_wl4}p~ zmtQ@<)dFB8{(SlO*11pq`Vav0tJ?zb(JvpT)1q)}_v`zAY@Nf4A?3yA-*?|WH~F|* zU<)2KghF@#`XE)SxO6;{mdB)CxL4`IL|UQ?paqW`GX0lTmDJ2El~pb7(Q1$y)B?SFq~A)+Wa4WhSf z>V^Z<}d)Vp8uJcmN%@@SA6EL$?C2kTvqB;5bAsURLD)PonkUN&&Q5 zfz)yU+Hc)>e$O!~>8+jRhbsGCrEEbJWwo3PJkL3;>_|&qZ9x@fwH#}3%JV-wd9XF` zwII+KnU*|!K^4{0awJi3o>iJkKf@MOsam6a0NwQU<<-_OL;}W-N%^c;D9bCrfCW`a zk}+CwzJx>pTwh+nlig!;IqS*p@m4-68bd0}11}NH^!$*R zWK_F_wb_FgAi!OS(#w1R8J^~q$_ugUH%qA@lm}uK9?A>R@=^h@dlZh+p~cJE-n1fGt#9zAYV;Kwh%zC+B~+?0v6*<5tD)UFnof#*3dz36Ve{O*a>+Y-|Nt-#syEl}FI zrM!5Q0_Y9cx^*OZwpc_V_IkBczDu8r8|#5*rS-MP4h$G+u6fI{(j4TB&T6Wc!U&|I$J%*Nq7l~2%+1&C9-Iwp5kMgldrMUSu wJ|2Icd`wMEO-)TrO-)TrO-)TrO-)tlANSmv&^It)0ssI207*qoM6N<$g3Px$>`6pHRCt{2m@#g{FbqXMi;h`3bnH;T=g2X7iXJDY$T4z&oFG7kjv2CKXBXKO z6f2f3Qbv*g78@2NkzW#J2_S?JM^tsMs>cQOj;gx%@G0iSv+)o>7v6tn)az|KZxO_+ z;W&5z??Zq~#NBFUC}W7D>yx4>JKq96N>QCAFQwF#RX`T8xPRs`_ow&KiAI3ozkG%`6xl z{TY21Q6<1k2X50Q^KENIuuOu7%=hdF7Lco(>uWaF+n||at@`lz74{UtyltaP~_B@j7ixn>VZ+h(}r z01rn!+5`-iKn@YvsVV?$!>1E~&%S)@NE{Kb|ASZXfb%X89FLd6xu9BX;~G5RcqJAS z5@SCTIz%%e;f%l0cMl$DBmht(Li6B(h5`V07a3hrJgj@K3(zs#l3)Qfr$jSa{|cA< z8?-o44z$hzJ==`g5PYi~u-0K{Y?Pahe9xLWV3mMVp8h!8dK)adz-+btfyM#=(;~DC z9#}yDATL7e;0Ymw5JCtcgb+dqA%qY@2zerY0g+O>+l$jd)&Kwi07*qoM6N<$g56f@ A=Kufz literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/plasma-laser-mount.png b/core/assets-raw/sprites/units/weapons/plasma-laser-mount.png new file mode 100644 index 0000000000000000000000000000000000000000..8a7ac5a27ba6c220330f00c049314aa4fd1afa3f GIT binary patch literal 1206 zcmV;n1WEgeP)Px(a7jc#RCt{2Td__XK@c6Hlx||Ih=KxHq_L3bQcxgBaYYJ5%|{?Uz=ex^K$I#< z9Z{ncE?hVk1qB@vasxsXE-VDNaqm=Pt=F@&GrMz66y2+=^X|;NcXzvQcWrk!NTG2}5GJH;%jHOcnE;Z)TCMG997!8X z5ljTo6vPpfkWb0UAzOfyLMWmd@;SMEWC=hOFn%1t_;HjImP9CSt_Y@spj)7&2xY|u zFchGSyS|ECCIV0e4TtxlYpMOLM93CEO1_NmEJVl>Kuex$liDv=1k*uKLgHV`ku1RX zA3qnSpXBmJ|_GOG_%jujy~HZL_Gu&eg8A9ul{EH}KGVCBHkW9WGtdESbd(Idy#z znniv7Iah?+y_@-GUj&LgLMnpEEZ`R6k_T8MpoQPpjm?qQz-$^ksvRzoN2G`l>eDRh zX5;~keE|2kYeeAMT8%4+x0b>B-2p%s;Qg0(+w?WBMDcIWQ4`D##b1alS5&=eMNlpo;UZwYxf zm*iE8!DJfb3XzH+B`?XTfteBF%uh@NEqP0$Wom_=+7q^LA1P+W+GUz9h5*6^YK%~* zZ5bBZTmXQu4b~iwOYE=(OkM$;3f+MT3-(F3TH8UJ>$h#3uaj-5e5uGXLT)h#w4dCtzE9MgwXopAc*FO+b$KIXPjZAKH^r81Uf6XXyP0s~5@&MuY9bvgGPUXz`?NC&J z7ERG7uO>5)F(ZXUTM4tLg^=X{1vo4XU z1#6DY2GJ*g3c&5_xnk{H?MBIS*Zlt6ouz7$Ood_}fD0fg`PAS0Oa5;lsX@9h%65W& zVnpg43FVQ**X99G&H|ypl0ipPl%#Dar*=W4UJUsOtjTNo__@paV^xI;75-oR0Wk2P UrFk?Yn*aa+07*qoM6N<$g4QM?5dZ)H literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/plasma-missile-mount.png b/core/assets-raw/sprites/units/weapons/plasma-missile-mount.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c5476bdc09d550f77abc4721f381f46f303aa6 GIT binary patch literal 833 zcmV-H1HSx;P)Px%`$fqc0+8T6e*&JDzfCBA_^2PC=hi-ji^vQfFcDSfT$2P za*ZflxG+`<_ejW<6{ScKp(Hf!O%cs<_wJcHuT29!Ql!nyeVpUjx${_HgAM*~Y~?zB zoy^v{?d9+&Dqe}N#)BLuMgRalef~n*V{CjeLr7NbOLTQ{Sv|jb{ffqw;hAHG7oCrtWp-mCllMb-ShtQUYpuv5R zcW6U|*?FJj9ZE%bJ95ZBdwx_sPsg+9z7i319k6P$OP~`;g8F}}&x#qtiKJ~YJL6rAlg?V^fm-H zbGl+M0*l2Gi^Z~P*W_i})x~9HZ^iWG)`;4T%0P@fBg&xrEDwtFboNZCUA)+yTz?}&BLop5- z!G%oUu>E@X+iNQ;NREw;t#dO(t;8`R7_sSiM&}Q+%^?pE0X|1dgpWLXW;@Zb@DJh-@mTF$CCo|S00000 LNkvXXu0mjfrjvnz literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/plasma-mount-weapon.png b/core/assets-raw/sprites/units/weapons/plasma-mount-weapon.png new file mode 100644 index 0000000000000000000000000000000000000000..57dad881debcb7a05c055801e12c40b2b97b869b GIT binary patch literal 415 zcmV;Q0bu@#P)Px$SxH1eRA_alTCAW)@HsMNyPDros%iPPdsmHX8J(zu{7q z3=R}aF%cI4WF0Prs{pMBGs8mhoE?|_)e0|FH&laT0%6-7}LMNyPD<_1edbg)^wp!fg)002ov JPDHLkV1kJ;t?vK; literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/point-defense-mount.png b/core/assets-raw/sprites/units/weapons/point-defense-mount.png new file mode 100644 index 0000000000000000000000000000000000000000..b14acbc73b1aa76c4dd24315da4415bf7ee95d9d GIT binary patch literal 505 zcmVPx$vq?ljRA_+(Rv?on;R&48t%C165y|X1(gLLEfcCm z3^a>xYm=gCz!$jR?y%qP*gmPK8KA^d&s5(O#0H*U-fH{b-QT`o|D9h8{#-GLil+*= zUVopp_4`-sSZ>T@JdD;8odI{XsI5Q=MKQTU@{(dxZ|<6=#tOWaVsg_3XuUfAZ5Z8T+v%}+5ZZuiD&~6Wb@1Bm?e#rNmRu ze&3WM6v*^V{Bup;1}?wq;r_)pu~dkLctE!Xf(2=b2b7CAM3|;{K&b|NM$0)_1h4lG v>hZD|0M1S15TBjVa$YuN!!QiPFbn{{^TO3H_Wt1B00000NkvXXu0mjfi<0I} literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/repair-beam-weapon-center.png b/core/assets-raw/sprites/units/weapons/repair-beam-weapon-center.png new file mode 100644 index 0000000000000000000000000000000000000000..4f38701c30f57da7ee00fd1cd9b7af1aefff195e GIT binary patch literal 377 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^T<#-1*YAsLNt zryFK5If}H|JA18`QH;=S+S}S9qFKb2y-}$DfuYLQB5| z|LzmMhi=||c+dN{duGmURp4maooF;yR_*f5hqsO3o)+LH*SP_K#9GzFaS{uRc)u(6@dq&&v6YU`f>} z!lfsgKUz=V+Wo|F58JlWvgh;tm%pC#qiknz+m-VUo&mc2%Y;sKT0L=`<4}FoYHRSr zEpxvGD%f*x4cGnFJYV_3{q5_fu?KTLb&>s)aK|k;ak`B}|K-rDjqjf^z4~=p=(rJc z=+&Js<}p<=sRp_WU1HG_3{h8%kNw>5`q8||M#?KWYFm4;=ZdGzK<`0NbP>gTe~DWM4fF!-Ni literal 0 HcmV?d00001 diff --git a/core/assets-raw/sprites/units/weapons/repair-beam-weapon.png b/core/assets-raw/sprites/units/weapons/repair-beam-weapon.png new file mode 100644 index 0000000000000000000000000000000000000000..373cf433ba38078faa3bd9ae637f22eda8247d4c GIT binary patch literal 344 zcmV-e0jK_nP)Px$5=lfsRA_$VQCfgPp-@?Z3WY);7N8cDSb`;~Rkk1zm54&2vIVO7 z`NSaHJ3{WM=4U4J-pPA;|9`+3WB!K8batLhXXkhK-=WC7EO0MLcH#kv0C1hKDtQ1X zsV)Yzi3g|^==54o&oml!W=MVuJ(6H24~Samp2Tr~P=w-EVBK6r_bRn2GgJn>g;GmM z#%M|j*?S;JsCqv@71%9KSI;RV=oPq?2>iqp3gjg=n{BpMNKgg(<#x7CB|)#iD-uj3 zEtHqYqmYn|(Nq#L0U$2HL@EGizCiQH1puuCRbbF<6=HiTiDKP0000x)zX literal 0 HcmV?d00001 diff --git a/core/assets/bloomshaders/alpha_bloom.frag b/core/assets/bloomshaders/alpha_bloom.frag index b3475ff259..346dbfe232 100644 --- a/core/assets/bloomshaders/alpha_bloom.frag +++ b/core/assets/bloomshaders/alpha_bloom.frag @@ -7,7 +7,7 @@ varying vec2 v_texCoords; void main(){ vec4 original = texture2D(u_texture0, v_texCoords) * OriginalIntensity; - vec4 bloom = texture2D(u_texture1, v_texCoords) * BloomIntensity; - original = original * (vec4(1.0) - bloom); - gl_FragColor = original + bloom; + vec4 bloom = texture2D(u_texture1, v_texCoords) * BloomIntensity; + original = original * (vec4(1.0) - bloom); + gl_FragColor = original + bloom; } diff --git a/core/assets/bloomshaders/maskedtreshold.frag b/core/assets/bloomshaders/maskedtreshold.frag deleted file mode 100644 index ec2634ef3f..0000000000 --- a/core/assets/bloomshaders/maskedtreshold.frag +++ /dev/null @@ -1,9 +0,0 @@ -uniform lowp sampler2D u_texture0; -uniform lowp vec2 threshold; -varying vec2 v_texCoords; - -void main(){ - vec4 tex = texture2D(u_texture0, v_texCoords); - vec3 colors = (tex.rgb - threshold.r) * threshold.g * tex.a; - gl_FragColor = vec4(colors, tex.a); -} diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 0ff58a6fab..e918d798aa 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -67,6 +67,14 @@ schematic.delete.confirm = This schematic will be utterly eradicated. schematic.rename = Rename Schematic schematic.info = {0}x{1}, {2} blocks schematic.disabled = [scarlet]Schematics disabled[]\nYou are not allowed to use schematics on this [accent]map[] or [accent]server. +schematic.tags = Tags: +schematic.edittags = Edit Tags +schematic.addtag = Add Tag +schematic.texttag = Text Tag +schematic.icontag = Icon Tag +schematic.renametag = Rename Tag +schematic.tagdelconfirm = Delete this tag completely? +schematic.tagexists = That tag already exists. stats = Stats stat.wave = Waves Defeated:[accent] {0} @@ -306,7 +314,6 @@ data.exported = Data exported. data.invalid = This isn't valid game data. data.import.confirm = Importing external data will overwrite[scarlet] all[] your current game data.\n[accent]This cannot be undone![]\n\nOnce the data is imported, your game will exit immediately. quit.confirm = Are you sure you want to quit? -quit.confirm.tutorial = Are you sure you know what you're doing?\nThe tutorial can be re-taken in[accent] Settings->Game->Re-Take Tutorial.[] loading = [accent]Loading... reloading = [accent]Reloading Mods... saving = [accent]Saving... @@ -498,6 +505,7 @@ load = Load save = Save fps = FPS: {0} ping = Ping: {0}ms +tps = TPS: {0} memory = Mem: {0}mb memory2 = Mem:\n {0}mb +\n {1}mb language.restart = Restart your game for the language settings to take effect. @@ -530,7 +538,7 @@ launch.from = Launching From: [accent]{0} launch.destination = Destination: {0} configure.invalid = Amount must be a number between 0 and {0}. add = Add... -boss.health = Guardian Health +guardian = Guardian connectfail = [scarlet]Connection error:\n\n[accent]{0} error.unreachable = Server unreachable.\nIs the address spelled correctly? @@ -574,6 +582,7 @@ sector.attacked = Sector [accent]{0}[white] under attack! sector.lost = Sector [accent]{0}[white] lost! #note: the missing space in the line below is intentional sector.captured = Sector [accent]{0}[white]captured! +sector.changeicon = Change Icon threat.low = Low threat.medium = Medium @@ -626,6 +635,7 @@ status.wet.name = Wet status.muddy.name = Muddy status.melting.name = Melting status.sapped.name = Sapped +status.electrified.name = Electrified status.spore-slowed.name = Spore Slowed status.tarred.name = Tarred status.overclock.name = Overclock @@ -654,6 +664,7 @@ settings.clearcampaignsaves.confirm = Are you sure you want to clear all of your paused = [accent]< Paused > clear = Clear banned = [scarlet]Banned +unsupported.environment = [scarlet]Unsupported Environment yes = Yes no = No info.title = Info @@ -692,6 +703,7 @@ stat.memorycapacity = Memory Capacity stat.basepowergeneration = Base Power Generation stat.productiontime = Production Time stat.repairtime = Block Full Repair Time +stat.repairspeed = Repair Speed stat.weapons = Weapons stat.bullet = Bullet stat.speedincrease = Speed Increase @@ -741,10 +753,11 @@ stat.healing = Healing ability.forcefield = Force Field ability.repairfield = Repair Field -ability.statusfield = Status Field +ability.statusfield = {0} Status Field ability.unitspawn = {0} Factory ability.shieldregenfield = Shield Regen Field ability.movelightning = Movement Lightning +ability.energyfield = Energy Field: [accent]{0}[] damage ~ [accent]{1}[] blocks / [accent]{2}[] targets bar.drilltierreq = Better Drill Required bar.noresources = Missing Resources @@ -767,6 +780,7 @@ bar.power = Power bar.progress = Build Progress bar.input = Input bar.output = Output +bar.strength = [stat]{0}[lightgray]x strength units.processorcontrol = [lightgray]Processor Controlled @@ -975,6 +989,7 @@ rules.wavetimer = Wave Timer rules.waves = Waves rules.attack = Attack Mode rules.buildai = AI Building +rules.corecapture = Capture Core On Destruction rules.enemyCheat = Infinite AI (Red Team) Resources rules.blockhealthmultiplier = Block Health Multiplier rules.blockdamagemultiplier = Block Damage Multiplier @@ -1030,6 +1045,7 @@ item.blast-compound.name = Blast Compound item.pyratite.name = Pyratite item.metaglass.name = Metaglass item.scrap.name = Scrap + liquid.water.name = Water liquid.slag.name = Slag liquid.oil.name = Oil @@ -1121,6 +1137,7 @@ block.sand-water.name = Sand Water block.darksand-water.name = Dark Sand Water block.char.name = Char block.dacite.name = Dacite +block.rhyolite.name = Rhyolite block.dacite-wall.name = Dacite Wall block.dacite-boulder.name = Dacite Boulder block.ice-snow.name = Ice Snow @@ -1228,6 +1245,7 @@ block.solar-panel.name = Solar Panel block.solar-panel-large.name = Large Solar Panel block.oil-extractor.name = Oil Extractor block.repair-point.name = Repair Point +block.repair-turret.name = Repair Turret block.pulse-conduit.name = Pulse Conduit block.plated-conduit.name = Plated Conduit block.phase-conduit.name = Phase Conduit @@ -1289,9 +1307,8 @@ block.memory-cell.name = Memory Cell block.memory-bank.name = Memory Bank team.blue.name = blue -team.crux.name = red -team.sharded.name = orange -team.orange.name = orange +team.crux.name = crux +team.sharded.name = sharded team.derelict.name = derelict team.green.name = green team.purple.name = purple @@ -1312,6 +1329,7 @@ hint.placeConveyor.mobile = Conveyors move items from drills into other blocks. 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 [accent]hammer[] in the bottom right and tap to break blocks.\n\nHold down your finger for a second and drag to break in a selection. +hint.blockInfo = View information of a block by selecting it in the [accent]build menu[], then selecting the [accent][[?][] button at the right. 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. @@ -1345,7 +1363,7 @@ item.graphite.description = Used in electrical components and turret ammunition. item.sand.description = Used for production of other refined materials. item.coal.description = Used for fuel and refined material production. item.coal.details = Appears to be fossilized plant matter, formed long before the seeding event. -item.titanium.description = Used in liquid transportation structures, drills and aircraft. +item.titanium.description = Used in liquid transportation structures, drills and factories. item.thorium.description = Used in durable structures and as nuclear fuel. item.scrap.description = Used in Melters and Pulverizers for refining into other materials. item.scrap.details = Leftover remnants of old structures and units. @@ -1566,7 +1584,7 @@ logic.nounitbuild = [red]Unit building logic is not allowed here. lenum.type = Type of building/unit.\ne.g. for any router, this will return [accent]@router[].\nNot a string. lenum.shoot = Shoot at a position. lenum.shootp = Shoot at a unit/building with velocity prediction. -lenum.configure = Building configuration, e.g. sorter item. +lenum.config = Building configuration, e.g. sorter item. lenum.enabled = Whether the block is enabled. laccess.color = Illuminator color. @@ -1574,6 +1592,7 @@ laccess.controller = Unit controller. If processor controlled, returns processor laccess.dead = Whether a unit/building is dead or no longer valid. laccess.controlled = Returns:\n[accent]@ctrlProcessor[] if unit controller is processor\n[accent]@ctrlPlayer[] if unit/building controller is player\n[accent]@ctrlFormation[] if unit is in formation\nOtherwise, 0. laccess.commanded = [red]Deprecated. Will be removed![]\nUse [accent]controlled[] instead. +laccess.progress = Action progress, 0 to 1.\nReturns production, turret reload or construction progress. graphicstype.clear = Fill the display with a color. graphicstype.color = Set color for next drawing operations. diff --git a/core/assets/bundles/bundle_ru.properties b/core/assets/bundles/bundle_ru.properties index 90d1731a0b..94bde81323 100644 --- a/core/assets/bundles/bundle_ru.properties +++ b/core/assets/bundles/bundle_ru.properties @@ -740,7 +740,7 @@ stat.reactive = Реактивен ability.forcefield = Силовое поле ability.repairfield = Ремонтирующее поле -ability.statusfield = Усиливающее поле +ability.statusfield = {0} Усиливающее поле ability.unitspawn = Завод единиц «{0}» ability.shieldregenfield = Поле восстановления щита ability.movelightning = Молнии при движении diff --git a/core/assets/icons/icons.properties b/core/assets/icons/icons.properties index 88c6b9ccb7..552da17570 100755 --- a/core/assets/icons/icons.properties +++ b/core/assets/icons/icons.properties @@ -1,336 +1,353 @@ -63743=spawn|block-spawn-medium -63742=deepwater|block-deepwater-medium -63741=water|block-water-medium -63740=tainted-water|block-tainted-water-medium -63739=darksand-tainted-water|block-darksand-tainted-water-medium -63738=sand-water|block-sand-water-medium -63737=darksand-water|block-darksand-water-medium -63736=tar|block-tar-medium -63735=stone|block-stone-medium -63734=craters|block-craters-medium -63733=char|block-char-medium -63732=ignarock|block-ignarock-medium -63731=hotrock|block-hotrock-medium -63730=magmarock|block-magmarock-medium -63729=sand|block-sand-medium -63728=darksand|block-darksand-medium -63726=grass|block-grass-medium -63725=salt|block-salt-medium -63724=snow|block-snow-medium -63723=ice|block-ice-medium -63722=ice-snow|block-ice-snow-medium -63721=cliffs|block-cliffs-medium -63718=rock|block-rock-medium -63717=snowrock|block-snowrock-medium -63711=spore-pine|block-spore-pine-medium -63710=snow-pine|block-snow-pine-medium -63709=pine|block-pine-medium -63708=shrubs|block-shrubs-medium -63707=white-tree-dead|block-white-tree-dead-medium -63706=white-tree|block-white-tree-medium -63705=spore-cluster|block-spore-cluster-medium -63704=shale|block-shale-medium -63702=shale-boulder|block-shale-boulder-medium -63701=sand-boulder|block-sand-boulder-medium -63700=moss|block-moss-medium -63699=spore-moss|block-spore-moss-medium -63698=metal-floor|block-metal-floor-medium -63697=metal-floor-damaged|block-metal-floor-damaged-medium -63696=metal-floor-2|block-metal-floor-2-medium -63695=metal-floor-3|block-metal-floor-3-medium -63694=metal-floor-5|block-metal-floor-5-medium -63693=dark-panel-1|block-dark-panel-1-medium -63692=dark-panel-2|block-dark-panel-2-medium -63691=dark-panel-3|block-dark-panel-3-medium -63690=dark-panel-4|block-dark-panel-4-medium -63689=dark-panel-5|block-dark-panel-5-medium -63688=dark-panel-6|block-dark-panel-6-medium -63687=dark-metal|block-dark-metal-medium -63686=pebbles|block-pebbles-medium -63685=tendrils|block-tendrils-medium -63684=ore-copper|block-ore-copper-medium -63683=ore-lead|block-ore-lead-medium -63682=ore-scrap|block-ore-scrap-medium -63681=ore-coal|block-ore-coal-medium -63680=ore-titanium|block-ore-titanium-medium -63679=ore-thorium|block-ore-thorium-medium -63678=graphite-press|block-graphite-press-medium -63677=multi-press|block-multi-press-medium -63676=silicon-smelter|block-silicon-smelter-medium -63675=kiln|block-kiln-medium -63674=plastanium-compressor|block-plastanium-compressor-medium -63673=phase-weaver|block-phase-weaver-medium -63672=alloy-smelter|block-alloy-smelter-medium -63671=cryofluid-mixer|block-cryofluid-mixer-medium -63670=blast-mixer|block-blast-mixer-medium -63669=pyratite-mixer|block-pyratite-mixer-medium -63668=melter|block-melter-medium -63667=separator|block-separator-medium -63666=spore-press|block-spore-press-medium -63665=pulverizer|block-pulverizer-medium -63664=coal-centrifuge|block-coal-centrifuge-medium -63663=incinerator|block-incinerator-medium -63662=copper-wall|block-copper-wall-medium -63661=copper-wall-large|block-copper-wall-large-medium -63660=titanium-wall|block-titanium-wall-medium -63659=titanium-wall-large|block-titanium-wall-large-medium -63658=plastanium-wall|block-plastanium-wall-medium -63657=plastanium-wall-large|block-plastanium-wall-large-medium -63656=thorium-wall|block-thorium-wall-medium -63655=thorium-wall-large|block-thorium-wall-large-medium -63654=phase-wall|block-phase-wall-medium -63653=phase-wall-large|block-phase-wall-large-medium -63652=surge-wall|block-surge-wall-medium -63651=surge-wall-large|block-surge-wall-large-medium -63650=door|block-door-medium -63649=door-large|block-door-large-medium -63648=scrap-wall|block-scrap-wall-medium -63647=scrap-wall-large|block-scrap-wall-large-medium -63646=scrap-wall-huge|block-scrap-wall-huge-medium -63645=scrap-wall-gigantic|block-scrap-wall-gigantic-medium -63644=thruster|block-thruster-medium -63643=mender|block-mender-medium -63642=mend-projector|block-mend-projector-medium -63641=overdrive-projector|block-overdrive-projector-medium -63640=force-projector|block-force-projector-medium -63639=shock-mine|block-shock-mine-medium -63638=conveyor|block-conveyor-medium -63637=titanium-conveyor|block-titanium-conveyor-medium -63636=armored-conveyor|block-armored-conveyor-medium -63635=junction|block-junction-medium -63634=bridge-conveyor|block-bridge-conveyor-medium -63633=phase-conveyor|block-phase-conveyor-medium -63632=sorter|block-sorter-medium -63631=inverted-sorter|block-inverted-sorter-medium -63630=router|block-router-medium -63629=distributor|block-distributor-medium -63628=overflow-gate|block-overflow-gate-medium -63627=mass-driver|block-mass-driver-medium -63626=mechanical-pump|block-mechanical-pump-medium -63625=rotary-pump|block-rotary-pump-medium -63624=thermal-pump|block-thermal-pump-medium -63623=conduit|block-conduit-medium -63622=pulse-conduit|block-pulse-conduit-medium -63621=plated-conduit|block-plated-conduit-medium -63620=liquid-router|block-liquid-router-medium -63619=liquid-tank|block-liquid-tank-medium -63618=liquid-junction|block-liquid-junction-medium -63617=bridge-conduit|block-bridge-conduit-medium -63616=phase-conduit|block-phase-conduit-medium -63615=power-node|block-power-node-medium -63614=power-node-large|block-power-node-large-medium -63613=surge-tower|block-surge-tower-medium -63612=diode|block-diode-medium -63611=battery|block-battery-medium -63610=battery-large|block-battery-large-medium -63609=combustion-generator|block-combustion-generator-medium -63608=thermal-generator|block-thermal-generator-medium -63607=steam-generator|block-steam-generator-medium -63606=differential-generator|block-differential-generator-medium -63605=rtg-generator|block-rtg-generator-medium -63604=solar-panel|block-solar-panel-medium -63603=solar-panel-large|block-solar-panel-large-medium -63602=thorium-reactor|block-thorium-reactor-medium -63601=impact-reactor|block-impact-reactor-medium -63600=mechanical-drill|block-mechanical-drill-medium -63599=pneumatic-drill|block-pneumatic-drill-medium -63598=laser-drill|block-laser-drill-medium -63597=blast-drill|block-blast-drill-medium -63596=water-extractor|block-water-extractor-medium -63595=cultivator|block-cultivator-medium -63594=oil-extractor|block-oil-extractor-medium -63593=core-shard|block-core-shard-medium -63592=core-foundation|block-core-foundation-medium -63591=core-nucleus|block-core-nucleus-medium -63590=vault|block-vault-medium -63589=container|block-container-medium -63588=unloader|block-unloader-medium -63587=launch-pad|block-launch-pad-medium -63586=launch-pad-large|block-launch-pad-large-medium -63585=duo|block-duo-medium -63584=scatter|block-scatter-medium -63583=scorch|block-scorch-medium -63582=hail|block-hail-medium -63581=wave|block-wave-medium -63580=lancer|block-lancer-medium -63579=arc|block-arc-medium -63578=swarmer|block-swarmer-medium -63577=salvo|block-salvo-medium -63576=fuse|block-fuse-medium -63575=ripple|block-ripple-medium -63574=cyclone|block-cyclone-medium -63573=spectre|block-spectre-medium -63572=meltdown|block-meltdown-medium -63571=draug-factory|block-draug-factory-medium -63570=spirit-factory|block-spirit-factory-medium -63569=phantom-factory|block-phantom-factory-medium -63568=command-center|block-command-center-medium -63567=wraith-factory|block-wraith-factory-medium -63566=ghoul-factory|block-ghoul-factory-medium -63565=revenant-factory|block-revenant-factory-medium -63564=dagger-factory|block-dagger-factory-medium -63563=crawler-factory|block-crawler-factory-medium -63562=titan-factory|block-titan-factory-medium -63561=fortress-factory|block-fortress-factory-medium -63560=repair-point|block-repair-point-medium -63559=dart-mech-pad|block-dart-mech-pad-medium -63558=delta-mech-pad|block-delta-mech-pad-medium -63557=tau-mech-pad|block-tau-mech-pad-medium -63556=omega-mech-pad|block-omega-mech-pad-medium -63555=javelin-ship-pad|block-javelin-ship-pad-medium -63554=trident-ship-pad|block-trident-ship-pad-medium -63553=glaive-ship-pad|block-glaive-ship-pad-medium -63552=power-source|block-power-source-medium -63551=power-void|block-power-void-medium -63550=item-source|block-item-source-medium -63549=item-void|block-item-void-medium -63548=liquid-source|block-liquid-source-medium -63547=liquid-void|block-liquid-void-medium -63546=message|block-message-medium -63545=illuminator|block-illuminator-medium -63544=copper|item-copper-icon -63543=lead|item-lead-icon -63542=metaglass|item-metaglass-icon -63541=graphite|item-graphite-icon -63540=sand|item-sand-icon -63539=coal|item-coal-icon -63538=titanium|item-titanium-icon -63537=thorium|item-thorium-icon -63536=scrap|item-scrap-icon -63535=silicon|item-silicon-icon -63534=plastanium|item-plastanium-icon -63533=phase-fabric|item-phase-fabric-icon -63532=surge-alloy|item-surge-alloy-icon -63531=spore-pod|item-spore-pod-icon -63530=blast-compound|item-blast-compound-icon -63529=pyratite|item-pyratite-icon -63528=water|liquid-water-icon -63527=slag|liquid-slag-icon -63526=oil|liquid-oil-icon -63525=cryofluid|liquid-cryofluid-icon -63524=underflow-gate|block-underflow-gate-medium -63523=dart-ship-pad|block-dart-ship-pad-medium -63522=alpha-mech-pad|block-alpha-mech-pad-medium -63521=cliff|block-cliff-medium -63520=legacy-mech-pad|block-legacy-mech-pad-medium -63519=ground-factory|block-ground-factory-medium -63518=legacy-unit-factory|block-legacy-unit-factory-medium -63517=mass-conveyor|block-mass-conveyor-medium -63516=legacy-command-center|block-legacy-command-center-medium -63515=block-forge|block-block-forge-medium -63514=block-launcher|block-block-launcher-medium -63513=plastanium-conveyor|block-plastanium-conveyor-medium +63743=spawn|block-spawn-ui +63742=deepwater|block-deepwater-ui +63741=water|block-water-ui +63740=tainted-water|block-tainted-water-ui +63739=darksand-tainted-water|block-darksand-tainted-water-ui +63738=sand-water|block-sand-water-ui +63737=darksand-water|block-darksand-water-ui +63736=tar|block-tar-ui +63735=stone|block-stone-ui +63734=craters|block-craters-ui +63733=char|block-char-ui +63732=ignarock|block-ignarock-ui +63731=hotrock|block-hotrock-ui +63730=magmarock|block-magmarock-ui +63729=sand|block-sand-ui +63728=darksand|block-darksand-ui +63726=grass|block-grass-ui +63725=salt|block-salt-ui +63724=snow|block-snow-ui +63723=ice|block-ice-ui +63722=ice-snow|block-ice-snow-ui +63721=cliffs|block-cliffs-ui +63718=rock|block-rock-ui +63717=snowrock|block-snowrock-ui +63711=spore-pine|block-spore-pine-ui +63710=snow-pine|block-snow-pine-ui +63709=pine|block-pine-ui +63708=shrubs|block-shrubs-ui +63707=white-tree-dead|block-white-tree-dead-ui +63706=white-tree|block-white-tree-ui +63705=spore-cluster|block-spore-cluster-ui +63704=shale|block-shale-ui +63702=shale-boulder|block-shale-boulder-ui +63701=sand-boulder|block-sand-boulder-ui +63700=moss|block-moss-ui +63699=spore-moss|block-spore-moss-ui +63698=metal-floor|block-metal-floor-ui +63697=metal-floor-damaged|block-metal-floor-damaged-ui +63696=metal-floor-2|block-metal-floor-2-ui +63695=metal-floor-3|block-metal-floor-3-ui +63694=metal-floor-5|block-metal-floor-5-ui +63693=dark-panel-1|block-dark-panel-1-ui +63692=dark-panel-2|block-dark-panel-2-ui +63691=dark-panel-3|block-dark-panel-3-ui +63690=dark-panel-4|block-dark-panel-4-ui +63689=dark-panel-5|block-dark-panel-5-ui +63688=dark-panel-6|block-dark-panel-6-ui +63687=dark-metal|block-dark-metal-ui +63686=pebbles|block-pebbles-ui +63685=tendrils|block-tendrils-ui +63684=ore-copper|block-ore-copper-ui +63683=ore-lead|block-ore-lead-ui +63682=ore-scrap|block-ore-scrap-ui +63681=ore-coal|block-ore-coal-ui +63680=ore-titanium|block-ore-titanium-ui +63679=ore-thorium|block-ore-thorium-ui +63678=graphite-press|block-graphite-press-ui +63677=multi-press|block-multi-press-ui +63676=silicon-smelter|block-silicon-smelter-ui +63675=kiln|block-kiln-ui +63674=plastanium-compressor|block-plastanium-compressor-ui +63673=phase-weaver|block-phase-weaver-ui +63672=alloy-smelter|block-alloy-smelter-ui +63671=cryofluid-mixer|block-cryofluid-mixer-ui +63670=blast-mixer|block-blast-mixer-ui +63669=pyratite-mixer|block-pyratite-mixer-ui +63668=melter|block-melter-ui +63667=separator|block-separator-ui +63666=spore-press|block-spore-press-ui +63665=pulverizer|block-pulverizer-ui +63664=coal-centrifuge|block-coal-centrifuge-ui +63663=incinerator|block-incinerator-ui +63662=copper-wall|block-copper-wall-ui +63661=copper-wall-large|block-copper-wall-large-ui +63660=titanium-wall|block-titanium-wall-ui +63659=titanium-wall-large|block-titanium-wall-large-ui +63658=plastanium-wall|block-plastanium-wall-ui +63657=plastanium-wall-large|block-plastanium-wall-large-ui +63656=thorium-wall|block-thorium-wall-ui +63655=thorium-wall-large|block-thorium-wall-large-ui +63654=phase-wall|block-phase-wall-ui +63653=phase-wall-large|block-phase-wall-large-ui +63652=surge-wall|block-surge-wall-ui +63651=surge-wall-large|block-surge-wall-large-ui +63650=door|block-door-ui +63649=door-large|block-door-large-ui +63648=scrap-wall|block-scrap-wall-ui +63647=scrap-wall-large|block-scrap-wall-large-ui +63646=scrap-wall-huge|block-scrap-wall-huge-ui +63645=scrap-wall-gigantic|block-scrap-wall-gigantic-ui +63644=thruster|block-thruster-ui +63643=mender|block-mender-ui +63642=mend-projector|block-mend-projector-ui +63641=overdrive-projector|block-overdrive-projector-ui +63640=force-projector|block-force-projector-ui +63639=shock-mine|block-shock-mine-ui +63638=conveyor|block-conveyor-ui +63637=titanium-conveyor|block-titanium-conveyor-ui +63636=armored-conveyor|block-armored-conveyor-ui +63635=junction|block-junction-ui +63634=bridge-conveyor|block-bridge-conveyor-ui +63633=phase-conveyor|block-phase-conveyor-ui +63632=sorter|block-sorter-ui +63631=inverted-sorter|block-inverted-sorter-ui +63630=router|block-router-ui +63629=distributor|block-distributor-ui +63628=overflow-gate|block-overflow-gate-ui +63627=mass-driver|block-mass-driver-ui +63626=mechanical-pump|block-mechanical-pump-ui +63625=rotary-pump|block-rotary-pump-ui +63624=thermal-pump|block-thermal-pump-ui +63623=conduit|block-conduit-ui +63622=pulse-conduit|block-pulse-conduit-ui +63621=plated-conduit|block-plated-conduit-ui +63620=liquid-router|block-liquid-router-ui +63619=liquid-tank|block-liquid-tank-ui +63618=liquid-junction|block-liquid-junction-ui +63617=bridge-conduit|block-bridge-conduit-ui +63616=phase-conduit|block-phase-conduit-ui +63615=power-node|block-power-node-ui +63614=power-node-large|block-power-node-large-ui +63613=surge-tower|block-surge-tower-ui +63612=diode|block-diode-ui +63611=battery|block-battery-ui +63610=battery-large|block-battery-large-ui +63609=combustion-generator|block-combustion-generator-ui +63608=thermal-generator|block-thermal-generator-ui +63607=steam-generator|block-steam-generator-ui +63606=differential-generator|block-differential-generator-ui +63605=rtg-generator|block-rtg-generator-ui +63604=solar-panel|block-solar-panel-ui +63603=solar-panel-large|block-solar-panel-large-ui +63602=thorium-reactor|block-thorium-reactor-ui +63601=impact-reactor|block-impact-reactor-ui +63600=mechanical-drill|block-mechanical-drill-ui +63599=pneumatic-drill|block-pneumatic-drill-ui +63598=laser-drill|block-laser-drill-ui +63597=blast-drill|block-blast-drill-ui +63596=water-extractor|block-water-extractor-ui +63595=cultivator|block-cultivator-ui +63594=oil-extractor|block-oil-extractor-ui +63593=core-shard|block-core-shard-ui +63592=core-foundation|block-core-foundation-ui +63591=core-nucleus|block-core-nucleus-ui +63590=vault|block-vault-ui +63589=container|block-container-ui +63588=unloader|block-unloader-ui +63587=launch-pad|block-launch-pad-ui +63586=launch-pad-large|block-launch-pad-large-ui +63585=duo|block-duo-ui +63584=scatter|block-scatter-ui +63583=scorch|block-scorch-ui +63582=hail|block-hail-ui +63581=wave|block-wave-ui +63580=lancer|block-lancer-ui +63579=arc|block-arc-ui +63578=swarmer|block-swarmer-ui +63577=salvo|block-salvo-ui +63576=fuse|block-fuse-ui +63575=ripple|block-ripple-ui +63574=cyclone|block-cyclone-ui +63573=spectre|block-spectre-ui +63572=meltdown|block-meltdown-ui +63571=draug-factory|block-draug-factory-ui +63570=spirit-factory|block-spirit-factory-ui +63569=phantom-factory|block-phantom-factory-ui +63568=command-center|block-command-center-ui +63567=wraith-factory|block-wraith-factory-ui +63566=ghoul-factory|block-ghoul-factory-ui +63565=revenant-factory|block-revenant-factory-ui +63564=dagger-factory|block-dagger-factory-ui +63563=crawler-factory|block-crawler-factory-ui +63562=titan-factory|block-titan-factory-ui +63561=fortress-factory|block-fortress-factory-ui +63560=repair-point|block-repair-point-ui +63559=dart-mech-pad|block-dart-mech-pad-ui +63558=delta-mech-pad|block-delta-mech-pad-ui +63557=tau-mech-pad|block-tau-mech-pad-ui +63556=omega-mech-pad|block-omega-mech-pad-ui +63555=javelin-ship-pad|block-javelin-ship-pad-ui +63554=trident-ship-pad|block-trident-ship-pad-ui +63553=glaive-ship-pad|block-glaive-ship-pad-ui +63552=power-source|block-power-source-ui +63551=power-void|block-power-void-ui +63550=item-source|block-item-source-ui +63549=item-void|block-item-void-ui +63548=liquid-source|block-liquid-source-ui +63547=liquid-void|block-liquid-void-ui +63546=message|block-message-ui +63545=illuminator|block-illuminator-ui +63544=copper|item-copper-ui +63543=lead|item-lead-ui +63542=metaglass|item-metaglass-ui +63541=graphite|item-graphite-ui +63540=sand|item-sand-ui +63539=coal|item-coal-ui +63538=titanium|item-titanium-ui +63537=thorium|item-thorium-ui +63536=scrap|item-scrap-ui +63535=silicon|item-silicon-ui +63534=plastanium|item-plastanium-ui +63533=phase-fabric|item-phase-fabric-ui +63532=surge-alloy|item-surge-alloy-ui +63531=spore-pod|item-spore-pod-ui +63530=blast-compound|item-blast-compound-ui +63529=pyratite|item-pyratite-ui +63528=water|liquid-water-ui +63527=slag|liquid-slag-ui +63526=oil|liquid-oil-ui +63525=cryofluid|liquid-cryofluid-ui +63524=underflow-gate|block-underflow-gate-ui +63523=dart-ship-pad|block-dart-ship-pad-ui +63522=alpha-mech-pad|block-alpha-mech-pad-ui +63521=cliff|block-cliff-ui +63520=legacy-mech-pad|block-legacy-mech-pad-ui +63519=ground-factory|block-ground-factory-ui +63518=legacy-unit-factory|block-legacy-unit-factory-ui +63517=mass-conveyor|block-mass-conveyor-ui +63516=legacy-command-center|block-legacy-command-center-ui +63515=block-forge|block-block-forge-ui +63514=block-launcher|block-block-launcher-ui +63513=plastanium-conveyor|block-plastanium-conveyor-ui 63512=crater|crater -63511=naval-factory|block-naval-factory-medium -63510=air-factory|block-air-factory-medium -63509=basic-reconstructor|block-basic-reconstructor-medium -63508=block-loader|block-block-loader-medium -63507=block-unloader|block-block-unloader-medium -63506=core-silo|block-core-silo-medium -63505=data-processor|block-data-processor-medium -63504=payload-router|block-payload-router-medium -63503=silicon-crucible|block-silicon-crucible-medium -63502=segment|block-segment-medium -63501=large-overdrive-projector|block-large-overdrive-projector-medium -63500=disassembler|block-disassembler-medium -63499=advanced-reconstructor|block-advanced-reconstructor-medium -63498=reconstructor-basis|block-reconstructor-basis-medium -63497=reconstructor-morphism|block-reconstructor-morphism-medium -63496=reconstructor-functor|block-reconstructor-functor-medium -63495=reconstructor-prime|block-reconstructor-prime-medium -63494=additive-reconstructor|block-additive-reconstructor-medium -63493=multiplicative-reconstructor|block-multiplicative-reconstructor-medium -63492=exponential-reconstructor|block-exponential-reconstructor-medium -63491=tetrative-reconstructor|block-tetrative-reconstructor-medium -63490=resupply-point|block-resupply-point-medium -63489=parallax|block-parallax-medium -63488=dagger|unit-dagger-medium -63487=mace|unit-mace-medium -63486=fortress|unit-fortress-medium -63485=nova|unit-nova-medium -63484=pulsar|unit-pulsar-medium -63483=quasar|unit-quasar-medium -63482=crawler|unit-crawler-medium -63481=atrax|unit-atrax-medium -63480=spiroct|unit-spiroct-medium -63479=arkyid|unit-arkyid-medium -63478=flare|unit-flare-medium -63477=horizon|unit-horizon-medium -63476=zenith|unit-zenith-medium -63475=antumbra|unit-antumbra-medium -63474=eclipse|unit-eclipse-medium -63473=mono|unit-mono-medium -63472=poly|unit-poly-medium -63471=mega|unit-mega-medium -63470=risse|unit-risse-medium -63469=minke|unit-minke-medium -63468=bryde|unit-bryde-medium -63467=alpha|unit-alpha-medium -63466=beta|unit-beta-medium -63465=gamma|unit-gamma-medium -63464=block|unit-block-medium -63463=risso|unit-risso-medium -63462=overdrive-dome|block-overdrive-dome-medium -63461=logic-processor|block-logic-processor-medium -63460=micro-processor|block-micro-processor-medium -63459=logic-display|block-logic-display-medium -63458=switch|block-switch-medium -63457=memory-cell|block-memory-cell-medium -63456=payload-conveyor|block-payload-conveyor-medium -63455=hyper-processor|block-hyper-processor-medium -63454=toxopid|unit-toxopid-medium -63453=vestige|unit-vestige-medium -63452=cataclyst|unit-cataclyst-medium -63451=scepter|unit-scepter-medium -63450=reign|unit-reign-medium -63449=dirt|block-dirt-medium -63447=stone-wall|block-stone-wall-medium -63446=spore-wall|block-spore-wall-medium -63445=ice-wall|block-ice-wall-medium -63444=snow-wall|block-snow-wall-medium -63443=dune-wall|block-dune-wall-medium -63442=sand-wall|block-sand-wall-medium -63441=salt-wall|block-salt-wall-medium -63440=shale-wall|block-shale-wall-medium -63439=dirt-wall|block-dirt-wall-medium -63437=basalt|block-basalt-medium -63436=dacite|block-dacite-medium -63435=boulder|block-boulder-medium -63434=snow-boulder|block-snow-boulder-medium -63433=dacite-wall|block-dacite-wall-medium -63432=dacite-boulder|block-dacite-boulder-medium -63431=large-logic-display|block-large-logic-display-medium -63430=omura|unit-omura-medium -63429=mud|block-mud-medium -63428=sei|unit-sei-medium -63427=quad|unit-quad-medium -63426=oct|unit-oct-medium -63425=vela|unit-vela-medium -63424=corvus|unit-corvus-medium -63423=memory-bank|block-memory-bank-medium -63422=foreshadow|block-foreshadow-medium -63421=tsunami|block-tsunami-medium -63420=space|block-space-medium -63419=legacy-unit-factory-air|block-legacy-unit-factory-air-medium -63418=legacy-unit-factory-ground|block-legacy-unit-factory-ground-medium -63417=interplanetary-accelerator|block-interplanetary-accelerator-medium -63416=basalt-boulder|block-basalt-boulder-medium -63415=none|status-none-icon -63414=burning|status-burning-icon -63413=freezing|status-freezing-icon -63412=unmoving|status-unmoving-icon -63411=slow|status-slow-icon -63410=wet|status-wet-icon -63409=muddy|status-muddy-icon -63408=melting|status-melting-icon -63407=sapped|status-sapped-icon -63406=spore-slowed|status-spore-slowed-icon -63405=tarred|status-tarred-icon -63404=overdrive|status-overdrive-icon -63403=overclock|status-overclock-icon -63402=shielded|status-shielded-icon -63401=boss|status-boss-icon -63400=shocked|status-shocked-icon -63399=blasted|status-blasted-icon -63398=corroded|status-corroded-icon -63397=disarmed|status-disarmed-icon +63511=naval-factory|block-naval-factory-ui +63510=air-factory|block-air-factory-ui +63509=basic-reconstructor|block-basic-reconstructor-ui +63508=block-loader|block-block-loader-ui +63507=block-unloader|block-block-unloader-ui +63506=core-silo|block-core-silo-ui +63505=data-processor|block-data-processor-ui +63504=payload-router|block-payload-router-ui +63503=silicon-crucible|block-silicon-crucible-ui +63502=segment|block-segment-ui +63501=large-overdrive-projector|block-large-overdrive-projector-ui +63500=disassembler|block-disassembler-ui +63499=advanced-reconstructor|block-advanced-reconstructor-ui +63498=reconstructor-basis|block-reconstructor-basis-ui +63497=reconstructor-morphism|block-reconstructor-morphism-ui +63496=reconstructor-functor|block-reconstructor-functor-ui +63495=reconstructor-prime|block-reconstructor-prime-ui +63494=additive-reconstructor|block-additive-reconstructor-ui +63493=multiplicative-reconstructor|block-multiplicative-reconstructor-ui +63492=exponential-reconstructor|block-exponential-reconstructor-ui +63491=tetrative-reconstructor|block-tetrative-reconstructor-ui +63490=resupply-point|block-resupply-point-ui +63489=parallax|block-parallax-ui +63488=dagger|unit-dagger-ui +63487=mace|unit-mace-ui +63486=fortress|unit-fortress-ui +63485=nova|unit-nova-ui +63484=pulsar|unit-pulsar-ui +63483=quasar|unit-quasar-ui +63482=crawler|unit-crawler-ui +63481=atrax|unit-atrax-ui +63480=spiroct|unit-spiroct-ui +63479=arkyid|unit-arkyid-ui +63478=flare|unit-flare-ui +63477=horizon|unit-horizon-ui +63476=zenith|unit-zenith-ui +63475=antumbra|unit-antumbra-ui +63474=eclipse|unit-eclipse-ui +63473=mono|unit-mono-ui +63472=poly|unit-poly-ui +63471=mega|unit-mega-ui +63470=risse|unit-risse-ui +63469=minke|unit-minke-ui +63468=bryde|unit-bryde-ui +63467=alpha|unit-alpha-ui +63466=beta|unit-beta-ui +63465=gamma|unit-gamma-ui +63464=block|unit-block-ui +63463=risso|unit-risso-ui +63462=overdrive-dome|block-overdrive-dome-ui +63461=logic-processor|block-logic-processor-ui +63460=micro-processor|block-micro-processor-ui +63459=logic-display|block-logic-display-ui +63458=switch|block-switch-ui +63457=memory-cell|block-memory-cell-ui +63456=payload-conveyor|block-payload-conveyor-ui +63455=hyper-processor|block-hyper-processor-ui +63454=toxopid|unit-toxopid-ui +63453=vestige|unit-vestige-ui +63452=cataclyst|unit-cataclyst-ui +63451=scepter|unit-scepter-ui +63450=reign|unit-reign-ui +63449=dirt|block-dirt-ui +63447=stone-wall|block-stone-wall-ui +63446=spore-wall|block-spore-wall-ui +63445=ice-wall|block-ice-wall-ui +63444=snow-wall|block-snow-wall-ui +63443=dune-wall|block-dune-wall-ui +63442=sand-wall|block-sand-wall-ui +63441=salt-wall|block-salt-wall-ui +63440=shale-wall|block-shale-wall-ui +63439=dirt-wall|block-dirt-wall-ui +63437=basalt|block-basalt-ui +63436=dacite|block-dacite-ui +63435=boulder|block-boulder-ui +63434=snow-boulder|block-snow-boulder-ui +63433=dacite-wall|block-dacite-wall-ui +63432=dacite-boulder|block-dacite-boulder-ui +63431=large-logic-display|block-large-logic-display-ui +63430=omura|unit-omura-ui +63429=mud|block-mud-ui +63428=sei|unit-sei-ui +63427=quad|unit-quad-ui +63426=oct|unit-oct-ui +63425=vela|unit-vela-ui +63424=corvus|unit-corvus-ui +63423=memory-bank|block-memory-bank-ui +63422=foreshadow|block-foreshadow-ui +63421=tsunami|block-tsunami-ui +63420=space|block-space-ui +63419=legacy-unit-factory-air|block-legacy-unit-factory-air-ui +63418=legacy-unit-factory-ground|block-legacy-unit-factory-ground-ui +63417=interplanetary-accelerator|block-interplanetary-accelerator-ui +63416=basalt-boulder|block-basalt-boulder-ui +63415=none|status-none-ui +63414=burning|status-burning-ui +63413=freezing|status-freezing-ui +63412=unmoving|status-unmoving-ui +63411=slow|status-slow-ui +63410=wet|status-wet-ui +63409=muddy|status-muddy-ui +63408=melting|status-melting-ui +63407=sapped|status-sapped-ui +63406=spore-slowed|status-spore-slowed-ui +63405=tarred|status-tarred-ui +63404=overdrive|status-overdrive-ui +63403=overclock|status-overclock-ui +63402=shielded|status-shielded-ui +63401=boss|status-boss-ui +63400=shocked|status-shocked-ui +63399=blasted|status-blasted-ui +63398=corroded|status-corroded-ui +63397=disarmed|status-disarmed-ui +63385=duct|block-duct-ui +63376=repair-turret|block-repair-turret-ui +63375=payload-propulsion-tower|block-payload-propulsion-tower-ui +63374=payload-incinerator|block-payload-incinerator-ui +63373=payload-void|block-payload-void-ui +63372=payload-source|block-payload-source-ui +63368=retusa|unit-retusa-ui +63367=directional-item-bridge|block-directional-item-bridge-ui +63366=duct-router|block-duct-router-ui +63365=duct-bridge|block-duct-bridge-ui +63364=oxynoe|unit-oxynoe-ui +63363=cyerce|unit-cyerce-ui +63362=aegires|unit-aegires-ui +63361=electrified|status-electrified-ui +63360=navanax|unit-navanax-ui +63354=payload-launch-pad|block-payload-launch-pad-ui +63353=silicon-arc-furnace|block-silicon-arc-furnace-ui diff --git a/core/assets/maps/shoreline.msav b/core/assets/maps/shoreline.msav deleted file mode 100644 index b0320b43502badf58cbf8f5531194d339846214e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15212 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 diff --git a/core/assets/scripts/global.js b/core/assets/scripts/global.js index ac1a812efd..2c40cc4466 100755 --- a/core/assets/scripts/global.js +++ b/core/assets/scripts/global.js @@ -53,10 +53,12 @@ function extend(/*Base, ..., def*/){ const extendContent = extend; importPackage(Packages.arc) +importPackage(Packages.arc.audio) importPackage(Packages.arc.func) importPackage(Packages.arc.graphics) importPackage(Packages.arc.graphics.g2d) importPackage(Packages.arc.graphics.gl) +importPackage(Packages.arc.input) importPackage(Packages.arc.math) importPackage(Packages.arc.math.geom) importPackage(Packages.arc.scene) @@ -68,6 +70,12 @@ importPackage(Packages.arc.scene.ui.layout) importPackage(Packages.arc.scene.utils) importPackage(Packages.arc.struct) importPackage(Packages.arc.util) +importPackage(Packages.arc.util.async) +importPackage(Packages.arc.util.io) +importPackage(Packages.arc.util.noise) +importPackage(Packages.arc.util.pooling) +importPackage(Packages.arc.util.serialization) +importPackage(Packages.arc.util.viewport) importPackage(Packages.mindustry) importPackage(Packages.mindustry.ai) importPackage(Packages.mindustry.ai.formations) @@ -97,7 +105,9 @@ importPackage(Packages.mindustry.maps.filters) importPackage(Packages.mindustry.maps.generators) importPackage(Packages.mindustry.maps.planet) importPackage(Packages.mindustry.net) +importPackage(Packages.mindustry.service) importPackage(Packages.mindustry.type) +importPackage(Packages.mindustry.type.weapons) importPackage(Packages.mindustry.type.weather) importPackage(Packages.mindustry.ui) importPackage(Packages.mindustry.ui.dialogs) @@ -123,7 +133,6 @@ importPackage(Packages.mindustry.world.blocks.units) importPackage(Packages.mindustry.world.consumers) importPackage(Packages.mindustry.world.draw) importPackage(Packages.mindustry.world.meta) -importPackage(Packages.mindustry.world.meta.values) importPackage(Packages.mindustry.world.modules) const PlayerIpUnbanEvent = Packages.mindustry.game.EventType.PlayerIpUnbanEvent const PlayerIpBanEvent = Packages.mindustry.game.EventType.PlayerIpBanEvent @@ -132,6 +141,8 @@ const PlayerBanEvent = Packages.mindustry.game.EventType.PlayerBanEvent const PlayerLeave = Packages.mindustry.game.EventType.PlayerLeave const PlayerConnect = Packages.mindustry.game.EventType.PlayerConnect const PlayerJoin = Packages.mindustry.game.EventType.PlayerJoin +const ConnectPacketEvent = Packages.mindustry.game.EventType.ConnectPacketEvent +const ConnectionEvent = Packages.mindustry.game.EventType.ConnectionEvent const UnitChangeEvent = Packages.mindustry.game.EventType.UnitChangeEvent const UnitUnloadEvent = Packages.mindustry.game.EventType.UnitUnloadEvent const UnitCreateEvent = Packages.mindustry.game.EventType.UnitCreateEvent @@ -145,6 +156,7 @@ const ResearchEvent = Packages.mindustry.game.EventType.ResearchEvent const UnlockEvent = Packages.mindustry.game.EventType.UnlockEvent const StateChangeEvent = Packages.mindustry.game.EventType.StateChangeEvent const TileChangeEvent = Packages.mindustry.game.EventType.TileChangeEvent +const TilePreChangeEvent = Packages.mindustry.game.EventType.TilePreChangeEvent const GameOverEvent = Packages.mindustry.game.EventType.GameOverEvent const UnitControlEvent = Packages.mindustry.game.EventType.UnitControlEvent const PickupEvent = Packages.mindustry.game.EventType.PickupEvent diff --git a/core/assets/shaders/atmosphere.frag b/core/assets/shaders/atmosphere.frag index f5f437f298..1c8c4966df 100644 --- a/core/assets/shaders/atmosphere.frag +++ b/core/assets/shaders/atmosphere.frag @@ -1,3 +1,5 @@ +#define HIGHP + const float PI = 3.14159265359; const float MAX = 10000.0; diff --git a/core/assets/shaders/blockbuild.frag b/core/assets/shaders/blockbuild.frag index fe877e873b..6009ea6e27 100644 --- a/core/assets/shaders/blockbuild.frag +++ b/core/assets/shaders/blockbuild.frag @@ -1,3 +1,5 @@ +#define HIGHP + uniform sampler2D u_texture; uniform vec2 u_texsize; diff --git a/core/assets/shaders/caustics.frag b/core/assets/shaders/caustics.frag new file mode 100644 index 0000000000..ddc4b80bbc --- /dev/null +++ b/core/assets/shaders/caustics.frag @@ -0,0 +1,24 @@ +#define HIGHP + +#define NSCALE 200.0 / 1.8 + +uniform sampler2D u_texture; +uniform sampler2D u_noise; + +uniform vec2 u_campos; +uniform vec2 u_resolution; +uniform float u_time; + +varying vec2 v_texCoords; + +void main(){ + vec2 c = v_texCoords.xy; + vec2 coords = vec2(c.x * u_resolution.x + u_campos.x, c.y * u_resolution.y + u_campos.y); + + float btime = u_time / 3400.0; + vec4 noise1 = texture2D(u_noise, (coords) / NSCALE + vec2(btime) * vec2(-0.9, 0.8)); + vec4 noise2 = texture2D(u_noise, (coords) / NSCALE + vec2(btime * 1.1) * vec2(0.8, -1.0)); + //vec4 noise3 = texture2D(u_noise, (coords) / (NSCALE * 2.0) + vec2(btime * 0.9) * vec2(0.8, 1.0)); + + gl_FragColor = vec4(vec3(min(noise1.r, noise2.r)), 0.2); +} diff --git a/core/assets/shaders/water.frag b/core/assets/shaders/water.frag index e7f4ee1016..51c56f26a1 100644 --- a/core/assets/shaders/water.frag +++ b/core/assets/shaders/water.frag @@ -14,7 +14,6 @@ const float mth = 7.0; void main(){ vec2 c = v_texCoords; - vec2 v = vec2(1.0/u_resolution.x, 1.0/u_resolution.y); vec2 coords = vec2(c.x / v.x + u_campos.x, c.y / v.y + u_campos.y); diff --git a/core/assets/sprites/clouds.png b/core/assets/sprites/clouds.png new file mode 100644 index 0000000000000000000000000000000000000000..93a8684689803c7b35871e52903f40b554721e8f GIT binary patch literal 9675 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 N002ovPDHLkV1kG-dc6Pu literal 0 HcmV?d00001 diff --git a/core/src/mindustry/ClientLauncher.java b/core/src/mindustry/ClientLauncher.java index f2ec9a1291..6042a38b19 100644 --- a/core/src/mindustry/ClientLauncher.java +++ b/core/src/mindustry/ClientLauncher.java @@ -47,12 +47,14 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform loadFileLogger(); platform = this; + maxTextureSize = Gl.getInt(Gl.maxTextureSize); beginTime = Time.millis(); //debug GL information Log.info("[GL] Version: @", graphics.getGLVersion()); - Log.info("[GL] Max texture size: @", Gl.getInt(Gl.maxTextureSize)); + Log.info("[GL] Max texture size: @", maxTextureSize); Log.info("[GL] Using @ context.", gl30 != null ? "OpenGL 3" : "OpenGL 2"); + if(maxTextureSize < 4096) Log.warn("[GL] Your maximum texture size is below the recommended minimum of 4096. This will cause severe performance issues."); Log.info("[JAVA] Version: @", System.getProperty("java.version")); Time.setDeltaProvider(() -> { @@ -81,11 +83,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform Fonts.loadDefaultFont(); //load fallback atlas if max texture size is below 4096 - assets.load(new AssetDescriptor<>(Gl.getInt(Gl.maxTextureSize) >= 4096 ? "sprites/sprites.atlas" : "sprites/fallback/sprites.atlas", TextureAtlas.class)).loaded = t -> { - atlas = (TextureAtlas)t; - Fonts.mergeFontAtlas(atlas); - }; - + assets.load(new AssetDescriptor<>(maxTextureSize >= 4096 ? "sprites/sprites.aatls" : "sprites/fallback/sprites.aatls", TextureAtlas.class)).loaded = t -> atlas = (TextureAtlas)t; assets.loadRun("maps", Map.class, () -> maps.loadPreviews()); Musics.load(); @@ -99,6 +97,9 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform content.createModContent(); }); + assets.load(mods); + assets.loadRun("mergeUI", PixmapPacker.class, () -> {}, () -> Fonts.mergeFontAtlas(atlas)); + add(logic = new Logic()); add(control = new Control()); add(renderer = new Renderer()); @@ -106,7 +107,6 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform add(netServer = new NetServer()); add(netClient = new NetClient()); - assets.load(mods); assets.load(schematics); assets.loadRun("contentinit", ContentLoader.class, () -> content.init(), () -> content.load()); @@ -150,6 +150,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform mods.eachClass(Mod::init); finished = true; Events.fire(new ClientLoadEvent()); + clientLoaded = true; super.resize(graphics.getWidth(), graphics.getHeight()); app.post(() -> app.post(() -> app.post(() -> app.post(() -> { super.resize(graphics.getWidth(), graphics.getHeight()); diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index d9a2632c42..2c263508cf 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -11,10 +11,13 @@ import arc.util.Log.*; import mindustry.ai.*; import mindustry.async.*; import mindustry.core.*; +import mindustry.ctype.*; +import mindustry.editor.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; +import mindustry.graphics.*; import mindustry.input.*; import mindustry.io.*; import mindustry.logic.*; @@ -23,6 +26,7 @@ import mindustry.maps.*; import mindustry.mod.*; import mindustry.net.Net; import mindustry.net.*; +import mindustry.service.*; import mindustry.world.*; import java.io.*; @@ -42,6 +46,10 @@ public class Vars implements Loadable{ public static boolean experimental = false; /** Name of current Steam player. */ public static String steamPlayerName = ""; + /** Default accessible content types used for player-selectable icons. */ + public static final ContentType[] defaultContentIcons = {ContentType.item, ContentType.liquid, ContentType.block}; + /** Wall darkness radius. */ + public static final int darkRadius = 4; /** Maximum extra padding around deployment schematics. */ public static final int maxLoadoutSchematicPad = 5; /** Maximum schematic size.*/ @@ -81,7 +89,7 @@ public class Vars implements Loadable{ /** displayed item size when ingame. */ public static final float itemSize = 5f; /** units outside of this bound will die instantly */ - public static final float finalWorldBounds = 500; + public static final float finalWorldBounds = 250; /** range for building */ public static final float buildingRange = 220f; /** range for moving items */ @@ -102,6 +110,8 @@ public class Vars implements Loadable{ public static final int tilesize = 8; /** size of one tile payload (^2) */ public static final float tilePayload = tilesize * tilesize; + /** icon sizes for UI */ + public static final float iconXLarge = 8*6f, iconLarge = 8*5f, iconMed = 8*4f, iconSmall = 8*3f; /** tile used in certain situations, instead of null */ public static Tile emptyTile; /** for map generator dialog */ @@ -131,6 +141,14 @@ public class Vars implements Loadable{ public static final int multicastPort = 20151; /** multicast group for discovery.*/ public static final String multicastGroup = "227.2.7.7"; + /** whether the graphical game client has loaded */ + public static boolean clientLoaded = false; + /** max GL texture size */ + public static int maxTextureSize = 2048; + /** Whether to show the core landing animation. */ + public static boolean showLandAnimation = true; + /** Whether to prompt the user to confirm exiting. */ + public static boolean confirmExit = true; /** if true, UI is not drawn */ public static boolean disableUI; /** if true, game is set up in mobile mode, even on desktop. used for debugging */ @@ -199,6 +217,8 @@ public class Vars implements Loadable{ public static AsyncCore asyncCore; public static BaseRegistry bases; public static GlobalConstants constants; + public static MapEditor editor; + public static GameService service = new GameService(); public static Universe universe; public static World world; @@ -243,6 +263,7 @@ public class Vars implements Loadable{ } Version.init(); + CacheLayer.init(); dataDirectory = settings.getDataDirectory(); screenshotDirectory = dataDirectory.child("screenshots/"); @@ -266,6 +287,7 @@ public class Vars implements Loadable{ universe = new Universe(); becontrol = new BeControl(); asyncCore = new AsyncCore(); + if(!headless) editor = new MapEditor(); maps = new Maps(); spawner = new WaveSpawner(); @@ -394,7 +416,7 @@ public class Vars implements Loadable{ Log.info("NOTE: external translation bundle has been loaded."); if(!headless){ - Time.run(10f, () -> ui.showInfo("Note: You have successfully loaded an external translation bundle.")); + Time.run(10f, () -> ui.showInfo("Note: You have successfully loaded an external translation bundle.\n[accent]" + handle.absolutePath())); } }catch(Throwable e){ //no external bundle found diff --git a/core/src/mindustry/ai/BaseAI.java b/core/src/mindustry/ai/BaseAI.java index 768b529759..bebbdc1e8c 100644 --- a/core/src/mindustry/ai/BaseAI.java +++ b/core/src/mindustry/ai/BaseAI.java @@ -16,6 +16,7 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.defense.*; import mindustry.world.blocks.distribution.*; +import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.production.*; import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -208,7 +209,7 @@ public class BaseAI{ } Tile wtile = world.tile(realX, realY); - if(tile.block instanceof PayloadConveyor || tile.block instanceof PayloadAcceptor){ + if(tile.block instanceof PayloadConveyor || tile.block instanceof PayloadBlock){ //near a building for(Point2 point : Edges.getEdges(tile.block.size)){ var t = world.build(tile.x + point.x, tile.y + point.y); @@ -288,7 +289,7 @@ public class BaseAI{ } Tile o = world.tile(tile.x + p.x, tile.y + p.y); - if(o != null && (o.block() instanceof PayloadAcceptor || o.block() instanceof PayloadConveyor)){ + if(o != null && (o.block() instanceof PayloadBlock || o.block() instanceof PayloadConveyor)){ continue outer; } diff --git a/core/src/mindustry/ai/BlockIndexer.java b/core/src/mindustry/ai/BlockIndexer.java index ca945b67a9..6a232f426c 100644 --- a/core/src/mindustry/ai/BlockIndexer.java +++ b/core/src/mindustry/ai/BlockIndexer.java @@ -4,11 +4,9 @@ import arc.*; import arc.func.*; import arc.math.*; import arc.math.geom.*; -import arc.struct.EnumSet; import arc.struct.*; import arc.util.*; import mindustry.content.*; -import mindustry.core.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; @@ -25,16 +23,16 @@ import static mindustry.Vars.*; /** Class used for indexing special target blocks for AI. */ public class BlockIndexer{ /** Size of one quadrant. */ - private static final int quadrantSize = 16; + private static final int quadrantSize = 20; + private static final Rect rect = new Rect(); + private static boolean returnBool = false; - /** Set of all ores that are being scanned. */ - private final ObjectSet scanOres = new ObjectSet<>(); private final IntSet intSet = new IntSet(); - private final ObjectSet itemSet = new ObjectSet<>(); - /** Stores all ore quadtrants on the map. */ - private ObjectMap ores = new ObjectMap<>(); - /** Maps each team ID to a quarant. A quadrant is a grid of bits, where each bit is set if and only if there is a block of that team in that quadrant. */ - private GridBits[] structQuadrants; + + private int quadWidth, quadHeight; + + /** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */ + private IntSeq[][][] ores; /** Stores all damaged tile entities by team. */ private ObjectSet[] damagedTiles = new ObjectSet[Team.all.length]; /** All ores available on this map. */ @@ -43,28 +41,27 @@ public class BlockIndexer{ private Seq activeTeams = new Seq<>(Team.class); /** Maps teams to a map of flagged tiles by flag. */ private TileArray[][] flagMap = new TileArray[Team.all.length][BlockFlag.all.length]; - /** Max units by team. */ - private int[] unitCaps = new int[Team.all.length]; - /** Maps tile positions to their last known tile index data. */ - private IntMap typeMap = new IntMap<>(); + /** Empty set used for returning. */ private TileArray emptySet = new TileArray(); /** Array used for returning and reusing. */ private Seq returnArray = new Seq<>(); /** Array used for returning and reusing. */ - private Seq breturnArray = new Seq<>(); + private Seq breturnArray = new Seq<>(Building.class); public BlockIndexer(){ + + Events.on(TilePreChangeEvent.class, event -> { + removeIndex(event.tile); + }); + Events.on(TileChangeEvent.class, event -> { - updateIndices(event.tile); + addIndex(event.tile); }); Events.on(WorldLoadEvent.class, event -> { - scanOres.clear(); - scanOres.addAll(Item.getAllOres()); damagedTiles = new ObjectSet[Team.all.length]; flagMap = new TileArray[Team.all.length][BlockFlag.all.length]; - unitCaps = new int[Team.all.length]; activeTeams = new Seq<>(Team.class); for(int i = 0; i < flagMap.length; i++){ @@ -73,12 +70,10 @@ public class BlockIndexer{ } } - typeMap.clear(); allOres.clear(); - ores = null; - - //create bitset for each team type that contains each quadrant - structQuadrants = new GridBits[Team.all.length]; + ores = new IntSeq[content.items().size][][]; + quadWidth = Mathf.ceil(world.width() / (float)quadrantSize); + quadHeight = Mathf.ceil(world.height() / (float)quadrantSize); for(Tile tile : world.tiles){ process(tile); @@ -87,59 +82,83 @@ public class BlockIndexer{ notifyTileDamaged(tile.build); } - if(tile.drop() != null) allOres.add(tile.drop()); - } + var drop = tile.drop(); - for(int x = 0; x < quadWidth(); x++){ - for(int y = 0; y < quadHeight(); y++){ - updateQuadrant(world.tile(x * quadrantSize, y * quadrantSize)); + if(drop != null){ + allOres.add(drop); + + int qx = (tile.x / quadrantSize); + int qy = (tile.y / quadrantSize); + + //add position of quadrant to list + if(tile.block() == Blocks.air){ + if(ores[drop.id] == null){ + ores[drop.id] = new IntSeq[quadWidth][quadHeight]; + } + if(ores[drop.id][qx][qy] == null){ + ores[drop.id][qx][qy] = new IntSeq(false, 16); + } + ores[drop.id][qx][qy].add(tile.pos()); + } } } - - scanOres(); }); } - public void updateIndices(Tile tile){ - if(typeMap.get(tile.pos()) != null){ - TileIndex index = typeMap.get(tile.pos()); - for(BlockFlag flag : index.flags){ - getFlagged(index.team)[flag.ordinal()].remove(tile); + public void removeIndex(Tile tile){ + var team = tile.team(); + if(team != Team.derelict && tile.isCenter()){ + var flags = tile.block().flags; + var data = team.data(); + + if(flags.size() > 0){ + for(BlockFlag flag : flags){ + getFlagged(team)[flag.ordinal()].remove(tile); + } } - if(index.flags.contains(BlockFlag.unitModifier)){ - updateCap(index.team); + //update the unit cap when building is remove + data.unitCap -= tile.block().unitCapModifier; + + //unregister building from building quadtree + if(data.buildings != null){ + data.buildings.remove(tile.build); } } + } + + public void addIndex(Tile tile){ process(tile); - updateQuadrant(tile); + + var drop = tile.drop(); + if(drop != null){ + int qx = tile.x / quadrantSize; + int qy = tile.y / quadrantSize; + + if(ores[drop.id] == null){ + ores[drop.id] = new IntSeq[quadWidth][quadHeight]; + } + if(ores[drop.id][qx][qy] == null){ + ores[drop.id][qx][qy] = new IntSeq(false, 16); + } + + int pos = tile.pos(); + var seq = ores[drop.id][qx][qy]; + + //when the drop can be mined, record the ore position + if(tile.block() == Blocks.air && !seq.contains(pos)){ + seq.add(pos); + }else{ + //otherwise, it likely became blocked, remove it (even if it wasn't there) + seq.removeValue(pos); + } + } } private TileArray[] getFlagged(Team team){ return flagMap[team.id]; } - private GridBits structQuadrant(Team t){ - if(structQuadrants[t.id] == null){ - structQuadrants[t.id] = new GridBits(Mathf.ceil(world.width() / (float)quadrantSize), Mathf.ceil(world.height() / (float)quadrantSize)); - } - return structQuadrants[t.id]; - } - - /** Updates all the structure quadrants for a newly activated team. */ - public void updateTeamIndex(Team team){ - if(structQuadrants == null) return; - - //go through every tile... ouch - for(Tile tile : world.tiles){ - if(tile.team() == team){ - int quadrantX = tile.x / quadrantSize; - int quadrantY = tile.y / quadrantSize; - structQuadrant(team).set(quadrantX, quadrantY); - } - } - } - /** @return whether this item is present on this map. */ public boolean hasOre(Item item){ return allOres.contains(item); @@ -181,31 +200,28 @@ public class BlockIndexer{ return eachBlock(team.team(), team.getX(), team.getY(), range, pred, cons); } - public boolean eachBlock(Team team, float wx, float wy, float range, Boolf pred, Cons cons){ - intSet.clear(); + public boolean eachBlock(@Nullable Team team, float wx, float wy, float range, Boolf pred, Cons cons){ + returnBool = false; - int tx = World.toTile(wx); - int ty = World.toTile(wy); - - int tileRange = (int)(range / tilesize + 1); - boolean any = false; - - for(int x = -tileRange + tx; x <= tileRange + tx; x++){ - for(int y = -tileRange + ty; y <= tileRange + ty; y++){ - if(!Mathf.within(x * tilesize, y * tilesize, wx, wy, range)) continue; - - Building other = world.build(x, y); - - if(other == null) continue; - - if((team == null || other.team == team) && pred.get(other) && intSet.add(other.pos())){ - cons.get(other); - any = true; + if(team == null){ + allBuildings(wx, wy, range, b -> { + if(pred.get(b)){ + returnBool = true; + cons.get(b); } - } + }); + }else{ + var buildings = team.data().buildings; + if(buildings == null) return false; + buildings.intersect(wx - range, wy - range, range*2f, range*2f, b -> { + if(b.within(wx, wy, range + b.hitSize() / 2f) && pred.get(b)){ + returnBool = true; + cons.get(b); + } + }); } - return any; + return returnBool; } /** Get all enemy blocks with a flag. */ @@ -247,7 +263,20 @@ public class BlockIndexer{ damagedTiles[entity.team.id].add(entity); } - public Building findEnemyTile(Team team, float x, float y, float range, Boolf pred){ + public void allBuildings(float x, float y, float range, Cons cons){ + for(int i = 0; i < activeTeams.size; i++){ + Team team = activeTeams.items[i]; + var buildings = team.data().buildings; + if(buildings == null) continue; + buildings.intersect(x - range, y - range, range*2f, range*2f, b -> { + if(b.within(x, y, range + b.hitSize()/2f)){ + cons.get(b); + } + }); + } + } + + public Building findEnemyTile(@Nullable Team team, float x, float y, float range, Boolf pred){ for(int i = 0; i < activeTeams.size; i++){ Team enemy = activeTeams.items[i]; @@ -269,58 +298,50 @@ public class BlockIndexer{ public Building findTile(Team team, float x, float y, float range, Boolf pred, boolean usePriority){ Building closest = null; float dst = 0; + var buildings = team.data().buildings; + if(buildings == null) return null; - for(int rx = Math.max((int)((x - range) / tilesize / quadrantSize), 0); rx <= (int)((x + range) / tilesize / quadrantSize) && rx < quadWidth(); rx++){ - for(int ry = Math.max((int)((y - range) / tilesize / quadrantSize), 0); ry <= (int)((y + range) / tilesize / quadrantSize) && ry < quadHeight(); ry++){ + breturnArray.clear(); + buildings.intersect(rect.setCentered(x, y, range * 2f), breturnArray); - if(!getQuad(team, rx, ry)) continue; + for(int i = 0; i < breturnArray.size; i++){ + var next = breturnArray.items[i]; - for(int tx = rx * quadrantSize; tx < (rx + 1) * quadrantSize && tx < world.width(); tx++){ - for(int ty = ry * quadrantSize; ty < (ry + 1) * quadrantSize && ty < world.height(); ty++){ - Building e = world.build(tx, ty); + if(!pred.get(next) || !next.block.targetable) continue; - if(e == null || e.team != team || !pred.get(e) || !e.block.targetable || e.team == Team.derelict) continue; - - float bdst = e.dst(x, y) - e.hitSize() / 2f; - if(bdst < range && (closest == null || - //this one is closer, and it is at least of equal priority - (bdst < dst && (!usePriority || closest.block.priority.ordinal() <= e.block.priority.ordinal())) || - //priority is used, and new block has higher priority regardless of range - (usePriority && closest.block.priority.ordinal() < e.block.priority.ordinal()))){ - dst = bdst; - closest = e; - } - } - } + float bdst = next.dst(x, y) - next.hitSize() / 2f; + if(bdst < range && (closest == null || + //this one is closer, and it is at least of equal priority + (bdst < dst && (!usePriority || closest.block.priority.ordinal() <= next.block.priority.ordinal())) || + //priority is used, and new block has higher priority regardless of range + (usePriority && closest.block.priority.ordinal() < next.block.priority.ordinal()))){ + dst = bdst; + closest = next; } } return closest; } - /** - * Returns a set of tiles that have ores of the specified type nearby. - * While each tile in the set is not guaranteed to have an ore directly on it, - * each tile will at least have an ore within {@link #quadrantSize} / 2 blocks of it. - * Only specific ore types are scanned. See {@link #scanOres}. - */ - public TileArray getOrePositions(Item item){ - return ores.get(item, emptySet); - } - /** Find the closest ore block relative to a position. */ public Tile findClosestOre(float xp, float yp, Item item){ - Tile tile = Geometry.findClosest(xp, yp, getOrePositions(item)); - - if(tile == null) return null; - - for(int x = Math.max(0, tile.x - quadrantSize / 2); x < tile.x + quadrantSize / 2 && x < world.width(); x++){ - for(int y = Math.max(0, tile.y - quadrantSize / 2); y < tile.y + quadrantSize / 2 && y < world.height(); y++){ - Tile res = world.tile(x, y); - if(res.block() == Blocks.air && res.drop() == item){ - return res; + if(ores[item.id] != null){ + float minDst = 0f; + Tile closest = null; + for(int qx = 0; qx < quadWidth; qx++){ + for(int qy = 0; qy < quadHeight; qy++){ + var arr = ores[item.id][qx][qy]; + if(arr != null && arr.size > 0){ + Tile tile = world.tile(arr.first()); + float dst = Mathf.dst2(xp, yp, tile.worldx(), tile.worldy()); + if(closest == null || dst < minDst){ + closest = tile; + minDst = dst; + } + } } } + return closest; } return null; @@ -331,147 +352,36 @@ public class BlockIndexer{ return findClosestOre(unit.x, unit.y, item); } - /** @return extra unit cap of a team. This is added onto the base value. */ - public int getExtraUnits(Team team){ - return unitCaps[team.id]; - } - - private void updateCap(Team team){ - TileArray capped = getFlagged(team)[BlockFlag.unitModifier.ordinal()]; - unitCaps[team.id] = 0; - for(Tile capper : capped){ - unitCaps[team.id] += capper.block().unitCapModifier; - } - } - private void process(Tile tile){ - if(tile.block().flags.size() > 0 && tile.team() != Team.derelict && tile.isCenter()){ - TileArray[] map = getFlagged(tile.team()); + var team = tile.team(); + //only process entity changes with centered tiles + if(tile.isCenter() && team != Team.derelict){ + var data = team.data(); + if(tile.block().flags.size() > 0 && tile.isCenter()){ + TileArray[] map = getFlagged(team); - for(BlockFlag flag : tile.block().flags){ + for(BlockFlag flag : tile.block().flags){ - TileArray arr = map[flag.ordinal()]; + TileArray arr = map[flag.ordinal()]; - arr.add(tile); + arr.add(tile); - map[flag.ordinal()] = arr; - } - - if(tile.block().flags.contains(BlockFlag.unitModifier)){ - updateCap(tile.team()); - } - - typeMap.put(tile.pos(), new TileIndex(tile.block().flags, tile.team())); - } - - if(!activeTeams.contains(tile.team())){ - activeTeams.add(tile.team()); - } - - if(ores == null) return; - - int quadrantX = tile.x / quadrantSize; - int quadrantY = tile.y / quadrantSize; - itemSet.clear(); - - Tile rounded = world.rawTile(Mathf.clamp(quadrantX * quadrantSize + quadrantSize / 2, 0, world.width() - 1), Mathf.clamp(quadrantY * quadrantSize + quadrantSize / 2, 0, world.height() - 1)); - - //find all items that this quadrant contains - for(int x = Math.max(0, rounded.x - quadrantSize / 2); x < rounded.x + quadrantSize / 2 && x < world.width(); x++){ - for(int y = Math.max(0, rounded.y - quadrantSize / 2); y < rounded.y + quadrantSize / 2 && y < world.height(); y++){ - Tile result = world.tile(x, y); - if(result == null || result.drop() == null || !scanOres.contains(result.drop()) || result.block() != Blocks.air) continue; - - itemSet.add(result.drop()); - } - } - - //update quadrant at this position - for(Item item : scanOres){ - TileArray set = ores.get(item); - - //update quadrant status depending on whether the item is in it - if(!itemSet.contains(item)){ - set.remove(rounded); - }else{ - set.add(rounded); - } - } - } - - private void updateQuadrant(Tile tile){ - if(structQuadrants == null) return; - - //this quadrant is now 'dirty', re-scan the whole thing - int quadrantX = tile.x / quadrantSize; - int quadrantY = tile.y / quadrantSize; - - for(Team team : activeTeams){ - GridBits bits = structQuadrant(team); - - //fast-set this quadrant to 'occupied' if the tile just placed is already of this team - if(tile.team() == team && tile.build != null && tile.block().targetable){ - bits.set(quadrantX, quadrantY); - continue; //no need to process futher - } - - bits.set(quadrantX, quadrantY, false); - - outer: - for(int x = quadrantX * quadrantSize; x < world.width() && x < (quadrantX + 1) * quadrantSize; x++){ - for(int y = quadrantY * quadrantSize; y < world.height() && y < (quadrantY + 1) * quadrantSize; y++){ - Building result = world.build(x, y); - //when a targetable block is found, mark this quadrant as occupied and stop searching - if(result != null && result.team == team){ - bits.set(quadrantX, quadrantY); - break outer; - } + map[flag.ordinal()] = arr; } } - } - } - private boolean getQuad(Team team, int quadrantX, int quadrantY){ - return structQuadrant(team).get(quadrantX, quadrantY); - } + //update the unit cap when new tile is registered + data.unitCap += tile.block().unitCapModifier; - private int quadWidth(){ - return Mathf.ceil(world.width() / (float)quadrantSize); - } - - private int quadHeight(){ - return Mathf.ceil(world.height() / (float)quadrantSize); - } - - private void scanOres(){ - ores = new ObjectMap<>(); - - //initialize ore map with empty sets - for(Item item : scanOres){ - ores.put(item, new TileArray()); - } - - for(Tile tile : world.tiles){ - int qx = (tile.x / quadrantSize); - int qy = (tile.y / quadrantSize); - - //add position of quadrant to list when an ore is found - if(tile.drop() != null && scanOres.contains(tile.drop()) && tile.block() == Blocks.air){ - ores.get(tile.drop()).add(world.tile( - //make sure to clamp quadrant middle position, since it might go off bounds - Mathf.clamp(qx * quadrantSize + quadrantSize / 2, 0, world.width() - 1), - Mathf.clamp(qy * quadrantSize + quadrantSize / 2, 0, world.height() - 1))); + if(!activeTeams.contains(team)){ + activeTeams.add(team); } - } - } - private static class TileIndex{ - public final EnumSet flags; - public final Team team; - - public TileIndex(EnumSet flags, Team team){ - this.flags = flags; - this.team = team; + //insert the new tile into the quadtree for targeting + if(data.buildings == null){ + data.buildings = new QuadTree<>(new Rect(0, 0, world.unitWidth(), world.unitHeight())); + } + data.buildings.insert(tile.build); } } diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index 40a008c6c7..5a3c5b00f8 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -70,13 +70,15 @@ public class BuilderAI extends AIController{ for(Player player : Groups.player){ if(player.isBuilder() && player.unit().activelyBuilding() && player.unit().buildPlan().samePos(req) && player.unit().buildPlan().breaking){ unit.plans.removeFirst(); + //remove from list of plans + unit.team.data().blocks.remove(p -> p.x == req.x && p.y == req.y); return; } } } boolean valid = - (req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.cblock == req.block) || + (req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.current == req.block) || (req.breaking ? Build.validBreak(unit.team(), req.x, req.y) : Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)); diff --git a/core/src/mindustry/ai/types/FormationAI.java b/core/src/mindustry/ai/types/FormationAI.java index 36d6c5169f..a08d9bb95c 100644 --- a/core/src/mindustry/ai/types/FormationAI.java +++ b/core/src/mindustry/ai/types/FormationAI.java @@ -33,7 +33,12 @@ public class FormationAI extends AIController implements FormationMember{ } if(unit.type.canBoost){ - unit.elevation = Mathf.approachDelta(unit.elevation, unit.onSolid() ? 1f : leader.type.canBoost ? leader.elevation : 0f, unit.type.riseSpeed); + unit.elevation = Mathf.approachDelta(unit.elevation, + unit.onSolid() ? 1f : //definitely cannot land + unit.isFlying() && !unit.canLand() ? unit.elevation : //try to maintain altitude + leader.type.canBoost ? leader.elevation : //follow leader + 0f, + unit.type.riseSpeed); } unit.controlWeapons(true, leader.isShooting); @@ -85,7 +90,7 @@ public class FormationAI extends AIController implements FormationMember{ @Override public float formationSize(){ - return unit.hitSize * 1.1f; + return unit.hitSize * 1.3f; } @Override diff --git a/core/src/mindustry/ai/types/GroundAI.java b/core/src/mindustry/ai/types/GroundAI.java index 5a9db8db6a..4ebdfe1595 100644 --- a/core/src/mindustry/ai/types/GroundAI.java +++ b/core/src/mindustry/ai/types/GroundAI.java @@ -8,8 +8,6 @@ import mindustry.gen.*; import mindustry.world.*; import mindustry.world.meta.*; -import java.util.*; - import static mindustry.Vars.*; public class GroundAI extends AIController{ @@ -19,16 +17,16 @@ public class GroundAI extends AIController{ Building core = unit.closestEnemyCore(); - if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){ + if(core != null && unit.within(core, unit.range() / 1.3f + core.block.size * tilesize / 2f)){ target = core; - for(int i = 0; i < targets.length; i++){ - if(unit.mounts[i].weapon.bullet.collidesGround){ - targets[i] = core; + for(var mount : unit.mounts){ + if(mount.weapon.controllable && mount.weapon.bullet.collidesGround){ + mount.target = core; } } } - if((core == null || !unit.within(core, unit.range() * 0.5f)) && command() == UnitCommand.attack){ + if((core == null || !unit.within(core, unit.type.range * 0.5f)) && command() == UnitCommand.attack){ boolean move = true; if(state.rules.waves && unit.team == state.rules.defaultTeam){ @@ -47,7 +45,7 @@ public class GroundAI extends AIController{ } } - if(unit.type.canBoost && !unit.onSolid()){ + if(unit.type.canBoost && unit.elevation > 0.001f && !unit.onSolid()){ unit.elevation = Mathf.approachDelta(unit.elevation, 0f, unit.type.riseSpeed); } diff --git a/core/src/mindustry/ai/types/HugAI.java b/core/src/mindustry/ai/types/HugAI.java new file mode 100644 index 0000000000..76acdec8c8 --- /dev/null +++ b/core/src/mindustry/ai/types/HugAI.java @@ -0,0 +1,81 @@ +package mindustry.ai.types; + +import arc.math.*; +import arc.math.geom.*; +import mindustry.*; +import mindustry.ai.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.world.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +public class HugAI extends AIController{ + + @Override + public void updateMovement(){ + + Building core = unit.closestEnemyCore(); + + if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){ + target = core; + for(var mount : unit.mounts){ + if(mount.weapon.controllable && mount.weapon.bullet.collidesGround){ + mount.target = core; + } + } + } + + if(command() == UnitCommand.attack){ + boolean move = true; + + if(state.rules.waves && unit.team == state.rules.defaultTeam){ + Tile spawner = getClosestSpawner(); + if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)) move = false; + } + + //raycast for target + if(target != null && unit.within(target, unit.type.range) && !Vars.world.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> { + for(Point2 p : Geometry.d4c){ + if(!unit.canPass(x + p.x, y + p.y)){ + return true; + } + } + return false; + })){ + if(unit.within(target, (unit.hitSize + (target instanceof Sized s ? s.hitSize() : 1f)) * 0.6f)){ + //circle target + unit.moveAt(vec.set(target).sub(unit).rotate(90f).setLength(unit.speed())); + }else{ + //move toward target in a straight line + unit.moveAt(vec.set(target).sub(unit).limit(unit.speed())); + } + }else if(move){ + pathfind(Pathfinder.fieldCore); + } + } + + if(command() == UnitCommand.rally){ + Teamc target = targetFlag(unit.x, unit.y, BlockFlag.rally, false); + + if(target != null && !unit.within(target, 70f)){ + pathfind(Pathfinder.fieldRally); + } + } + + if(unit.type.canBoost && unit.elevation > 0.001f && !unit.onSolid()){ + unit.elevation = Mathf.approachDelta(unit.elevation, 0f, unit.type.riseSpeed); + } + + if(!Units.invalidateTarget(target, unit, unit.range()) && unit.type.rotateShooting){ + if(unit.type.hasWeapons()){ + unit.lookAt(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed)); + } + }else if(unit.moving()){ + unit.lookAt(unit.vel().angle()); + } + + } +} diff --git a/core/src/mindustry/ai/types/LogicAI.java b/core/src/mindustry/ai/types/LogicAI.java index abd76201e6..8c1128c7e0 100644 --- a/core/src/mindustry/ai/types/LogicAI.java +++ b/core/src/mindustry/ai/types/LogicAI.java @@ -98,7 +98,7 @@ public class LogicAI extends AIController{ } if(unit.type.canBoost && !unit.type.flying){ - unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid()), 0.08f); + unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid() || (unit.isFlying() && !unit.canLand())), unit.type.riseSpeed); } //look where moving if there's nothing to aim at diff --git a/core/src/mindustry/ai/types/MinerAI.java b/core/src/mindustry/ai/types/MinerAI.java index fbf7cf5526..5e80c52779 100644 --- a/core/src/mindustry/ai/types/MinerAI.java +++ b/core/src/mindustry/ai/types/MinerAI.java @@ -31,7 +31,7 @@ public class MinerAI extends AIController{ //core full of the target item, do nothing if(targetItem != null && core.acceptStack(targetItem, 1, unit) == 0){ unit.clearItem(); - unit.mineTile(null); + unit.mineTile =null; return; } @@ -46,7 +46,7 @@ public class MinerAI extends AIController{ if(ore != null){ moveTo(ore, unit.type.miningRange / 2f, 20f); - if(unit.within(ore, unit.type.miningRange)){ + if(ore.block() == Blocks.air && unit.within(ore, unit.type.miningRange)){ unit.mineTile = ore; } diff --git a/core/src/mindustry/ai/types/SuicideAI.java b/core/src/mindustry/ai/types/SuicideAI.java index e4c1461227..8f2a04c2dc 100644 --- a/core/src/mindustry/ai/types/SuicideAI.java +++ b/core/src/mindustry/ai/types/SuicideAI.java @@ -9,8 +9,11 @@ import mindustry.gen.*; import mindustry.world.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.liquid.*; +import mindustry.world.blocks.storage.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + public class SuicideAI extends GroundAI{ static boolean blockedByBlock; @@ -29,6 +32,10 @@ public class SuicideAI extends GroundAI{ boolean rotate = false, shoot = false, moveToTarget = false; + if(target == null){ + target = core; + } + if(!Units.invalidateTarget(target, unit, unit.range()) && unit.hasWeapons()){ rotate = true; shoot = unit.within(target, unit.type.weapons.first().bullet.range() + @@ -39,7 +46,7 @@ public class SuicideAI extends GroundAI{ } //do not move toward walls or transport blocks - if(!(target instanceof Building build && ( + if(!(target instanceof Building build && !(build.block instanceof CoreBlock) && ( build.block.group == BlockGroup.walls || build.block.group == BlockGroup.liquids || build.block.group == BlockGroup.transportation @@ -81,8 +88,20 @@ public class SuicideAI extends GroundAI{ if(target != null && !unit.within(target, 70f)){ pathfind(Pathfinder.fieldRally); } - }else if(command() == UnitCommand.attack && core != null){ - pathfind(Pathfinder.fieldCore); + }else if(command() == UnitCommand.attack){ + boolean move = true; + + //stop moving toward the drop zone if applicable + if(core == null && state.rules.waves && unit.team == state.rules.defaultTeam){ + Tile spawner = getClosestSpawner(); + if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)){ + move = false; + } + } + + if(move){ + pathfind(Pathfinder.fieldCore); + } } if(unit.moving()) unit.lookAt(unit.vel().angle()); diff --git a/core/src/mindustry/async/AsyncCore.java b/core/src/mindustry/async/AsyncCore.java index a9d4cdd90d..88272feb95 100644 --- a/core/src/mindustry/async/AsyncCore.java +++ b/core/src/mindustry/async/AsyncCore.java @@ -10,19 +10,14 @@ import static mindustry.Vars.*; public class AsyncCore{ //all processes to be executed each frame - private final Seq processes = Seq.with( + public final Seq processes = Seq.with( new PhysicsProcess() ); //futures to be awaited private final Seq> futures = new Seq<>(); - private final ExecutorService executor = Executors.newFixedThreadPool(processes.size, r -> { - Thread thread = new Thread(r, "AsyncLogic-Thread"); - thread.setDaemon(true); - thread.setUncaughtExceptionHandler((t, e) -> Core.app.post(() -> { throw new RuntimeException(e); })); - return thread; - }); + private ExecutorService executor; public AsyncCore(){ Events.on(WorldLoadEvent.class, e -> { @@ -49,6 +44,16 @@ public class AsyncCore{ futures.clear(); + //init executor with size of potentially-modified process list + if(executor == null){ + executor = Executors.newFixedThreadPool(processes.size, r -> { + Thread thread = new Thread(r, "AsyncLogic-Thread"); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler((t, e) -> Core.app.post(() -> { throw new RuntimeException(e); })); + return thread; + }); + } + //submit all tasks for(AsyncProcess p : processes){ if(p.shouldProcess()){ diff --git a/core/src/mindustry/audio/SoundControl.java b/core/src/mindustry/audio/SoundControl.java index c7d61df9b9..a5c34814bb 100644 --- a/core/src/mindustry/audio/SoundControl.java +++ b/core/src/mindustry/audio/SoundControl.java @@ -134,6 +134,8 @@ public class SoundControl{ } } + Core.audio.setPaused(Core.audio.soundBus.id, state.isPaused()); + if(state.isMenu()){ silenced = false; if(ui.planet.isShown()){ diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index 4f8bb350af..0f1a658539 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -20,6 +20,7 @@ import mindustry.world.blocks.experimental.*; import mindustry.world.blocks.legacy.*; import mindustry.world.blocks.liquid.*; import mindustry.world.blocks.logic.*; +import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.power.*; import mindustry.world.blocks.production.*; import mindustry.world.blocks.sandbox.*; @@ -36,7 +37,8 @@ public class Blocks implements ContentList{ //environment air, spawn, cliff, deepwater, water, taintedWater, tar, slag, stone, craters, charr, sand, darksand, dirt, mud, ice, snow, darksandTaintedWater, space, - dacite, stoneWall, dirtWall, sporeWall, iceWall, daciteWall, sporePine, snowPine, pine, shrubs, whiteTree, whiteTreeDead, sporeCluster, + dacite, + stoneWall, dirtWall, sporeWall, iceWall, daciteWall, sporePine, snowPine, pine, shrubs, whiteTree, whiteTreeDead, sporeCluster, iceSnow, sandWater, darksandWater, duneWall, sandWall, moss, sporeMoss, shale, shaleWall, shaleBoulder, sandBoulder, daciteBoulder, boulder, snowBoulder, basaltBoulder, grass, salt, metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor5, basalt, magmarock, hotrock, snowWall, saltWall, darkPanel1, darkPanel2, darkPanel3, darkPanel4, darkPanel5, darkPanel6, darkMetal, @@ -50,7 +52,7 @@ public class Blocks implements ContentList{ melter, separator, disassembler, sporePress, pulverizer, incinerator, coalCentrifuge, //sandbox - powerSource, powerVoid, itemSource, itemVoid, liquidSource, liquidVoid, illuminator, + powerSource, powerVoid, itemSource, itemVoid, liquidSource, liquidVoid, payloadVoid, payloadSource, illuminator, //defense copperWall, copperWallLarge, titaniumWall, titaniumWallLarge, plastaniumWall, plastaniumWallLarge, thoriumWall, thoriumWallLarge, door, doorLarge, @@ -59,7 +61,8 @@ public class Blocks implements ContentList{ //transport conveyor, titaniumConveyor, plastaniumConveyor, armoredConveyor, distributor, junction, itemBridge, phaseConveyor, sorter, invertedSorter, router, - overflowGate, underflowGate, massDriver, payloadConveyor, payloadRouter, + overflowGate, underflowGate, massDriver, + duct, ductRouter, ductBridge, //liquid mechanicalPump, rotaryPump, thermalPump, conduit, pulseConduit, platedConduit, liquidRouter, liquidTank, liquidJunction, bridgeConduit, phaseConduit, @@ -81,16 +84,20 @@ public class Blocks implements ContentList{ commandCenter, groundFactory, airFactory, navalFactory, additiveReconstructor, multiplicativeReconstructor, exponentialReconstructor, tetrativeReconstructor, - repairPoint, resupplyPoint, + repairPoint, repairTurret, resupplyPoint, + + //payloads + payloadConveyor, payloadRouter, payloadPropulsionTower, //logic message, switchBlock, microProcessor, logicProcessor, hyperProcessor, largeLogicDisplay, logicDisplay, memoryCell, memoryBank, //campaign - launchPad, launchPadLarge, interplanetaryAccelerator, + launchPad, interplanetaryAccelerator, //misc experimental - blockForge, blockLoader, blockUnloader; + blockForge, blockLoader, blockUnloader + ; @Override public void load(){ @@ -361,32 +368,37 @@ public class Blocks implements ContentList{ whiteTree = new TreeBlock("white-tree"); - sporeCluster = new Boulder("spore-cluster"){{ + sporeCluster = new Prop("spore-cluster"){{ variants = 3; }}; - boulder = new Boulder("boulder"){{ + //glowBlob = new Prop("glowblob"){{ + // variants = 1; + //}}; + + boulder = new Prop("boulder"){{ variants = 2; + stone.asFloor().decoration = this; }}; - snowBoulder = new Boulder("snow-boulder"){{ + snowBoulder = new Prop("snow-boulder"){{ variants = 2; snow.asFloor().decoration = ice.asFloor().decoration = iceSnow.asFloor().decoration = salt.asFloor().decoration = this; }}; - shaleBoulder = new Boulder("shale-boulder"){{ + shaleBoulder = new Prop("shale-boulder"){{ variants = 2; }}; - sandBoulder = new Boulder("sand-boulder"){{ + sandBoulder = new Prop("sand-boulder"){{ variants = 2; }}; - daciteBoulder = new Boulder("dacite-boulder"){{ + daciteBoulder = new Prop("dacite-boulder"){{ variants = 2; }}; - basaltBoulder = new Boulder("basalt-boulder"){{ + basaltBoulder = new Prop("basalt-boulder"){{ variants = 2; }}; @@ -491,6 +503,7 @@ public class Blocks implements ContentList{ craftEffect = Fx.pulverizeMedium; outputItem = new ItemStack(Items.graphite, 2); craftTime = 30f; + itemCapacity = 20; size = 3; hasItems = true; hasLiquids = true; @@ -501,7 +514,7 @@ public class Blocks implements ContentList{ consumes.liquid(Liquids.water, 0.1f); }}; - siliconSmelter = new GenericSmelter("silicon-smelter"){{ + siliconSmelter = new GenericCrafter("silicon-smelter"){{ requirements(Category.crafting, with(Items.copper, 30, Items.lead, 25)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.silicon, 1); @@ -509,13 +522,13 @@ public class Blocks implements ContentList{ size = 2; hasPower = true; hasLiquids = false; - flameColor = Color.valueOf("ffef99"); + drawer = new DrawSmelter(Color.valueOf("ffef99")); consumes.items(with(Items.coal, 1, Items.sand, 2)); consumes.power(0.50f); }}; - siliconCrucible = new AttributeSmelter("silicon-crucible"){{ + siliconCrucible = new AttributeCrafter("silicon-crucible"){{ requirements(Category.crafting, with(Items.titanium, 120, Items.metaglass, 80, Items.plastanium, 35, Items.silicon, 60)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.silicon, 8); @@ -523,22 +536,22 @@ public class Blocks implements ContentList{ size = 3; hasPower = true; hasLiquids = false; - flameColor = Color.valueOf("ffef99"); itemCapacity = 30; boostScale = 0.15f; + drawer = new DrawSmelter(Color.valueOf("ffef99")); consumes.items(with(Items.coal, 4, Items.sand, 6, Items.pyratite, 1)); consumes.power(4f); }}; - kiln = new GenericSmelter("kiln"){{ + kiln = new GenericCrafter("kiln"){{ requirements(Category.crafting, with(Items.copper, 60, Items.graphite, 30, Items.lead, 30)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.metaglass, 1); craftTime = 30f; size = 2; hasPower = hasItems = true; - flameColor = Color.valueOf("ffc099"); + drawer = new DrawSmelter(Color.valueOf("ffc099")); consumes.items(with(Items.lead, 1, Items.sand, 1)); consumes.power(0.60f); @@ -579,7 +592,7 @@ public class Blocks implements ContentList{ itemCapacity = 20; }}; - surgeSmelter = new GenericSmelter("alloy-smelter"){{ + surgeSmelter = new GenericCrafter("alloy-smelter"){{ requirements(Category.crafting, with(Items.silicon, 80, Items.lead, 80, Items.thorium, 70)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.surgeAlloy, 1); @@ -587,6 +600,7 @@ public class Blocks implements ContentList{ size = 3; hasPower = true; itemCapacity = 20; + drawer = new DrawSmelter(); consumes.power(4f); consumes.items(with(Items.copper, 3, Items.lead, 4, Items.titanium, 2, Items.silicon, 3)); @@ -610,9 +624,8 @@ public class Blocks implements ContentList{ consumes.liquid(Liquids.water, 0.2f); }}; - pyratiteMixer = new GenericSmelter("pyratite-mixer"){{ + pyratiteMixer = new GenericCrafter("pyratite-mixer"){{ requirements(Category.crafting, with(Items.copper, 50, Items.lead, 25)); - flameColor = Color.clear; hasItems = true; hasPower = true; outputItem = new ItemStack(Items.pyratite, 1); @@ -757,7 +770,7 @@ public class Blocks implements ContentList{ plastaniumWall = new Wall("plastanium-wall"){{ requirements(Category.defense, with(Items.plastanium, 5, Items.metaglass, 2)); - health = 130 * wallHealthMultiplier; + health = 125 * wallHealthMultiplier; insulated = true; absorbLasers = true; schematicPriority = 10; @@ -765,7 +778,7 @@ public class Blocks implements ContentList{ plastaniumWallLarge = new Wall("plastanium-wall-large"){{ requirements(Category.defense, ItemStack.mult(plastaniumWall.requirements, 4)); - health = 130 * wallHealthMultiplier * 4; + health = 125 * wallHealthMultiplier * 4; size = 2; insulated = true; absorbLasers = true; @@ -972,7 +985,6 @@ public class Blocks implements ContentList{ phaseConveyor = new ItemBridge("phase-conveyor"){{ requirements(Category.distribution, with(Items.phaseFabric, 5, Items.silicon, 7, Items.lead, 10, Items.graphite, 10)); range = 12; - canOverdrive = false; hasPower = true; consumes.power(0.30f); }}; @@ -1018,14 +1030,20 @@ public class Blocks implements ContentList{ consumes.power(1.75f); }}; - payloadConveyor = new PayloadConveyor("payload-conveyor"){{ - requirements(Category.distribution, with(Items.graphite, 10, Items.copper, 20)); - canOverdrive = false; + //special transport blocks + + duct = new Duct("duct"){{ + requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.graphite, 5, Items.copper, 5)); + speed = 5f; }}; - payloadRouter = new PayloadRouter("payload-router"){{ - requirements(Category.distribution, with(Items.graphite, 15, Items.copper, 20)); - canOverdrive = false; + ductRouter = new DuctRouter("duct-router"){{ + requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.graphite, 10, Items.copper, 5)); + speed = 5f; + }}; + + ductBridge = new DuctBridge("duct-bridge"){{ + requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.graphite, 20, Items.copper, 15)); }}; //endregion @@ -1294,21 +1312,30 @@ public class Blocks implements ContentList{ liquidCapacity = 30f; rotateSpeed = 1.4f; attribute = Attribute.water; + envRequired |= Env.groundWater; consumes.power(1.5f); }}; - cultivator = new Cultivator("cultivator"){{ + cultivator = new AttributeCrafter("cultivator"){{ requirements(Category.production, with(Items.copper, 25, Items.lead, 25, Items.silicon, 10)); outputItem = new ItemStack(Items.sporePod, 1); - craftTime = 140; + craftTime = 100; size = 2; hasLiquids = true; hasPower = true; hasItems = true; - consumes.power(0.9f); - consumes.liquid(Liquids.water, 0.2f); + craftEffect = Fx.none; + envRequired |= Env.spores; + attribute = Attribute.spores; + + legacyReadWarmup = true; + drawer = new DrawCultivator(); + maxBoost = 2f; + + consumes.power(80f / 60f); + consumes.liquid(Liquids.water, 20f / 60f); }}; oilExtractor = new Fracker("oil-extractor"){{ @@ -1353,7 +1380,7 @@ public class Blocks implements ContentList{ size = 4; unitCapModifier = 16; - researchCostMultiplier = 0.04f; + researchCostMultiplier = 0.07f; }}; coreNucleus = new CoreBlock("core-nucleus"){{ @@ -1365,24 +1392,26 @@ public class Blocks implements ContentList{ size = 5; unitCapModifier = 24; - researchCostMultiplier = 0.06f; + researchCostMultiplier = 0.11f; }}; vault = new StorageBlock("vault"){{ requirements(Category.effect, with(Items.titanium, 250, Items.thorium, 125)); size = 3; itemCapacity = 1000; + health = size * size * 55; }}; container = new StorageBlock("container"){{ requirements(Category.effect, with(Items.titanium, 100)); size = 2; itemCapacity = 300; + health = size * size * 55; }}; unloader = new Unloader("unloader"){{ requirements(Category.effect, with(Items.titanium, 25, Items.silicon, 30)); - speed = 6f; + speed = 60f / 11f; group = BlockGroup.transportation; }}; @@ -1403,12 +1432,14 @@ public class Blocks implements ContentList{ alternate = true; reloadTime = 20f; restitution = 0.03f; - range = 100; + range = 110; shootCone = 15f; ammoUseEffect = Fx.casing1; health = 250; inaccuracy = 2f; rotateSpeed = 10f; + + limitRange(); }}; scatter = new ItemTurret("scatter"){{ @@ -1419,7 +1450,7 @@ public class Blocks implements ContentList{ Items.metaglass, Bullets.flakGlass ); reloadTime = 18f; - range = 160f; + range = 220f; size = 2; burstSpacing = 5f; shots = 2; @@ -1432,6 +1463,8 @@ public class Blocks implements ContentList{ health = 200 * size * size; shootSound = Sounds.shootSnap; + + limitRange(2); }}; scorch = new ItemTurret("scorch"){{ @@ -1461,11 +1494,12 @@ public class Blocks implements ContentList{ targetAir = false; reloadTime = 60f; recoilAmount = 2f; - range = 230f; + range = 235f; inaccuracy = 1f; shootCone = 10f; health = 260; shootSound = Sounds.bang; + limitRange(0f); }}; wave = new LiquidTurret("wave"){{ @@ -1478,7 +1512,7 @@ public class Blocks implements ContentList{ ); size = 2; recoilAmount = 0f; - reloadTime = 2f; + reloadTime = 3f; inaccuracy = 5f; shootCone = 50f; liquidCapacity = 10f; @@ -1512,12 +1546,12 @@ public class Blocks implements ContentList{ shootType = new LaserBulletType(140){{ colors = new Color[]{Pal.lancerLaser.cpy().a(0.4f), Pal.lancerLaser, Color.white}; hitEffect = Fx.hitLancer; - despawnEffect = Fx.none; hitSize = 4; lifetime = 16f; drawSize = 400f; collidesAir = false; length = 173f; + ammoMultiplier = 1f; }}; }}; @@ -1527,6 +1561,7 @@ public class Blocks implements ContentList{ damage = 20; lightningLength = 25; collidesAir = false; + ammoMultiplier = 1f; }}; reloadTime = 35f; shootCone = 40f; @@ -1547,9 +1582,9 @@ public class Blocks implements ContentList{ hasPower = true; size = 2; - force = 8f; - scaledForce = 7f; - range = 230f; + force = 12f; + scaledForce = 6f; + range = 240f; damage = 0.3f; health = 160 * size * size; rotateSpeed = 10; @@ -1568,15 +1603,17 @@ public class Blocks implements ContentList{ shots = 4; burstSpacing = 5; inaccuracy = 10f; - range = 200f; + range = 210f; xRand = 6f; size = 2; health = 300 * size * size; shootSound = Sounds.missile; + + limitRange(2f); }}; salvo = new ItemTurret("salvo"){{ - requirements(Category.turret, with(Items.copper, 100, Items.graphite, 90, Items.titanium, 60)); + requirements(Category.turret, with(Items.copper, 100, Items.graphite, 80, Items.titanium, 50)); ammo( Items.copper, Bullets.standardCopper, Items.graphite, Bullets.standardDense, @@ -1586,7 +1623,7 @@ public class Blocks implements ContentList{ ); size = 2; - range = 150f; + range = 180f; reloadTime = 38f; restitution = 0.03f; ammoEjectBack = 3f; @@ -1598,6 +1635,8 @@ public class Blocks implements ContentList{ ammoUseEffect = Fx.casing2; health = 240 * size * size; shootSound = Sounds.shootBig; + + limitRange(); }}; segment = new PointDefenseTurret("segment"){{ @@ -1610,7 +1649,7 @@ public class Blocks implements ContentList{ size = 2; shootLength = 5f; bulletDamage = 30f; - reloadTime = 9f; + reloadTime = 8f; }}; tsunami = new LiquidTurret("tsunami"){{ @@ -1622,7 +1661,7 @@ public class Blocks implements ContentList{ Liquids.oil, Bullets.heavyOilShot ); size = 3; - reloadTime = 2f; + reloadTime = 3f; shots = 2; velocityInaccuracy = 0.1f; inaccuracy = 4f; @@ -1721,6 +1760,7 @@ public class Blocks implements ContentList{ shootSound = Sounds.shootSnap; health = 145 * size * size; + limitRange(); }}; foreshadow = new ItemTurret("foreshadow"){{ @@ -1736,7 +1776,7 @@ public class Blocks implements ContentList{ despawnEffect = Fx.instBomb; trailSpacing = 20f; damage = 1350; - buildingDamageMultiplier = 0.3f; + buildingDamageMultiplier = 0.25f; speed = brange; hitShake = 6f; ammoMultiplier = 1f; @@ -1744,7 +1784,7 @@ public class Blocks implements ContentList{ ); maxAmmo = 40; - ammoPerShot = 4; + ammoPerShot = 5; rotateSpeed = 2f; reloadTime = 200f; ammoUseEffect = Fx.casing3Double; @@ -1773,11 +1813,11 @@ public class Blocks implements ContentList{ Items.pyratite, Bullets.standardIncendiaryBig, Items.thorium, Bullets.standardThoriumBig ); - reloadTime = 6f; + reloadTime = 7f; coolantMultiplier = 0.5f; restitution = 0.1f; ammoUseEffect = Fx.casing3; - range = 200f; + range = 260f; inaccuracy = 3f; recoilAmount = 3f; spread = 8f; @@ -1790,6 +1830,8 @@ public class Blocks implements ContentList{ health = 160 * size * size; coolantUsage = 1f; + + limitRange(); }}; meltdown = new LaserTurret("meltdown"){{ @@ -1799,7 +1841,7 @@ public class Blocks implements ContentList{ recoilAmount = 4f; size = 4; shootShake = 2f; - range = 190f; + range = 195f; reloadTime = 90f; firingMoveFract = 0.5f; shootDuration = 220f; @@ -1817,6 +1859,7 @@ public class Blocks implements ContentList{ incendChance = 0.4f; incendSpread = 5f; incendAmount = 1; + ammoMultiplier = 1f; }}; health = 200 * size * size; @@ -1856,7 +1899,8 @@ public class Blocks implements ContentList{ navalFactory = new UnitFactory("naval-factory"){{ requirements(Category.units, with(Items.copper, 150, Items.lead, 130, Items.metaglass, 120)); plans = Seq.with( - new UnitPlan(UnitTypes.risso, 60f * 45f, with(Items.silicon, 20, Items.metaglass, 35)) + new UnitPlan(UnitTypes.risso, 60f * 45f, with(Items.silicon, 20, Items.metaglass, 35)), + new UnitPlan(UnitTypes.retusa, 60f * 60f, with(Items.silicon, 15, Items.metaglass, 25, Items.titanium, 20)) ); size = 3; consumes.power(1.2f); @@ -1878,7 +1922,8 @@ public class Blocks implements ContentList{ new UnitType[]{UnitTypes.crawler, UnitTypes.atrax}, new UnitType[]{UnitTypes.flare, UnitTypes.horizon}, new UnitType[]{UnitTypes.mono, UnitTypes.poly}, - new UnitType[]{UnitTypes.risso, UnitTypes.minke} + new UnitType[]{UnitTypes.risso, UnitTypes.minke}, + new UnitType[]{UnitTypes.retusa, UnitTypes.oxynoe} ); }}; @@ -1944,10 +1989,26 @@ public class Blocks implements ContentList{ }}; repairPoint = new RepairPoint("repair-point"){{ - requirements(Category.units, with(Items.lead, 15, Items.copper, 15, Items.silicon, 15)); + requirements(Category.units, with(Items.lead, 20, Items.copper, 20, Items.silicon, 15)); repairSpeed = 0.5f; repairRadius = 65f; + beamWidth = 0.73f; powerUse = 1f; + pulseRadius = 5f; + }}; + + repairTurret = new RepairPoint("repair-turret"){{ + requirements(Category.units, with(Items.silicon, 70, Items.thorium, 60, Items.plastanium, 60)); + size = 2; + length = 6f; + repairSpeed = 5f; + repairRadius = 140f; + powerUse = 5f; + beamWidth = 1.1f; + pulseRadius = 6.1f; + coolantUse = 0.15f; + coolantMultiplier = 1.7f; + acceptCoolant = true; }}; resupplyPoint = new ResupplyPoint("resupply-point"){{ @@ -1961,6 +2022,28 @@ public class Blocks implements ContentList{ consumes.item(Items.copper, 1); }}; + //endregion + //region payloads + + payloadConveyor = new PayloadConveyor("payload-conveyor"){{ + requirements(Category.units, with(Items.graphite, 10, Items.copper, 20)); + canOverdrive = false; + }}; + + payloadRouter = new PayloadRouter("payload-router"){{ + requirements(Category.units, with(Items.graphite, 15, Items.copper, 20)); + canOverdrive = false; + }}; + + payloadPropulsionTower = new PayloadMassDriver("payload-propulsion-tower"){{ + requirements(Category.units, with(Items.thorium, 300, Items.silicon, 200, Items.plastanium, 200, Items.phaseFabric, 50)); + size = 5; + reloadTime = 150f; + chargeTime = 100f; + range = 300f; + consumes.power(10f); + }}; + //endregion //region sandbox @@ -1995,10 +2078,21 @@ public class Blocks implements ContentList{ alwaysUnlocked = true; }}; + payloadVoid = new PayloadVoid("payload-void"){{ + requirements(Category.units, BuildVisibility.sandboxOnly, with()); + size = 5; + }}; + + payloadSource = new PayloadSource("payload-source"){{ + requirements(Category.units, BuildVisibility.sandboxOnly, with()); + size = 5; + }}; + + //TODO move illuminator = new LightBlock("illuminator"){{ requirements(Category.effect, BuildVisibility.lightingOnly, with(Items.graphite, 12, Items.silicon, 8)); brightness = 0.75f; - radius = 120f; + radius = 140f; consumes.power(0.05f); }}; @@ -2027,16 +2121,6 @@ public class Blocks implements ContentList{ consumes.power(4f); }}; - //TODO remove - launchPadLarge = new LaunchPad("launch-pad-large"){{ - requirements(Category.effect, BuildVisibility.debugOnly, ItemStack.with(Items.titanium, 200, Items.silicon, 150, Items.lead, 250, Items.plastanium, 75)); - size = 4; - itemCapacity = 300; - launchTime = 60f * 35; - hasPower = true; - consumes.power(6f); - }}; - interplanetaryAccelerator = new Accelerator("interplanetary-accelerator"){{ requirements(Category.effect, BuildVisibility.campaignOnly, with(Items.copper, 16000, Items.silicon, 11000, Items.thorium, 13000, Items.titanium, 12000, Items.surgeAlloy, 6000, Items.phaseFabric, 5000)); researchCostMultiplier = 0.1f; @@ -2059,7 +2143,7 @@ public class Blocks implements ContentList{ }}; microProcessor = new LogicBlock("micro-processor"){{ - requirements(Category.logic, with(Items.copper, 80, Items.lead, 50, Items.silicon, 30)); + requirements(Category.logic, with(Items.copper, 90, Items.lead, 50, Items.silicon, 50)); instructionsPerTick = 2; @@ -2067,7 +2151,7 @@ public class Blocks implements ContentList{ }}; logicProcessor = new LogicBlock("logic-processor"){{ - requirements(Category.logic, with(Items.lead, 320, Items.silicon, 60, Items.graphite, 60, Items.thorium, 50)); + requirements(Category.logic, with(Items.lead, 320, Items.silicon, 80, Items.graphite, 60, Items.thorium, 50)); instructionsPerTick = 8; @@ -2077,7 +2161,7 @@ public class Blocks implements ContentList{ }}; hyperProcessor = new LogicBlock("hyper-processor"){{ - requirements(Category.logic, with(Items.lead, 450, Items.silicon, 130, Items.thorium, 75, Items.surgeAlloy, 50)); + requirements(Category.logic, with(Items.lead, 450, Items.silicon, 150, Items.thorium, 75, Items.surgeAlloy, 50)); consumes.liquid(Liquids.cryofluid, 0.08f); hasLiquids = true; @@ -2122,21 +2206,21 @@ public class Blocks implements ContentList{ //region experimental blockForge = new BlockForge("block-forge"){{ - requirements(Category.crafting, BuildVisibility.debugOnly, with(Items.thorium, 100)); + requirements(Category.units, BuildVisibility.debugOnly, with(Items.thorium, 100)); hasPower = true; consumes.power(2f); size = 3; }}; blockLoader = new BlockLoader("block-loader"){{ - requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.thorium, 100)); + requirements(Category.units, BuildVisibility.debugOnly, with(Items.thorium, 100)); hasPower = true; consumes.power(2f); size = 3; }}; blockUnloader = new BlockUnloader("block-unloader"){{ - requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.thorium, 100)); + requirements(Category.units, BuildVisibility.debugOnly, with(Items.thorium, 100)); hasPower = true; consumes.power(2f); size = 3; diff --git a/core/src/mindustry/content/Bullets.java b/core/src/mindustry/content/Bullets.java index 6cc2a15757..e318d80f98 100644 --- a/core/src/mindustry/content/Bullets.java +++ b/core/src/mindustry/content/Bullets.java @@ -9,7 +9,6 @@ import mindustry.entities.*; import mindustry.entities.bullet.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.io.*; import mindustry.world.*; import static mindustry.Vars.*; @@ -53,8 +52,7 @@ public class Bullets implements ContentList{ }}; //this is just a copy of the damage lightning bullet that doesn't damage air units - damageLightningGround = new BulletType(0.0001f, 0f){}; - JsonIO.copy(damageLightning, damageLightningGround); + damageLightningGround = damageLightning.copy(); damageLightningGround.collidesAir = false; artilleryDense = new ArtilleryBulletType(3f, 20, "shell"){{ @@ -262,7 +260,7 @@ public class Bullets implements ContentList{ drag = -0.01f; splashDamageRadius = 30f; splashDamage = 30f * 1.5f; - ammoMultiplier = 4f; + ammoMultiplier = 5f; hitEffect = Fx.blastExplosion; despawnEffect = Fx.blastExplosion; @@ -281,6 +279,7 @@ public class Bullets implements ContentList{ splashDamageRadius = 20f; splashDamage = 20f * 1.5f; makeFire = true; + ammoMultiplier = 5f; hitEffect = Fx.blastExplosion; status = StatusEffects.burning; }}; @@ -291,9 +290,10 @@ public class Bullets implements ContentList{ shrinkY = 0f; drag = -0.01f; splashDamageRadius = 25f; - splashDamage = 25f * 1.5f; + splashDamage = 25f * 1.4f; hitEffect = Fx.blastExplosion; despawnEffect = Fx.blastExplosion; + ammoMultiplier = 4f; lightningDamage = 10; lightning = 2; lightningLength = 10; @@ -346,12 +346,14 @@ public class Bullets implements ContentList{ }}; standardDenseBig = new BasicBulletType(7f, 55, "bullet"){{ + hitSize = 5; width = 15f; height = 21f; shootEffect = Fx.shootBig; }}; standardThoriumBig = new BasicBulletType(8f, 80, "bullet"){{ + hitSize = 5; width = 16f; height = 23f; shootEffect = Fx.shootBig; @@ -361,6 +363,7 @@ public class Bullets implements ContentList{ }}; standardIncendiaryBig = new BasicBulletType(7f, 60, "bullet"){{ + hitSize = 5; width = 16f; height = 21f; frontColor = Pal.lightishOrange; diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index fa492d0bee..f0bef945ce 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -12,7 +12,6 @@ import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.type.*; -import mindustry.ui.*; import static arc.graphics.g2d.Draw.rect; import static arc.graphics.g2d.Draw.*; @@ -21,16 +20,29 @@ import static arc.math.Angles.*; import static mindustry.Vars.*; public class Fx{ + private static final Rand rand = new Rand(); + private static final Vec2 v = new Vec2(); + public static final Effect none = new Effect(0, 0f, e -> {}), + trailFade = new Effect(400f, e -> { + if(!(e.data instanceof Trail trail)) return; + //lifetime is how many frames it takes to fade out the trail + e.lifetime = trail.length * 1.4f; + + trail.shorten(); + trail.drawCap(e.color, e.rotation); + trail.draw(e.color, e.rotation); + }), + unitSpawn = new Effect(30f, e -> { if(!(e.data instanceof UnitType unit)) return; float scl = 1f + e.fout() * 2f; - TextureRegion region = unit.icon(Cicon.full); + TextureRegion region = unit.fullIcon; alpha(e.fout()); mixcol(Color.white, e.fin()); @@ -41,9 +53,7 @@ public class Fx{ alpha(e.fin()); - rect(region, e.x, e.y, - region.width * Draw.scl * scl, region.height * Draw.scl * scl, e.rotation - 90); - + rect(region, e.x, e.y, region.width * Draw.scl * scl, region.height * Draw.scl * scl, e.rotation - 90); }), unitCapKill = new Effect(80f, e -> { @@ -62,7 +72,7 @@ public class Fx{ mixcol(Pal.accent, 1f); alpha(e.fout()); - rect(block ? ((BlockUnitc)select).tile().block.icon(Cicon.full) : select.type.icon(Cicon.full), select.x, select.y, block ? 0f : select.rotation - 90f); + rect(block ? ((BlockUnitc)select).tile().block.fullIcon : select.type.fullIcon, select.x, select.y, block ? 0f : select.rotation - 90f); alpha(1f); Lines.stroke(e.fslope()); Lines.square(select.x, select.y, e.fout() * select.hitSize * 2f, 45); @@ -80,7 +90,7 @@ public class Fx{ Draw.scl *= scl; mixcol(Pal.accent, 1f); - rect(select.type.icon(Cicon.full), select.x, select.y, select.rotation - 90f); + rect(select.type.fullIcon, select.x, select.y, select.rotation - 90f); reset(); Draw.scl = p; @@ -125,10 +135,10 @@ public class Fx{ Position pos = e.data(); - Draw.color(e.color); - Draw.alpha(e.fout()); + Draw.color(e.color, e.fout()); Lines.stroke(1.5f); Lines.line(e.x, e.y, pos.getX(), pos.getY()); + Drawf.light(null, e.x, e.y, pos.getX(), pos.getY(), 20f, e.color, 0.6f * e.fout()); }), pointHit = new Effect(8f, e -> { @@ -258,6 +268,14 @@ public class Fx{ Fill.circle(e.x + trnsx(ang, len), e.y + trnsy(ang, len), 2f * e.fout()); }), + breakProp = new Effect(23, e -> { + float scl = Math.max(e.rotation, 1); + color(Tmp.c1.set(e.color).mul(1.1f)); + randLenVectors(e.id, 6, 19f * e.finpow() * scl, (x, y) -> { + Fill.circle(e.x + x, e.y + y, e.fout() * 3.5f * scl + 0.3f); + }); + }).layer(Layer.debris), + unitDrop = new Effect(30, e -> { color(Pal.lightishGray); randLenVectors(e.id, 9, 3 + 20f * e.finpow(), (x, y) -> { @@ -300,7 +318,8 @@ public class Fx{ greenBomb = new Effect(40f, 100f, e -> { color(Pal.heal); stroke(e.fout() * 2f); - Lines.circle(e.x, e.y, 4f + e.finpow() * 65f); + float circleRad = 4f + e.finpow() * 65f; + Lines.circle(e.x, e.y, circleRad); color(Pal.heal); for(int i = 0; i < 4; i++){ @@ -311,6 +330,8 @@ public class Fx{ for(int i = 0; i < 4; i++){ Drawf.tri(e.x, e.y, 3f, 35f * e.fout(), i*90); } + + Drawf.light(e.x, e.y, circleRad * 1.6f, Pal.heal, e.fout()); }), greenLaserCharge = new Effect(80f, 100f, e -> { @@ -322,11 +343,13 @@ public class Fx{ randLenVectors(e.id, 20, 40f * e.fout(), (x, y) -> { Fill.circle(e.x + x, e.y + y, e.fin() * 5f); + Drawf.light(e.x + x, e.y + y, e.fin() * 15f, Pal.heal, 0.7f); }); color(); Fill.circle(e.x, e.y, e.fin() * 10); + Drawf.light(e.x, e.y, e.fin() * 20f, Pal.heal, 0.7f); }), greenLaserChargeSmall = new Effect(40f, 100f, e -> { @@ -335,6 +358,13 @@ public class Fx{ Lines.circle(e.x, e.y, e.fout() * 50f); }), + greenCloud = new Effect(80f, e -> { + color(Pal.heal); + randLenVectors(e.id, e.fin(), 7, 9f, (x, y, fin, fout) -> { + Fill.circle(e.x + x, e.y + y, 5f * fout); + }); + }), + healWaveDynamic = new Effect(22, e -> { color(Pal.heal); stroke(e.fout() * 2f); @@ -353,14 +383,20 @@ public class Fx{ Lines.circle(e.x, e.y, 2f + e.finpow() * 7f); }), + healDenamic = new Effect(11, e -> { + color(Pal.heal); + stroke(e.fout() * 2f); + Lines.circle(e.x, e.y, 2f + e.finpow() * e.rotation); + }), + shieldWave = new Effect(22, e -> { - color(Pal.shield); + color(e.color, 0.7f); stroke(e.fout() * 2f); Lines.circle(e.x, e.y, 4f + e.finpow() * 60f); }), shieldApply = new Effect(11, e -> { - color(Pal.shield); + color(e.color, 0.7f); stroke(e.fout() * 2f); Lines.circle(e.x, e.y, 2f + e.finpow() * 7f); }), @@ -379,6 +415,8 @@ public class Fx{ float ang = Mathf.angle(x, y); lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f); }); + + Drawf.light(e.x, e.y, 20f, Pal.lightOrange, 0.6f * e.fout()); }), hitFuse = new Effect(14, e -> { @@ -417,6 +455,16 @@ public class Fx{ }); }), + hitFlamePlasma = new Effect(14, e -> { + color(Color.white, Pal.heal, e.fin()); + stroke(0.5f + e.fout()); + + randLenVectors(e.id, 2, e.fin() * 15f, e.rotation, 50f, (x, y) -> { + float ang = Mathf.angle(x, y); + lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f); + }); + }), + hitLiquid = new Effect(16, e -> { color(e.color); @@ -435,6 +483,16 @@ public class Fx{ }); }), + hitEmpSpark = new Effect(40, e -> { + color(Pal.heal); + stroke(e.fout() * 1.6f); + + randLenVectors(e.id, 18, e.finpow() * 27f, e.rotation, 360f, (x, y) -> { + float ang = Mathf.angle(x, y); + lineAngle(e.x + x, e.y + y, ang, e.fout() * 6 + 1f); + }); + }), + hitLancer = new Effect(12, e -> { color(Color.white); stroke(e.fout() * 1.5f); @@ -488,6 +546,8 @@ public class Fx{ for(int i = 0; i < 4; i++){ Drawf.tri(e.x, e.y, 3f, 30f * e.fout(), i*90 + 45); } + + Drawf.light(e.x, e.y, 150f, Pal.bulletYellowBack, 0.9f * e.fout()); }), instTrail = new Effect(30, e -> { @@ -501,6 +561,8 @@ public class Fx{ Drawf.tri(e.x, e.y, w, (30f + Mathf.randomSeedRange(e.id, 15f)) * m, rot); Drawf.tri(e.x, e.y, w, 10f * m, rot + 180f); } + + Drawf.light(e.x, e.y, 60f, Pal.bulletYellowBack, 0.6f * e.fout()); }), instShoot = new Effect(24f, e -> { @@ -516,6 +578,8 @@ public class Fx{ Drawf.tri(e.x, e.y, 13f * e.fout(), 85f, e.rotation + 90f * i); Drawf.tri(e.x, e.y, 13f * e.fout(), 50f, e.rotation + 20f * i); } + + Drawf.light(e.x, e.y, 180f, Pal.bulletYellowBack, 0.9f * e.fout()); }), instHit = new Effect(20f, 200f, e -> { @@ -552,6 +616,8 @@ public class Fx{ color(Color.white, Pal.heal, e.fin()); stroke(0.5f + e.fout()); Lines.circle(e.x, e.y, e.fin() * 5f); + + Drawf.light(e.x, e.y, 23f, Pal.heal, e.fout() * 0.7f); }), hitYellowLaser = new Effect(8, e -> { @@ -571,6 +637,12 @@ public class Fx{ }), + airBubble = new Effect(100f, e -> { + randLenVectors(e.id, 1, e.fin() * 12f, (x, y) -> { + rect(renderer.bubbles[Math.min((int)(renderer.bubbles.length * Mathf.curveMargin(e.fin(), 0.11f, 0.06f)), renderer.bubbles.length - 1)], e.x + x, e.y + y); + }); + }).layer(Layer.flyingUnitLow + 1), + flakExplosion = new Effect(20, e -> { color(Pal.bulletYellow); @@ -591,6 +663,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 50f, Pal.lighterOrange, 0.8f * e.fout()); }), plasticExplosion = new Effect(24, e -> { @@ -613,6 +687,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 25f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 50f, Pal.plastaniumBack, 0.8f * e.fout()); }), plasticExplosionFlak = new Effect(28, e -> { @@ -657,6 +733,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 45f, Pal.missileYellowBack, 0.8f * e.fout()); }), sapExplosion = new Effect(25, e -> { @@ -679,6 +757,8 @@ public class Fx{ randLenVectors(e.id + 1, 8, 1f + 60f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 90f, Pal.sapBulletBack, 0.8f * e.fout()); }), massiveExplosion = new Effect(30, e -> { @@ -701,6 +781,8 @@ public class Fx{ randLenVectors(e.id + 1, 6, 1f + 29f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 4f); }); + + Drawf.light(e.x, e.y, 50f, Pal.missileYellowBack, 0.8f * e.fout()); }), artilleryTrail = new Effect(50, e -> { @@ -716,7 +798,7 @@ public class Fx{ missileTrail = new Effect(50, e -> { color(e.color); Fill.circle(e.x, e.y, e.rotation * e.fout()); - }), + }).layer(Layer.bullet - 0.001f), //below bullets absorb = new Effect(12, e -> { color(Pal.accent); @@ -757,6 +839,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 60f, Pal.bulletYellowBack, 0.7f * e.fout()); }), burning = new Effect(35f, e -> { @@ -767,6 +851,12 @@ public class Fx{ }); }), + fireRemove = new Effect(70f, e -> { + if(Fire.regions[0] == null) return; + alpha(e.fout()); + rect(Fire.regions[((int)(e.rotation + e.fin() * Fire.frames)) % Fire.frames], e.x, e.y); + }), + fire = new Effect(50f, e -> { color(Pal.lightFlame, Pal.darkFlame, e.fin()); @@ -849,6 +939,14 @@ public class Fx{ }); }), + electrified = new Effect(40f, e -> { + color(Pal.heal); + + randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> { + Fill.square(e.x + x, e.y + y, e.fslope() * 1.1f, 45f); + }); + }), + sporeSlowed = new Effect(40f, e -> { color(Pal.spore); @@ -881,7 +979,7 @@ public class Fx{ float length = 20f * e.finpow(); float size = 7f * e.fout(); - rect(((Item)e.data).icon(Cicon.medium), e.x + trnsx(e.rotation, length), e.y + trnsy(e.rotation, length), size, size); + rect(((Item)e.data).fullIcon, e.x + trnsx(e.rotation, length), e.y + trnsy(e.rotation, length), size, size); }), shockwave = new Effect(10f, 80f, e -> { @@ -935,26 +1033,82 @@ public class Fx{ }); }), - dynamicExplosion = new Effect(30, 100f, e -> { - float intensity = e.rotation; - - e.scaled(5 + intensity * 2, i -> { - stroke(3.1f * i.fout()); - Lines.circle(e.x, e.y, (3f + i.fin() * 14f) * intensity); - }); + dynamicExplosion = new Effect(30, 500f, b -> { + float intensity = b.rotation; + float baseLifetime = 26f + intensity * 15f; + b.lifetime = 43f + intensity * 35f; color(Color.gray); + //TODO awful borders with linear filtering here + alpha(0.9f); + for(int i = 0; i < 4; i++){ + rand.setSeed(b.id*2 + i); + float lenScl = rand.random(0.4f, 1f); + int fi = i; + b.scaled(b.lifetime * lenScl, e -> { + randLenVectors(e.id + fi - 1, e.fin(Interp.pow10Out), (int)(3f * intensity), 14f * intensity, (x, y, in, out) -> { + float fout = e.fout(Interp.pow5Out) * rand.random(0.5f, 1f); + Fill.circle(e.x + x, e.y + y, fout * ((2f + intensity) * 1.8f)); + }); + }); + } - randLenVectors(e.id, e.finpow(), (int)(6 * intensity), 21f * intensity, (x, y, in, out) -> { - Fill.circle(e.x + x, e.y + y, out * (2f + intensity) * 3 + 0.5f); - Fill.circle(e.x + x / 2f, e.y + y / 2f, out * (intensity) * 3); + b.scaled(baseLifetime, e -> { + e.scaled(5 + intensity * 2.5f, i -> { + stroke((3.1f + intensity/5f) * i.fout()); + Lines.circle(e.x, e.y, (3f + i.fin() * 14f) * intensity); + Drawf.light(e.x, e.y, i.fin() * 14f * 2f * intensity, Color.white, 0.9f * e.fout()); + }); + + color(Pal.lighterOrange, Pal.lightOrange, Color.gray, e.fin()); + stroke((1.7f * e.fout()) * (1f + (intensity - 1f) / 2f)); + + Draw.z(Layer.effect + 0.001f); + randLenVectors(e.id + 1, e.finpow() + 0.001f, (int)(9 * intensity), 40f * intensity, (x, y, in, out) -> { + lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + out * 4 * (3f + intensity)); + Drawf.light(e.x + x, e.y + y, (out * 4 * (3f + intensity)) * 3.5f, Draw.getColor(), 0.8f); + }); }); + }), - color(Pal.lighterOrange, Pal.lightOrange, Color.gray, e.fin()); - stroke((1.7f * e.fout()) * (1f + (intensity - 1f) / 2f)); + reactorExplosion = new Effect(30, 500f, b -> { + float intensity = 6.8f; + float baseLifetime = 25f + intensity * 11f; + b.lifetime = 50f + intensity * 65f; - randLenVectors(e.id + 1, e.finpow(), (int)(9 * intensity), 40f * intensity, (x, y, in, out) -> { - lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + out * 4 * (3f + intensity)); + color(Pal.reactorPurple2); + alpha(0.7f); + for(int i = 0; i < 4; i++){ + rand.setSeed(b.id*2 + i); + float lenScl = rand.random(0.4f, 1f); + int fi = i; + b.scaled(b.lifetime * lenScl, e -> { + randLenVectors(e.id + fi - 1, e.fin(Interp.pow10Out), (int)(2.9f * intensity), 22f * intensity, (x, y, in, out) -> { + float fout = e.fout(Interp.pow5Out) * rand.random(0.5f, 1f); + float rad = fout * ((2f + intensity) * 2.35f); + + Fill.circle(e.x + x, e.y + y, rad); + Drawf.light(e.x + x, e.y + y, rad * 2.5f, Pal.reactorPurple, 0.5f); + }); + }); + } + + b.scaled(baseLifetime, e -> { + Draw.color(); + e.scaled(5 + intensity * 2f, i -> { + stroke((3.1f + intensity/5f) * i.fout()); + Lines.circle(e.x, e.y, (3f + i.fin() * 14f) * intensity); + Drawf.light(e.x, e.y, i.fin() * 14f * 2f * intensity, Color.white, 0.9f * e.fout()); + }); + + color(Pal.lighterOrange, Pal.reactorPurple, e.fin()); + stroke((2f * e.fout())); + + Draw.z(Layer.effect + 0.001f); + randLenVectors(e.id + 1, e.finpow() + 0.001f, (int)(8 * intensity), 28f * intensity, (x, y, in, out) -> { + lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + out * 4 * (4f + intensity)); + Drawf.light(e.x + x, e.y + y, (out * 4 * (3f + intensity)) * 3.5f, Draw.getColor(), 0.8f); + }); }); }), @@ -1047,6 +1201,19 @@ public class Fx{ }); }), + shootPayloadDriver = new Effect(30f, e -> { + color(Pal.accent); + Lines.stroke(0.5f + 0.5f*e.fout()); + float spread = 9f; + + rand.setSeed(e.id); + for(int i = 0; i < 20; i++){ + float ang = e.rotation + rand.range(17f); + v.trns(ang, rand.random(e.fin() * 55f)); + Lines.lineAngle(e.x + v.x + rand.range(spread), e.y + v.y + rand.range(spread), ang, e.fout() * 5f * rand.random(1f) + 1f); + } + }), + shootSmallFlame = new Effect(32f, 80f, e -> { color(Pal.lightFlame, Pal.darkFlame, Color.gray, e.fin()); @@ -1188,6 +1355,8 @@ public class Fx{ for(int i : Mathf.signs){ Drawf.tri(e.x, e.y, 10f * e.fout(), 24f, e.rotation + 90 + 90f * i); } + + Drawf.light(e.x, e.y, 60f * e.fout(), Pal.orangeSpark, 0.5f); }), railHit = new Effect(18f, 200f, e -> { @@ -1478,30 +1647,44 @@ public class Fx{ Lines.spikes(e.x, e.y, e.fin() * 5f, 2, 8); }), + mineSmall = new Effect(30, e -> { + color(e.color, Color.lightGray, e.fin()); + randLenVectors(e.id, 3, e.fin() * 5f, (x, y) -> { + Fill.square(e.x + x, e.y + y, e.fout() + 0.5f, 45); + }); + }), + mine = new Effect(20, e -> { + color(e.color, Color.lightGray, e.fin()); randLenVectors(e.id, 6, 3f + e.fin() * 6f, (x, y) -> { - color(e.color, Color.lightGray, e.fin()); Fill.square(e.x + x, e.y + y, e.fout() * 2f, 45); }); }), mineBig = new Effect(30, e -> { + color(e.color, Color.lightGray, e.fin()); randLenVectors(e.id, 6, 4f + e.fin() * 8f, (x, y) -> { - color(e.color, Color.lightGray, e.fin()); Fill.square(e.x + x, e.y + y, e.fout() * 2f + 0.2f, 45); }); }), mineHuge = new Effect(40, e -> { + color(e.color, Color.lightGray, e.fin()); randLenVectors(e.id, 8, 5f + e.fin() * 10f, (x, y) -> { - color(e.color, Color.lightGray, e.fin()); Fill.square(e.x + x, e.y + y, e.fout() * 2f + 0.5f, 45); }); }), + payloadReceive = new Effect(30, e -> { + color(Color.white, Pal.accent, e.fin()); + randLenVectors(e.id, 12, 7f + e.fin() * 13f, (x, y) -> { + Fill.square(e.x + x, e.y + y, e.fout() * 2.1f + 0.5f, 45); + }); + }), + smelt = new Effect(20, e -> { + color(Color.white, e.color, e.fin()); randLenVectors(e.id, 6, 2f + e.fin() * 5f, (x, y) -> { - color(Color.white, e.color, e.fin()); Fill.square(e.x + x, e.y + y, 0.5f + e.fout() * 2f, 45); }); }), @@ -1635,7 +1818,7 @@ public class Fx{ float radius = unit.hitSize() * 1.3f; e.scaled(16f, c -> { - color(Pal.shield); + color(e.color, 0.9f); stroke(c.fout() * 2f + 0.1f); randLenVectors(e.id, (int)(radius * 1.2f), radius/2f + c.finpow() * radius*1.25f, (x, y) -> { @@ -1643,11 +1826,85 @@ public class Fx{ }); }); - color(Pal.shield, e.fout()); + color(e.color, e.fout() * 0.9f); stroke(e.fout()); Lines.circle(e.x, e.y, radius); }), + chainLightning = new Effect(20f, 300f, e -> { + if(!(e.data instanceof Position p)) return; + float tx = p.getX(), ty = p.getY(), dst = Mathf.dst(e.x, e.y, tx, ty); + Tmp.v1.set(p).sub(e.x, e.y).nor(); + + float normx = Tmp.v1.x, normy = Tmp.v1.y; + float range = 6f; + int links = Mathf.ceil(dst / range); + float spacing = dst / links; + + Lines.stroke(2.5f * e.fout()); + Draw.color(Color.white, e.color, e.fin()); + + Lines.beginLine(); + + Lines.linePoint(e.x, e.y); + + rand.setSeed(e.id); + + for(int i = 0; i < links; i++){ + float nx, ny; + if(i == links - 1){ + nx = tx; + ny = ty; + }else{ + float len = (i + 1) * spacing; + Tmp.v1.setToRandomDirection(rand).scl(range/2f); + nx = e.x + normx * len + Tmp.v1.x; + ny = e.y + normy * len + Tmp.v1.y; + } + + Lines.linePoint(nx, ny); + } + + Lines.endLine(); + }).followParent(false), + + chainEmp = new Effect(30f, 300f, e -> { + if(!(e.data instanceof Position p)) return; + float tx = p.getX(), ty = p.getY(), dst = Mathf.dst(e.x, e.y, tx, ty); + Tmp.v1.set(p).sub(e.x, e.y).nor(); + + float normx = Tmp.v1.x, normy = Tmp.v1.y; + float range = 6f; + int links = Mathf.ceil(dst / range); + float spacing = dst / links; + + Lines.stroke(4f * e.fout()); + Draw.color(Color.white, e.color, e.fin()); + + Lines.beginLine(); + + Lines.linePoint(e.x, e.y); + + rand.setSeed(e.id); + + for(int i = 0; i < links; i++){ + float nx, ny; + if(i == links - 1){ + nx = tx; + ny = ty; + }else{ + float len = (i + 1) * spacing; + Tmp.v1.setToRandomDirection(rand).scl(range/2f); + nx = e.x + normx * len + Tmp.v1.x; + ny = e.y + normy * len + Tmp.v1.y; + } + + Lines.linePoint(nx, ny); + } + + Lines.endLine(); + }).followParent(false), + coreLand = new Effect(120f, e -> { }); } diff --git a/core/src/mindustry/content/Items.java b/core/src/mindustry/content/Items.java index bd883c3ce9..360a965c45 100644 --- a/core/src/mindustry/content/Items.java +++ b/core/src/mindustry/content/Items.java @@ -5,8 +5,9 @@ import mindustry.ctype.*; import mindustry.type.*; public class Items implements ContentList{ - public static Item scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, phaseFabric, surgeAlloy, - sporePod, sand, blastCompound, pyratite, metaglass; + public static Item + scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, + phaseFabric, surgeAlloy, sporePod, sand, blastCompound, pyratite, metaglass; @Override public void load(){ diff --git a/core/src/mindustry/content/Liquids.java b/core/src/mindustry/content/Liquids.java index 7ff39522f1..664a9362ba 100644 --- a/core/src/mindustry/content/Liquids.java +++ b/core/src/mindustry/content/Liquids.java @@ -24,7 +24,7 @@ public class Liquids implements ContentList{ }}; oil = new Liquid("oil", Color.valueOf("313131")){{ - viscosity = 0.7f; + viscosity = 0.75f; flammability = 1.2f; explosiveness = 1.2f; heatCapacity = 0.7f; diff --git a/core/src/mindustry/content/Planets.java b/core/src/mindustry/content/Planets.java index 9450c441da..7134a6ef28 100644 --- a/core/src/mindustry/content/Planets.java +++ b/core/src/mindustry/content/Planets.java @@ -9,7 +9,6 @@ import mindustry.type.*; public class Planets implements ContentList{ public static Planet sun, - //tantros, serpulo; @Override @@ -18,8 +17,6 @@ public class Planets implements ContentList{ bloom = true; accessible = false; - //lightColor = Color.valueOf("f4ee8e"); - meshLoader = () -> new SunMesh( this, 4, 5, 0.3, 1.7, 1.2, 1, @@ -33,15 +30,6 @@ public class Planets implements ContentList{ ); }}; - /*tantros = new Planet("tantros", sun, 2, 0.8f){{ - generator = new TantrosPlanetGenerator(); - meshLoader = () -> new HexMesh(this, 4); - atmosphereColor = Color.valueOf("3db899"); - startSector = 10; - atmosphereRadIn = -0.01f; - atmosphereRadOut = 0.3f; - }};*/ - serpulo = new Planet("serpulo", sun, 3, 1){{ generator = new SerpuloPlanetGenerator(); meshLoader = () -> new HexMesh(this, 6); @@ -49,6 +37,7 @@ public class Planets implements ContentList{ atmosphereRadIn = 0.02f; atmosphereRadOut = 0.3f; startSector = 15; + alwaysUnlocked = true; }}; } } diff --git a/core/src/mindustry/content/SectorPresets.java b/core/src/mindustry/content/SectorPresets.java index e84547dd5f..e66908e247 100644 --- a/core/src/mindustry/content/SectorPresets.java +++ b/core/src/mindustry/content/SectorPresets.java @@ -14,12 +14,14 @@ public class SectorPresets implements ContentList{ @Override public void load(){ + //region serpulo groundZero = new SectorPreset("groundZero", serpulo, 15){{ alwaysUnlocked = true; addStartingItems = true; captureWave = 10; difficulty = 1; + startWaveTimeMultiplier = 3f; }}; saltFlats = new SectorPreset("saltFlats", serpulo, 101){{ @@ -95,5 +97,7 @@ public class SectorPresets implements ContentList{ planetaryTerminal = new SectorPreset("planetaryTerminal", serpulo, 93){{ difficulty = 10; }}; + + //endregion } } diff --git a/core/src/mindustry/content/StatusEffects.java b/core/src/mindustry/content/StatusEffects.java index 1fef859f8c..2ea8af6c9d 100644 --- a/core/src/mindustry/content/StatusEffects.java +++ b/core/src/mindustry/content/StatusEffects.java @@ -12,7 +12,7 @@ import mindustry.graphics.*; import static mindustry.Vars.*; public class StatusEffects implements ContentList{ - public static StatusEffect none, burning, freezing, unmoving, slow, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed, disarmed; + public static StatusEffect none, burning, freezing, unmoving, slow, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed, disarmed, electrified; @Override public void load(){ @@ -27,10 +27,10 @@ public class StatusEffects implements ContentList{ init(() -> { opposite(wet, freezing); - affinity(tarred, ((unit, time, newTime, result) -> { + affinity(tarred, ((unit, result, time) -> { unit.damagePierce(transitionDamage); Fx.burning.at(unit.x + Mathf.range(unit.bounds() / 2f), unit.y + Mathf.range(unit.bounds() / 2f)); - result.set(burning, Math.min(time + newTime, 300f)); + result.set(burning, Math.min(time + result.time, 300f)); })); }); }}; @@ -45,7 +45,7 @@ public class StatusEffects implements ContentList{ init(() -> { opposite(melting, burning); - affinity(blasted, ((unit, time, newTime, result) -> { + affinity(blasted, ((unit, result, time) -> { unit.damagePierce(transitionDamage); result.set(freezing, time); })); @@ -70,14 +70,14 @@ public class StatusEffects implements ContentList{ transitionDamage = 14; init(() -> { - affinity(shocked, ((unit, time, newTime, result) -> { + affinity(shocked, ((unit, result, time) -> { unit.damagePierce(transitionDamage); if(unit.team == state.rules.waveTeam){ Events.fire(Trigger.shock); } result.set(wet, time); })); - opposite(burning); + opposite(burning, melting); }); }}; @@ -86,6 +86,7 @@ public class StatusEffects implements ContentList{ speedMultiplier = 0.94f; effect = Fx.muddy; effectChance = 0.09f; + show = false; }}; melting = new StatusEffect("melting"){{ @@ -97,10 +98,10 @@ public class StatusEffects implements ContentList{ init(() -> { opposite(wet, freezing); - affinity(tarred, ((unit, time, newTime, result) -> { + affinity(tarred, ((unit, result, time) -> { unit.damagePierce(8f); Fx.burning.at(unit.x + Mathf.range(unit.bounds() / 2f), unit.y + Mathf.range(unit.bounds() / 2f)); - result.set(melting, Math.min(time + newTime, 200f)); + result.set(melting, Math.min(time + result.time, 200f)); })); }); }}; @@ -113,6 +114,14 @@ public class StatusEffects implements ContentList{ effectChance = 0.1f; }}; + electrified = new StatusEffect("electrified"){{ + color = Pal.heal; + speedMultiplier = 0.7f; + reloadMultiplier = 0.6f; + effect = Fx.electrified; + effectChance = 0.1f; + }}; + sporeSlowed = new StatusEffect("spore-slowed"){{ color = Pal.spore; speedMultiplier = 0.8f; @@ -126,8 +135,8 @@ public class StatusEffects implements ContentList{ effect = Fx.oily; init(() -> { - affinity(melting, ((unit, time, newTime, result) -> result.set(melting, newTime + time))); - affinity(burning, ((unit, time, newTime, result) -> result.set(burning, newTime + time))); + affinity(melting, ((unit, result, time) -> result.set(melting, result.time + time))); + affinity(burning, ((unit, result, time) -> result.set(burning, result.time + time))); }); }}; diff --git a/core/src/mindustry/content/TechTree.java b/core/src/mindustry/content/TechTree.java index 2e0a0f3646..d642808c22 100644 --- a/core/src/mindustry/content/TechTree.java +++ b/core/src/mindustry/content/TechTree.java @@ -445,7 +445,6 @@ public class TechTree implements ContentList{ node(ruinousShores, Seq.with( new SectorComplete(craters), new Research(graphitePress), - new Research(combustionGenerator), new Research(kiln), new Research(mechanicalPump) ), () -> { @@ -549,8 +548,7 @@ public class TechTree implements ContentList{ node(fungalPass, Seq.with( new SectorComplete(stainedMountains), new Research(groundFactory), - new Research(door), - new Research(siliconSmelter) + new Research(door) ), () -> { node(nuclearComplex, Seq.with( new SectorComplete(fungalPass), diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index 3677050654..d54f7cf46f 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -1,62 +1,65 @@ package mindustry.content; import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; import arc.struct.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.ctype.*; +import mindustry.entities.*; import mindustry.entities.abilities.*; import mindustry.entities.bullet.*; +import mindustry.entities.effect.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.type.*; +import mindustry.type.weapons.*; import mindustry.world.meta.*; +import static arc.graphics.g2d.Draw.*; +import static arc.graphics.g2d.Lines.*; +import static arc.math.Angles.*; import static mindustry.Vars.*; public class UnitTypes implements ContentList{ //region definitions - //(the wall of shame - should fix the legacy stuff eventually...) - //mech - public static @EntityDef({Unitc.class, Mechc.class}) UnitType mace, dagger, crawler, fortress, scepter, reign; + public static @EntityDef({Unitc.class, Mechc.class}) UnitType mace, dagger, crawler, fortress, scepter, reign, vela; - //mech + //mech, legacy public static @EntityDef(value = {Unitc.class, Mechc.class}, legacy = true) UnitType nova, pulsar, quasar; - //mech - public static @EntityDef({Unitc.class, Mechc.class}) UnitType vela; - //legs public static @EntityDef({Unitc.class, Legsc.class}) UnitType corvus, atrax; - //legs + //legs, legacy public static @EntityDef(value = {Unitc.class, Legsc.class}, legacy = true) UnitType spiroct, arkyid, toxopid; //air public static @EntityDef({Unitc.class}) UnitType flare, eclipse, horizon, zenith, antumbra; - //air + //air, legacy public static @EntityDef(value = {Unitc.class}, legacy = true) UnitType mono; - //air + //air, legacy public static @EntityDef(value = {Unitc.class}, legacy = true) UnitType poly; //air + payload public static @EntityDef({Unitc.class, Payloadc.class}) UnitType mega; - //air + payload + //air + payload, legacy public static @EntityDef(value = {Unitc.class, Payloadc.class}, legacy = true) UnitType quad; //air + payload + ammo distribution public static @EntityDef({Unitc.class, Payloadc.class, AmmoDistributec.class}) UnitType oct; - //air + //air, legacy public static @EntityDef(value = {Unitc.class}, legacy = true) UnitType alpha, beta, gamma; - //water - public static @EntityDef({Unitc.class, WaterMovec.class}) UnitType risso, minke, bryde, sei, omura; + //naval + public static @EntityDef({Unitc.class, WaterMovec.class}) UnitType risso, minke, bryde, sei, omura, retusa, oxynoe, cyerce, aegires, navanax; //special block unit type public static @EntityDef({Unitc.class, BlockUnitc.class}) UnitType block; @@ -70,9 +73,9 @@ public class UnitTypes implements ContentList{ dagger = new UnitType("dagger"){{ speed = 0.5f; hitSize = 8f; - health = 140; + health = 150; weapons.add(new Weapon("large-weapon"){{ - reload = 14f; + reload = 13f; x = 4f; y = 2f; top = false; @@ -82,9 +85,9 @@ public class UnitTypes implements ContentList{ }}; mace = new UnitType("mace"){{ - speed = 0.4f; - hitSize = 9f; - health = 500; + speed = 0.45f; + hitSize = 10f; + health = 530; armor = 4f; immunities.add(StatusEffects.burning); @@ -93,10 +96,10 @@ public class UnitTypes implements ContentList{ top = false; shootSound = Sounds.flame; shootY = 2f; - reload = 14f; + reload = 12f; recoil = 1f; ejectEffect = Fx.none; - bullet = new BulletType(3.9f, 30f){{ + bullet = new BulletType(4.1f, 32f){{ ammoMultiplier = 3f; hitSize = 7f; lifetime = 12f; @@ -113,11 +116,11 @@ public class UnitTypes implements ContentList{ }}; fortress = new UnitType("fortress"){{ - speed = 0.39f; + speed = 0.43f; hitSize = 13f; rotateSpeed = 3f; targetAir = false; - health = 800; + health = 820; armor = 9f; mechFrontSway = 0.55f; @@ -133,12 +136,12 @@ public class UnitTypes implements ContentList{ bullet = new ArtilleryBulletType(2f, 8, "shell"){{ hitEffect = Fx.blastExplosion; knockback = 0.8f; - lifetime = 110f; + lifetime = 120f; width = height = 14f; collides = true; collidesTiles = true; - splashDamageRadius = 28f; - splashDamage = 54f; + splashDamageRadius = 30f; + splashDamage = 60f; backColor = Pal.bulletYellowBack; frontColor = Pal.bulletYellow; }}; @@ -147,7 +150,7 @@ public class UnitTypes implements ContentList{ scepter = new UnitType("scepter"){{ speed = 0.35f; - hitSize = 20f; + hitSize = 22f; rotateSpeed = 2.1f; health = 9000; armor = 10f; @@ -243,7 +246,7 @@ public class UnitTypes implements ContentList{ splashDamage = 16f; splashDamageRadius = 13f; - fragBullets = 2; + fragBullets = 3; fragLifeMin = 0f; fragCone = 30f; @@ -291,8 +294,8 @@ public class UnitTypes implements ContentList{ recoil = 2f; shootSound = Sounds.lasershoot; - bullet = new LaserBoltBulletType(5.2f, 14){{ - lifetime = 32f; + bullet = new LaserBoltBulletType(5.2f, 13){{ + lifetime = 30f; healPercent = 5f; collidesTeam = true; backColor = Pal.heal; @@ -305,7 +308,7 @@ public class UnitTypes implements ContentList{ canBoost = true; boostMultiplier = 1.6f; speed = 0.7f; - hitSize = 10f; + hitSize = 11f; health = 320f; buildSpeed = 0.9f; armor = 4f; @@ -325,7 +328,7 @@ public class UnitTypes implements ContentList{ y = 0.5f; shootY = 2.5f; - reload = 38f; + reload = 36f; shots = 3; inaccuracy = 35; shotDelay = 0.5f; @@ -360,7 +363,7 @@ public class UnitTypes implements ContentList{ quasar = new UnitType("quasar"){{ mineTier = 3; boostMultiplier = 2f; - health = 650f; + health = 640f; buildSpeed = 1.7f; canBoost = true; armor = 9f; @@ -372,7 +375,7 @@ public class UnitTypes implements ContentList{ ammoType = AmmoTypes.power; speed = 0.4f; - hitSize = 10f; + hitSize = 13f; mineSpeed = 6f; drawShields = false; @@ -384,7 +387,7 @@ public class UnitTypes implements ContentList{ shake = 2f; shootY = 4f; x = 6.5f; - reload = 50f; + reload = 55f; recoil = 4f; shootSound = Sounds.laser; @@ -396,13 +399,14 @@ public class UnitTypes implements ContentList{ sideLength = 70f; healPercent = 10f; collidesTeam = true; + length = 135f; colors = new Color[]{Pal.heal.cpy().a(0.4f), Pal.heal, Color.white}; }}; }}); }}; vela = new UnitType("vela"){{ - hitSize = 23f; + hitSize = 24f; rotateSpeed = 1.6f; canDrown = false; @@ -494,8 +498,6 @@ public class UnitTypes implements ContentList{ speed = 0.3f; - mineTier = 2; - mineSpeed = 7f; drawShields = false; weapons.add(new Weapon("corvus-weapon"){{ @@ -553,7 +555,7 @@ public class UnitTypes implements ContentList{ speed = 1f; hitSize = 8f; - health = 180; + health = 200; mechSideSway = 0.25f; range = 40f; @@ -568,9 +570,9 @@ public class UnitTypes implements ContentList{ hitEffect = Fx.pulverize; lifetime = 10f; speed = 1f; - splashDamageRadius = 58f; + splashDamageRadius = 60f; instantDisappear = true; - splashDamage = 85f; + splashDamage = 88f; killShooter = true; hittable = false; collidesAir = true; @@ -581,7 +583,7 @@ public class UnitTypes implements ContentList{ atrax = new UnitType("atrax"){{ speed = 0.5f; drag = 0.4f; - hitSize = 10f; + hitSize = 13f; rotateSpeed = 3f; targetAir = false; health = 600; @@ -601,7 +603,7 @@ public class UnitTypes implements ContentList{ weapons.add(new Weapon("eruption"){{ top = false; shootY = 3f; - reload = 10f; + reload = 9f; ejectEffect = Fx.none; recoil = 1f; x = 7f; @@ -619,11 +621,11 @@ public class UnitTypes implements ContentList{ }}; spiroct = new UnitType("spiroct"){{ - speed = 0.4f; + speed = 0.45f; drag = 0.4f; - hitSize = 12f; + hitSize = 15f; rotateSpeed = 3f; - health = 900; + health = 910; immunities = ObjectSet.with(StatusEffects.burning, StatusEffects.melting); legCount = 6; legLength = 13f; @@ -642,7 +644,7 @@ public class UnitTypes implements ContentList{ weapons.add(new Weapon("spiroct-weapon"){{ shootY = 4f; - reload = 15f; + reload = 14f; ejectEffect = Fx.none; recoil = 2f; rotate = true; @@ -652,7 +654,7 @@ public class UnitTypes implements ContentList{ y = -1.5f; bullet = new SapBulletType(){{ - sapStrength = 0.4f; + sapStrength = 0.5f; length = 75f; damage = 20; shootEffect = Fx.shootSmall; @@ -665,7 +667,7 @@ public class UnitTypes implements ContentList{ }}); weapons.add(new Weapon("mount-purple-weapon"){{ - reload = 20f; + reload = 18f; rotate = true; x = 4f; y = 3f; @@ -687,8 +689,8 @@ public class UnitTypes implements ContentList{ arkyid = new UnitType("arkyid"){{ drag = 0.1f; - speed = 0.5f; - hitSize = 21f; + speed = 0.6f; + hitSize = 23f; health = 8000; armor = 6f; @@ -737,7 +739,7 @@ public class UnitTypes implements ContentList{ shootSound = Sounds.sap; }}, new Weapon("spiroct-weapon"){{ - reload = 15f; + reload = 14f; x = 9f; y = 6f; rotate = true; @@ -745,7 +747,7 @@ public class UnitTypes implements ContentList{ shootSound = Sounds.sap; }}, new Weapon("spiroct-weapon"){{ - reload = 23f; + reload = 22f; x = 14f; y = 0f; rotate = true; @@ -790,9 +792,10 @@ public class UnitTypes implements ContentList{ toxopid = new UnitType("toxopid"){{ drag = 0.1f; speed = 0.5f; - hitSize = 21f; + hitSize = 26f; health = 22000; armor = 13f; + lightRadius = 140f; rotateSpeed = 1.9f; @@ -878,6 +881,9 @@ public class UnitTypes implements ContentList{ lightningLength = 20; smokeEffect = Fx.shootBigSmoke2; hitShake = 10f; + lightRadius = 40f; + lightColor = Pal.sap; + lightOpacity = 0.6f; status = StatusEffects.sapped; statusDuration = 60f * 10; @@ -899,6 +905,9 @@ public class UnitTypes implements ContentList{ lightningLength = 5; smokeEffect = Fx.shootBigSmoke2; hitShake = 5f; + lightRadius = 30f; + lightColor = Pal.sap; + lightOpacity = 0.5f; status = StatusEffects.sapped; statusDuration = 60f * 10; @@ -921,6 +930,7 @@ public class UnitTypes implements ContentList{ targetAir = false; commandLimit = 4; circleTarget = true; + hitSize = 7; weapons.add(new Weapon(){{ y = 0f; @@ -941,7 +951,7 @@ public class UnitTypes implements ContentList{ horizon = new UnitType("horizon"){{ health = 340; - speed = 1.7f; + speed = 1.65f; accel = 0.08f; drag = 0.016f; flying = true; @@ -980,7 +990,7 @@ public class UnitTypes implements ContentList{ zenith = new UnitType("zenith"){{ health = 700; - speed = 1.8f; + speed = 1.7f; accel = 0.04f; drag = 0.016f; flying = true; @@ -1010,8 +1020,8 @@ public class UnitTypes implements ContentList{ homingRange = 60f; keepVelocity = false; splashDamageRadius = 25f; - splashDamage = 16f; - lifetime = 60f; + splashDamage = 15f; + lifetime = 50f; trailColor = Pal.unitBack; backColor = Pal.unitBack; frontColor = Pal.unitFront; @@ -1034,16 +1044,16 @@ public class UnitTypes implements ContentList{ armor = 9f; engineOffset = 21; engineSize = 5.3f; - hitSize = 56f; + hitSize = 46f; targetFlag = BlockFlag.battery; - BulletType missiles = new MissileBulletType(2.7f, 10){{ + BulletType missiles = new MissileBulletType(2.7f, 14){{ width = 8f; height = 8f; shrinkY = 0f; drag = -0.01f; splashDamageRadius = 20f; - splashDamage = 30f; + splashDamage = 34f; ammoMultiplier = 4f; lifetime = 50f; hitEffect = Fx.blastExplosion; @@ -1087,7 +1097,7 @@ public class UnitTypes implements ContentList{ shootSound = Sounds.shootBig; rotate = true; shadow = 8f; - bullet = new BasicBulletType(7f, 50){{ + bullet = new BasicBulletType(7f, 55){{ width = 12f; height = 18f; lifetime = 25f; @@ -1104,7 +1114,7 @@ public class UnitTypes implements ContentList{ rotateSpeed = 1f; flying = true; lowAltitude = true; - health = 20000; + health = 21000; engineOffset = 38; engineSize = 7.3f; hitSize = 58f; @@ -1115,7 +1125,7 @@ public class UnitTypes implements ContentList{ BulletType fragBullet = new FlakBulletType(4f, 5){{ shootEffect = Fx.shootBig; ammoMultiplier = 4f; - splashDamage = 42f; + splashDamage = 50f; splashDamageRadius = 25f; collidesGround = true; lifetime = 38f; @@ -1138,7 +1148,7 @@ public class UnitTypes implements ContentList{ rotate = true; bullet = new LaserBulletType(){{ - damage = 90f; + damage = 100f; sideAngle = 20f; sideWidth = 1.5f; sideLength = 80f; @@ -1208,7 +1218,7 @@ public class UnitTypes implements ContentList{ health = 400; buildSpeed = 0.5f; engineOffset = 6.5f; - hitSize = 8f; + hitSize = 9f; lowAltitude = true; ammoType = AmmoTypes.power; @@ -1216,7 +1226,7 @@ public class UnitTypes implements ContentList{ mineTier = 2; mineSpeed = 3.5f; - abilities.add(new RepairFieldAbility(5f, 60f * 5, 50f)); + abilities.add(new RepairFieldAbility(5f, 60f * 8, 50f)); weapons.add(new Weapon("heal-weapon-mount"){{ top = false; @@ -1235,7 +1245,7 @@ public class UnitTypes implements ContentList{ homingPower = 0.08f; weaveMag = 4; weaveScale = 4; - lifetime = 56f; + lifetime = 50f; keepVelocity = false; shootEffect = Fx.shootHeal; smokeEffect = Fx.hitLaser; @@ -1265,7 +1275,7 @@ public class UnitTypes implements ContentList{ flying = true; engineOffset = 10.5f; rotateShooting = false; - hitSize = 15f; + hitSize = 16.05f; engineSize = 3f; payloadCapacity = (2 * 2) * tilePayload; buildSpeed = 2.6f; @@ -1276,7 +1286,7 @@ public class UnitTypes implements ContentList{ weapons.add( new Weapon("heal-weapon-mount"){{ shootSound = Sounds.lasershoot; - reload = 25f; + reload = 24f; x = 8f; y = -6f; rotate = true; @@ -1317,7 +1327,7 @@ public class UnitTypes implements ContentList{ engineOffset = 12f; engineSize = 6f; rotateShooting = false; - hitSize = 32f; + hitSize = 36f; payloadCapacity = (3 * 3) * tilePayload; buildSpeed = 2.5f; buildBeamOffset = 23; @@ -1352,7 +1362,7 @@ public class UnitTypes implements ContentList{ shootCone = 180f; ejectEffect = Fx.none; - despawnShake = 4f; + hitShake = 4f; collidesAir = false; @@ -1388,7 +1398,7 @@ public class UnitTypes implements ContentList{ engineOffset = 46f; engineSize = 7.8f; rotateShooting = false; - hitSize = 60f; + hitSize = 66f; payloadCapacity = (5.3f * 5.3f) * tilePayload; buildSpeed = 4f; drawShields = false; @@ -1408,7 +1418,7 @@ public class UnitTypes implements ContentList{ risso = new UnitType("risso"){{ speed = 1.1f; drag = 0.13f; - hitSize = 9f; + hitSize = 10f; health = 280; accel = 0.4f; rotateSpeed = 3.3f; @@ -1429,7 +1439,7 @@ public class UnitTypes implements ContentList{ weapons.add(new Weapon("missiles-mount"){{ mirror = false; - reload = 20f; + reload = 23f; x = 0f; y = -5f; rotate = true; @@ -1444,7 +1454,7 @@ public class UnitTypes implements ContentList{ homingRange = 60f; splashDamageRadius = 25f; splashDamage = 10f; - lifetime = 80f; + lifetime = 65f; trailColor = Color.gray; backColor = Pal.bulletYellowBack; frontColor = Pal.bulletYellow; @@ -1460,7 +1470,7 @@ public class UnitTypes implements ContentList{ health = 600; speed = 0.9f; drag = 0.15f; - hitSize = 11f; + hitSize = 13f; armor = 4f; accel = 0.3f; rotateSpeed = 2.6f; @@ -1500,12 +1510,12 @@ public class UnitTypes implements ContentList{ }}; bryde = new UnitType("bryde"){{ - health = 900; + health = 910; speed = 0.85f; accel = 0.2f; rotateSpeed = 1.8f; drag = 0.17f; - hitSize = 16f; + hitSize = 20f; armor = 7f; rotateShooting = false; @@ -1537,13 +1547,13 @@ public class UnitTypes implements ContentList{ trailMult = 0.8f; hitEffect = Fx.massiveExplosion; knockback = 1.5f; - lifetime = 100f; + lifetime = 80f; height = 15.5f; width = 15f; collidesTiles = false; ammoMultiplier = 4f; splashDamageRadius = 40f; - splashDamage = 80f; + splashDamage = 70f; backColor = Pal.missileYellowBack; frontColor = Pal.missileYellow; trailEffect = Fx.artilleryTrail; @@ -1582,7 +1592,7 @@ public class UnitTypes implements ContentList{ keepVelocity = false; splashDamageRadius = 25f; splashDamage = 10f; - lifetime = 80f; + lifetime = 70f; trailColor = Color.gray; backColor = Pal.bulletYellowBack; frontColor = Pal.bulletYellow; @@ -1595,7 +1605,7 @@ public class UnitTypes implements ContentList{ }}; sei = new UnitType("sei"){{ - health = 10500; + health = 11000; armor = 12f; speed = 0.73f; @@ -1684,7 +1694,7 @@ public class UnitTypes implements ContentList{ health = 22000; speed = 0.62f; drag = 0.18f; - hitSize = 50f; + hitSize = 58f; armor = 16f; accel = 0.19f; rotateSpeed = 0.9f; @@ -1730,6 +1740,483 @@ public class UnitTypes implements ContentList{ }}); }}; + //endregion + //region naval support + retusa = new UnitType("retusa"){{ + defaultController = HugAI::new; + speed = 0.9f; + targetAir = false; + drag = 0.14f; + hitSize = 11f; + health = 270; + accel = 0.4f; + rotateSpeed = 5f; + trailLength = 20; + trailX = 5f; + trailScl = 1.3f; + rotateShooting = false; + range = 100f; + + armor = 3f; + + buildSpeed = 2f; + + weapons.add(new RepairBeamWeapon("repair-beam-weapon-center"){{ + x = 0f; + y = -5.5f; + shootY = 6f; + beamWidth = 0.8f; + mirror = false; + repairSpeed = 0.7f; + + bullet = new BulletType(){{ + maxRange = 120f; + }}; + }}); + + weapons.add(new Weapon(){{ + mirror = false; + reload = 80f; + shots = 3; + shotDelay = 7f; + x = y = shootX = shootY = 0f; + + bullet = new BasicBulletType(){{ + sprite = "mine-bullet"; + width = height = 11f; + layer = Layer.scorch; + shootEffect = smokeEffect = Fx.none; + + maxRange = 50f; + ignoreRotation = true; + healPercent = 4f; + + backColor = Pal.heal; + frontColor = Color.white; + mixColorTo = Color.white; + + hitSound = Sounds.plasmaboom; + + shootCone = 360f; + ejectEffect = Fx.none; + hitSize = 22f; + + collidesAir = false; + + lifetime = 500f; + + hitEffect = new MultiEffect(Fx.blastExplosion, Fx.greenCloud); + keepVelocity = false; + + shrinkX = shrinkY = 0f; + + speed = 0.001f; + + splashDamage = 50f; + splashDamageRadius = 40f; + }}; + }}); + }}; + + oxynoe = new UnitType("oxynoe"){{ + health = 560; + speed = 0.83f; + drag = 0.14f; + hitSize = 14f; + armor = 4f; + accel = 0.4f; + rotateSpeed = 4f; + rotateShooting = false; + + trailLength = 22; + trailX = 5.5f; + trailY = -4f; + trailScl = 1.9f; + + buildSpeed = 2.5f; + + weapons.add(new Weapon("plasma-mount-weapon"){{ + + reload = 5f; + x = 4.5f; + y = 6.5f; + rotate = true; + rotateSpeed = 5f; + inaccuracy = 10f; + ejectEffect = Fx.casing1; + shootSound = Sounds.flame; + shootCone = 30f; + + bullet = new BulletType(3.4f, 23f){{ + healPercent = 1.5f; + collidesTeam = true; + ammoMultiplier = 3f; + hitSize = 7f; + lifetime = 18f; + pierce = true; + collidesAir = false; + statusDuration = 60f * 4; + hitEffect = Fx.hitFlamePlasma; + ejectEffect = Fx.none; + despawnEffect = Fx.none; + status = StatusEffects.burning; + keepVelocity = false; + hittable = false; + shootEffect = new Effect(32f, 80f, e -> { + color(Color.white, Pal.heal, Color.gray, e.fin()); + + randLenVectors(e.id, 8, e.finpow() * 60f, e.rotation, 10f, (x, y) -> { + Fill.circle(e.x + x, e.y + y, 0.65f + e.fout() * 1.5f); + Drawf.light(e.x + x, e.y + y, 16f * e.fout(), Pal.heal, 0.6f); + }); + }); + }}; + }}); + + weapons.add(new PointDefenseWeapon("point-defense-mount"){{ + mirror = false; + x = 0f; + y = 1f; + reload = 9f; + targetInterval = 10f; + targetSwitchInterval = 15f; + + bullet = new BulletType(){{ + shootEffect = Fx.sparkShoot; + hitEffect = Fx.pointHit; + maxRange = 100f; + damage = 15f; + }}; + }}); + + }}; + + cyerce = new UnitType("cyerce"){{ + health = 870; + speed = 0.86f; + accel = 0.22f; + rotateSpeed = 2.6f; + drag = 0.16f; + hitSize = 20f; + armor = 6f; + rotateShooting = false; + + trailLength = 23; + trailX = 9f; + trailY = -9f; + trailScl = 2f; + + buildSpeed = 3f; + + weapons.add(new RepairBeamWeapon("repair-beam-weapon-center"){{ + x = 11f; + y = -10f; + shootY = 6f; + beamWidth = 0.8f; + repairSpeed = 0.7f; + + bullet = new BulletType(){{ + maxRange = 130f; + }}; + }}); + + weapons.add(new Weapon("plasma-missile-mount"){{ + reload = 60f; + x = 9f; + y = 3f; + + shadow = 5f; + + rotateSpeed = 4f; + rotate = true; + inaccuracy = 1f; + velocityRnd = 0.1f; + shootSound = Sounds.missile; + + ejectEffect = Fx.none; + bullet = new FlakBulletType(2.5f, 25){{ + sprite = "missile-large"; + collides = false; + //for targeting + collidesGround = collidesAir = true; + explodeRange = 40f; + width = height = 12f; + shrinkY = 0f; + drag = -0.003f; + homingRange = 60f; + keepVelocity = false; + lightRadius = 60f; + lightOpacity = 0.7f; + lightColor = Pal.heal; + + splashDamageRadius = 30f; + splashDamage = 28f; + + lifetime = 90f; + backColor = Pal.heal; + frontColor = Color.white; + + hitEffect = new ExplosionEffect(){{ + lifetime = 28f; + waveStroke = 6f; + waveLife = 10f; + waveRadBase = 7f; + waveColor = Pal.heal; + waveRad = 30f; + smokes = 6; + smokeColor = Color.white; + sparkColor = Pal.heal; + sparks = 6; + sparkRad = 35f; + sparkStroke = 1.5f; + sparkLen = 4f; + }}; + + weaveScale = 8f; + weaveMag = 1f; + + trailColor = Pal.heal; + trailParam = 5f; + trailInterval = 3f; + + fragBullets = 7; + fragVelocityMin = 0.3f; + + fragBullet = new MissileBulletType(3.9f, 12){{ + homingPower = 0.2f; + weaveMag = 4; + weaveScale = 4; + lifetime = 70f; + shootEffect = Fx.shootHeal; + smokeEffect = Fx.hitLaser; + splashDamage = 13f; + splashDamageRadius = 20f; + frontColor = Color.white; + hitSound = Sounds.none; + + lightColor = Pal.heal; + lightRadius = 40f; + lightOpacity = 0.7f; + + trailInterval = 2f; + trailParam = 3f; + healPercent = 2.8f; + collidesTeam = true; + backColor = Pal.heal; + trailColor = Pal.heal; + + despawnEffect = Fx.none; + hitEffect = new ExplosionEffect(){{ + lifetime = 20f; + waveStroke = 2f; + waveColor = Pal.heal; + waveRad = 12f; + smokeSize = 0f; + smokeSizeBase = 0f; + sparkColor = Pal.heal; + sparks = 9; + sparkRad = 35f; + sparkLen = 4f; + sparkStroke = 1.5f; + }}; + }}; + }}; + }}); + }}; + + aegires = new UnitType("aegires"){{ + health = 12000; + armor = 12f; + + speed = 0.7f; + drag = 0.17f; + hitSize = 44f; + accel = 0.2f; + rotateSpeed = 1.4f; + rotateShooting = false; + + //clip size is massive due to energy field + clipSize = 250f; + + trailLength = 50; + trailX = 18f; + trailY = -17f; + trailScl = 3.2f; + + buildSpeed = 3.5f; + + abilities.add(new EnergyFieldAbility(35f, 65f, 180f){{ + repair = 35f; + statusDuration = 60f * 6f; + maxTargets = 25; + }}); + + for(float mountY : new float[]{-18f, 14}){ + weapons.add(new PointDefenseWeapon("point-defense-mount"){{ + x = 12.5f; + y = mountY; + reload = 6f; + targetInterval = 8f; + targetSwitchInterval = 8f; + + bullet = new BulletType(){{ + shootEffect = Fx.sparkShoot; + hitEffect = Fx.pointHit; + maxRange = 180f; + damage = 24f; + }}; + }}); + } + }}; + + navanax = new UnitType("navanax"){{ + health = 20000; + speed = 0.65f; + drag = 0.17f; + hitSize = 58f; + armor = 16f; + accel = 0.2f; + rotateSpeed = 1.1f; + rotateShooting = false; + + trailLength = 70; + trailX = 23f; + trailY = -32f; + trailScl = 3.5f; + + buildSpeed = 3.8f; + + for(float mountY : new float[]{-117/4f, 50/4f}){ + for(float sign : Mathf.signs){ + weapons.add(new Weapon("plasma-laser-mount"){{ + shadow = 20f; + controllable = false; + autoTarget = true; + mirror = false; + shake = 3f; + shootY = 7f; + rotate = true; + x = 84f/4f * sign; + y = mountY; + + targetInterval = 20f; + targetSwitchInterval = 35f; + + rotateSpeed = 3.5f; + reload = 170f; + recoil = 1f; + shootSound = Sounds.beam; + continuous = true; + cooldownTime = reload; + immunities.add(StatusEffects.burning); + + bullet = new ContinuousLaserBulletType(){{ + maxRange = 90f; + damage = 25f; + length = 90f; + hitEffect = Fx.hitMeltHeal; + drawSize = 200f; + lifetime = 155f; + shake = 1f; + + shootEffect = Fx.shootHeal; + smokeEffect = Fx.none; + width = 4f; + largeHit = false; + + incendChance = 0.03f; + incendSpread = 5f; + incendAmount = 1; + + healPercent = 0.4f; + collidesTeam = true; + + colors = new Color[]{Pal.heal.cpy().a(.2f), Pal.heal.cpy().a(.5f), Pal.heal.cpy().mul(1.2f), Color.white}; + }}; + }}); + } + } + + weapons.add(new Weapon("emp-cannon-mount"){{ + rotate = true; + + x = 70f/4f; + y = -26f/4f; + + reload = 70f; + shake = 3f; + rotateSpeed = 2f; + shadow = 30f; + shootY = 7f; + recoil = 4f; + cooldownTime = reload - 10f; + + bullet = new EmpBulletType(){{ + float rad = 100f; + + lightOpacity = 0.7f; + unitDamageScl = 0.8f; + healPercent = 20f; + timeIncrease = 3f; + timeDuration = 60f * 20f; + powerDamageScl = 3f; + damage = 40; + hitColor = lightColor = Pal.heal; + lightRadius = 70f; + clipSize = 250f; + shootEffect = Fx.hitEmpSpark; + smokeEffect = Fx.shootBigSmoke2; + lifetime = 60f; + sprite = "circle-bullet"; + backColor = Pal.heal; + frontColor = Color.white; + width = height = 12f; + speed = 5f; + trailLength = 20; + trailWidth = 6f; + trailColor = Pal.heal; + trailInterval = 3f; + splashDamage = 40f; + splashDamageRadius = rad; + hitShake = 4f; + trailRotation = true; + status = StatusEffects.electrified; + + trailEffect = new Effect(16f, e -> { + color(Pal.heal); + for(int s : Mathf.signs){ + Drawf.tri(e.x, e.y, 4f, 30f * e.fslope(), e.rotation + 90f*s); + } + }); + + hitEffect = new Effect(50f, 100f, e -> { + e.scaled(7f, b -> { + color(Pal.heal, b.fout()); + Fill.circle(e.x, e.y, rad); + }); + + color(Pal.heal); + stroke(e.fout() * 3f); + Lines.circle(e.x, e.y, rad); + + int points = 10; + float offset = Mathf.randomSeed(e.id, 360f); + for(int i = 0; i < points; i++){ + float angle = i* 360f / points + offset; + //for(int s : Mathf.zeroOne){ + Drawf.tri(e.x + Angles.trnsx(angle, rad), e.y + Angles.trnsy(angle, rad), 6f, 50f * e.fout(), angle/* + s*180f*/); + //} + } + + Fill.circle(e.x, e.y, 12f * e.fout()); + color(); + Fill.circle(e.x, e.y, 6f * e.fout()); + Drawf.light(e.x, e.y, rad * 1.6f, Pal.heal, e.fout()); + }); + }}; + }}); + }}; + //endregion //region core diff --git a/core/src/mindustry/content/Weathers.java b/core/src/mindustry/content/Weathers.java index 8e3ea38a92..ee89e4f82c 100644 --- a/core/src/mindustry/content/Weathers.java +++ b/core/src/mindustry/content/Weathers.java @@ -14,7 +14,8 @@ public class Weathers implements ContentList{ snow, sandstorm, sporestorm, - fog; + fog, + suspendParticles; @Override public void load(){ @@ -102,5 +103,19 @@ public class Weathers implements ContentList{ attrs.set(Attribute.water, 0.05f); opacityMultiplier = 0.47f; }}; + + suspendParticles = new ParticleWeather("suspend-particles"){{ + color = noiseColor = Color.valueOf("a7c1fa"); + particleRegion = "particle"; + statusGround = false; + useWindVector = true; + sizeMax = 4f; + sizeMin = 1.4f; + minAlpha = 0.5f; + maxAlpha = 1f; + density = 10000f; + baseSpeed = 0.03f; + }}; } + } diff --git a/core/src/mindustry/core/ContentLoader.java b/core/src/mindustry/core/ContentLoader.java index 582bbd8bf8..e00fec3061 100644 --- a/core/src/mindustry/core/ContentLoader.java +++ b/core/src/mindustry/core/ContentLoader.java @@ -104,6 +104,7 @@ public class ContentLoader{ /** Calls Content#load() on everything. Use only after all modules have been created on the client.*/ public void load(){ + initialize(Content::loadIcon); initialize(Content::load); } @@ -132,9 +133,9 @@ public class ContentLoader{ /** Loads block colors. */ public void loadColors(){ Pixmap pixmap = new Pixmap(files.internal("sprites/block_colors.png")); - for(int i = 0; i < pixmap.getWidth(); i++){ + for(int i = 0; i < pixmap.width; i++){ if(blocks().size > i){ - int color = pixmap.getPixel(i, 0); + int color = pixmap.get(i, 0); if(color == 0 || color == 255) continue; @@ -290,6 +291,10 @@ public class ContentLoader{ return getBy(ContentType.unit); } + public UnitType unit(int id){ + return getByID(ContentType.unit, id); + } + public Seq planets(){ return getBy(ContentType.planet); } diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index 1799d7674c..bb4c1082b1 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -28,7 +28,6 @@ import mindustry.maps.Map; import mindustry.maps.*; import mindustry.net.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.ui.dialogs.*; import mindustry.world.*; @@ -123,6 +122,10 @@ public class Control implements ApplicationListener, Loadable{ //add player when world loads regardless Events.on(WorldLoadEvent.class, e -> { player.add(); + //make player admin on any load when hosting + if(net.active() && net.server()){ + player.admin = true; + } }); //autohost for pvp maps @@ -130,7 +133,7 @@ public class Control implements ApplicationListener, Loadable{ if(state.rules.pvp && !net.active()){ try{ net.host(port); - player.admin(true); + player.admin = true; }catch(IOException e){ ui.showException("@server.error", e); state.set(State.menu); @@ -148,7 +151,7 @@ public class Control implements ApplicationListener, Loadable{ if(e.content instanceof SectorPreset){ for(TechNode node : TechTree.all){ if(!node.content.unlocked() && node.objectives.contains(o -> o instanceof SectorComplete sec && sec.preset == e.content) && !node.objectives.contains(o -> !o.complete())){ - ui.hudfrag.showToast(new TextureRegionDrawable(node.content.icon(Cicon.large)), bundle.get("available")); + ui.hudfrag.showToast(new TextureRegionDrawable(node.content.uiIcon), iconLarge, bundle.get("available")); } } } @@ -192,33 +195,36 @@ public class Control implements ApplicationListener, Loadable{ }); Events.run(Trigger.newGame, () -> { - Building core = player.closestCore(); + Building core = player.bestCore(); if(core == null) return; - //TODO this sounds pretty bad due to conflict - if(settings.getInt("musicvol") > 0){ - Musics.land.stop(); - Musics.land.play(); - Musics.land.setVolume(settings.getInt("musicvol") / 100f); - } - - app.post(() -> ui.hudfrag.showLand()); - renderer.zoomIn(Fx.coreLand.lifetime); - app.post(() -> Fx.coreLand.at(core.getX(), core.getY(), 0, core.block)); camera.position.set(core); player.set(core); - Time.run(Fx.coreLand.lifetime, () -> { - Fx.launch.at(core); - Effect.shake(5f, 5f, core); - - if(state.isCampaign()){ - ui.announce("[accent]" + state.rules.sector.name() + "\n" + - (state.rules.sector.info.resources.any() ? "[lightgray]" + bundle.get("sectors.resources") + "[white] " + - state.rules.sector.info.resources.toString(" ", u -> u.emoji()) : ""), 5); + if(showLandAnimation){ + //TODO this sounds pretty bad due to conflict + if(settings.getInt("musicvol") > 0){ + Musics.land.stop(); + Musics.land.play(); + Musics.land.setVolume(settings.getInt("musicvol") / 100f); } - }); + + app.post(() -> ui.hudfrag.showLand()); + renderer.zoomIn(Fx.coreLand.lifetime); + app.post(() -> Fx.coreLand.at(core.getX(), core.getY(), 0, core.block)); + + Time.run(Fx.coreLand.lifetime, () -> { + Fx.launch.at(core); + Effect.shake(5f, 5f, core); + + if(state.isCampaign()){ + ui.announce("[accent]" + state.rules.sector.name() + "\n" + + (state.rules.sector.info.resources.any() ? "[lightgray]" + bundle.get("sectors.resources") + "[white] " + + state.rules.sector.info.resources.toString(" ", u -> u.emoji()) : ""), 5); + } + }); + } }); } @@ -345,7 +351,7 @@ public class Control implements ApplicationListener, Loadable{ //reset wave so things are more fair state.wave = 1; //set up default wave time - state.wavetime = state.rules.waveSpacing * 2f; + state.wavetime = state.rules.waveSpacing * (sector.preset == null ? 2f : sector.preset.startWaveTimeMultiplier); //reset captured state sector.info.wasCaptured = false; //re-enable waves diff --git a/core/src/mindustry/core/GameState.java b/core/src/mindustry/core/GameState.java index de7cf6f60c..33050b996e 100644 --- a/core/src/mindustry/core/GameState.java +++ b/core/src/mindustry/core/GameState.java @@ -17,7 +17,9 @@ public class GameState{ /** Wave countdown in ticks. */ public float wavetime; /** Whether the game is in game over state. */ - public boolean gameOver = false, serverPaused = false, wasTimeout; + public boolean gameOver = false, serverPaused = false; + /** Server ticks/second. Only valid in multiplayer. */ + public int serverTps = -1; /** Map that is currently being played on. */ public Map map = emptyMap; /** The current game rules. */ @@ -33,8 +35,9 @@ public class GameState{ /** Current game state. */ private State state = State.menu; + @Nullable public Unit boss(){ - return teams.boss; + return teams.bosses.firstOpt(); } public void set(State astate){ diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 5f39ad8017..cd9c86810b 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -14,6 +14,7 @@ import mindustry.maps.*; import mindustry.type.*; import mindustry.type.Weather.*; import mindustry.world.*; +import mindustry.world.blocks.storage.CoreBlock.*; import java.util.*; @@ -34,8 +35,8 @@ public class Logic implements ApplicationListener{ Events.on(BlockDestroyEvent.class, event -> { //blocks that get broken are appended to the team's broken block queue Tile tile = event.tile; - //skip null entities or un-rebuildables, for obvious reasons; also skip client since they can't modify these requests - if(tile.build == null || !tile.block().rebuildable || net.client()) return; + //skip null entities or un-rebuildables, for obvious reasons + if(tile.build == null || !tile.block().rebuildable) return; tile.build.addPlan(true); }); @@ -174,10 +175,11 @@ public class Logic implements ApplicationListener{ if(!state.isCampaign()){ for(TeamData team : state.teams.getActive()){ if(team.hasCore()){ - Building entity = team.core(); + CoreBuild entity = team.core(); entity.items.clear(); for(ItemStack stack : state.rules.loadout){ - entity.items.add(stack.item, stack.amount); + //make sure to cap storage + entity.items.add(stack.item, Math.min(stack.amount, entity.storageCapacity - entity.items.get(stack.item))); } } } @@ -406,6 +408,7 @@ public class Logic implements ApplicationListener{ //apply weather attributes state.envAttrs.clear(); + state.envAttrs.add(state.rules.attributes); Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity)); Groups.update(); diff --git a/core/src/mindustry/core/NetClient.java b/core/src/mindustry/core/NetClient.java index be8fb108a0..e1ac7f97f1 100644 --- a/core/src/mindustry/core/NetClient.java +++ b/core/src/mindustry/core/NetClient.java @@ -16,9 +16,9 @@ import mindustry.entities.*; import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.game.*; +import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.net.Administration.*; -import mindustry.net.Net.*; import mindustry.net.*; import mindustry.net.Packets.*; import mindustry.ui.*; @@ -32,9 +32,9 @@ import java.util.zip.*; import static mindustry.Vars.*; public class NetClient implements ApplicationListener{ - private static final float dataTimeout = 60 * 18; - private static final float playerSyncTime = 2; - public static final float viewScale = 2f; + private static final float dataTimeout = 60 * 20; + private static final float playerSyncTime = 5; + private static final Reads dataReads = new Reads(null); private long ping; private Interval timer = new Interval(5); @@ -62,7 +62,7 @@ public class NetClient implements ApplicationListener{ net.handleClient(Connect.class, packet -> { Log.info("Connecting to server: @", packet.addressTCP); - player.admin(false); + player.admin = false; reset(); @@ -92,7 +92,7 @@ public class NetClient implements ApplicationListener{ c.mods = mods.getModStrings(); c.mobile = mobile; c.versionType = Version.type; - c.color = player.color().rgba(); + c.color = player.color.rgba(); c.usid = getUsid(packet.addressTCP); c.uuid = platform.getUUID(); @@ -103,7 +103,7 @@ public class NetClient implements ApplicationListener{ return; } - net.send(c, SendMode.tcp); + net.send(c, true); }); net.handleClient(Disconnect.class, packet -> { @@ -112,19 +112,19 @@ public class NetClient implements ApplicationListener{ connecting = false; logic.reset(); platform.updateRPC(); - player.name(Core.settings.getString("name")); - player.color().set(Core.settings.getInt("color-0")); + player.name = Core.settings.getString("name"); + player.color.set(Core.settings.getInt("color-0")); if(quiet) return; Time.runTask(3f, ui.loadfrag::hide); if(packet.reason != null){ - switch(packet.reason){ - case "closed" -> ui.showSmall("@disconnect", "@disconnect.closed"); - case "timeout" -> ui.showSmall("@disconnect", "@disconnect.timeout"); - case "error" -> ui.showSmall("@disconnect", "@disconnect.error"); - } + ui.showSmall(switch(packet.reason){ + case "closed" -> "@disconnect.closed"; + case "timeout" -> "@disconnect.timeout"; + default -> "@disconnect.error"; + }, "@disconnect.closed"); }else{ ui.showErrorMessage("@disconnect"); } @@ -136,10 +136,6 @@ public class NetClient implements ApplicationListener{ finishConnecting(); }); - - net.handleClient(InvokePacket.class, packet -> { - RemoteReadClient.readPacket(packet.reader(), packet.type); - }); } public void addPacketHandler(String type, Cons handler){ @@ -428,9 +424,9 @@ public class NetClient implements ApplicationListener{ } @Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true) - public static void entitySnapshot(short amount, short dataLen, byte[] data){ + public static void entitySnapshot(short amount, byte[] data){ try{ - netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen)); + netClient.byteStream.setBytes(data); DataInputStream input = netClient.dataStream; for(int j = 0; j < amount; j++){ @@ -474,9 +470,9 @@ public class NetClient implements ApplicationListener{ } @Remote(variants = Variant.both, priority = PacketPriority.low, unreliable = true) - public static void blockSnapshot(short amount, short dataLen, byte[] data){ + public static void blockSnapshot(short amount, byte[] data){ try{ - netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen)); + netClient.byteStream.setBytes(data); DataInputStream input = netClient.dataStream; for(int i = 0; i < amount; i++){ @@ -499,7 +495,7 @@ public class NetClient implements ApplicationListener{ } @Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true) - public static void stateSnapshot(float waveTime, int wave, int enemies, boolean paused, boolean gameOver, int timeData, short coreDataLen, byte[] coreData){ + public static void stateSnapshot(float waveTime, int wave, int enemies, boolean paused, boolean gameOver, int timeData, byte tps, byte[] coreData){ try{ if(wave > state.wave){ state.wave = wave; @@ -511,21 +507,22 @@ public class NetClient implements ApplicationListener{ state.wave = wave; state.enemies = enemies; state.serverPaused = paused; + state.serverTps = tps & 0xff; universe.updateNetSeconds(timeData); - netClient.byteStream.setBytes(net.decompressSnapshot(coreData, coreDataLen)); + netClient.byteStream.setBytes(coreData); DataInputStream input = netClient.dataStream; + dataReads.input = input; - int cores = input.readInt(); - for(int i = 0; i < cores; i++){ - int pos = input.readInt(); - Tile tile = world.tile(pos); - - if(tile != null && tile.build != null){ - tile.build.items.read(Reads.get(input)); + int teams = input.readUnsignedByte(); + for(int i = 0; i < teams; i++){ + int team = input.readUnsignedByte(); + TeamData data = state.teams.get(Team.all[team]); + if(data.cores.any()){ + data.cores.first().items.read(dataReads); }else{ - new ItemModule().read(Reads.get(input)); + new ItemModule().read(dataReads); } } @@ -665,7 +662,7 @@ public class NetClient implements ApplicationListener{ player.boosting, player.shooting, ui.chatfrag.shown(), control.input.isBuilding, requests, Core.camera.position.x, Core.camera.position.y, - Core.camera.width * viewScale, Core.camera.height * viewScale + Core.camera.width, Core.camera.height ); } diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index 1e26c88c11..dbdb804e75 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -23,7 +23,6 @@ import mindustry.net.*; import mindustry.net.Administration.*; import mindustry.net.Packets.*; import mindustry.world.*; -import mindustry.world.blocks.storage.CoreBlock.*; import java.io.*; import java.net.*; @@ -38,8 +37,8 @@ public class NetServer implements ApplicationListener{ private static final int maxSnapshotSize = 800, timerBlockSync = 0, serverSyncTime = 200; private static final float blockSyncTime = 60 * 6; private static final FloatBuffer fbuffer = FloatBuffer.allocate(20); + private static final Writes dataWrites = new Writes(null); 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 = tilesize * 14f; @@ -101,6 +100,8 @@ public class NetServer implements ApplicationListener{ packet.uuid = con.address.substring("steam:".length()); } + Events.fire(new ConnectPacketEvent(con, packet)); + con.connectTime = Time.millis(); String uuid = packet.uuid; @@ -109,8 +110,7 @@ public class NetServer implements ApplicationListener{ crc.update(buuid, 0, 8); ByteBuffer buff = ByteBuffer.allocate(8); buff.put(buuid, 8, 8); - buff.position(0); - if(crc.getValue() != buff.getLong()){ + if(crc.getValue() != buff.getLong(0)){ con.kick(KickReason.clientOutdated); return; } @@ -254,22 +254,6 @@ public class NetServer implements ApplicationListener{ Events.fire(new PlayerConnect(player)); }); - net.handleServer(InvokePacket.class, (con, packet) -> { - if(con.player == null || con.kicked) return; - - try{ - RemoteReadServer.readPacket(packet.reader(), packet.type, con.player); - }catch(ValidateException e){ - debug("Validation failed for '@': @", e.player, e.getMessage()); - }catch(RuntimeException e){ - if(e.getCause() instanceof ValidateException v){ - debug("Validation failed for '@': @", v.player, v.getMessage()); - }else{ - throw e; - } - } - }); - registerCommands(); } @@ -659,7 +643,7 @@ public class NetServer implements ApplicationListener{ if(!player.dead()){ Unit unit = player.unit(); - long elapsed = Time.timeSinceMillis(con.lastReceivedClientTime); + long elapsed = Math.min(Time.timeSinceMillis(con.lastReceivedClientTime), 1500); float maxSpeed = unit.realSpeed(); float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.2f; @@ -847,8 +831,7 @@ public class NetServer implements ApplicationListener{ if(syncStream.size() > maxSnapshotSize){ dataStream.close(); - byte[] stateBytes = syncStream.toByteArray(); - Call.blockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes)); + Call.blockSnapshot(sent, syncStream.toByteArray()); sent = 0; syncStream.reset(); } @@ -856,31 +839,30 @@ public class NetServer implements ApplicationListener{ if(sent > 0){ dataStream.close(); - byte[] stateBytes = syncStream.toByteArray(); - Call.blockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes)); + Call.blockSnapshot(sent, syncStream.toByteArray()); } } public void writeEntitySnapshot(Player player) throws IOException{ + byte tps = (byte)Math.min(Core.graphics.getFramesPerSecond(), 255); syncStream.reset(); - int sum = state.teams.present.sum(t -> t.cores.size); + int activeTeams = (byte)state.teams.present.count(t -> t.cores.size > 0); - dataStream.writeInt(sum); + dataStream.writeByte(activeTeams); + dataWrites.output = dataStream; + //block data isn't important, just send the items for each team, they're synced across cores for(TeamData data : state.teams.present){ - for(CoreBuild entity : data.cores){ - dataStream.writeInt(entity.tile.pos()); - entity.items.write(Writes.get(dataStream)); + if(data.cores.size > 0){ + dataStream.writeByte(data.team.id); + data.cores.first().items.write(dataWrites); } } dataStream.close(); - byte[] stateBytes = syncStream.toByteArray(); //write basic state data. - Call.stateSnapshot(player.con, state.wavetime, state.wave, state.enemies, state.serverPaused, state.gameOver, universe.seconds(), (short)stateBytes.length, net.compressSnapshot(stateBytes)); - - viewport.setSize(player.con.viewWidth, player.con.viewHeight).setCenter(player.con.viewX, player.con.viewY); + Call.stateSnapshot(player.con, state.wavetime, state.wave, state.enemies, state.serverPaused, state.gameOver, universe.seconds(), tps, syncStream.toByteArray()); syncStream.reset(); @@ -896,8 +878,7 @@ public class NetServer implements ApplicationListener{ if(syncStream.size() > maxSnapshotSize){ dataStream.close(); - byte[] syncBytes = syncStream.toByteArray(); - Call.entitySnapshot(player.con, (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes)); + Call.entitySnapshot(player.con, (short)sent, syncStream.toByteArray()); sent = 0; syncStream.reset(); } @@ -906,8 +887,7 @@ public class NetServer implements ApplicationListener{ if(sent > 0){ dataStream.close(); - byte[] syncBytes = syncStream.toByteArray(); - Call.entitySnapshot(player.con, (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes)); + Call.entitySnapshot(player.con, (short)sent, syncStream.toByteArray()); } } diff --git a/core/src/mindustry/core/Platform.java b/core/src/mindustry/core/Platform.java index 911583f822..2adf5dc790 100644 --- a/core/src/mindustry/core/Platform.java +++ b/core/src/mindustry/core/Platform.java @@ -21,8 +21,8 @@ import static mindustry.Vars.*; public interface Platform{ /** Dynamically creates a class loader for a jar file. */ - default ClassLoader loadJar(Fi jar, String mainClass) throws Exception{ - return new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, getClass().getClassLoader()); + default ClassLoader loadJar(Fi jar, ClassLoader parent) throws Exception{ + return new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, parent); } /** Steam: Update lobby visibility.*/ @@ -59,6 +59,15 @@ public interface Platform{ } default Context getScriptContext(){ + ContextFactory.getGlobalSetter().setContextFactoryGlobal(new ContextFactory(){ + @Override + protected Context makeContext(){ + Context ctx = super.makeContext(); + ctx.setClassShutter(Scripts::allowClass); + return ctx; + } + }); + Context c = Context.enter(); c.setOptimizationLevel(9); return c; diff --git a/core/src/mindustry/core/Renderer.java b/core/src/mindustry/core/Renderer.java index 37953f0f9c..83f32408a8 100644 --- a/core/src/mindustry/core/Renderer.java +++ b/core/src/mindustry/core/Renderer.java @@ -3,17 +3,19 @@ package mindustry.core; import arc.*; import arc.files.*; import arc.graphics.*; +import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.graphics.gl.*; import arc.math.*; import arc.scene.ui.layout.*; +import arc.struct.*; import arc.util.*; +import arc.util.async.*; import mindustry.content.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.graphics.g3d.*; -import mindustry.ui.*; import mindustry.world.blocks.storage.CoreBlock.*; import static arc.Core.*; @@ -35,6 +37,8 @@ public class Renderer implements ApplicationListener{ public boolean animateShields, drawWeather = true, drawStatus; /** minZoom = zooming out, maxZoom = zooming in */ public float minZoom = 1.5f, maxZoom = 6f; + public Seq envRenderers = new Seq<>(); + public TextureRegion[] bubbles = new TextureRegion[16], splashes = new TextureRegion[12]; private @Nullable CoreBuild landCore; private Color clearColor = new Color(0f, 0f, 0f, 1f); @@ -51,6 +55,10 @@ public class Renderer implements ApplicationListener{ shaketime = Math.max(shaketime, duration); } + public void addEnvRenderer(int mask, Runnable render){ + envRenderers.add(new EnvRenderer(mask, render)); + } + @Override public void init(){ planets = new PlanetRenderer(); @@ -59,9 +67,18 @@ public class Renderer implements ApplicationListener{ setupBloom(); } - Events.on(WorldLoadEvent.class, e -> { + Events.run(Trigger.newGame, () -> { landCore = player.bestCore(); }); + + EnvRenderers.init(); + for(int i = 0; i < bubbles.length; i++) bubbles[i] = atlas.find("bubble-" + i); + for(int i = 0; i < splashes.length; i++) splashes[i] = atlas.find("splash-" + i); + + assets.load("sprites/clouds.png", Texture.class).loaded = t -> { + ((Texture)t).setWrap(TextureWrap.repeat); + ((Texture)t).setFilter(TextureFilter.linear); + }; } @Override @@ -77,7 +94,9 @@ public class Renderer implements ApplicationListener{ drawStatus = Core.settings.getBool("blockstatus"); if(landTime > 0){ - landTime -= Time.delta; + if(!state.isPaused()){ + landTime -= Time.delta; + } landscale = Interp.pow5In.apply(minZoomScl, Scl.scl(4f), 1f - landTime / Fx.coreLand.lifetime); camerascale = landscale; weatherAlpha = 0f; @@ -204,6 +223,13 @@ public class Renderer implements ApplicationListener{ Draw.drawRange(Layer.blockBuilding, () -> Draw.shader(Shaders.blockbuild, true), Draw::shader); + //render all matching environments + for(var renderer : envRenderers){ + if((renderer.env & state.rules.environment) == renderer.env){ + renderer.renderer.run(); + } + } + if(state.rules.lighting){ Draw.draw(Layer.light, lights::draw); } @@ -214,8 +240,8 @@ public class Renderer implements ApplicationListener{ if(bloom != null){ bloom.resize(graphics.getWidth() / 4, graphics.getHeight() / 4); - Draw.draw(Layer.bullet - 0.01f, bloom::capture); - Draw.draw(Layer.effect + 0.01f, bloom::render); + Draw.draw(Layer.bullet - 0.02f, bloom::capture); + Draw.draw(Layer.effect + 0.02f, bloom::render); } Draw.draw(Layer.plans, overlays::drawBottom); @@ -252,24 +278,38 @@ public class Renderer implements ApplicationListener{ private void drawLanding(){ CoreBuild entity = landCore == null ? player.bestCore() : landCore; + //var clouds = assets.get("sprites/clouds.png", Texture.class); if(landTime > 0 && entity != null){ - float fract = landTime / Fx.coreLand.lifetime; + float fout = landTime / Fx.coreLand.lifetime; - TextureRegion reg = entity.block.icon(Cicon.full); + //TODO clouds + /* + float scaling = 10000f; + float sscl = 1f + fout*1.5f; + float offset = -0.38f; + + Tmp.tr1.set(clouds); + Tmp.tr1.set((camera.position.x - camera.width/2f * sscl) / scaling, (camera.position.y - camera.height/2f * sscl) / scaling, (camera.position.x + camera.width/2f * sscl) / scaling, (camera.position.y + camera.height/2f * sscl) / scaling); + Draw.alpha(Mathf.slope(Mathf.clamp(((1f - fout) + offset)/(1f + offset)))); + Draw.mixcol(Pal.spore, 0.5f); + Draw.rect(Tmp.tr1, camera.position.x, camera.position.y, camera.width, camera.height); + Draw.reset();*/ + + TextureRegion reg = entity.block.fullIcon; float scl = Scl.scl(4f) / camerascale; - float s = reg.width * Draw.scl * scl * 4f * fract; + float s = reg.width * Draw.scl * scl * 4f * fout; Draw.color(Pal.lightTrail); Draw.rect("circle-shadow", entity.x, entity.y, s, s); - Angles.randLenVectors(1, (1f- fract), 100, 1000f * scl * (1f-fract), (x, y, fin, fout) -> { - Lines.stroke(scl * fin); - Lines.lineAngle(entity.x + x, entity.y + y, Mathf.angle(x, y), (fin * 20 + 1f) * scl); + Angles.randLenVectors(1, (1f- fout), 100, 1000f * scl * (1f-fout), (x, y, ffin, ffout) -> { + Lines.stroke(scl * ffin); + Lines.lineAngle(entity.x + x, entity.y + y, Mathf.angle(x, y), (ffin * 20 + 1f) * scl); }); Draw.color(); - Draw.mixcol(Color.white, fract); - Draw.rect(reg, entity.x, entity.y, reg.width * Draw.scl * scl, reg.height * Draw.scl * scl, fract * 135f); + Draw.mixcol(Color.white, fout); + Draw.rect(reg, entity.x, entity.y, reg.width * Draw.scl * scl, reg.height * Draw.scl * scl, fout * 135f); Draw.reset(); } @@ -330,25 +370,39 @@ public class Renderer implements ApplicationListener{ camera.position.y = h / 2f + tilesize / 2f; buffer.begin(); draw(); + Draw.flush(); + byte[] lines = ScreenUtils.getFrameBufferPixels(0, 0, w, h, true); buffer.end(); disableUI = false; camera.width = vpW; camera.height = vpH; camera.position.set(px, py); - buffer.begin(); - byte[] lines = ScreenUtils.getFrameBufferPixels(0, 0, w, h, true); - for(int i = 0; i < lines.length; i += 4){ - lines[i + 3] = (byte)255; - } - buffer.end(); - Pixmap fullPixmap = new Pixmap(w, h, Pixmap.Format.rgba8888); - Buffers.copy(lines, 0, fullPixmap.getPixels(), lines.length); - Fi file = screenshotDirectory.child("screenshot-" + Time.millis() + ".png"); - PixmapIO.writePNG(file, fullPixmap); - fullPixmap.dispose(); - ui.showInfoFade(Core.bundle.format("screenshot", file.toString())); drawWeather = true; - buffer.dispose(); + + Threads.thread(() -> { + for(int i = 0; i < lines.length; i += 4){ + lines[i + 3] = (byte)255; + } + Pixmap fullPixmap = new Pixmap(w, h); + Buffers.copy(lines, 0, fullPixmap.getPixels(), lines.length); + Fi file = screenshotDirectory.child("screenshot-" + Time.millis() + ".png"); + PixmapIO.writePng(file, fullPixmap); + fullPixmap.dispose(); + app.post(() -> ui.showInfoFade(Core.bundle.format("screenshot", file.toString()))); + }); } + + public static class EnvRenderer{ + /** Environment bitmask; must match env exactly when and-ed. */ + public final int env; + /** Rendering callback. */ + public final Runnable renderer; + + public EnvRenderer(int env, Runnable renderer){ + this.env = env; + this.renderer = renderer; + } + } + } diff --git a/core/src/mindustry/core/UI.java b/core/src/mindustry/core/UI.java index 279427f6f4..44d4008dba 100644 --- a/core/src/mindustry/core/UI.java +++ b/core/src/mindustry/core/UI.java @@ -33,6 +33,8 @@ import static arc.scene.actions.Actions.*; import static mindustry.Vars.*; public class UI implements ApplicationListener, Loadable{ + private static String billions, millions, thousands; + public static PixmapPacker packer; public MenuFragment menufrag; @@ -56,7 +58,7 @@ public class UI implements ApplicationListener, Loadable{ public HostDialog host; public PausedDialog paused; public SettingsMenuDialog settings; - public ControlsDialog controls; + public KeybindDialog controls; public MapEditorDialog editor; public LanguageDialog language; public BansDialog bans; @@ -152,6 +154,10 @@ public class UI implements ApplicationListener, Loadable{ @Override public void init(){ + billions = Core.bundle.get("unit.billions"); + millions = Core.bundle.get("unit.millions"); + thousands = Core.bundle.get("unit.thousands"); + menuGroup = new WidgetGroup(); hudGroup = new WidgetGroup(); @@ -166,7 +172,7 @@ public class UI implements ApplicationListener, Loadable{ picker = new ColorPicker(); editor = new MapEditorDialog(); - controls = new ControlsDialog(); + controls = new KeybindDialog(); restart = new GameOverDialog(); join = new JoinDialog(); discord = new DiscordDialog(); @@ -347,17 +353,11 @@ public class UI implements ApplicationListener, Loadable{ } public void showInfo(String info){ - showInfo(info, () -> {}); - } - - public void showInfo(String info, Runnable listener){ new Dialog(""){{ getCell(cont).growX(); cont.margin(15).add(info).width(400f).wrap().get().setAlignment(Align.center, Align.center); - buttons.button("@ok", () -> { - hide(); - listener.run(); - }).size(110, 50).pad(4); + buttons.button("@ok", this::hide).size(110, 50).pad(4); + keyDown(KeyCode.enter, this::hide); closeOnBack(); }}.show(); } @@ -458,6 +458,10 @@ public class UI implements ApplicationListener, Loadable{ }}.show(); } + public void showConfirm(String text, Runnable confirmed){ + showConfirm("@confirm", text, null, confirmed); + } + public void showConfirm(String title, String text, Runnable confirmed){ showConfirm(title, text, null, confirmed); } @@ -535,6 +539,13 @@ public class UI implements ApplicationListener, Loadable{ dialog.show(); } + public static String formatTime(float ticks){ + int time = (int)(ticks / 60); + if(time < 60) return "0:" + (time < 10 ? "0" : "") + time; + int mod = time % 60; + return (time / 60) + ":" + (mod < 10 ? "0" : "") + mod; + } + public static String formatAmount(long number){ //prevent overflow if(number == Long.MIN_VALUE) number ++; @@ -542,13 +553,13 @@ public class UI implements ApplicationListener, Loadable{ long mag = Math.abs(number); String sign = number < 0 ? "-" : ""; if(mag >= 1_000_000_000){ - return sign + Strings.fixed(mag / 1_000_000_000f, 1) + "[gray]" + Core.bundle.get("unit.billions") + "[]"; + return sign + Strings.fixed(mag / 1_000_000_000f, 1) + "[gray]" + billions+ "[]"; }else if(mag >= 1_000_000){ - return sign + Strings.fixed(mag / 1_000_000f, 1) + "[gray]" + Core.bundle.get("unit.millions") + "[]"; + return sign + Strings.fixed(mag / 1_000_000f, 1) + "[gray]" +millions + "[]"; }else if(mag >= 10_000){ - return number / 1000 + "[gray]" + Core.bundle.get("unit.thousands") + "[]"; + return number / 1000 + "[gray]" + thousands + "[]"; }else if(mag >= 1000){ - return sign + Strings.fixed(mag / 1000f, 1) + "[gray]" + Core.bundle.get("unit.thousands") + "[]"; + return sign + Strings.fixed(mag / 1000f, 1) + "[gray]" + thousands + "[]"; }else{ return number + ""; } diff --git a/core/src/mindustry/core/World.java b/core/src/mindustry/core/World.java index dcb53437c3..38a4954792 100644 --- a/core/src/mindustry/core/World.java +++ b/core/src/mindustry/core/World.java @@ -271,13 +271,13 @@ public class World{ private void setSectorRules(Sector sector){ state.map = new Map(StringMap.of("name", sector.preset == null ? sector.planet.localizedName + "; Sector " + sector.id : sector.preset.localizedName)); state.rules.sector = sector; - state.rules.weather.clear(); - //apply weather based on terrain - ObjectIntMap floorc = new ObjectIntMap<>(); + sector.planet.generator.addWeather(sector, state.rules); + ObjectSet content = new ObjectSet<>(); + //TODO duplicate code? for(Tile tile : world.tiles){ if(world.getDarkness(tile.x, tile.y) >= 3){ continue; @@ -287,47 +287,6 @@ public class World{ if(tile.floor().itemDrop != null) content.add(tile.floor().itemDrop); if(tile.overlay().itemDrop != null) content.add(tile.overlay().itemDrop); if(liquid != null) content.add(liquid); - - if(!tile.block().isStatic()){ - floorc.increment(tile.floor()); - if(tile.overlay() != Blocks.air){ - floorc.increment(tile.overlay()); - } - } - } - - //sort counts in descending order - Seq> entries = floorc.entries().toArray(); - entries.sort(e -> -e.value); - //remove all blocks occuring < 30 times - unimportant - entries.removeAll(e -> e.value < 30); - - Block[] floors = new Block[entries.size]; - for(int i = 0; i < entries.size; i++){ - floors[i] = entries.get(i).key; - } - - //TODO bad code - boolean hasSnow = floors[0].name.contains("ice") || floors[0].name.contains("snow"); - boolean hasRain = !hasSnow && content.contains(Liquids.water) && !floors[0].name.contains("sand"); - boolean hasDesert = !hasSnow && !hasRain && floors[0] == Blocks.sand; - boolean hasSpores = floors[0].name.contains("spore") || floors[0].name.contains("moss") || floors[0].name.contains("tainted"); - - if(hasSnow){ - state.rules.weather.add(new WeatherEntry(Weathers.snow)); - } - - if(hasRain){ - state.rules.weather.add(new WeatherEntry(Weathers.rain)); - state.rules.weather.add(new WeatherEntry(Weathers.fog)); - } - - if(hasDesert){ - state.rules.weather.add(new WeatherEntry(Weathers.sandstorm)); - } - - if(hasSpores){ - state.rules.weather.add(new WeatherEntry(Weathers.sporestorm)); } sector.info.resources = content.asArray(); @@ -393,12 +352,6 @@ public class World{ if(invalidMap) Core.app.post(() -> state.set(State.menu)); } - public void notifyChanged(Tile tile){ - if(!generating){ - Core.app.post(() -> Events.fire(new TileChangeEvent(tile))); - } - } - public void raycastEachWorld(float x0, float y0, float x1, float y1, Raycaster cons){ raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons); } @@ -464,7 +417,7 @@ public class World{ byte[] dark = new byte[tiles.width * tiles.height]; byte[] writeBuffer = new byte[tiles.width * tiles.height]; - byte darkIterations = 4; + byte darkIterations = darkRadius; for(int i = 0; i < dark.length; i++){ Tile tile = tiles.geti(i); @@ -498,7 +451,7 @@ public class World{ tile.data = dark[idx]; } - if(dark[idx] == 4){ + if(dark[idx] == darkRadius){ boolean full = true; for(Point2 p : Geometry.d4){ int px = p.x + tile.x, py = p.y + tile.y; @@ -509,11 +462,28 @@ public class World{ } } - if(full) tile.data = 5; + if(full) tile.data = darkRadius + 1; } } } + public byte getWallDarkness(Tile tile){ + if(tile.isDarkened()){ + int minDst = darkRadius + 1; + for(int cx = tile.x - darkRadius; cx <= tile.x + darkRadius; cx++){ + for(int cy = tile.y - darkRadius; cy <= tile.y + darkRadius; cy++){ + if(tiles.in(cx, cy) && !rawTile(cx, cy).isDarkened()){ + minDst = Math.min(minDst, Math.abs(cx - tile.x) + Math.abs(cy - tile.y)); + } + } + } + + return (byte)Math.max((minDst - 1), 0); + } + return 0; + } + + //TODO optimize; this is very slow and called too often! public float getDarkness(int x, int y){ int edgeBlend = 2; diff --git a/core/src/mindustry/ctype/Content.java b/core/src/mindustry/ctype/Content.java index 71bf965e3f..4103f4899e 100644 --- a/core/src/mindustry/ctype/Content.java +++ b/core/src/mindustry/ctype/Content.java @@ -7,7 +7,7 @@ import mindustry.mod.Mods.*; /** Base class for a content type that is loaded in {@link mindustry.core.ContentLoader}. */ public abstract class Content implements Comparable, Disposable{ - public final short id; + public short id; /** Info on which mod this content was loaded from. */ public ModContentInfo minfo = new ModContentInfo(); @@ -31,6 +31,9 @@ public abstract class Content implements Comparable, Disposable{ */ public void load(){} + /** Called right after load(). */ + public void loadIcon(){} + /** @return whether an error occurred during mod loading. */ public boolean hasErrored(){ return minfo.error != null; diff --git a/core/src/mindustry/ctype/UnlockableContent.java b/core/src/mindustry/ctype/UnlockableContent.java index 85b29469ae..c28496bb81 100644 --- a/core/src/mindustry/ctype/UnlockableContent.java +++ b/core/src/mindustry/ctype/UnlockableContent.java @@ -3,7 +3,6 @@ package mindustry.ctype; import arc.*; import arc.func.*; import arc.graphics.g2d.*; -import arc.scene.ui.layout.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -30,8 +29,10 @@ public abstract class UnlockableContent extends MappableContent{ public boolean inlineDescription = true; /** Special logic icon ID. */ public int iconId = 0; - /** Icons by Cicon ID.*/ - protected TextureRegion[] cicons = new TextureRegion[Cicon.all.length]; + /** Icon of the content to use in UI. */ + public TextureRegion uiIcon; + /** Icon of the full content. Unscaled.*/ + public TextureRegion fullIcon; /** Unlock state. Loaded from settings. Do not modify outside of the constructor. */ protected boolean unlocked; @@ -44,11 +45,29 @@ public abstract class UnlockableContent extends MappableContent{ this.unlocked = Core.settings != null && Core.settings.getBool(this.name + "-unlocked", false); } + @Override + public void loadIcon(){ + fullIcon = + Core.atlas.find(getContentType().name() + "-" + name + "-full", + Core.atlas.find(name + "-full", + Core.atlas.find(name, + Core.atlas.find(getContentType().name() + "-" + name, + Core.atlas.find(name + "1"))))); + + uiIcon = Core.atlas.find(getContentType().name() + "-" + name + "-ui", fullIcon); + } + /** @return the tech node for this content. may be null. */ public @Nullable TechNode node(){ return TechTree.get(this); } + /** Use fullIcon / uiIcon instead! This will be removed. */ + @Deprecated + public TextureRegion icon(Cicon icon){ + return icon == Cicon.full ? fullIcon : uiIcon; + } + public String displayDescription(){ return minfo.mod == null ? description : description + "\n" + Core.bundle.format("mod.display", minfo.mod.meta.displayName()); } @@ -65,7 +84,10 @@ public abstract class UnlockableContent extends MappableContent{ public void setStats(){ } - /** Generate any special icons for this content. Called asynchronously.*/ + /** + * Generate any special icons for this content. Called synchronously. + * No regions are loaded at this point; grab pixmaps from the packer. + * */ @CallSuper public void createIcons(MultiPacker packer){ @@ -80,23 +102,8 @@ public abstract class UnlockableContent extends MappableContent{ return Fonts.getUnicodeStr(name); } - /** Returns a specific content icon, or the region {contentType}-{name} if not found.*/ - public TextureRegion icon(Cicon icon){ - if(cicons[icon.ordinal()] == null){ - cicons[icon.ordinal()] = - Core.atlas.find(getContentType().name() + "-" + name + "-" + icon.name(), - Core.atlas.find(getContentType().name() + "-" + name + "-full", - Core.atlas.find(name + "-" + icon.name(), - Core.atlas.find(name + "-full", - Core.atlas.find(name, - Core.atlas.find(getContentType().name() + "-" + name, - Core.atlas.find(name + "1"))))))); - } - return cicons[icon.ordinal()]; - } - - public Cicon prefDatabaseIcon(){ - return Cicon.xlarge; + public boolean hasEmoji(){ + return Fonts.hasUnicodeStr(name); } /** Iterates through any implicit dependencies of this content. @@ -105,11 +112,6 @@ public abstract class UnlockableContent extends MappableContent{ } - /** This should show all necessary info about this content in the specified table. */ - public void display(Table table){ - - } - /** Called when this content is unlocked. Use this to unlock other related content. */ public void onUnlock(){ } @@ -144,7 +146,7 @@ public abstract class UnlockableContent extends MappableContent{ } public boolean unlocked(){ - if(net != null && net.client()) return unlocked || alwaysUnlocked || state.rules.researched.contains(name); + if(net != null && net.client()) return alwaysUnlocked || state.rules.researched.contains(name); return unlocked || alwaysUnlocked; } diff --git a/core/src/mindustry/editor/DrawOperation.java b/core/src/mindustry/editor/DrawOperation.java index 53134663ea..8681c679ff 100755 --- a/core/src/mindustry/editor/DrawOperation.java +++ b/core/src/mindustry/editor/DrawOperation.java @@ -10,13 +10,8 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; public class DrawOperation{ - private MapEditor editor; private LongSeq array = new LongSeq(); - public DrawOperation(MapEditor editor){ - this.editor = editor; - } - public boolean isEmpty(){ return array.isEmpty(); } diff --git a/core/src/mindustry/editor/EditorTile.java b/core/src/mindustry/editor/EditorTile.java index e4be334888..69b00f3495 100644 --- a/core/src/mindustry/editor/EditorTile.java +++ b/core/src/mindustry/editor/EditorTile.java @@ -77,7 +77,7 @@ public class EditorTile extends Tile{ op(OpType.team, (byte)getTeamID()); super.setTeam(team); - getLinkedTiles(t -> ui.editor.editor.renderer.updatePoint(t.x, t.y)); + getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y)); } @Override @@ -140,14 +140,14 @@ public class EditorTile extends Tile{ } private void update(){ - ui.editor.editor.renderer.updatePoint(x, y); + editor.renderer.updatePoint(x, y); } private boolean skip(){ - return state.isGame() || ui.editor.editor.isLoading(); + return state.isGame() || editor.isLoading(); } private void op(OpType type, short value){ - ui.editor.editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value)); + editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value)); } } diff --git a/core/src/mindustry/editor/EditorTool.java b/core/src/mindustry/editor/EditorTool.java index cd4b63935e..b560d56e5b 100644 --- a/core/src/mindustry/editor/EditorTool.java +++ b/core/src/mindustry/editor/EditorTool.java @@ -9,11 +9,12 @@ import arc.util.*; import mindustry.content.*; import mindustry.game.*; import mindustry.world.*; +import static mindustry.Vars.*; public enum EditorTool{ zoom(KeyCode.v), pick(KeyCode.i){ - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ if(!Structs.inBounds(x, y, editor.width(), editor.height())) return; Tile tile = editor.tile(x, y); @@ -23,7 +24,7 @@ public enum EditorTool{ line(KeyCode.l, "replace", "orthogonal"){ @Override - public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){ + public void touchedLine(int x1, int y1, int x2, int y2){ //straight if(mode == 1){ if(Math.abs(x2 - x1) > Math.abs(y2 - y1)){ @@ -51,7 +52,7 @@ public enum EditorTool{ } @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ if(mode == -1){ //normal mode editor.drawBlocks(x, y); @@ -75,7 +76,7 @@ public enum EditorTool{ } @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ editor.drawCircle(x, y, tile -> { if(mode == -1){ //erase block @@ -95,13 +96,13 @@ public enum EditorTool{ IntSeq stack = new IntSeq(); @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ if(!Structs.inBounds(x, y, editor.width(), editor.height())) return; Tile tile = editor.tile(x, y); if(editor.drawBlock.isMultiblock()){ //don't fill multiblocks, thanks - pencil.touched(editor, x, y); + pencil.touched(x, y); return; } @@ -133,19 +134,19 @@ public enum EditorTool{ } //replace only when the mode is 0 using the specified functions - fill(editor, x, y, mode == 0, tester, setter); + fill(x, y, mode == 0, tester, setter); }else if(mode == 1){ //mode 1 is team fill //only fill synthetic blocks, it's meaningless otherwise if(tile.synthetic()){ Team dest = tile.team(); if(dest == editor.drawTeam) return; - fill(editor, x, y, false, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(editor.drawTeam)); + fill(x, y, false, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(editor.drawTeam)); } } } - void fill(MapEditor editor, int x, int y, boolean replace, Boolf tester, Cons filler){ + void fill(int x, int y, boolean replace, Boolf tester, Cons filler){ int width = editor.width(), height = editor.height(); if(replace){ @@ -215,7 +216,7 @@ public enum EditorTool{ } @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ //floor spray if(editor.drawBlock.isFloor()){ @@ -263,7 +264,7 @@ public enum EditorTool{ this.key = code; } - public void touched(MapEditor editor, int x, int y){} + public void touched(int x, int y){} - public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){} + public void touchedLine(int x1, int y1, int x2, int y2){} } diff --git a/core/src/mindustry/editor/MapEditor.java b/core/src/mindustry/editor/MapEditor.java index 9b62ae40a6..b603182e43 100644 --- a/core/src/mindustry/editor/MapEditor.java +++ b/core/src/mindustry/editor/MapEditor.java @@ -21,7 +21,7 @@ public class MapEditor{ public static final int[] brushSizes = {1, 2, 3, 4, 5, 9, 15, 20}; public StringMap tags = new StringMap(); - public MapRenderer renderer = new MapRenderer(this); + public MapRenderer renderer = new MapRenderer(); private final Context context = new Context(); private OperationStack stack = new OperationStack(); @@ -62,7 +62,7 @@ public class MapEditor{ public void beginEdit(Pixmap pixmap){ reset(); - createTiles(pixmap.getWidth(), pixmap.getHeight()); + createTiles(pixmap.width, pixmap.height); load(() -> MapIO.readImage(pixmap, tiles())); renderer.resize(width(), height()); } @@ -330,7 +330,7 @@ public class MapEditor{ public void addTileOp(long data){ if(loading) return; - if(currentOp == null) currentOp = new DrawOperation(this); + if(currentOp == null) currentOp = new DrawOperation(); currentOp.addOperation(data); renderer.updatePoint(TileOp.x(data), TileOp.y(data)); diff --git a/core/src/mindustry/editor/MapEditorDialog.java b/core/src/mindustry/editor/MapEditorDialog.java index f862256995..ac7631cff3 100644 --- a/core/src/mindustry/editor/MapEditorDialog.java +++ b/core/src/mindustry/editor/MapEditorDialog.java @@ -33,8 +33,6 @@ import mindustry.world.meta.*; import static mindustry.Vars.*; public class MapEditorDialog extends Dialog implements Disposable{ - public final MapEditor editor; - private MapView view; private MapInfoDialog infoDialog; private MapLoadDialog loadDialog; @@ -53,10 +51,9 @@ public class MapEditorDialog extends Dialog implements Disposable{ background(Styles.black); - editor = new MapEditor(); - view = new MapView(editor); - infoDialog = new MapInfoDialog(editor); - generateDialog = new MapGenerateDialog(editor, true); + view = new MapView(); + infoDialog = new MapInfoDialog(); + generateDialog = new MapGenerateDialog(true); menu = new BaseDialog("@menu"); menu.addCloseButton(); @@ -120,7 +117,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ "@editor.exportimage", "@editor.exportimage.description", Icon.fileImage, (Runnable)() -> platform.export(editor.tags.get("name", "unknown"), "png", file -> { Pixmap out = MapIO.writeImage(editor.tiles()); - file.writePNG(out); + file.writePng(out); out.dispose(); }))); }); @@ -173,7 +170,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ menu.hide(); }).size(swidth * 2f + 10, 60f); - resizeDialog = new MapResizeDialog(editor, (x, y) -> { + resizeDialog = new MapResizeDialog((x, y) -> { if(!(editor.width() == x && editor.height() == y)){ ui.loadAnd(() -> { editor.resize(x, y); @@ -639,7 +636,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ for(int x = 0; x < editor.width(); x++){ for(int y = 0; y < editor.height(); y++){ Tile tile = editor.tile(x, y); - if(tile.block().breakable && tile.block() instanceof Boulder){ + if(tile.block().breakable && tile.block() instanceof Prop){ tile.setBlock(Blocks.air); editor.renderer.updatePoint(x, y); } @@ -714,7 +711,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ int i = 0; for(Block block : blocksOut){ - TextureRegion region = block.icon(Cicon.medium); + TextureRegion region = block.uiIcon; if(!Core.atlas.isFound(region) || !block.inEditor || block.buildVisibility == BuildVisibility.debugOnly diff --git a/core/src/mindustry/editor/MapGenerateDialog.java b/core/src/mindustry/editor/MapGenerateDialog.java index 025dbeedaf..71f4f60264 100644 --- a/core/src/mindustry/editor/MapGenerateDialog.java +++ b/core/src/mindustry/editor/MapGenerateDialog.java @@ -26,28 +26,27 @@ import static mindustry.Vars.*; @SuppressWarnings("unchecked") public class MapGenerateDialog extends BaseDialog{ - private final Prov[] filterTypes = new Prov[]{ + final Prov[] filterTypes = new Prov[]{ NoiseFilter::new, ScatterFilter::new, TerrainFilter::new, DistortFilter::new, RiverNoiseFilter::new, OreFilter::new, OreMedianFilter::new, MedianFilter::new, BlendFilter::new, MirrorFilter::new, ClearFilter::new, CoreSpawnFilter::new, EnemySpawnFilter::new, SpawnPathFilter::new }; - private final MapEditor editor; - private final boolean applied; + final boolean applied; - private Pixmap pixmap; - private Texture texture; - private GenerateInput input = new GenerateInput(); + Pixmap pixmap; + Texture texture; + GenerateInput input = new GenerateInput(); Seq filters = new Seq<>(); - private int scaling = mobile ? 3 : 1; - private Table filterTable; + int scaling = mobile ? 3 : 1; + Table filterTable; - private AsyncExecutor executor = new AsyncExecutor(1); - private AsyncResult result; + AsyncExecutor executor = new AsyncExecutor(1); + AsyncResult result; boolean generating; - private long[] buffer1, buffer2; - private Cons> applier; + long[] buffer1, buffer2; + Cons> applier; CachedTile ctile = new CachedTile(){ //nothing. @Override @@ -62,35 +61,79 @@ public class MapGenerateDialog extends BaseDialog{ }; /** @param applied whether or not to use the applied in-game mode. */ - public MapGenerateDialog(MapEditor editor, boolean applied){ + public MapGenerateDialog(boolean applied){ super("@editor.generate"); - this.editor = editor; this.applied = applied; shown(this::setup); - addCloseButton(); + addCloseListener(); + + var style = Styles.cleart; + + buttons.defaults().size(180f, 64f).pad(2f); + buttons.button("@back", Icon.left, this::hide); + if(applied){ buttons.button("@editor.apply", Icon.ok, () -> { ui.loadAnd(() -> { apply(); hide(); }); - }).size(160f, 64f); - }else{ - buttons.button("@settings.reset", () -> { - filters.set(maps.readFilters("")); - rebuildFilters(); - update(); - }).size(160f, 64f); + }); } + buttons.button("@editor.randomize", Icon.refresh, () -> { for(GenerateFilter filter : filters){ filter.randomize(); } update(); - }).size(160f, 64f); + }); - buttons.button("@add", Icon.add, this::showAdd).height(64f).width(150f); + buttons.button("@edit", Icon.edit, () -> { + BaseDialog dialog = new BaseDialog("@editor.export"); + dialog.cont.pane(p -> { + p.margin(10f); + p.table(Tex.button, in -> { + in.defaults().size(280f, 60f).left(); + + in.button("@waves.copy", Icon.copy, style, () -> { + dialog.hide(); + + Core.app.setClipboardText(JsonIO.write(filters)); + }).marginLeft(12f).row(); + in.button("@waves.load", Icon.download, style, () -> { + dialog.hide(); + try{ + filters.set(JsonIO.read(Seq.class, Core.app.getClipboardText())); + + rebuildFilters(); + update(); + }catch(Throwable e){ + ui.showException(e); + } + }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null).row(); + in.button("@clear", Icon.none, style, () -> { + dialog.hide(); + filters.clear(); + rebuildFilters(); + update(); + }).marginLeft(12f).row(); + if(!applied){ + in.button("@settings.reset", Icon.refresh, style, () -> { + dialog.hide(); + filters.set(maps.readFilters("")); + rebuildFilters(); + update(); + }).marginLeft(12f).row(); + } + }); + }); + + dialog.addCloseButton(); + dialog.show(); + }); + + buttons.button("@add", Icon.add, this::showAdd); if(!applied){ hidden(this::apply); @@ -173,7 +216,7 @@ public class MapGenerateDialog extends BaseDialog{ @Override public void draw(){ super.draw(); - for(GenerateFilter filter : filters){ + for(var filter : filters){ filter.draw(this); } } @@ -214,7 +257,7 @@ public class MapGenerateDialog extends BaseDialog{ filterTable.top().left(); int i = 0; - for(GenerateFilter filter : filters){ + for(var filter : filters){ //main container filterTable.table(Tex.pane, c -> { @@ -284,31 +327,33 @@ public class MapGenerateDialog extends BaseDialog{ } void showAdd(){ - BaseDialog selection = new BaseDialog("@add"); + var selection = new BaseDialog("@add"); selection.cont.pane(p -> { + p.background(Tex.button); p.marginRight(14); - p.defaults().size(210f, 60f); + p.defaults().size(195f, 56f); int i = 0; - for(Prov gen : filterTypes){ - GenerateFilter filter = gen.get(); + for(var gen : filterTypes){ + var filter = gen.get(); + var icon = filter.icon(); - if((filter.isPost() && applied)) continue; + if(filter.isPost() && applied) continue; - p.button(filter.name(), () -> { + p.button((icon == '\0' ? "" : icon + " ") + filter.name(), Styles.cleart, () -> { filters.add(filter); rebuildFilters(); update(); selection.hide(); - }); - if(++i % 2 == 0) p.row(); + }).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left); + if(++i % 3 == 0) p.row(); } - p.button("@filter.defaultores", () -> { + p.button(Iconc.refresh + " " + Core.bundle.get("filter.defaultores"), Styles.cleart, () -> { maps.addDefaultOres(filters); rebuildFilters(); update(); selection.hide(); - }); + }).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left); }).get().setScrollingDisabled(true, false); selection.addCloseButton(); @@ -350,25 +395,25 @@ public class MapGenerateDialog extends BaseDialog{ return; } - Seq copy = new Seq<>(filters); + var copy = filters.copy(); result = executor.submit(() -> { try{ - int w = pixmap.getWidth(); + int w = pixmap.width; world.setGenerating(true); generating = true; if(!filters.isEmpty()){ //write to buffer1 for reading - for(int px = 0; px < pixmap.getWidth(); px++){ - for(int py = 0; py < pixmap.getHeight(); py++){ + for(int px = 0; px < pixmap.width; px++){ + for(int py = 0; py < pixmap.height; py++){ buffer1[px + py*w] = pack(editor.tile(px * scaling, py * scaling)); } } } - for(GenerateFilter filter : copy){ - input.begin(filter, editor.width(), editor.height(), (x, y) -> unpack(buffer1[Mathf.clamp(x / scaling, 0, pixmap.getWidth()-1) + w* Mathf.clamp(y / scaling, 0, pixmap.getHeight()-1)])); + for(var filter : copy){ + input.begin(filter, editor.width(), editor.height(), (x, y) -> unpack(buffer1[Mathf.clamp(x / scaling, 0, pixmap.width -1) + w* Mathf.clamp(y / scaling, 0, pixmap.height -1)])); //read from buffer1 and write to buffer2 pixmap.each((px, py) -> { @@ -382,8 +427,8 @@ public class MapGenerateDialog extends BaseDialog{ pixmap.each((px, py) -> buffer1[px + py*w] = buffer2[px + py*w]); } - for(int px = 0; px < pixmap.getWidth(); px++){ - for(int py = 0; py < pixmap.getHeight(); py++){ + for(int px = 0; px < pixmap.width; px++){ + for(int py = 0; py < pixmap.height; py++){ int color; //get result from buffer1 if there's filters left, otherwise get from editor directly if(filters.isEmpty()){ @@ -393,7 +438,7 @@ public class MapGenerateDialog extends BaseDialog{ long tile = buffer1[px + py*w]; color = MapIO.colorFor(content.block(PackTile.block(tile)), content.block(PackTile.floor(tile)), content.block(PackTile.overlay(tile)), Team.derelict); } - pixmap.draw(px, pixmap.getHeight() - 1 - py, color); + pixmap.set(px, pixmap.height - 1 - py, color); } } diff --git a/core/src/mindustry/editor/MapInfoDialog.java b/core/src/mindustry/editor/MapInfoDialog.java index dd9959b199..a18828f1f0 100644 --- a/core/src/mindustry/editor/MapInfoDialog.java +++ b/core/src/mindustry/editor/MapInfoDialog.java @@ -9,17 +9,17 @@ import mindustry.io.*; import mindustry.ui.*; import mindustry.ui.dialogs.*; +import static mindustry.Vars.*; + public class MapInfoDialog extends BaseDialog{ - private final MapEditor editor; private final WaveInfoDialog waveInfo; private final MapGenerateDialog generate; private final CustomRulesDialog ruleInfo = new CustomRulesDialog(); - public MapInfoDialog(MapEditor editor){ + public MapInfoDialog(){ super("@editor.mapinfo"); - this.editor = editor; - this.waveInfo = new WaveInfoDialog(editor); - this.generate = new MapGenerateDialog(editor, false); + this.waveInfo = new WaveInfoDialog(); + this.generate = new MapGenerateDialog(false); addCloseButton(); diff --git a/core/src/mindustry/editor/MapRenderer.java b/core/src/mindustry/editor/MapRenderer.java index 919c534128..abbd4baa1a 100644 --- a/core/src/mindustry/editor/MapRenderer.java +++ b/core/src/mindustry/editor/MapRenderer.java @@ -18,14 +18,7 @@ public class MapRenderer implements Disposable{ private IndexedRenderer[][] chunks; private IntSet updates = new IntSet(); private IntSet delayedUpdates = new IntSet(); - private MapEditor editor; private int width, height; - private Texture texture; - - public MapRenderer(MapEditor editor){ - this.editor = editor; - this.texture = Core.atlas.find("clear-editor").texture; - } public void resize(int width, int height){ updates.clear(); @@ -64,6 +57,8 @@ public class MapRenderer implements Disposable{ return; } + var texture = Core.atlas.find("clear-editor").texture; + for(int x = 0; x < chunks.length; x++){ for(int y = 0; y < chunks[0].length; y++){ IndexedRenderer mesh = chunks[x][y]; diff --git a/core/src/mindustry/editor/MapResizeDialog.java b/core/src/mindustry/editor/MapResizeDialog.java index 9d961a4246..f95edff43f 100644 --- a/core/src/mindustry/editor/MapResizeDialog.java +++ b/core/src/mindustry/editor/MapResizeDialog.java @@ -6,13 +6,14 @@ import arc.scene.ui.TextField.*; import arc.scene.ui.layout.*; import arc.util.*; import mindustry.ui.dialogs.*; +import static mindustry.Vars.*; public class MapResizeDialog extends BaseDialog{ public static int minSize = 50, maxSize = 500, increment = 50; int width, height; - public MapResizeDialog(MapEditor editor, Intc2 cons){ + public MapResizeDialog(Intc2 cons){ super("@editor.resizemap"); shown(() -> { cont.clear(); diff --git a/core/src/mindustry/editor/MapView.java b/core/src/mindustry/editor/MapView.java index 69ae67d7c9..fce417ecad 100644 --- a/core/src/mindustry/editor/MapView.java +++ b/core/src/mindustry/editor/MapView.java @@ -19,7 +19,6 @@ import mindustry.ui.*; import static mindustry.Vars.*; public class MapView extends Element implements GestureListener{ - private MapEditor editor; EditorTool tool = EditorTool.pencil; private float offsetx, offsety; private float zoom = 1f; @@ -35,8 +34,7 @@ public class MapView extends Element implements GestureListener{ float mousex, mousey; EditorTool lastTool; - public MapView(MapEditor editor){ - this.editor = editor; + public MapView(){ for(int i = 0; i < MapEditor.brushSizes.length; i++){ float size = MapEditor.brushSizes[i]; @@ -92,7 +90,7 @@ public class MapView extends Element implements GestureListener{ lasty = p.y; startx = p.x; starty = p.y; - tool.touched(editor, p.x, p.y); + tool.touched(p.x, p.y); firstTouch.set(p); if(tool.edit){ @@ -115,7 +113,7 @@ public class MapView extends Element implements GestureListener{ if(tool == EditorTool.line){ ui.editor.resetSaved(); - tool.touchedLine(editor, startx, starty, p.x, p.y); + tool.touchedLine(startx, starty, p.x, p.y); } editor.flushOp(); @@ -136,7 +134,7 @@ public class MapView extends Element implements GestureListener{ if(drawing && tool.draggable && !(p.x == lastx && p.y == lasty)){ ui.editor.resetSaved(); - Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(editor, cx, cy)); + Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(cx, cy)); } if(tool == EditorTool.line && tool.mode == 1){ diff --git a/core/src/mindustry/editor/WaveGraph.java b/core/src/mindustry/editor/WaveGraph.java index fd0b05bfb9..cc36f22620 100644 --- a/core/src/mindustry/editor/WaveGraph.java +++ b/core/src/mindustry/editor/WaveGraph.java @@ -177,7 +177,7 @@ public class WaveGraph extends Table{ t.button(b -> { Color tcolor = color(type).cpy(); b.image().size(32f).update(i -> i.setColor(b.isChecked() ? Tmp.c1.set(tcolor).mul(0.5f) : tcolor)).get().act(1); - b.image(type.icon(Cicon.medium)).size(32f).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1); + b.image(type.uiIcon).size(32f).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1); b.margin(0f); }, Styles.fullTogglet, () -> { if(!hidden.add(type)){ diff --git a/core/src/mindustry/editor/WaveInfoDialog.java b/core/src/mindustry/editor/WaveInfoDialog.java index a71d0a2d8f..d83fa11f3e 100644 --- a/core/src/mindustry/editor/WaveInfoDialog.java +++ b/core/src/mindustry/editor/WaveInfoDialog.java @@ -30,7 +30,7 @@ public class WaveInfoDialog extends BaseDialog{ private float updateTimer, updatePeriod = 1f; private WaveGraph graph = new WaveGraph(); - public WaveInfoDialog(MapEditor editor){ + public WaveInfoDialog(){ super("@waves.title"); shown(this::setup); @@ -160,7 +160,7 @@ public class WaveInfoDialog extends BaseDialog{ t.margin(0).defaults().pad(3).padLeft(5f).growX().left(); t.button(b -> { b.left(); - b.image(group.type.icon(Cicon.medium)).size(32f).padRight(3).scaling(Scaling.fit); + b.image(group.type.uiIcon).size(32f).padRight(3).scaling(Scaling.fit); b.add(group.type.localizedName).color(Pal.accent); b.add().growX(); @@ -263,7 +263,7 @@ public class WaveInfoDialog extends BaseDialog{ if(type.isHidden()) continue; p.button(t -> { t.left(); - t.image(type.icon(Cicon.medium)).size(8 * 4).scaling(Scaling.fit).padRight(2f); + t.image(type.uiIcon).size(8 * 4).scaling(Scaling.fit).padRight(2f); t.add(type.localizedName); }, () -> { lastType = type; diff --git a/core/src/mindustry/entities/Damage.java b/core/src/mindustry/entities/Damage.java index 1e66934577..beb1e2b6c3 100644 --- a/core/src/mindustry/entities/Damage.java +++ b/core/src/mindustry/entities/Damage.java @@ -83,8 +83,17 @@ public class Damage{ } } + public static @Nullable Building findAbsorber(Team team, float x1, float y1, float x2, float y2){ + tmpBuilding = null; + + boolean found = world.raycast(World.toTile(x1), World.toTile(y1), World.toTile(x2), World.toTile(y2), + (x, y) -> (tmpBuilding = world.build(x, y)) != null && tmpBuilding.team != team && tmpBuilding.block.absorbLasers); + + return found ? tmpBuilding : null; + } + public static float findLaserLength(Bullet b, float length){ - Tmp.v1.trns(b.rotation(), length); + Tmp.v1.trnsExact(b.rotation(), length); furthest = null; @@ -125,7 +134,7 @@ public class Damage{ if(laser) length = findLaserLength(hitter, length); collidedBlocks.clear(); - tr.trns(angle, length); + tr.trnsExact(angle, length); Intc2 collider = (cx, cy) -> { Building tile = world.build(cx, cy); @@ -378,7 +387,7 @@ public class Damage{ //this needs to be compensated if(in != null && in.team != team && in.block.size > 1 && in.health > damage){ //deal the damage of an entire side, to be equivalent with maximum 'standard' damage - in.damage(damage * Math.min((in.block.size), baseRadius * 0.45f)); + in.damage(team, damage * Math.min((in.block.size), baseRadius * 0.4f)); //no need to continue with the explosion return; } @@ -435,7 +444,7 @@ public class Damage{ int cx = Point2.x(e.key), cy = Point2.y(e.key); var build = world.build(cx, cy); if(build != null){ - build.damage(e.value); + build.damage(team, e.value); } } }); diff --git a/core/src/mindustry/entities/Effect.java b/core/src/mindustry/entities/Effect.java index 6fa163b074..7b72ecdc01 100644 --- a/core/src/mindustry/entities/Effect.java +++ b/core/src/mindustry/entities/Effect.java @@ -29,6 +29,8 @@ public class Effect{ public float lifetime = 50f; /** Clip size. */ public float clip; + /** If true, parent unit is data are followed. */ + public boolean followParent; public float layer = Layer.effect; public float layerDuration; @@ -53,6 +55,11 @@ public class Effect{ public void init(){} + public Effect followParent(boolean follow){ + followParent = follow; + return this; + } + public Effect layer(float l){ layer = l; return this; @@ -148,11 +155,11 @@ public class Effect{ EffectState entity = EffectState.create(); entity.effect = effect; entity.rotation = rotation; - entity.data = (data); - entity.lifetime = (effect.lifetime); + entity.data = data; + entity.lifetime = effect.lifetime; entity.set(x, y); entity.color.set(color); - if(data instanceof Posc) entity.parent = ((Posc)data); + if(effect.followParent && data instanceof Posc) entity.parent = ((Posc)data); entity.add(); } } diff --git a/core/src/mindustry/entities/Sized.java b/core/src/mindustry/entities/Sized.java index 7039f0b2a5..d33778e3aa 100644 --- a/core/src/mindustry/entities/Sized.java +++ b/core/src/mindustry/entities/Sized.java @@ -1,5 +1,7 @@ package mindustry.entities; -public interface Sized{ +import arc.math.geom.*; + +public interface Sized extends Position{ float hitSize(); } diff --git a/core/src/mindustry/entities/Units.java b/core/src/mindustry/entities/Units.java index cb7d59ea41..d0fd94a63c 100644 --- a/core/src/mindustry/entities/Units.java +++ b/core/src/mindustry/entities/Units.java @@ -4,6 +4,7 @@ import arc.*; import arc.func.*; import arc.math.geom.*; import arc.struct.*; +import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.game.*; @@ -20,6 +21,7 @@ public class Units{ private static Unit result; private static float cdist; private static boolean boolResult; + private static int intResult; @Remote(called = Loc.server) public static void unitCapDeath(Unit unit){ @@ -75,7 +77,7 @@ public class Units{ if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam)){ return Integer.MAX_VALUE; } - return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + indexer.getExtraUnits(team) : state.rules.unitCap); + return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + team.data().unitCap : state.rules.unitCap); } /** @return whether this player can interact with a specific tile. if either of these are null, returns true.*/ @@ -138,23 +140,28 @@ public class Units{ return boolResult; } - /** Returns the neareset damaged tile. */ + /** Returns the nearest damaged tile. */ public static Building findDamagedTile(Team team, float x, float y){ return Geometry.findClosest(x, y, indexer.getDamaged(team)); } - /** Returns the neareset ally tile in a range. */ + /** Returns the nearest ally tile in a range. */ public static Building findAllyTile(Team team, float x, float y, float range, Boolf pred){ return indexer.findTile(team, x, y, range, pred); } - /** Returns the neareset enemy tile in a range. */ + /** Returns the nearest enemy tile in a range. */ public static Building findEnemyTile(Team team, float x, float y, float range, Boolf pred){ if(team == Team.derelict) return null; return indexer.findEnemyTile(team, x, y, range, pred); } + /** Iterates through all buildings in a range. */ + public static void nearbyBuildings(float x, float y, float range, Cons cons){ + indexer.allBuildings(x, y, range, cons); + } + /** Returns the closest target enemy. First, units are checked, then tile entities. */ public static Teamc closestTarget(Team team, float x, float y, float range){ return closestTarget(team, x, y, range, Unit::isValid); @@ -199,7 +206,7 @@ public class Units{ nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> { if(e.dead() || !predicate.get(e) || e.team == Team.derelict) return; - float dst2 = e.dst2(x, y); + float dst2 = e.dst2(x, y) - (e.hitSize * e.hitSize); if(dst2 < range*range && (result == null || dst2 < cdist)){ result = e; cdist = dst2; @@ -302,13 +309,40 @@ public class Units{ return result; } + /** @return whether any units exist in this square (centered) */ + public static int count(float x, float y, float size, Boolf filter){ + return count(x - size/2f, y - size/2f, size, size, filter); + } + + /** @return whether any units exist in this rectangle */ + public static int count(float x, float y, float width, float height, Boolf filter){ + intResult = 0; + Groups.unit.intersect(x, y, width, height, v -> { + if(filter.get(v)){ + intResult ++; + } + }); + return intResult; + } + + /** @return whether any units exist in this rectangle */ + public static boolean any(float x, float y, float width, float height, Boolf filter){ + return count(x, y, width, height, filter) > 0; + } + /** Iterates over all units in a rectangle. */ - public static void nearby(Team team, float x, float y, float width, float height, Cons cons){ - team.data().tree().intersect(x, y, width, height, cons); + public static void nearby(@Nullable Team team, float x, float y, float width, float height, Cons cons){ + if(team != null){ + team.data().tree().intersect(x, y, width, height, cons); + }else{ + for(var other : state.teams.getActive()){ + other.tree().intersect(x, y, width, height, cons); + } + } } /** Iterates over all units in a circle around this position. */ - public static void nearby(Team team, float x, float y, float radius, Cons cons){ + public static void nearby(@Nullable Team team, float x, float y, float radius, Cons cons){ nearby(team, x - radius, y - radius, radius*2f, radius*2f, unit -> { if(unit.within(x, y, radius + unit.hitSize/2f)){ cons.get(unit); @@ -336,6 +370,15 @@ public class Units{ } } + /** Iterates over all units that are enemies of this team. */ + public static void nearbyEnemies(Team team, float x, float y, float radius, Cons cons){ + nearbyEnemies(team, x - radius, y - radius, radius * 2f, radius * 2f, u -> { + if(u.within(x, y, radius + u.hitSize/2f)){ + cons.get(u); + } + }); + } + /** Iterates over all units that are enemies of this team. */ public static void nearbyEnemies(Team team, Rect rect, Cons cons){ nearbyEnemies(team, rect.x, rect.y, rect.width, rect.height, cons); diff --git a/core/src/mindustry/entities/abilities/EnergyFieldAbility.java b/core/src/mindustry/entities/abilities/EnergyFieldAbility.java new file mode 100644 index 0000000000..587fff2bdc --- /dev/null +++ b/core/src/mindustry/entities/abilities/EnergyFieldAbility.java @@ -0,0 +1,145 @@ +package mindustry.entities.abilities; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.type.*; + +public class EnergyFieldAbility extends Ability{ + private static final Seq all = new Seq<>(); + + public float damage = 1, repair = 20f, reload = 100, range = 60; + public Effect healEffect = Fx.heal, hitEffect = Fx.hitLaserBlast, damageEffect = Fx.chainLightning; + public StatusEffect status = StatusEffects.electrified; + public float statusDuration = 60f * 6f; + public float x, y; + public boolean hitBuildings = true; + public int maxTargets = 25; + public float healPercent = 3f; + + public float layer = Layer.bullet - 0.001f, blinkScl = 20f; + public float effectRadius = 5f, sectorRad = 0.14f, rotateSpeed = 0.5f; + public int sectors = 5; + public Color color = Pal.heal; + + protected float timer, curStroke; + protected boolean anyNearby = false; + + EnergyFieldAbility(){} + + public EnergyFieldAbility(float damage, float reload, float range){ + this.damage = damage; + this.reload = reload; + this.range = range; + } + + @Override + public String localized(){ + return Core.bundle.format("ability.energyfield", damage, range / Vars.tilesize, maxTargets); + } + + @Override + public void draw(Unit unit){ + super.draw(unit); + + Draw.z(layer); + Draw.color(color); + Tmp.v1.trns(unit.rotation - 90, x, y).add(unit.x, unit.y); + float rx = Tmp.v1.x, ry = Tmp.v1.y; + float orbRadius = effectRadius * (1f + Mathf.absin(blinkScl, 0.1f)); + + Fill.circle(rx, ry, orbRadius); + Draw.color(); + Fill.circle(rx, ry, orbRadius / 2f); + + Lines.stroke((0.7f + Mathf.absin(blinkScl, 0.7f)), color); + + for(int i = 0; i < sectors; i++){ + float rot = unit.rotation + i * 360f/sectors - Time.time * rotateSpeed; + Lines.swirl(rx, ry, orbRadius + 3f, sectorRad, rot); + } + + Lines.stroke(Lines.getStroke() * curStroke); + + if(curStroke > 0){ + for(int i = 0; i < sectors; i++){ + float rot = unit.rotation + i * 360f/sectors + Time.time * rotateSpeed; + Lines.swirl(rx, ry, range, sectorRad, rot); + } + } + + Drawf.light(rx, ry, range * 1.5f, color, curStroke * 0.8f); + + Draw.reset(); + } + + @Override + public void update(Unit unit){ + + curStroke = Mathf.lerpDelta(curStroke, anyNearby ? 1 : 0, 0.09f); + + if((timer += Time.delta) >= reload){ + + Tmp.v1.trns(unit.rotation - 90, x, y).add(unit.x, unit.y); + float rx = Tmp.v1.x, ry = Tmp.v1.y; + anyNearby = false; + + all.clear(); + + Units.nearby(null, rx, ry, range, other -> { + if(other != unit){ + all.add(other); + } + }); + + if(hitBuildings){ + Units.nearbyBuildings(rx, ry, range, all::add); + } + + all.sort(h -> h.dst2(rx, ry)); + int len = Math.min(all.size, maxTargets); + for(int i = 0; i < len; i++){ + Healthc other = all.get(i); + + //lightning gets absorbed by plastanium + var absorber = Damage.findAbsorber(unit.team, rx, ry, other.getX(), other.getY()); + if(absorber != null){ + other = absorber; + } + + if(((Teamc)other).team() == unit.team){ + if(other.damaged()){ + anyNearby = true; + other.heal(healPercent / 100f * other.maxHealth()); + healEffect.at(other); + damageEffect.at(rx, ry, 0f, color, other); + hitEffect.at(rx, ry, unit.angleTo(other), color); + + if(other instanceof Building b){ + Fx.healBlockFull.at(b.x, b.y, b.block.size, color); + } + } + }else{ + anyNearby = true; + other.damage(damage); + if(other instanceof Statusc s){ + s.apply(status, statusDuration); + } + hitEffect.at(other.x(), other.y(), unit.angleTo(other), color); + damageEffect.at(rx, ry, 0f, color, other); + hitEffect.at(rx, ry, unit.angleTo(other), color); + } + } + + timer = 0f; + } + } +} diff --git a/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java b/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java index 8af8d2390a..7eea17b8d1 100644 --- a/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java +++ b/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java @@ -33,13 +33,13 @@ public class ShieldRegenFieldAbility extends Ability{ if(other.shield < max){ other.shield = Math.max(other.shield + amount, max); other.shieldAlpha = 1f; //TODO may not be necessary - applyEffect.at(unit); + applyEffect.at(unit.x, unit.y, unit.team.color); applied = true; } }); if(applied){ - activeEffect.at(unit); + activeEffect.at(unit.x, unit.y, unit.team.color); } timer = 0f; diff --git a/core/src/mindustry/entities/abilities/StatusFieldAbility.java b/core/src/mindustry/entities/abilities/StatusFieldAbility.java index a1bf47d408..b41df1b851 100644 --- a/core/src/mindustry/entities/abilities/StatusFieldAbility.java +++ b/core/src/mindustry/entities/abilities/StatusFieldAbility.java @@ -1,5 +1,6 @@ package mindustry.entities.abilities; +import arc.*; import arc.util.*; import mindustry.content.*; import mindustry.entities.*; @@ -23,6 +24,11 @@ public class StatusFieldAbility extends Ability{ this.effect = effect; } + @Override + public String localized(){ + return Core.bundle.format("ability.statusfield", effect.emoji()); + } + @Override public void update(Unit unit){ timer += Time.delta; diff --git a/core/src/mindustry/entities/abilities/UnitSpawnAbility.java b/core/src/mindustry/entities/abilities/UnitSpawnAbility.java index ac4218946b..92d26e59cc 100644 --- a/core/src/mindustry/entities/abilities/UnitSpawnAbility.java +++ b/core/src/mindustry/entities/abilities/UnitSpawnAbility.java @@ -54,7 +54,7 @@ public class UnitSpawnAbility extends Ability{ if(Units.canCreate(unit.team, this.unit)){ Draw.draw(Draw.z(), () -> { float x = unit.x + Angles.trnsx(unit.rotation, spawnY, spawnX), y = unit.y + Angles.trnsy(unit.rotation, spawnY, spawnX); - Drawf.construct(x, y, this.unit.icon(Cicon.full), unit.rotation - 90, timer / spawnTime, 1f, timer); + Drawf.construct(x, y, this.unit.fullIcon, unit.rotation - 90, timer / spawnTime, 1f, timer); }); } } diff --git a/core/src/mindustry/entities/bullet/ArtilleryBulletType.java b/core/src/mindustry/entities/bullet/ArtilleryBulletType.java index a35c670316..1b9c76a7f4 100644 --- a/core/src/mindustry/entities/bullet/ArtilleryBulletType.java +++ b/core/src/mindustry/entities/bullet/ArtilleryBulletType.java @@ -18,6 +18,24 @@ public class ArtilleryBulletType extends BasicBulletType{ hitSound = Sounds.explosion; shootEffect = Fx.shootBig; trailEffect = Fx.artilleryTrail; + + //default settings: + shrinkX = 0.15f; + shrinkY = 0.63f; + + //for trail: + + /* + trailLength = 27; + trailWidth = 3.5f; + trailEffect = Fx.none; + trailColor = Pal.bulletYellowBack; + + trailInterp = Interp.slope; + + shrinkX = 0.8f; + shrinkY = 0.3f; + */ } public ArtilleryBulletType(float speed, float damage){ @@ -39,15 +57,13 @@ public class ArtilleryBulletType extends BasicBulletType{ @Override public void draw(Bullet b){ - float baseScale = 0.7f; - float scale = (baseScale + b.fslope() * (1f - baseScale)); - - float height = this.height * ((1f - shrinkY) + shrinkY * b.fout()); + drawTrail(b); + float xscale = (1f - shrinkX + b.fslope() * (shrinkX)), yscale = (1f - shrinkY + b.fslope() * (shrinkY)), rot = b.rotation(); Draw.color(backColor); - Draw.rect(backRegion, b.x, b.y, width * scale, height * scale, b.rotation() - 90); + Draw.rect(backRegion, b.x, b.y, width * xscale, height * yscale, rot - 90); Draw.color(frontColor); - Draw.rect(frontRegion, b.x, b.y, width * scale, height * scale, b.rotation() - 90); + Draw.rect(frontRegion, b.x, b.y, width * xscale, height * yscale, rot - 90); Draw.color(); } } diff --git a/core/src/mindustry/entities/bullet/BasicBulletType.java b/core/src/mindustry/entities/bullet/BasicBulletType.java index ac33c28868..84cbca0e0a 100644 --- a/core/src/mindustry/entities/bullet/BasicBulletType.java +++ b/core/src/mindustry/entities/bullet/BasicBulletType.java @@ -42,6 +42,7 @@ public class BasicBulletType extends BulletType{ @Override public void draw(Bullet b){ + super.draw(b); float height = this.height * ((1f - shrinkY) + shrinkY * b.fout()); float width = this.width * ((1f - shrinkX) + shrinkX * b.fout()); float offset = -90 + (spin != 0 ? Mathf.randomSeed(b.id, 360f) + b.time * spin : 0f); diff --git a/core/src/mindustry/entities/bullet/BulletType.java b/core/src/mindustry/entities/bullet/BulletType.java index 7c3a4414af..40a09a37f3 100644 --- a/core/src/mindustry/entities/bullet/BulletType.java +++ b/core/src/mindustry/entities/bullet/BulletType.java @@ -1,39 +1,60 @@ package mindustry.entities.bullet; +import arc.*; import arc.audio.*; import arc.graphics.*; +import arc.graphics.g2d.*; import arc.math.*; import arc.util.*; +import mindustry.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.ctype.*; import mindustry.entities.*; +import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.*; +import mindustry.world.blocks.defense.Wall.*; import static mindustry.Vars.*; -public abstract class BulletType extends Content{ +public class BulletType extends Content implements Cloneable{ + /** Lifetime in ticks. */ public float lifetime = 40f; - public float speed; - public float damage; + /** Speed in units/tick. */ + public float speed = 1f; + /** Direct damage dealt on hit. */ + public float damage = 1f; + /** Hitbox size. */ public float hitSize = 4; + /** Clipping hitbox. */ public float drawSize = 40f; + /** Drag as fraction of velocity. */ public float drag = 0f; - public boolean pierce, pierceBuilding; + /** Whether to pierce units. */ + public boolean pierce; + /** Whether to pierce buildings. */ + public boolean pierceBuilding; + /** Maximum # of pierced objects. */ public int pierceCap = -1; - public Effect hitEffect, despawnEffect; - + /** Z layer to drawn on. */ + public float layer = Layer.bullet; + /** Effect shown on direct hit. */ + public Effect hitEffect = Fx.hitBulletSmall; + /** Effect shown when bullet despawns. */ + public Effect despawnEffect = Fx.hitBulletSmall; /** Effect created when shooting. */ public Effect shootEffect = Fx.shootSmall; /** Extra smoke effect created when shooting. */ public Effect smokeEffect = Fx.shootSmallSmoke; /** Sound made when hitting something or getting removed.*/ public Sound hitSound = Sounds.none; + /** Sound made when hitting something or getting removed.*/ + public Sound despawnSound = Sounds.none; /** Pitch of the sound made when hitting something*/ public float hitSoundPitch = 1; /** Volume of the sound made when hitting something*/ @@ -80,15 +101,17 @@ public abstract class BulletType extends Content{ public boolean reflectable = true; /** Whether this projectile can be absorbed by shields. */ public boolean absorbable = true; - /** Whether to move the bullet back depending on delta to fix some delta-time realted issues. + /** Whether to move the bullet back depending on delta to fix some delta-time related issues. * Do not change unless you know what you're doing. */ public boolean backMove = true; /** Bullet range override. */ public float maxRange = -1f; /** % of block health healed **/ public float healPercent = 0f; - /** whether to make fire on impact */ + /** Whether to make fire on impact */ public boolean makeFire = false; + /** Whether to create hit effects on despawn. Forced to true if this bullet has any special effects like splash damage. */ + public boolean despawnHit = false; //additional effects @@ -101,8 +124,14 @@ public abstract class BulletType extends Content{ public Color trailColor = Pal.missileYellowBack; public float trailChance = -0.0001f; + public float trailInterval = 0f; public Effect trailEffect = Fx.missileTrail; public float trailParam = 2f; + public boolean trailRotation = false; + public Interp trailInterp = Interp.one; + /** Any value <= 0 disables the trail. */ + public int trailLength = -1; + public float trailWidth = 2f; /** Use a negative value to disable splash damage. */ public float splashDamageRadius = -1f; @@ -134,19 +163,27 @@ public abstract class BulletType extends Content{ public float puddleAmount = 5f; public Liquid puddleLiquid = Liquids.water; - public float lightRadius = 16f; + public float lightRadius = -1f; public float lightOpacity = 0.3f; public Color lightColor = Pal.powerLight; public BulletType(float speed, float damage){ this.speed = speed; this.damage = damage; - hitEffect = Fx.hitBulletSmall; - despawnEffect = Fx.hitBulletSmall; } public BulletType(){ - this(1f, 1f); + } + + public BulletType copy(){ + try{ + BulletType copy = (BulletType)clone(); + copy.id = (short)Vars.content.getBy(getContentType()).size; + Vars.content.handleContent(copy); + return copy; + }catch(Exception e){ + throw new RuntimeException("death to checked exceptions", e); + } } /** @return estimated damage per shot. this can be very inaccurate. */ @@ -181,13 +218,13 @@ public abstract class BulletType extends Content{ if(healPercent > 0f && build.team == b.team && !(build.block instanceof ConstructBlock)){ Fx.healBlockFull.at(build.x, build.y, build.block.size, Pal.heal); - build.heal(healPercent / 100f * build.maxHealth()); + build.heal(healPercent / 100f * build.maxHealth); }else if(build.team != b.team && direct){ hit(b); } } - public void hitEntity(Bullet b, Hitboxc entity, float initialHealth){ + public void hitEntity(Bullet b, Hitboxc entity, float health){ if(entity instanceof Healthc h){ h.damage(b.damage); } @@ -198,6 +235,11 @@ public abstract class BulletType extends Content{ unit.impulse(Tmp.v3); unit.apply(status, statusDuration); } + + //for achievements + if(b.owner instanceof WallBuild && player != null && b.team == player.team() && entity instanceof Unit unit && unit.dead){ + Events.fire(Trigger.phaseDeflectHit); + } } public void hit(Bullet b){ @@ -205,7 +247,6 @@ public abstract class BulletType extends Content{ } public void hit(Bullet b, float x, float y){ - b.hit = true; hitEffect.at(x, y, b.rotation(), hitColor); hitSound.at(x, y, hitSoundPitch, hitSoundVolume); @@ -226,7 +267,7 @@ public abstract class BulletType extends Content{ } } - if(Mathf.chance(incendChance)){ + if(incendChance > 0 && Mathf.chance(incendChance)){ Damage.createIncend(x, y, incendSpread, incendAmount); } @@ -245,9 +286,7 @@ public abstract class BulletType extends Content{ } if(makeFire){ - indexer.eachBlock(null, x, y, splashDamageRadius, other -> other.team != b.team, other -> { - Fires.create(other.tile); - }); + indexer.eachBlock(null, x, y, splashDamageRadius, other -> other.team != b.team, other -> Fires.create(other.tile)); } } @@ -256,21 +295,40 @@ public abstract class BulletType extends Content{ } } + /** Called when the bullet reaches the end of its lifetime of is destroyed by something external. */ public void despawned(Bullet b){ + if(despawnHit){ + hit(b); + } despawnEffect.at(b.x, b.y, b.rotation(), hitColor); - hitSound.at(b); + despawnSound.at(b); Effect.shake(despawnShake, despawnShake, b); + } - if(!b.hit && (fragBullet != null || splashDamageRadius > 0 || lightning > 0)){ - hit(b); + /** Called when the bullet is removed for any reason. */ + public void removed(Bullet b){ + if(trailLength > 0 && b.trail != null && b.trail.size() > 0){ + Fx.trailFade.at(b.x, b.y, trailWidth, trailColor, b.trail.copy()); } } public void draw(Bullet b){ + drawTrail(b); + } + + public void drawTrail(Bullet b){ + if(trailLength > 0 && b.trail != null){ + //draw below bullets? TODO + float z = Draw.z(); + Draw.z(z - 0.0001f); + b.trail.draw(trailColor, trailWidth); + Draw.z(z); + } } public void drawLight(Bullet b){ + if(lightOpacity <= 0f || lightRadius <= 0f) return; Drawf.light(b.team, b, lightRadius, lightColor, lightOpacity); } @@ -283,11 +341,37 @@ public abstract class BulletType extends Content{ if(instantDisappear){ b.time = lifetime; } + + if(fragBullet != null || splashDamageRadius > 0 || lightning > 0){ + despawnHit = true; + } + + if(lightRadius == -1){ + lightRadius = Math.max(18, hitSize * 5f); + } + drawSize = Math.max(drawSize, trailLength * speed * 2f); } public void update(Bullet b){ + if(!headless && trailLength > 0){ + if(b.trail == null){ + b.trail = new Trail(trailLength); + } + b.trail.length = trailLength; + b.trail.update(b.x, b.y, trailInterp.apply(b.fin())); + } + if(homingPower > 0.0001f && b.time >= homingDelay){ - Teamc target = Units.closestTarget(b.team, b.x, b.y, homingRange, e -> e.checkTarget(collidesAir, collidesGround), t -> collidesGround); + Teamc target; + //home in on allies if possible + if(healPercent > 0){ + target = Units.closestTarget(null, b.x, b.y, homingRange, + e -> e.checkTarget(collidesAir, collidesGround) && e.team != b.team, + t -> collidesGround && (t.team != b.team || t.damaged())); + }else{ + target = Units.closestTarget(b.team, b.x, b.y, homingRange, e -> e.checkTarget(collidesAir, collidesGround), t -> collidesGround); + } + if(target != null){ b.vel.setAngle(Angles.moveToward(b.rotation(), b.angleTo(target), homingPower * Time.delta * 50f)); } @@ -299,7 +383,13 @@ public abstract class BulletType extends Content{ if(trailChance > 0){ if(Mathf.chanceDelta(trailChance)){ - trailEffect.at(b.x, b.y, trailParam, trailColor); + trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor); + } + } + + if(trailInterval > 0f){ + if(b.timer(0, trailInterval)){ + trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor); } } } @@ -366,6 +456,10 @@ public abstract class BulletType extends Content{ bullet.drag = drag; bullet.hitSize = hitSize; bullet.damage = (damage < 0 ? this.damage : damage) * bullet.damageMultiplier(); + //reset trail + if(bullet.trail != null){ + bullet.trail.clear(); + } bullet.add(); if(keepVelocity && owner instanceof Velc v) bullet.vel.add(v.vel().x, v.vel().y); diff --git a/core/src/mindustry/entities/bullet/EmpBulletType.java b/core/src/mindustry/entities/bullet/EmpBulletType.java new file mode 100644 index 0000000000..7e281e3b08 --- /dev/null +++ b/core/src/mindustry/entities/bullet/EmpBulletType.java @@ -0,0 +1,70 @@ +package mindustry.entities.bullet; + +import mindustry.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; + +public class EmpBulletType extends BasicBulletType{ + public float radius = 100f; + public float timeIncrease = 2.5f, timeDuration = 60f * 10f; + public float powerDamageScl = 2f, powerSclDecrease = 0.2f; + public Effect hitPowerEffect = Fx.hitEmpSpark, chainEffect = Fx.chainEmp, applyEffect = Fx.heal; + public boolean hitUnits = true; + public float unitDamageScl = 0.7f; + + @Override + public void hit(Bullet b, float x, float y){ + super.hit(b, x, y); + + if(!b.absorbed){ + Vars.indexer.allBuildings(x, y, radius, other -> { + if(other.team == b.team){ + if(other.block.hasPower && other.block.canOverdrive && other.timeScale < timeIncrease){ + if(timeIncrease >= other.timeScale){ + other.timeScale = Math.max(other.timeScale, timeIncrease); + } + other.timeScaleDuration = Math.max(other.timeScaleDuration, timeDuration); + chainEffect.at(x, y, 0, hitColor, other); + applyEffect.at(other, other.block.size * 7f); + } + + if(other.block.hasPower && other.damaged()){ + other.heal(healPercent / 100f * other.maxHealth()); + Fx.healBlockFull.at(other.x, other.y, other.block.size, hitColor); + applyEffect.at(other, other.block.size * 7f); + } + }else if(other.power != null){ + var absorber = Damage.findAbsorber(b.team, x, y, other.x, other.y); + if(absorber != null){ + other = absorber; + } + + if(other.power != null && other.power.graph.getLastPowerProduced() > 0f){ + other.timeScale = Math.min(other.timeScale, powerSclDecrease); + other.timeScaleDuration = timeDuration; + other.damage(damage * powerDamageScl); + hitPowerEffect.at(other.x, other.y, b.angleTo(other), hitColor); + chainEffect.at(x, y, 0, hitColor, other); + } + } + }); + + if(hitUnits){ + Units.nearbyEnemies(b.team, x, y, radius, other -> { + if(other.team != b.team){ + var absorber = Damage.findAbsorber(b.team, x, y, other.x, other.y); + if(absorber != null){ + return; + } + + hitPowerEffect.at(other.x, other.y, b.angleTo(other), hitColor); + chainEffect.at(x, y, 0, hitColor, other); + other.damage(damage * unitDamageScl); + other.apply(status, statusDuration); + } + }); + } + } + } +} diff --git a/core/src/mindustry/entities/bullet/FlakBulletType.java b/core/src/mindustry/entities/bullet/FlakBulletType.java index ad5f8be7fa..a1ae9c2ee8 100644 --- a/core/src/mindustry/entities/bullet/FlakBulletType.java +++ b/core/src/mindustry/entities/bullet/FlakBulletType.java @@ -6,7 +6,7 @@ import mindustry.entities.*; import mindustry.gen.*; public class FlakBulletType extends BasicBulletType{ - public float explodeRange = 30f; + public float explodeRange = 30f, explodeDelay = 5f; public FlakBulletType(float speed, float damage){ super(speed, damage, "shell"); @@ -25,17 +25,21 @@ public class FlakBulletType extends BasicBulletType{ @Override public void update(Bullet b){ super.update(b); - if(b.data() instanceof Integer) return; + //don't check for targets if primed to explode + if(b.fdata < 0f) return; if(b.timer(2, 6)){ Units.nearbyEnemies(b.team, Tmp.r1.setSize(explodeRange * 2f).setCenter(b.x, b.y), unit -> { - if(b.data() instanceof Float || !unit.checkTarget(collidesAir, collidesGround)) return; + //fadata < 0 means it's primed to explode + if(b.fdata < 0f || !unit.checkTarget(collidesAir, collidesGround)) return; - if(unit.dst(b) < explodeRange){ - b.data(0); - Time.run(5f, () -> { - if(b.data() instanceof Integer){ - b.time(b.lifetime()); + if(unit.within(b, explodeRange)){ + //mark as primed + b.fdata = -1f; + Time.run(explodeDelay, () -> { + //explode + if(b.fdata < 0){ + b.time = b.lifetime; } }); } diff --git a/core/src/mindustry/entities/bullet/LaserBoltBulletType.java b/core/src/mindustry/entities/bullet/LaserBoltBulletType.java index cd5d89d5da..beff118b7a 100644 --- a/core/src/mindustry/entities/bullet/LaserBoltBulletType.java +++ b/core/src/mindustry/entities/bullet/LaserBoltBulletType.java @@ -3,6 +3,7 @@ package mindustry.entities.bullet; import arc.graphics.g2d.*; import mindustry.gen.*; import mindustry.content.*; +import mindustry.graphics.*; public class LaserBoltBulletType extends BasicBulletType{ public float width = 2f, height = 7f; @@ -15,6 +16,8 @@ public class LaserBoltBulletType extends BasicBulletType{ despawnEffect = Fx.hitLaser; hittable = false; reflectable = false; + lightColor = Pal.heal; + lightOpacity = 0.6f; } public LaserBoltBulletType(){ @@ -23,6 +26,7 @@ public class LaserBoltBulletType extends BasicBulletType{ @Override public void draw(Bullet b){ + super.draw(b); Draw.color(backColor); Lines.stroke(width); Lines.lineAngleCenter(b.x, b.y, b.rotation(), height); diff --git a/core/src/mindustry/entities/bullet/LiquidBulletType.java b/core/src/mindustry/entities/bullet/LiquidBulletType.java index e99b7afefc..24c91fd209 100644 --- a/core/src/mindustry/entities/bullet/LiquidBulletType.java +++ b/core/src/mindustry/entities/bullet/LiquidBulletType.java @@ -63,9 +63,10 @@ public class LiquidBulletType extends BulletType{ @Override public void draw(Bullet b){ + super.draw(b); Draw.color(liquid.color, Color.white, b.fout() / 100f); - Fill.circle(b.x, b.y, orbSize); + Draw.reset(); } @Override diff --git a/core/src/mindustry/entities/bullet/RailBulletType.java b/core/src/mindustry/entities/bullet/RailBulletType.java index bb833669ac..d141c69998 100644 --- a/core/src/mindustry/entities/bullet/RailBulletType.java +++ b/core/src/mindustry/entities/bullet/RailBulletType.java @@ -69,9 +69,9 @@ public class RailBulletType extends BulletType{ } @Override - public void hitEntity(Bullet b, Hitboxc entity, float initialHealth){ - handle(b, entity, initialHealth); - super.hitEntity(b, entity, initialHealth); + public void hitEntity(Bullet b, Hitboxc entity, float health){ + handle(b, entity, health); + super.hitEntity(b, entity, health); } @Override diff --git a/core/src/mindustry/entities/bullet/SapBulletType.java b/core/src/mindustry/entities/bullet/SapBulletType.java index 969e17dcb0..ee3cd6699c 100644 --- a/core/src/mindustry/entities/bullet/SapBulletType.java +++ b/core/src/mindustry/entities/bullet/SapBulletType.java @@ -25,6 +25,8 @@ public class SapBulletType extends BulletType{ hittable = false; hitEffect = Fx.hitLiquid; status = StatusEffects.sapped; + lightColor = Pal.sap; + lightOpacity = 0.6f; statusDuration = 60f * 3f; impact = true; } @@ -40,7 +42,7 @@ public class SapBulletType extends BulletType{ Draw.reset(); - Drawf.light(b.team, b.x, b.y, Tmp.v1.x, Tmp.v1.y, 15f * b.fout(), lightColor, 0.6f); + Drawf.light(b.team, b.x, b.y, Tmp.v1.x, Tmp.v1.y, 15f * b.fout(), lightColor, lightOpacity); } } diff --git a/core/src/mindustry/entities/bullet/ShrapnelBulletType.java b/core/src/mindustry/entities/bullet/ShrapnelBulletType.java index 387c7164ff..ccc5532e7d 100644 --- a/core/src/mindustry/entities/bullet/ShrapnelBulletType.java +++ b/core/src/mindustry/entities/bullet/ShrapnelBulletType.java @@ -29,6 +29,7 @@ public class ShrapnelBulletType extends BulletType{ pierce = true; hittable = false; absorbable = false; + lightOpacity = 0.6f; } @Override @@ -52,11 +53,11 @@ public class ShrapnelBulletType extends BulletType{ @Override public void draw(Bullet b){ - float realLength = b.fdata; + float realLength = b.fdata, rot = b.rotation(); Draw.color(fromColor, toColor, b.fin()); for(int i = 0; i < (int)(serrations * realLength / length); i++){ - Tmp.v1.trns(b.rotation(), i * serrationSpacing); + Tmp.v1.trns(rot, i * serrationSpacing); float sl = Mathf.clamp(b.fout() - serrationFadeOffset) * (serrationSpaceOffset - i * serrationLenScl); Drawf.tri(b.x + Tmp.v1.x, b.y + Tmp.v1.y, serrationWidth, sl, b.rotation() + 90); Drawf.tri(b.x + Tmp.v1.x, b.y + Tmp.v1.y, serrationWidth, sl, b.rotation() - 90); @@ -64,5 +65,7 @@ public class ShrapnelBulletType extends BulletType{ Drawf.tri(b.x, b.y, width * b.fout(), (realLength + 50), b.rotation()); Drawf.tri(b.x, b.y, width * b.fout(), 10f, b.rotation() + 180f); Draw.reset(); + + Drawf.light(b.team, b.x, b.y, b.x + Angles.trnsx(rot, realLength), b.y + Angles.trnsy(rot, realLength), width * 2.5f * b.fout(), toColor, lightOpacity); } } diff --git a/core/src/mindustry/entities/comp/BlockUnitComp.java b/core/src/mindustry/entities/comp/BlockUnitComp.java index d0ec5b9a32..a0b1fdebcd 100644 --- a/core/src/mindustry/entities/comp/BlockUnitComp.java +++ b/core/src/mindustry/entities/comp/BlockUnitComp.java @@ -4,7 +4,6 @@ import arc.graphics.g2d.*; import mindustry.annotations.Annotations.*; import mindustry.game.*; import mindustry.gen.*; -import mindustry.ui.*; import static mindustry.Vars.*; @@ -34,7 +33,7 @@ abstract class BlockUnitComp implements Unitc{ @Replace @Override public TextureRegion icon(){ - return tile.block.icon(Cicon.full); + return tile.block.fullIcon; } @Override diff --git a/core/src/mindustry/entities/comp/BoundedComp.java b/core/src/mindustry/entities/comp/BoundedComp.java index c02d34a760..9976ed31c8 100644 --- a/core/src/mindustry/entities/comp/BoundedComp.java +++ b/core/src/mindustry/entities/comp/BoundedComp.java @@ -9,7 +9,7 @@ import static mindustry.Vars.*; @Component abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{ - static final float warpDst = 180f; + static final float warpDst = 40f; @Import float x, y; @Import Vec2 vel; @@ -17,11 +17,16 @@ abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{ @Override public void update(){ if(!net.client() || isLocal()){ + + float dx = 0f, dy = 0f; + //repel unit out of bounds - if(x < 0) vel.x += (-x/warpDst); - if(y < 0) vel.y += (-y/warpDst); - if(x > world.unitWidth()) vel.x -= (x - world.unitWidth())/warpDst; - if(y > world.unitHeight()) vel.y -= (y - world.unitHeight())/warpDst; + if(x < 0) dx += (-x/warpDst); + if(y < 0) dy += (-y/warpDst); + if(x > world.unitWidth()) dx -= (x - world.unitWidth())/warpDst; + if(y > world.unitHeight()) dy -= (y - world.unitHeight())/warpDst; + + velAddNet(dx, dy); } //clamp position if not flying diff --git a/core/src/mindustry/entities/comp/BuilderComp.java b/core/src/mindustry/entities/comp/BuilderComp.java index c9984f8ea4..9b456e5602 100644 --- a/core/src/mindustry/entities/comp/BuilderComp.java +++ b/core/src/mindustry/entities/comp/BuilderComp.java @@ -20,6 +20,7 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.*; import mindustry.world.blocks.ConstructBlock.*; +import mindustry.world.blocks.storage.CoreBlock.*; import java.util.*; @@ -69,7 +70,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{ } } - Building core = core(); + var core = core(); //nothing to build. if(buildPlan() == null) return; @@ -109,7 +110,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{ plans.removeFirst(); return; } - }else if((tile.team() != team && tile.team() != Team.derelict) || (!current.breaking && (cb.cblock != current.block || cb.tile != current.tile()))){ + }else if((tile.team() != team && tile.team() != Team.derelict) || (!current.breaking && (cb.current != current.block || cb.tile != current.tile()))){ plans.removeFirst(); return; } diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 62cd2b9b32..34cb209fc6 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -48,8 +48,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, //region vars and initialization static final float timeToSleep = 60f * 1, timeToUncontrol = 60f * 6; static final ObjectSet tmpTiles = new ObjectSet<>(); - static final Seq tempTileEnts = new Seq<>(); - static final Seq tempTiles = new Seq<>(); + static final Seq tempBuilds = new Seq<>(); static int sleepingEntities = 0; @Import float x, y, health, maxHealth; @@ -57,7 +56,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, transient Tile tile; transient Block block; - transient Seq proximity = new Seq<>(8); + transient Seq proximity = new Seq<>(6); transient boolean updateFlow; transient byte cdump; transient int rotation; @@ -71,6 +70,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, ConsumeModule cons; private transient float timeScale = 1f, timeScaleDuration; + private transient float dumpAccum; private transient @Nullable SoundLoop sound; @@ -213,8 +213,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, if(self() instanceof ConstructBuild entity){ //update block to reflect the fact that something was being constructed - if(entity.cblock != null && entity.cblock.synthetic() && entity.wasConstructing){ - block = entity.cblock; + if(entity.current != null && entity.current.synthetic() && entity.wasConstructing){ + block = entity.current; overrideConfig = entity.lastConfig; }else{ //otherwise this was a deconstruction that was interrupted, don't want to rebuild that @@ -401,6 +401,29 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, //endregion //region handler methods + /** @return whether the player can select (but not actually control) this building. */ + public boolean canControlSelect(Player player){ + return false; + } + + /** Called when a player control-selects this building - not called for ControlBlock subclasses. */ + public void onControlSelect(Player player){ + + } + + public void acceptPlayerPayload(Player player, Cons grabber){ + Fx.spawn.at(player); + var unit = player.unit(); + player.clearUnit(); + //player.deathTimer = Player.deathDelay + 1f; //for instant respawn + unit.remove(); + grabber.get(new UnitPayload(unit)); + Fx.unitDrop.at(unit); + if(Vars.net.client()){ + Vars.netClient.clearRemovedEntity(unit.id); + } + } + public boolean canUnload(){ return block.unloadable; } @@ -467,10 +490,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } - public void onProximityUpdate(){ - noSleep(); - } - public boolean acceptPayload(Building source, Payload payload){ return false; } @@ -688,13 +707,27 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, if(Vars.state.rules.sector != null && team == state.rules.defaultTeam) Vars.state.rules.sector.info.handleProduction(item, amount); } - /** Try dumping any item near the */ + /** Dumps any item with an accumulator. May dump multiple times per frame. Use with care. */ + public void dumpAccumulate(){ + dumpAccumulate(null); + } + + /** Dumps any item with an accumulator. May dump multiple times per frame. Use with care. */ + public void dumpAccumulate(Item item){ + dumpAccum += delta(); + while(dumpAccum >= 1f){ + dump(item); + dumpAccum -=1f; + } + } + + /** Try dumping any item near the building. */ public boolean dump(){ return dump(null); } /** - * Try dumping a specific item near the + * Try dumping a specific item near the building. * @param todump Item to dump. Can be null to dump anything. */ public boolean dump(Item todump){ @@ -753,20 +786,27 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, return false; } + /** Called shortly before this building is removed. */ public void onProximityRemoved(){ if(power != null){ powerGraphRemoved(); } } - /** in overrides, this does the exact same thing as onProximityUpdate, use that instead */ + /** Called after this building is created in the world. May be called multiple times, or when adjacent buildings change. */ public void onProximityAdded(){ - if(block.hasPower) updatePowerGraph(); + if(power != null){ + updatePowerGraph(); + } + } + + /** Called when anything adjacent to this building is placed/removed, including itself. */ + public void onProximityUpdate(){ + noSleep(); } public void updatePowerGraph(){ - - for(Building other : getPowerConnections(tempTileEnts)){ + for(Building other : getPowerConnections(tempBuilds)){ if(other.power != null){ other.power.graph.addGraph(power.graph); } @@ -970,7 +1010,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } - /** Called when the block is destroyed. */ + /** Called *after* the tile has been removed. */ + public void afterDestroyed(){ + + } + + /** Called when the block is destroyed. The tile is still intact at this stage. */ public void onDestroyed(){ float explosiveness = block.baseExplosiveness; float flammability = 0f; @@ -1022,7 +1067,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } public TextureRegion getDisplayIcon(){ - return block.icon(Cicon.medium); + return block.uiIcon; } @Override @@ -1063,7 +1108,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, l.left(); for(Item item : content.items()){ if(items.hasFlowItem(item)){ - l.image(item.icon(Cicon.small)).padRight(3f); + l.image(item.uiIcon).padRight(3f); l.label(() -> items.getFlowRate(item) < 0 ? "..." : Strings.fixed(items.getFlowRate(item), 1) + ps).color(Color.lightGray); l.row(); } @@ -1090,7 +1135,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, Runnable rebuild = () -> { l.clearChildren(); l.left(); - l.image(() -> liquids.current().icon(Cicon.small)).padRight(3f); + l.image(() -> liquids.current().uiIcon).padRight(3f); l.label(() -> liquids.getFlowRate() < 0 ? "..." : Strings.fixed(liquids.getFlowRate(), 2) + ps).color(Color.lightGray); }; @@ -1189,11 +1234,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, /** Handle a bullet collision. * @return whether the bullet should be removed. */ public boolean collision(Bullet other){ - damage(other.damage() * other.type().buildingDamageMultiplier); + damage(other.team, other.damage() * other.type().buildingDamageMultiplier); return true; } + /** Used to handle damage from splash damage for certain types of blocks. */ + public void damage(@Nullable Team source, float damage){ + damage(damage); + } + + /** Changes this building's team in a safe manner. */ + public void changeTeam(Team next){ + indexer.removeIndex(tile); + this.team = next; + indexer.addIndex(tile); + } + public boolean canPickup(){ return true; } @@ -1366,11 +1423,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, @Override public void control(LAccess type, Object p1, double p2, double p3, double p4){ //don't execute configure instructions that copy logic building configures; this can cause extreme lag - if(type == LAccess.configure && block.logicConfigurable && !(p1 instanceof LogicBuild)){ + if(type == LAccess.config && block.logicConfigurable && !(p1 instanceof LogicBuild)){ //change config only if it's new - if(senseObject(LAccess.config) != p1){ - configured(null, p1); - } + configured(null, p1); } } @@ -1388,6 +1443,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, onDestroyed(); tile.remove(); remove(); + afterDestroyed(); } @Final @@ -1409,7 +1465,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } } - if(team == Team.derelict){ + if(team == Team.derelict || !block.supportsEnv(state.rules.environment)){ enabled = false; } diff --git a/core/src/mindustry/entities/comp/BulletComp.java b/core/src/mindustry/entities/comp/BulletComp.java index a61ecc4d39..f9fe116008 100644 --- a/core/src/mindustry/entities/comp/BulletComp.java +++ b/core/src/mindustry/entities/comp/BulletComp.java @@ -1,6 +1,5 @@ package mindustry.entities.comp; -import arc.*; import arc.func.*; import arc.graphics.g2d.*; import arc.math.*; @@ -10,12 +9,10 @@ import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.core.*; import mindustry.entities.bullet.*; -import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.world.blocks.defense.Wall.*; import static mindustry.Vars.*; @@ -31,6 +28,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw BulletType type; float fdata; transient boolean absorbed, hit; + transient @Nullable Trail trail; @Override public void getCollisions(Cons consumer){ @@ -42,11 +40,6 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw } } - @Override - public void drawBullets(){ - type.draw(self()); - } - @Override public void add(){ type.init(self()); @@ -54,13 +47,17 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw @Override public void remove(){ - type.despawned(self()); + //'despawned' only counts when the bullet is killed externally or reaches the end of life + if(!hit){ + type.despawned(self()); + } + type.removed(self()); collided.clear(); } @Override public float damageMultiplier(){ - if(owner instanceof Unit) return ((Unit)owner).damageMultiplier() * state.rules.unitDamageMultiplier; + if(owner instanceof Unit u) return u.damageMultiplier() * state.rules.unitDamageMultiplier; if(owner instanceof Building) return state.rules.blockDamageMultiplier; return 1f; @@ -80,8 +77,8 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw @Replace @Override public boolean collides(Hitboxc other){ - return type.collides && (other instanceof Teamc && ((Teamc)other).team() != team) - && !(other instanceof Flyingc && !((Flyingc)other).checkTarget(type.collidesAir, type.collidesGround)) + return type.collides && (other instanceof Teamc t && t.team() != team) + && !(other instanceof Flyingc f && !f.checkTarget(type.collidesAir, type.collidesGround)) && !(type.pierce && collided.contains(other.id())); //prevent multiple collisions } @@ -89,24 +86,16 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw @Override public void collision(Hitboxc other, float x, float y){ type.hit(self(), x, y); - float health = 0f; - - if(other instanceof Healthc h){ - health = h.health(); - } //must be last. if(!type.pierce){ + hit = true; remove(); }else{ collided.add(other.id()); } - type.hitEntity(self(), other, health); - - if(owner instanceof WallBuild && player != null && team == player.team() && other instanceof Unit unit && unit.dead){ - Events.fire(Trigger.phaseDeflectHit); - } + type.hitEntity(self(), other, other instanceof Healthc h ? h.health() : 0f); } @Override @@ -130,6 +119,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw if(remove || type.collidesTeam){ if(!type.pierceBuilding){ + hit = true; remove(); }else{ collided.add(tile.id); @@ -146,13 +136,14 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw } if(type.pierceCap != -1 && collided.size >= type.pierceCap){ + hit = true; remove(); } } @Override public void draw(){ - Draw.z(Layer.bullet); + Draw.z(type.layer); type.draw(self()); type.drawLight(self()); diff --git a/core/src/mindustry/entities/comp/CommanderComp.java b/core/src/mindustry/entities/comp/CommanderComp.java index a3f0a0c625..4e10af62e1 100644 --- a/core/src/mindustry/entities/comp/CommanderComp.java +++ b/core/src/mindustry/entities/comp/CommanderComp.java @@ -83,7 +83,7 @@ abstract class CommanderComp implements Entityc, Posc{ clearCommand(); units.shuffle(); - float spacing = hitSize * 0.8f; + float spacing = hitSize * 0.9f; minFormationSpeed = type.speed; controlling.addAll(units); diff --git a/core/src/mindustry/entities/comp/EntityComp.java b/core/src/mindustry/entities/comp/EntityComp.java index c2ba271182..d44e5481b4 100644 --- a/core/src/mindustry/entities/comp/EntityComp.java +++ b/core/src/mindustry/entities/comp/EntityComp.java @@ -40,6 +40,7 @@ abstract class EntityComp{ return false; } + /** Replaced with `this` after code generation. */ T self(){ return (T)this; } @@ -48,11 +49,6 @@ abstract class EntityComp{ return (T)this; } - T with(Cons cons){ - cons.get((T)this); - return (T)this; - } - @InternalImpl abstract int classId(); diff --git a/core/src/mindustry/entities/comp/FireComp.java b/core/src/mindustry/entities/comp/FireComp.java index 89710ebdf1..7439af97c9 100644 --- a/core/src/mindustry/entities/comp/FireComp.java +++ b/core/src/mindustry/entities/comp/FireComp.java @@ -1,5 +1,7 @@ package mindustry.entities.comp; +import arc.*; +import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; import arc.util.*; @@ -9,6 +11,7 @@ import mindustry.content.*; import mindustry.entities.*; import mindustry.game.*; import mindustry.gen.*; +import mindustry.graphics.*; import mindustry.world.*; import mindustry.world.meta.*; @@ -16,24 +19,30 @@ import static mindustry.Vars.*; @EntityDef(value = {Firec.class}, pooled = true) @Component(base = true) -abstract class FireComp implements Timedc, Posc, Firec, Syncc{ - private static final float spreadChance = 0.04f, fireballChance = 0.06f; +abstract class FireComp implements Timedc, Posc, Syncc, Drawc{ + public static final int frames = 40, duration = 90; + + private static final float spreadDelay = 22f, fireballDelay = 40f, + ticksPerFrame = (float)duration / frames, warmupDuration = 20f, damageDelay = 40f, tileDamage = 1.8f, unitDamage = 3f; + + public static final TextureRegion[] regions = new TextureRegion[frames]; @Import float time, lifetime, x, y; Tile tile; private transient Block block; - private transient float baseFlammability = -1, puddleFlammability; + private transient float + baseFlammability = -1, puddleFlammability, damageTimer = Mathf.random(40f), + spreadTimer = Mathf.random(spreadDelay), fireballTimer = Mathf.random(fireballDelay), + warmup = 0f, + animation = Mathf.random(frames); @Override public void update(){ - if(Mathf.chance(0.09 * Time.delta)){ - Fx.fire.at(x + Mathf.range(4f), y + Mathf.range(4f)); - } - if(Mathf.chance(0.05 * Time.delta)){ - Fx.fireSmoke.at(x + Mathf.range(4f), y + Mathf.range(4f)); - } + animation += Time.delta / ticksPerFrame; + warmup += Time.delta; + animation %= frames; if(!headless){ control.sound.loop(Sounds.fire, this, 0.07f); @@ -55,46 +64,71 @@ abstract class FireComp implements Timedc, Posc, Firec, Syncc{ Building entity = tile.build; boolean damage = entity != null; + if(baseFlammability < 0 || block != tile.block()){ + baseFlammability = tile.getFlammability(); + block = tile.block(); + } + float flammability = baseFlammability + puddleFlammability; if(!damage && flammability <= 0){ time += Time.delta * 8; } - if(baseFlammability < 0 || block != tile.block()){ - baseFlammability = tile.build == null ? 0 : tile.getFlammability(); - block = tile.block(); - } - if(damage){ lifetime += Mathf.clamp(flammability / 8f, 0f, 0.6f) * Time.delta; } - if(flammability > 1f && Mathf.chance(spreadChance * Time.delta * Mathf.clamp(flammability / 5f, 0.3f, 2f))){ + if(flammability > 1f && (spreadTimer += Time.delta * Mathf.clamp(flammability / 5f, 0.3f, 2f)) >= spreadDelay){ + spreadTimer = 0f; Point2 p = Geometry.d4[Mathf.random(3)]; Tile other = world.tile(tile.x + p.x, tile.y + p.y); Fires.create(other); - - if(Mathf.chance(fireballChance * Time.delta * Mathf.clamp(flammability / 10f))){ - Bullets.fireball.createNet(Team.derelict, x, y, Mathf.random(360f), -1f, 1, 1); - } } - if(Mathf.chance(0.025 * Time.delta)){ + if(flammability > 0 && (fireballTimer += Time.delta * Mathf.clamp(flammability / 10f, 0f, 0.5f)) >= fireballDelay){ + fireballTimer = 0f; + Bullets.fireball.createNet(Team.derelict, x, y, Mathf.random(360f), -1f, 1, 1); + } + + //apply damage to nearby units & building + if((damageTimer += Time.delta) >= damageDelay){ + damageTimer = 0f; Puddlec p = Puddles.get(tile); puddleFlammability = p != null ? p.getFlammability() / 3f : 0; if(damage){ - entity.damage(1.6f); + entity.damage(tileDamage); } - Damage.damageUnits(null, tile.worldx(), tile.worldy(), tilesize, 3f, + Damage.damageUnits(null, tile.worldx(), tile.worldy(), tilesize, unitDamage, unit -> !unit.isFlying() && !unit.isImmune(StatusEffects.burning), unit -> unit.apply(StatusEffects.burning, 60 * 5)); } } + @Override + public void draw(){ + if(regions[0] == null){ + for(int i = 0; i < frames; i++){ + regions[i] = Core.atlas.find("fire" + i); + } + } + + Draw.alpha(Mathf.clamp(warmup / warmupDuration)); + Draw.z(Layer.effect); + Draw.rect(regions[(int)animation], x, y); + Draw.reset(); + } + + @Replace + @Override + public float clipSize(){ + return 25; + } + @Override public void remove(){ + Fx.fireRemove.at(x, y, animation); Fires.remove(tile); } diff --git a/core/src/mindustry/entities/comp/LaunchCoreComp.java b/core/src/mindustry/entities/comp/LaunchCoreComp.java index 4f642c8493..e91e3749fc 100644 --- a/core/src/mindustry/entities/comp/LaunchCoreComp.java +++ b/core/src/mindustry/entities/comp/LaunchCoreComp.java @@ -43,7 +43,7 @@ abstract class LaunchCoreComp implements Drawc, Timedc{ Draw.z(Layer.weather - 1); - TextureRegion region = block.icon(Cicon.full); + TextureRegion region = block.fullIcon; float rw = region.width * Draw.scl * scale, rh = region.height * Draw.scl * scale; Draw.alpha(alpha); diff --git a/core/src/mindustry/entities/comp/PayloadComp.java b/core/src/mindustry/entities/comp/PayloadComp.java index 9afd26bb9a..0f02434a92 100644 --- a/core/src/mindustry/entities/comp/PayloadComp.java +++ b/core/src/mindustry/entities/comp/PayloadComp.java @@ -83,8 +83,8 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{ Tile on = tileOn(); //clear removed state of unit so it can be synced - if(Vars.net.client() && payload instanceof UnitPayload){ - Vars.netClient.clearRemovedEntity(((UnitPayload)payload).unit.id); + if(Vars.net.client() && payload instanceof UnitPayload u){ + Vars.netClient.clearRemovedEntity(u.unit.id); } //drop off payload on an acceptor if possible @@ -106,7 +106,7 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{ Unit u = payload.unit; //can't drop ground units - if(!u.canPass(tileX(), tileY())){ + if(!u.canPass(tileX(), tileY()) || Units.count(x, y, u.physicSize(), o -> o.isGrounded()) > 1){ return false; } @@ -159,7 +159,7 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{ } for(Payload p : payloads){ - table.image(p.icon(Cicon.small)).size(itemSize).padRight(pad); + table.image(p.icon()).size(itemSize).padRight(pad); } } } diff --git a/core/src/mindustry/entities/comp/PhysicsComp.java b/core/src/mindustry/entities/comp/PhysicsComp.java index d68e392dca..1924de8eb9 100644 --- a/core/src/mindustry/entities/comp/PhysicsComp.java +++ b/core/src/mindustry/entities/comp/PhysicsComp.java @@ -11,7 +11,7 @@ import mindustry.gen.*; * Has mass.*/ @Component abstract class PhysicsComp implements Velc, Hitboxc, Flyingc{ - @Import float hitSize; + @Import float hitSize, x, y; @Import Vec2 vel; transient PhysicRef physref; diff --git a/core/src/mindustry/entities/comp/PlayerComp.java b/core/src/mindustry/entities/comp/PlayerComp.java index 4f77c5b78c..0323906021 100644 --- a/core/src/mindustry/entities/comp/PlayerComp.java +++ b/core/src/mindustry/entities/comp/PlayerComp.java @@ -19,7 +19,6 @@ import mindustry.net.Administration.*; import mindustry.net.*; import mindustry.net.Packets.*; import mindustry.ui.*; -import mindustry.world.*; import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -33,31 +32,31 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra @Import float x, y; @ReadOnly Unit unit = Nulls.unit; - transient private Unit lastReadUnit = Nulls.unit; transient @Nullable NetConnection con; - @ReadOnly Team team = Team.sharded; @SyncLocal boolean typing, shooting, boosting; - boolean admin; @SyncLocal float mouseX, mouseY; - String name = "noname"; + boolean admin; + String name = "frog"; Color color = new Color(); - //locale should not be synced. transient String locale = "en"; transient float deathTimer; transient String lastText = ""; transient float textFadeTime; + transient private Unit lastReadUnit = Nulls.unit; public boolean isBuilder(){ return unit.canBuild(); } - public @Nullable CoreBuild closestCore(){ + public @Nullable + CoreBuild closestCore(){ return state.teams.closestCore(x, y, team); } - public @Nullable CoreBuild core(){ + public @Nullable + CoreBuild core(){ return team.core(); } @@ -69,7 +68,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra public TextureRegion icon(){ //display default icon for dead players - if(dead()) return core() == null ? UnitTypes.alpha.icon(Cicon.full) : ((CoreBlock)core().block).unitType.icon(Cicon.full); + if(dead()) return core() == null ? UnitTypes.alpha.fullIcon : ((CoreBlock)core().block).unitType.fullIcon; return unit.icon(); } @@ -133,8 +132,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra //update some basic state to sync things if(unit.type.canBoost){ - Tile tile = unit.tileOn(); - unit.elevation = Mathf.approachDelta(unit.elevation, (tile != null && tile.solid()) || boosting ? 1f : 0f, unit.type.riseSpeed); + unit.elevation = Mathf.approachDelta(unit.elevation, unit.onSolid() || boosting || (unit.isFlying() && !unit.canLand()) ? 1f : 0f, unit.type.riseSpeed); } }else if((core = bestCore()) != null){ //have a small delay before death to prevent the camera from jumping around too quickly @@ -266,9 +264,9 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra layout.setText(font, text, Color.white, width, Align.bottom, true); - Draw.color(0f, 0f, 0f, 0.3f * (textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime)); - Fill.rect(unit.x, unit.y + textHeight + layout.height - layout.height/2f, layout.width + 2, layout.height + 3); - font.draw(text, unit.x - width/2f, unit.y + textHeight + layout.height, width, Align.center, true); + Draw.color(0f, 0f, 0f, 0.3f * (textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime)); + Fill.rect(unit.x, unit.y + textHeight + layout.height - layout.height / 2f, layout.width + 2, layout.height + 3); + font.draw(text, unit.x - width / 2f, unit.y + textHeight + layout.height, width, Align.center, true); } Draw.reset(); @@ -294,7 +292,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra sendMessage(text, from, NetClient.colorizeName(from.id(), from.name)); } - void sendMessage(String text, Player from, String fromName){ + void sendMessage(String text, Player from, String fromName){ if(isLocal()){ if(ui != null){ ui.chatfrag.addMessage(text, fromName); diff --git a/core/src/mindustry/entities/comp/PuddleComp.java b/core/src/mindustry/entities/comp/PuddleComp.java index cefebe8307..12f56fbaa4 100644 --- a/core/src/mindustry/entities/comp/PuddleComp.java +++ b/core/src/mindustry/entities/comp/PuddleComp.java @@ -21,10 +21,10 @@ import static mindustry.entities.Puddles.*; @Component(base = true) abstract class PuddleComp implements Posc, Puddlec, Drawc{ private static final int maxGeneration = 2; - private static final Color tmp = new Color(); private static final Rect rect = new Rect(), rect2 = new Rect(); private static int seeds; + @Import int id; @Import float x, y; transient float accepting, updateTime, lastRipple; @@ -92,13 +92,13 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc{ public void draw(){ Draw.z(Layer.debris - 1); - seeds = id(); + seeds = id; boolean onLiquid = tile.floor().isLiquid; float f = Mathf.clamp(amount / (maxLiquid / 1.5f)); float smag = onLiquid ? 0.8f : 0f; float sscl = 25f; - Draw.color(tmp.set(liquid.color).shiftValue(-0.05f)); + Draw.color(Tmp.c1.set(liquid.color).shiftValue(-0.05f)); Fill.circle(x + Mathf.sin(Time.time + seeds * 532, sscl, smag), y + Mathf.sin(Time.time + seeds * 53, sscl, smag), f * 8f); Angles.randLenVectors(id(), 3, f * 6f, (ex, ey) -> { Fill.circle(x + ex + Mathf.sin(Time.time + seeds * 532, sscl, smag), diff --git a/core/src/mindustry/entities/comp/ShieldComp.java b/core/src/mindustry/entities/comp/ShieldComp.java index 5a489686ff..d1d3b50a87 100644 --- a/core/src/mindustry/entities/comp/ShieldComp.java +++ b/core/src/mindustry/entities/comp/ShieldComp.java @@ -3,6 +3,7 @@ package mindustry.entities.comp; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; +import mindustry.game.*; import mindustry.gen.*; import static mindustry.Vars.*; @@ -11,10 +12,11 @@ import static mindustry.Vars.*; abstract class ShieldComp implements Healthc, Posc{ @Import float health, hitTime, x, y, healthMultiplier; @Import boolean dead; + @Import Team team; /** Absorbs health damage. */ float shield; - /** Substracts an amount from damage. */ + /** Subtracts an amount from damage. */ float armor; /** Shield opacity. */ transient float shieldAlpha = 0f; @@ -60,7 +62,7 @@ abstract class ShieldComp implements Healthc, Posc{ } if(hadShields && shield <= 0.0001f){ - Fx.unitShieldBreak.at(x, y, 0, this); + Fx.unitShieldBreak.at(x, y, 0, team.color, this); } } } diff --git a/core/src/mindustry/entities/comp/StatusComp.java b/core/src/mindustry/entities/comp/StatusComp.java index f029dfade1..70d1e38dcb 100644 --- a/core/src/mindustry/entities/comp/StatusComp.java +++ b/core/src/mindustry/entities/comp/StatusComp.java @@ -19,8 +19,9 @@ abstract class StatusComp implements Posc, Flyingc{ private Seq statuses = new Seq<>(); private transient Bits applied = new Bits(content.getBy(ContentType.status).size); - @ReadOnly transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1; - @ReadOnly transient boolean disarmed = false; + //these are considered read-only + transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1; + transient boolean disarmed = false; @Import UnitType type; @@ -46,15 +47,8 @@ abstract class StatusComp implements Posc, Flyingc{ if(entry.effect == effect){ entry.time = Math.max(entry.time, duration); return; - }else if(entry.effect.reactsWith(effect)){ //find opposite - StatusEntry.tmp.effect = entry.effect; - entry.effect.getTransition(self(), effect, entry.time, duration, StatusEntry.tmp); - entry.time = StatusEntry.tmp.time; - - if(StatusEntry.tmp.effect != entry.effect){ - entry.effect = StatusEntry.tmp.effect; - } - + }else if(entry.effect.applyTransition(self(), effect, entry, duration)){ //find reaction + //TODO effect may react with multiple other effects //stop looking when one is found return; } @@ -69,6 +63,11 @@ abstract class StatusComp implements Posc, Flyingc{ } } + float getDuration(StatusEffect effect){ + var entry = statuses.find(e -> e.effect == effect); + return entry == null ? 0 : entry.time; + } + void clearStatuses(){ statuses.clear(); } @@ -149,6 +148,10 @@ abstract class StatusComp implements Posc, Flyingc{ } } + public Bits statusBits(){ + return applied; + } + public void draw(){ for(StatusEntry e : statuses){ e.effect.draw(self()); diff --git a/core/src/mindustry/entities/comp/TeamComp.java b/core/src/mindustry/entities/comp/TeamComp.java index c58e97b7fd..b222229619 100644 --- a/core/src/mindustry/entities/comp/TeamComp.java +++ b/core/src/mindustry/entities/comp/TeamComp.java @@ -4,6 +4,7 @@ import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.game.*; import mindustry.gen.*; +import mindustry.world.blocks.storage.CoreBlock.*; import static mindustry.Vars.*; @@ -18,17 +19,17 @@ abstract class TeamComp implements Posc{ } @Nullable - public Building core(){ + public CoreBuild core(){ return team.core(); } @Nullable - public Building closestCore(){ + public CoreBuild closestCore(){ return state.teams.closestCore(x, y, team); } @Nullable - public Building closestEnemyCore(){ + public CoreBuild closestEnemyCore(){ return state.teams.closestEnemyCore(x, y, team); } } diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 27c5512052..47a10d113c 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -46,8 +46,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I double flag; transient Seq abilities = new Seq<>(0); + transient float healTime; private transient float resupplyTime = Mathf.random(10f); private transient boolean wasPlayer; + private transient float lastHealth; public void moveAt(Vec2 vector){ moveAt(vector, type.accel); @@ -67,6 +69,16 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I lookAt(x, y); } + /** @return approx. square size of the physical hitbox for physics */ + public float physicSize(){ + return hitSize * 0.7f; + } + + /** @return whether there is solid, un-occupied ground under this unit. */ + public boolean canLand(){ + return !onSolid() && Units.count(x, y, physicSize(), f -> f != self() && f.isGrounded()) == 0; + } + public boolean inRange(Position other){ return within(other, type.range); } @@ -98,7 +110,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I return angleTo(buildPlan()); }else if(mineTile != null){ return angleTo(mineTile); - }else if(moving()){ + }else if(moving() && type.omniMovement){ return vel().angle(); } return rotation; @@ -114,7 +126,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I if(isBuilding()){ return state.rules.infiniteResources ? Float.MAX_VALUE : Math.max(type.clipSize, type.region.width) + buildingRange + tilesize*4f; } - return Math.max(type.region.width * 2f, type.clipSize); + return type.clipSize; } @Override @@ -146,7 +158,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I controller instanceof FormationAI ? ctrlFormation : 0; case commanded -> controller instanceof FormationAI && isValid() ? 1 : 0; - case payloadCount -> self() instanceof Payloadc pay ? pay.payloads().size : 0; + case payloadCount -> ((Object)this) instanceof Payloadc pay ? pay.payloads().size : 0; case size -> hitSize / tilesize; default -> Float.NaN; }; @@ -159,7 +171,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I case name -> controller instanceof Player p ? p.name : null; case firstItem -> stack().amount == 0 ? null : item(); case controller -> !isValid() ? null : controller instanceof LogicAI log ? log.controller : controller instanceof FormationAI form ? form.leader : this; - case payloadType -> self() instanceof Payloadc pay ? + case payloadType -> ((Object)this) instanceof Payloadc pay ? (pay.payloads().isEmpty() ? null : pay.payloads().peek() instanceof UnitPayload p1 ? p1.unit.type : pay.payloads().peek() instanceof BuildPayload p2 ? p2.block() : null) : null; @@ -313,6 +325,18 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I type.update(self()); + if(health > lastHealth && lastHealth > 0 && healTime <= -1f){ + healTime = 1f; + } + healTime -= Time.delta / 20f; + lastHealth = health; + + //check if environment is unsupported + if(!type.supportsEnv(state.rules.environment) && !dead){ + Call.unitCapDeath(self()); + team.data().updateCount(type, -1); + } + if(state.rules.unitAmmo && ammo < type.ammoCapacity - 0.0001f){ resupplyTime += Time.delta; @@ -336,7 +360,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I float relativeSize = state.rules.dropZoneRadius + hitSize/2f + 1f; for(Tile spawn : spawner.getSpawns()){ if(within(spawn.worldx(), spawn.worldy(), relativeSize)){ - vel().add(Tmp.v1.set(this).sub(spawn.worldx(), spawn.worldy()).setLength(0.1f + 1f - dst(spawn) / relativeSize).scl(0.45f * Time.delta)); + velAddNet(Tmp.v1.set(this).sub(spawn.worldx(), spawn.worldy()).setLength(0.1f + 1f - dst(spawn) / relativeSize).scl(0.45f * Time.delta)); } } } @@ -367,7 +391,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I elevation -= type.fallSpeed * Time.delta; if(isGrounded()){ - destroy(); + Call.unitDestroy(id); } } @@ -414,7 +438,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I /** @return a preview icon for this unit. */ public TextureRegion icon(){ - return type.icon(Cicon.full); + return type.fullIcon; } /** Actually destroys the unit, removing it and creating explosions. **/ diff --git a/core/src/mindustry/entities/comp/VelComp.java b/core/src/mindustry/entities/comp/VelComp.java index 4b0272b5f4..3e79b04e17 100644 --- a/core/src/mindustry/entities/comp/VelComp.java +++ b/core/src/mindustry/entities/comp/VelComp.java @@ -1,5 +1,6 @@ package mindustry.entities.comp; +import arc.math.*; import arc.math.geom.*; import arc.util.*; import mindustry.annotations.Annotations.*; @@ -20,7 +21,11 @@ abstract class VelComp implements Posc{ @MethodPriority(-1) @Override public void update(){ + float px = x, py = y; move(vel.x * Time.delta, vel.y * Time.delta); + if(Mathf.equal(px, x)) vel.x = 0; + if(Mathf.equal(py, y)) vel.y = 0; + vel.scl(Math.max(1f - drag * Time.delta, 0)); } @@ -45,6 +50,10 @@ abstract class VelComp implements Posc{ return !vel.isZero(0.01f); } + void move(Vec2 v){ + move(v.x, v.y); + } + void move(float cx, float cy){ SolidPred check = solidity(); @@ -55,4 +64,20 @@ abstract class VelComp implements Posc{ y += cy; } } + + void velAddNet(Vec2 v){ + vel.add(v); + if(isRemote()){ + x += v.x; + y += v.y; + } + } + + void velAddNet(float vx, float vy){ + vel.add(vx, vy); + if(isRemote()){ + x += vx; + y += vy; + } + } } diff --git a/core/src/mindustry/entities/comp/WeaponsComp.java b/core/src/mindustry/entities/comp/WeaponsComp.java index 10f2d59b2f..bc0b0c85c5 100644 --- a/core/src/mindustry/entities/comp/WeaponsComp.java +++ b/core/src/mindustry/entities/comp/WeaponsComp.java @@ -1,28 +1,18 @@ package mindustry.entities.comp; -import arc.math.*; import arc.math.geom.*; import arc.util.*; import mindustry.annotations.Annotations.*; -import mindustry.audio.*; -import mindustry.entities.*; -import mindustry.entities.bullet.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.type.*; -import static mindustry.Vars.*; - @Component abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ - @Import float x, y, rotation, reloadMultiplier; + @Import float x, y; @Import boolean disarmed; - @Import Vec2 vel; @Import UnitType type; - /** temporary weapon sequence number */ - static int sequenceNum = 0; - /** weapon mount array, never null */ @SyncLocal WeaponMount[] mounts = {}; @ReadOnly transient boolean isRotate; @@ -43,7 +33,7 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ void setupWeapons(UnitType def){ mounts = new WeaponMount[def.weapons.size]; for(int i = 0; i < mounts.length; i++){ - mounts[i] = new WeaponMount(def.weapons.get(i)); + mounts[i] = def.weapons.get(i).mountType.get(def.weapons.get(i)); } } @@ -53,8 +43,10 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ void controlWeapons(boolean rotate, boolean shoot){ for(WeaponMount mount : mounts){ - mount.rotate = rotate; - mount.shoot = shoot; + if(mount.weapon.controllable){ + mount.rotate = rotate; + mount.shoot = shoot; + } } isRotate = rotate; isShooting = shoot; @@ -73,8 +65,10 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ y = Tmp.v1.y + this.y; for(WeaponMount mount : mounts){ - mount.aimX = x; - mount.aimY = y; + if(mount.weapon.controllable){ + mount.aimX = x; + mount.aimY = y; + } } aimX = x; @@ -88,7 +82,7 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ @Override public void remove(){ for(WeaponMount mount : mounts){ - if(mount.bullet != null){ + if(mount.bullet != null && mount.bullet.owner == self()){ mount.bullet.time = mount.bullet.lifetime - 10f; mount.bullet = null; } @@ -102,136 +96,8 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ /** Update shooting and rotation for this unit. */ @Override public void update(){ - boolean can = canShoot(); - for(WeaponMount mount : mounts){ - Weapon weapon = mount.weapon; - mount.reload = Math.max(mount.reload - Time.delta * reloadMultiplier, 0); - - float weaponRotation = this.rotation - 90 + (weapon.rotate ? mount.rotation : 0); - float mountX = this.x + Angles.trnsx(this.rotation - 90, weapon.x, weapon.y), - mountY = this.y + Angles.trnsy(this.rotation - 90, weapon.x, weapon.y); - float shootX = mountX + Angles.trnsx(weaponRotation, weapon.shootX, weapon.shootY), - shootY = mountY + Angles.trnsy(weaponRotation, weapon.shootX, weapon.shootY); - float shootAngle = weapon.rotate ? weaponRotation + 90 : Angles.angle(shootX, shootY, mount.aimX, mount.aimY) + (this.rotation - angleTo(mount.aimX, mount.aimY)); - - //update continuous state - if(weapon.continuous && mount.bullet != null){ - if(!mount.bullet.isAdded() || mount.bullet.time >= mount.bullet.lifetime || mount.bullet.type != weapon.bullet){ - mount.bullet = null; - }else{ - mount.bullet.rotation(weaponRotation + 90); - mount.bullet.set(shootX, shootY); - mount.reload = weapon.reload; - vel.add(Tmp.v1.trns(rotation + 180f, mount.bullet.type.recoil)); - if(weapon.shootSound != Sounds.none && !headless){ - if(mount.sound == null) mount.sound = new SoundLoop(weapon.shootSound, 1f); - mount.sound.update(x, y, true); - } - } - }else{ - //heat decreases when not firing - mount.heat = Math.max(mount.heat - Time.delta * reloadMultiplier / mount.weapon.cooldownTime, 0); - - if(mount.sound != null){ - mount.sound.update(x, y, false); - } - } - - //flip weapon shoot side for alternating weapons at half reload - if(weapon.otherSide != -1 && weapon.alternate && mount.side == weapon.flipSprite && - mount.reload + Time.delta * reloadMultiplier > weapon.reload/2f && mount.reload <= weapon.reload/2f){ - mounts[weapon.otherSide].side = !mounts[weapon.otherSide].side; - mount.side = !mount.side; - } - - //rotate if applicable - if(weapon.rotate && (mount.rotate || mount.shoot) && can){ - float axisX = this.x + Angles.trnsx(this.rotation - 90, weapon.x, weapon.y), - axisY = this.y + Angles.trnsy(this.rotation - 90, weapon.x, weapon.y); - - mount.targetRotation = Angles.angle(axisX, axisY, mount.aimX, mount.aimY) - this.rotation; - mount.rotation = Angles.moveToward(mount.rotation, mount.targetRotation, weapon.rotateSpeed * Time.delta); - }else if(!weapon.rotate){ - mount.rotation = 0; - mount.targetRotation = angleTo(mount.aimX, mount.aimY); - } - - //shoot if applicable - if(mount.shoot && //must be shooting - can && //must be able to shoot - (ammo > 0 || !state.rules.unitAmmo || team().rules().infiniteAmmo) && //check ammo - (!weapon.alternate || mount.side == weapon.flipSprite) && - //TODO checking for velocity this way isn't entirely correct - (vel.len() >= mount.weapon.minShootVelocity || (net.active() && !isLocal())) && //check velocity requirements - mount.reload <= 0.0001f && //reload has to be 0 - Angles.within(weapon.rotate ? mount.rotation : this.rotation, mount.targetRotation, mount.weapon.shootCone) //has to be within the cone - ){ - shoot(mount, shootX, shootY, mount.aimX, mount.aimY, mountX, mountY, shootAngle, Mathf.sign(weapon.x)); - - mount.reload = weapon.reload; - - ammo--; - if(ammo < 0) ammo = 0; - } + mount.weapon.update(self(), mount); } } - - private void shoot(WeaponMount mount, float x, float y, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ - Weapon weapon = mount.weapon; - - float baseX = this.x, baseY = this.y; - boolean delay = weapon.firstShotDelay + weapon.shotDelay > 0f; - - (delay ? weapon.chargeSound : weapon.continuous ? Sounds.none : weapon.shootSound).at(x, y, Mathf.random(weapon.soundPitchMin, weapon.soundPitchMax)); - - BulletType ammo = weapon.bullet; - float lifeScl = ammo.scaleVelocity ? Mathf.clamp(Mathf.dst(x, y, aimX, aimY) / ammo.range()) : 1f; - - sequenceNum = 0; - if(delay){ - Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> { - Time.run(sequenceNum * weapon.shotDelay + weapon.firstShotDelay, () -> { - if(!isAdded()) return; - mount.bullet = bullet(weapon, x + this.x - baseX, y + this.y - baseY, f + Mathf.range(weapon.inaccuracy), lifeScl); - }); - sequenceNum++; - }); - }else{ - Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> mount.bullet = bullet(weapon, x, y, f + Mathf.range(weapon.inaccuracy), lifeScl)); - } - - boolean parentize = ammo.keepVelocity; - - if(delay){ - Time.run(weapon.firstShotDelay, () -> { - if(!isAdded()) return; - - vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); - Effect.shake(weapon.shake, weapon.shake, x, y); - mount.heat = 1f; - if(!weapon.continuous){ - weapon.shootSound.at(x, y, Mathf.random(weapon.soundPitchMin, weapon.soundPitchMax)); - } - }); - }else{ - vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); - Effect.shake(weapon.shake, weapon.shake, x, y); - mount.heat = 1f; - } - - weapon.ejectEffect.at(mountX, mountY, rotation * side); - ammo.shootEffect.at(x, y, rotation, parentize ? this : null); - ammo.smokeEffect.at(x, y, rotation, parentize ? this : null); - apply(weapon.shootStatus, weapon.shootStatusDuration); - } - - private Bullet bullet(Weapon weapon, float x, float y, float angle, float lifescl){ - float xr = Mathf.range(weapon.xRand); - - return weapon.bullet.create(this, team(), - x + Angles.trnsx(angle, 0, xr), - y + Angles.trnsy(angle, 0, xr), - angle, (1f - weapon.velocityRnd) + Mathf.random(weapon.velocityRnd), lifescl); - } } diff --git a/core/src/mindustry/entities/effect/ExplosionEffect.java b/core/src/mindustry/entities/effect/ExplosionEffect.java new file mode 100644 index 0000000000..352e676f9c --- /dev/null +++ b/core/src/mindustry/entities/effect/ExplosionEffect.java @@ -0,0 +1,47 @@ +package mindustry.entities.effect; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import mindustry.entities.*; +import mindustry.graphics.*; + +import static arc.graphics.g2d.Draw.*; +import static arc.graphics.g2d.Lines.*; +import static arc.math.Angles.*; + +public class ExplosionEffect extends Effect{ + public Color waveColor = Pal.missileYellow, smokeColor = Color.gray, sparkColor = Pal.missileYellowBack; + public float waveLife = 6f, waveStroke = 3f, waveRad = 15f, waveRadBase = 2f, sparkStroke = 1f, sparkRad = 23f, sparkLen = 3f, smokeSize = 4f, smokeSizeBase = 0.5f, smokeRad = 23f; + public int smokes = 5, sparks = 4; + + public ExplosionEffect(){ + clip = 100f; + lifetime = 22; + + renderer = e -> { + color(waveColor); + + e.scaled(waveLife, i -> { + stroke(waveStroke * i.fout()); + Lines.circle(e.x, e.y, waveRadBase + i.fin() * waveRad); + }); + + color(smokeColor); + + if(smokeSize > 0){ + randLenVectors(e.id, smokes, 2f + smokeRad * e.finpow(), (x, y) -> { + Fill.circle(e.x + x, e.y + y, e.fout() * smokeSize + smokeSizeBase); + }); + } + + color(sparkColor); + stroke(e.fout() * sparkStroke); + + randLenVectors(e.id + 1, sparks, 1f + sparkRad * e.finpow(), (x, y) -> { + lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * sparkLen); + Drawf.light(e.x + x, e.y + y, e.fout() * sparkLen * 4f, sparkColor, 0.7f); + }); + }; + } +} diff --git a/core/src/mindustry/entities/effect/ParticleEffect.java b/core/src/mindustry/entities/effect/ParticleEffect.java index 281410ada3..a6e5df24a8 100644 --- a/core/src/mindustry/entities/effect/ParticleEffect.java +++ b/core/src/mindustry/entities/effect/ParticleEffect.java @@ -6,17 +6,28 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.util.*; import mindustry.entities.*; +import mindustry.graphics.*; /** The most essential effect class. Can create particles in various shapes. */ public class ParticleEffect extends Effect{ public Color colorFrom = Color.white.cpy(), colorTo = Color.white.cpy(); public int particles = 6; public float cone = 180f, length = 20f, baseLength = 0f; + /** Particle size/length/radius interpolation. */ public Interp interp = Interp.linear; + public float offsetX, offsetY; + public float lightScl = 2f, lightOpacity = 0.6f; + public @Nullable Color lightColor; //region only + + /** Spin in degrees per tick. */ + public float spin = 0f; + /** Controls the initial and final sprite sizes. */ public float sizeFrom = 2f, sizeTo = 0f; + /** Rotation offset. */ public float offset = 0; + /** Sprite to draw. */ public String region = "circle"; //line only @@ -37,19 +48,23 @@ public class ParticleEffect extends Effect{ float rawfin = e.fin(); float fin = e.fin(interp); float rad = interp.apply(sizeFrom, sizeTo, rawfin) * 2; + float ox = e.x + Angles.trnsx(e.rotation, offsetX, offsetY), oy = e.y + Angles.trnsy(e.rotation, offsetX, offsetY); Draw.color(colorFrom, colorTo, fin); + Color lightColor = this.lightColor == null ? Draw.getColor() : this.lightColor; if(line){ Lines.stroke(interp.apply(strokeFrom, strokeTo, rawfin)); float len = interp.apply(lenFrom, lenTo, rawfin); Angles.randLenVectors(e.id, particles, length * fin + baseLength, e.rotation, cone, (x, y) -> { - Lines.lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), len); + Lines.lineAngle(ox + x, oy + y, Mathf.angle(x, y), len); + Drawf.light(ox + x, oy + y, len * lightScl, lightColor, lightOpacity); }); }else{ Angles.randLenVectors(e.id, particles, length * fin + baseLength, e.rotation, cone, (x, y) -> { - Draw.rect(tex, e.x + x, e.y + y, rad, rad, e.rotation + offset); + Draw.rect(tex, ox + x, oy + y, rad, rad, e.rotation + offset + e.time * spin); + Drawf.light(ox + x, oy + y, rad * lightScl, lightColor, lightOpacity); }); } } diff --git a/core/src/mindustry/entities/effect/WaveEffect.java b/core/src/mindustry/entities/effect/WaveEffect.java index 88ff7391d2..7aaddd95b3 100644 --- a/core/src/mindustry/entities/effect/WaveEffect.java +++ b/core/src/mindustry/entities/effect/WaveEffect.java @@ -3,16 +3,21 @@ package mindustry.entities.effect; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.util.*; import mindustry.entities.*; +import mindustry.graphics.*; /** Effect that renders a basic shockwave. */ public class WaveEffect extends Effect{ public Color colorFrom = Color.white.cpy(), colorTo = Color.white.cpy(); - public float sizeFrom = 0f, sizeTo = 100f; + public @Nullable Color lightColor; + public float sizeFrom = 0f, sizeTo = 100f, lightScl = 3f, lightOpacity = 0.8f; public int sides = -1; public float rotation = 0f; public float strokeFrom = 2f, strokeTo = 0f; public Interp interp = Interp.linear; + public Interp lightInterp = Interp.reverse; + public float offsetX, offsetY; @Override public void init(){ @@ -23,11 +28,14 @@ public class WaveEffect extends Effect{ public void render(EffectContainer e){ float fin = e.fin(); float ifin = e.fin(interp); + float ox = e.x + Angles.trnsx(e.rotation, offsetX, offsetY), oy = e.y + Angles.trnsy(e.rotation, offsetX, offsetY); Draw.color(colorFrom, colorTo, ifin); Lines.stroke(interp.apply(strokeFrom, strokeTo, fin)); float rad = interp.apply(sizeFrom, sizeTo, fin); - Lines.poly(e.x, e.y, sides <= 0 ? Lines.circleVertices(rad) : sides, rad, rotation + e.rotation); + Lines.poly(ox, oy, sides <= 0 ? Lines.circleVertices(rad) : sides, rad, rotation + e.rotation); + + Drawf.light(ox, oy, rad * lightScl, lightColor == null ? Draw.getColor() : lightColor, lightOpacity * e.fin(lightInterp)); } } diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index 9aaac0a4e0..e0c3231309 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -23,8 +23,6 @@ public class AIController implements UnitController{ /** main target that is being faced */ protected Teamc target; - /** targets for each weapon */ - protected Teamc[] targets = {}; { timer.reset(0, Mathf.random(40f)); @@ -94,8 +92,6 @@ public class AIController implements UnitController{ } protected void updateWeapons(){ - if(targets.length != unit.mounts.length) targets = new Teamc[unit.mounts.length]; - float rotation = unit.rotation - 90; boolean ret = retarget(); @@ -109,39 +105,39 @@ public class AIController implements UnitController{ unit.isShooting = false; - for(int i = 0; i < targets.length; i++){ - WeaponMount mount = unit.mounts[i]; + for(var mount : unit.mounts){ Weapon weapon = mount.weapon; + //let uncontrollable weapons do their own thing + if(!weapon.controllable) continue; + float mountX = unit.x + Angles.trnsx(rotation, weapon.x, weapon.y), mountY = unit.y + Angles.trnsy(rotation, weapon.x, weapon.y); if(unit.type.singleTarget){ - targets[i] = target; + mount.target = target; }else{ if(ret){ - targets[i] = findTarget(mountX, mountY, weapon.bullet.range(), weapon.bullet.collidesAir, weapon.bullet.collidesGround); + mount.target = findTarget(mountX, mountY, weapon.bullet.range(), weapon.bullet.collidesAir, weapon.bullet.collidesGround); } - if(checkTarget(targets[i], mountX, mountY, weapon.bullet.range())){ - targets[i] = null; + if(checkTarget(mount.target, mountX, mountY, weapon.bullet.range())){ + mount.target = null; } } boolean shoot = false; - if(targets[i] != null){ - shoot = targets[i].within(mountX, mountY, weapon.bullet.range()) && shouldShoot(); + if(mount.target != null){ + shoot = mount.target.within(mountX, mountY, weapon.bullet.range() + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && shouldShoot(); - Vec2 to = Predict.intercept(unit, targets[i], weapon.bullet.speed); + Vec2 to = Predict.intercept(unit, mount.target, weapon.bullet.speed); mount.aimX = to.x; mount.aimY = to.y; } - mount.shoot = shoot; - mount.rotate = shoot; + unit.isShooting |= (mount.shoot = mount.rotate = shoot); - unit.isShooting |= shoot; if(shoot){ unit.aimX = mount.aimX; unit.aimY = mount.aimY; diff --git a/core/src/mindustry/entities/units/StatusEntry.java b/core/src/mindustry/entities/units/StatusEntry.java index 1c1f07f9b7..b733457772 100644 --- a/core/src/mindustry/entities/units/StatusEntry.java +++ b/core/src/mindustry/entities/units/StatusEntry.java @@ -3,8 +3,6 @@ package mindustry.entities.units; import mindustry.type.*; public class StatusEntry{ - public static final StatusEntry tmp = new StatusEntry(); - public StatusEffect effect; public float time; diff --git a/core/src/mindustry/entities/units/WeaponMount.java b/core/src/mindustry/entities/units/WeaponMount.java index e5f61679c7..8254c8ca2f 100644 --- a/core/src/mindustry/entities/units/WeaponMount.java +++ b/core/src/mindustry/entities/units/WeaponMount.java @@ -28,6 +28,10 @@ public class WeaponMount{ public @Nullable Bullet bullet; /** sound loop for continuous weapons */ public @Nullable SoundLoop sound; + /** current target; used for autonomous weapons and AI */ + public @Nullable Teamc target; + /** retarget counter */ + public float retarget = 0f; public WeaponMount(Weapon weapon){ this.weapon = weapon; diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index 975a9baf1e..61faeaffd0 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -6,6 +6,7 @@ import mindustry.ctype.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.net.*; +import mindustry.net.Packets.*; import mindustry.type.*; import mindustry.world.*; @@ -242,11 +243,31 @@ public class EventType{ } } - public static class TileChangeEvent{ - public final Tile tile; + /** + * Called *before* a tile has changed. + * WARNING! This event is special: its instance is reused! Do not cache or use with a timer. + * Do not modify any tiles inside listeners that use this tile. + * */ + public static class TilePreChangeEvent{ + public Tile tile; - public TileChangeEvent(Tile tile){ + public TilePreChangeEvent set(Tile tile){ this.tile = tile; + return this; + } + } + + /** + * Called *after* a tile has changed. + * WARNING! This event is special: its instance is reused! Do not cache or use with a timer. + * Do not modify any tiles inside listeners that use this tile. + * */ + public static class TileChangeEvent{ + public Tile tile; + + public TileChangeEvent set(Tile tile){ + this.tile = tile; + return this; } } @@ -392,6 +413,17 @@ public class EventType{ } } + /** Called when a player sends a connection packet. */ + public static class ConnectPacketEvent{ + public final NetConnection connection; + public final ConnectPacket packet; + + public ConnectPacketEvent(NetConnection connection, ConnectPacket packet){ + this.connection = connection; + this.packet = packet; + } + } + /** Called after connecting; when a player receives world data and is ready to play.*/ public static class PlayerJoin{ public final Player player; diff --git a/core/src/mindustry/game/Gamemode.java b/core/src/mindustry/game/Gamemode.java index e4c0d01c6c..a83fcd3e8d 100644 --- a/core/src/mindustry/game/Gamemode.java +++ b/core/src/mindustry/game/Gamemode.java @@ -2,6 +2,7 @@ package mindustry.game; import arc.*; import arc.func.*; +import arc.util.*; import mindustry.maps.*; import static mindustry.Vars.*; @@ -22,7 +23,7 @@ public enum Gamemode{ rules.waves = true; rules.waveTimer = true; - rules.waveSpacing /= 2f; + rules.waveSpacing = 60f * Time.toMinutes; rules.teams.get(rules.waveTeam).infiniteResources = true; }, map -> map.teams.contains(state.rules.waveTeam.id)), pvp(rules -> { diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 9b95bbf7d3..093a09f722 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -10,6 +10,8 @@ import mindustry.io.*; import mindustry.type.*; import mindustry.type.Weather.*; import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.meta.*; /** * Defines current rules on how the game should function. @@ -34,6 +36,8 @@ public class Rules{ public boolean editor = false; /** Whether a gameover can happen at all. Set this to false to implement custom gameover conditions. */ public boolean canGameOver = true; + /** Whether cores change teams when they are destroyed. */ + public boolean coreCapture = false; /** Whether reactors can explode and damage other blocks. */ public boolean reactorExplosions = true; /** Whether schematics are allowed. */ @@ -72,6 +76,10 @@ public class Rules{ public int winWave = 0; /** Base unit cap. Can still be increased by blocks. */ public int unitCap = 0; + /** Environmental flags that dictate visuals & how blocks function. */ + public int environment = Env.terrestrial | Env.spores | Env.groundOil | Env.groundWater; + /** Attributes of the environment. */ + public Attributes attributes = new Attributes(); /** Sector for saves that have them. */ public @Nullable Sector sector; /** Spawn layout. */ diff --git a/core/src/mindustry/game/Saves.java b/core/src/mindustry/game/Saves.java index c832f86471..b385e38884 100644 --- a/core/src/mindustry/game/Saves.java +++ b/core/src/mindustry/game/Saves.java @@ -214,7 +214,7 @@ public class Saves{ } previewExecutor.submit(() -> { try{ - previewFile().writePNG(renderer.minimap.getPixmap()); + previewFile().writePng(renderer.minimap.getPixmap()); requestedPreview = false; }catch(Throwable t){ Log.err(t); diff --git a/core/src/mindustry/game/Schematic.java b/core/src/mindustry/game/Schematic.java index e539ae69a2..89556e5881 100644 --- a/core/src/mindustry/game/Schematic.java +++ b/core/src/mindustry/game/Schematic.java @@ -15,6 +15,9 @@ import static mindustry.Vars.*; public class Schematic implements Publishable, Comparable{ public final Seq tiles; + /** These are used for the schematic tag UI. */ + public Seq labels = new Seq<>(); + /** Internal meta tags. */ public StringMap tags; public int width, height; public @Nullable Fi file; @@ -29,7 +32,7 @@ public class Schematic implements Publishable, Comparable{ } public float powerProduction(){ - return tiles.sumf(s -> s.block instanceof PowerGenerator ? ((PowerGenerator)s.block).powerProduction : 0f); + return tiles.sumf(s -> s.block instanceof PowerGenerator p ? p.powerProduction : 0f); } public float powerConsumption(){ diff --git a/core/src/mindustry/game/Schematics.java b/core/src/mindustry/game/Schematics.java index 4fd3a66079..f21381802c 100644 --- a/core/src/mindustry/game/Schematics.java +++ b/core/src/mindustry/game/Schematics.java @@ -26,7 +26,6 @@ import mindustry.input.*; import mindustry.input.Placement.*; import mindustry.io.*; import mindustry.world.*; -import mindustry.world.blocks.*; import mindustry.world.blocks.ConstructBlock.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.legacy.*; @@ -128,8 +127,6 @@ public class Schematics implements Loadable{ Log.err(e); ui.showException(e); } - - } private @Nullable Schematic loadFile(Fi file){ @@ -173,7 +170,7 @@ public class Schematics implements Loadable{ Draw.flush(); buffer.begin(); Pixmap pixmap = ScreenUtils.getFrameBufferPixmap(0, 0, buffer.getWidth(), buffer.getHeight()); - file.writePNG(pixmap); + file.writePng(pixmap); buffer.end(); } @@ -349,7 +346,7 @@ public class Schematics implements Loadable{ for(int cx = x; cx <= x2; cx++){ for(int cy = y; cy <= y2; cy++){ Building linked = world.build(cx, cy); - Block realBlock = linked == null ? null : linked instanceof ConstructBuild cons ? cons.cblock : linked.block; + Block realBlock = linked == null ? null : linked instanceof ConstructBuild cons ? cons.current : linked.block; if(linked != null && realBlock != null && (realBlock.isVisible() || realBlock instanceof CoreBlock)){ int top = realBlock.size/2; @@ -378,7 +375,7 @@ public class Schematics implements Loadable{ for(int cx = ox; cx <= ox2; cx++){ for(int cy = oy; cy <= oy2; cy++){ Building tile = world.build(cx, cy); - Block realBlock = tile == null ? null : tile instanceof ConstructBuild cons ? cons.cblock : tile.block; + Block realBlock = tile == null ? null : tile instanceof ConstructBuild cons ? cons.current : tile.block; if(tile != null && !counted.contains(tile.pos()) && realBlock != null && (realBlock.isVisible() || realBlock instanceof CoreBlock)){ @@ -424,7 +421,7 @@ public class Schematics implements Loadable{ Seq seq = new Seq<>(); if(coreTile == null) throw new IllegalArgumentException("Loadout schematic has no core tile!"); int ox = x - coreTile.x, oy = y - coreTile.y; - schem.tiles.each(st -> { + schem.tiles.copy().sort(s -> -s.block.schematicPriority).each(st -> { Tile tile = world.tile(st.x + ox, st.y + oy); if(tile == null) return; @@ -498,11 +495,19 @@ public class Schematics implements Loadable{ short width = stream.readShort(), height = stream.readShort(); StringMap map = new StringMap(); - byte tags = stream.readByte(); + int tags = stream.readUnsignedByte(); for(int i = 0; i < tags; i++){ map.put(stream.readUTF(), stream.readUTF()); } + String[] labels = null; + + //try to read the categories, but skip if it fails + try{ + labels = JsonIO.read(String[].class, map.get("labels", "[]")); + }catch(Exception ignored){ + } + IntMap blocks = new IntMap<>(); byte length = stream.readByte(); for(int i = 0; i < length; i++){ @@ -523,7 +528,9 @@ public class Schematics implements Loadable{ } } - return new Schematic(tiles, map, width, height); + Schematic out = new Schematic(tiles, map, width, height); + if(labels != null) out.labels.addAll(labels); + return out; } } @@ -540,6 +547,8 @@ public class Schematics implements Loadable{ stream.writeShort(schematic.width); stream.writeShort(schematic.height); + schematic.tags.put("labels", JsonIO.write(schematic.labels.toArray(String.class))); + stream.writeByte(schematic.tags.size); for(ObjectMap.Entry e : schematic.tags.entries()){ stream.writeUTF(e.key); diff --git a/core/src/mindustry/game/SectorInfo.java b/core/src/mindustry/game/SectorInfo.java index f49225d403..06dd82a59c 100644 --- a/core/src/mindustry/game/SectorInfo.java +++ b/core/src/mindustry/game/SectorInfo.java @@ -9,6 +9,7 @@ import mindustry.maps.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.storage.CoreBlock.*; +import mindustry.world.meta.*; import mindustry.world.modules.*; import java.util.*; @@ -69,6 +70,8 @@ public class SectorInfo{ public @Nullable String name; /** Displayed icon. */ public @Nullable String icon; + /** Displayed icon, as content. */ + public @Nullable UnlockableContent contentIcon; /** Version of generated waves. When it doesn't match, new waves are generated. */ public int waveVersion = -1; /** Whether this sector was indicated to the player or not. */ @@ -191,6 +194,13 @@ public class SectorInfo{ stat.mean = Math.min(stat.mean, rawProduction.get(item, ExportStat::new).mean); }); + var pads = indexer.getAllied(state.rules.defaultTeam, BlockFlag.launchPad); + + //disable export when launch pads are disabled, or there aren't any active ones + if(pads.size() == 0 || !Seq.with(pads).contains(t -> t.build.consValid())){ + export.clear(); + } + if(state.rules.sector != null){ state.rules.sector.saveInfo(); } diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index 610b32de86..92b85a2481 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -26,8 +26,8 @@ public class Teams{ public Seq active = new Seq<>(); /** Teams with block or unit presence. */ public Seq present = new Seq<>(TeamData.class); - /** Current boss unit. */ - public @Nullable Unit boss; + /** Current boss units. */ + public Seq bosses = new Seq<>(); public Teams(){ active.add(get(Team.crux)); @@ -117,7 +117,6 @@ public class Teams{ if(data.active() && !active.contains(data)){ active.add(data); updateEnemies(); - indexer.updateTeamIndex(data.team); } } @@ -145,7 +144,7 @@ public class Teams{ public void updateTeamStats(){ present.clear(); - boss = null; + bosses.clear(); for(Team team : Team.all){ TeamData data = team.data(); @@ -172,16 +171,17 @@ public class Teams{ } //update presence flag. - Groups.build.each( b -> b.team.data().presentFlag = true); + Groups.build.each(b -> b.team.data().presentFlag = true); for(Unit unit : Groups.unit){ + if(unit.type == null) continue; TeamData data = unit.team.data(); data.tree().insert(unit); data.units.add(unit); data.presentFlag = true; if(unit.team == state.rules.waveTeam && unit.isBoss()){ - boss = unit; + bosses.add(unit); } if(data.unitsByType == null || data.unitsByType.length <= unit.type.id){ @@ -241,12 +241,17 @@ public class Teams{ /** Target items to mine. */ public Seq mineItems = Seq.with(Items.copper, Items.lead, Items.titanium, Items.thorium); + /** Quadtree for all buildings of this team. Null if not active. */ + @Nullable + public QuadTree buildings; + /** Current unit cap. Do not modify externally. */ + public int unitCap; /** Total unit count. */ public int unitCount; /** Counts for each type of unit. Do not access directly. */ @Nullable public int[] typeCounts; - /** Quadtree for units of this type. Do not access directly. */ + /** Quadtree for units of this team. Do not access directly. */ @Nullable public QuadTree tree; /** Units of this team. Updated each frame. */ @@ -328,5 +333,16 @@ public class Teams{ this.block = block; this.config = config; } + + @Override + public String toString(){ + return "BlockPlan{" + + "x=" + x + + ", y=" + y + + ", rotation=" + rotation + + ", block=" + block + + ", config=" + config + + '}'; + } } } diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index 2389b22f7d..a9215f70d0 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -7,6 +7,7 @@ import arc.util.*; import mindustry.content.*; import mindustry.game.EventType.*; import mindustry.game.SectorInfo.*; +import mindustry.gen.*; import mindustry.maps.*; import mindustry.type.*; import mindustry.world.blocks.storage.*; @@ -236,6 +237,10 @@ public class Universe{ state.rules.winWave = waveMax; state.rules.waves = true; state.rules.attackMode = false; + //update rules in multiplayer + if(net.server()){ + Call.setRules(state.rules); + } }else{ sector.info.winWave = waveMax; sector.info.waves = true; diff --git a/core/src/mindustry/graphics/BlockRenderer.java b/core/src/mindustry/graphics/BlockRenderer.java index 8eb2a38f32..5183551b93 100644 --- a/core/src/mindustry/graphics/BlockRenderer.java +++ b/core/src/mindustry/graphics/BlockRenderer.java @@ -6,6 +6,7 @@ import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.graphics.gl.*; import arc.math.*; +import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.content.*; @@ -23,8 +24,7 @@ public class BlockRenderer{ public static final int crackRegions = 8, maxCrackSize = 9; private static final int initialRequests = 32 * 32; - private static final int expandr = 10; - private static final Color shadowColor = new Color(0, 0, 0, 0.71f); + private static final Color shadowColor = new Color(0, 0, 0, 0.71f), blendShadowColor = Color.white.cpy().lerp(Color.black, shadowColor.a); public final FloorRenderer floor = new FloorRenderer(); public TextureRegion[][] cracks; @@ -38,7 +38,11 @@ public class BlockRenderer{ private FrameBuffer dark = new FrameBuffer(); private Seq outArray2 = new Seq<>(); private Seq shadowEvents = new Seq<>(); - private IntSet procEntities = new IntSet(), procLinks = new IntSet(), procLights = new IntSet(); + private IntSet darkEvents = new IntSet(); + private IntSet procLinks = new IntSet(), procLights = new IntSet(); + + private BlockQuadtree blockTree; + private FloorQuadtree floorTree; public BlockRenderer(){ @@ -52,6 +56,8 @@ public class BlockRenderer{ }); Events.on(WorldLoadEvent.class, event -> { + blockTree = new BlockQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); + floorTree = new FloorQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); shadowEvents.clear(); lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated @@ -61,7 +67,7 @@ public class BlockRenderer{ Core.graphics.clear(Color.white); Draw.proj().setOrtho(0, 0, shadows.getWidth(), shadows.getHeight()); - Draw.color(shadowColor); + Draw.color(blendShadowColor); for(Tile tile : world.tiles){ if(tile.block().hasShadow){ @@ -73,17 +79,19 @@ public class BlockRenderer{ Draw.color(); shadows.end(); - dark.getTexture().setFilter(TextureFilter.linear, TextureFilter.linear); + dark.getTexture().setFilter(TextureFilter.linear); dark.resize(world.width(), world.height()); dark.begin(); Core.graphics.clear(Color.white); Draw.proj().setOrtho(0, 0, dark.getWidth(), dark.getHeight()); for(Tile tile : world.tiles){ + recordIndex(tile); + float darkness = world.getDarkness(tile.x, tile.y); if(darkness > 0){ - Draw.color(0f, 0f, 0f, Math.min((darkness + 0.5f) / 4f, 1f)); + Draw.colorl(1f - Math.min((darkness + 0.5f) / 4f, 1f)); Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); } } @@ -93,6 +101,11 @@ public class BlockRenderer{ dark.end(); }); + Events.on(TilePreChangeEvent.class, event -> { + if(indexBlock(event.tile)) blockTree.remove(event.tile); + if(indexFloor(event.tile)) floorTree.remove(event.tile); + }); + Events.on(TileChangeEvent.class, event -> { shadowEvents.add(event.tile); @@ -104,10 +117,60 @@ public class BlockRenderer{ if(Math.abs(avgx - event.tile.x) <= rangex && Math.abs(avgy - event.tile.y) <= rangey){ lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated } + + recordIndex(event.tile); }); } + boolean indexBlock(Tile tile){ + var block = tile.block(); + return tile.isCenter() && block != Blocks.air && block.cacheLayer == CacheLayer.normal; + } + + boolean indexFloor(Tile tile){ + return tile.block() == Blocks.air && tile.floor().emitLight && world.getDarkness(tile.x, tile.y) < 3; + } + + void recordIndex(Tile tile){ + if(indexBlock(tile)) blockTree.insert(tile); + if(indexFloor(tile)) floorTree.insert(tile); + } + + public void recacheWall(Tile tile){ + for(int cx = tile.x - darkRadius; cx <= tile.x + darkRadius; cx++){ + for(int cy = tile.y - darkRadius; cy <= tile.y + darkRadius; cy++){ + Tile other = world.tile(cx, cy); + if(other != null){ + darkEvents.add(other.pos()); + } + } + } + } + public void drawDarkness(){ + if(!darkEvents.isEmpty()){ + Draw.flush(); + + dark.begin(); + Draw.proj().setOrtho(0, 0, dark.getWidth(), dark.getHeight()); + + darkEvents.each(pos -> { + var tile = world.tile(pos); + tile.data = world.getWallDarkness(tile); + float darkness = world.getDarkness(tile.x, tile.y); + //then draw the shadow + Draw.colorl(!tile.isDarkened() || darkness <= 0f ? 1f : 1f - Math.min((darkness + 0.5f) / 4f, 1f)); + Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); + }); + + Draw.flush(); + Draw.color(); + dark.end(); + darkEvents.clear(); + + Draw.proj(camera); + } + Draw.shader(Shaders.darkness); Draw.fbo(dark, world.width(), world.height(), tilesize); Draw.shader(); @@ -129,7 +192,7 @@ public class BlockRenderer{ Draw.alpha(0.33f * brokenFade); Draw.mixcol(Color.white, 0.2f + Mathf.absin(Time.globalTime, 6f, 0.2f)); - Draw.rect(b.icon(Cicon.full), block.x * tilesize + b.offset, block.y * tilesize + b.offset, b.rotate ? block.rotation * 90 : 0f); + Draw.rect(b.fullIcon, block.x * tilesize + b.offset, block.y * tilesize + b.offset, b.rotate ? block.rotation * 90 : 0f); } Draw.reset(); } @@ -143,11 +206,8 @@ public class BlockRenderer{ Draw.proj().setOrtho(0, 0, shadows.getWidth(), shadows.getHeight()); for(Tile tile : shadowEvents){ - //clear it first - Draw.color(Color.white); - Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); - //then draw the shadow - Draw.color(!tile.block().hasShadow ? Color.white : shadowColor); + //draw white/shadow color depending on blend + Draw.color(!tile.block().hasShadow ? Color.white : blendShadowColor); Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); } @@ -176,12 +236,11 @@ public class BlockRenderer{ /** Process all blocks to draw. */ public void processBlocks(){ - int avgx = (int)(camera.position.x / tilesize); int avgy = (int)(camera.position.y / tilesize); - int rangex = (int)(camera.width / tilesize / 2) + 3; - int rangey = (int)(camera.height / tilesize / 2) + 3; + int rangex = (int)(camera.width / tilesize / 2); + int rangey = (int)(camera.height / tilesize / 2); if(avgx == lastCamX && avgy == lastCamY && lastRangeX == rangex && lastRangeY == rangey){ return; @@ -189,56 +248,32 @@ public class BlockRenderer{ tileview.clear(); lightview.clear(); - procEntities.clear(); procLinks.clear(); procLights.clear(); - int minx = Math.max(avgx - rangex - expandr, 0); - int miny = Math.max(avgy - rangey - expandr, 0); - int maxx = Math.min(world.width() - 1, avgx + rangex + expandr); - int maxy = Math.min(world.height() - 1, avgy + rangey + expandr); + var bounds = camera.bounds(Tmp.r3).grow(tilesize); - for(int x = minx; x <= maxx; x++){ - for(int y = miny; y <= maxy; y++){ - boolean expanded = (Math.abs(x - avgx) > rangex || Math.abs(y - avgy) > rangey); - Tile tile = world.rawTile(x, y); - Block block = tile.block(); - //link to center - if(tile.build != null){ - tile = tile.build.tile; - } + //draw floor lights + floorTree.intersect(bounds, tile -> lightview.add(tile)); - if(block != Blocks.air && block.cacheLayer == CacheLayer.normal && (tile.build == null || !procEntities.contains(tile.build.id))){ - if(block.expanded || !expanded){ - if(tile.build == null || procLinks.add(tile.build.id)){ - tileview.add(tile); - if(tile.build != null){ - procEntities.add(tile.build.id); - procLinks.add(tile.build.id); - } - } + blockTree.intersect(bounds, tile -> { + if(tile.build == null || procLinks.add(tile.build.id)){ + tileview.add(tile); + } + + //lights are drawn even in the expanded range + if(((tile.build != null && procLights.add(tile.build.pos())) || tile.block().emitLight)){ + lightview.add(tile); + } + + if(tile.build != null && tile.build.power != null && tile.build.power.links.size > 0){ + for(Building other : tile.build.getPowerConnections(outArray2)){ + if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections! + tileview.add(other.tile); } - - //lights are drawn even in the expanded range - if(((tile.build != null && procLights.add(tile.build.pos())) || tile.block().emitLight)){ - lightview.add(tile); - } - - if(tile.build != null && tile.build.power != null && tile.build.power.links.size > 0){ - for(Building other : tile.build.getPowerConnections(outArray2)){ - if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections! - tileview.add(other.tile); - } - } - } - } - - //special case for floors - if((block == Blocks.air && tile.floor().emitLight) && procLights.add(tile.pos())){ - lightview.add(tile); } } - } + }); lastCamX = avgx; lastCamY = avgy; @@ -246,7 +281,29 @@ public class BlockRenderer{ lastRangeY = rangey; } + //debug method for drawing block bounds + void drawTree(QuadTree tree){ + Draw.color(Color.blue); + Lines.rect(tree.bounds); + + Draw.color(Color.green); + for(var tile : tree.objects){ + var block = tile.block(); + Tmp.r1.setCentered(tile.worldx() + block.offset, tile.worldy() + block.offset, block.clipSize, block.clipSize); + Lines.rect(Tmp.r1); + } + + if(!tree.leaf){ + drawTree(tree.botLeft); + drawTree(tree.botRight); + drawTree(tree.topLeft); + drawTree(tree.topRight); + } + Draw.reset(); + } + public void drawBlocks(){ + drawDestroyed(); //draw most tile stuff @@ -291,13 +348,47 @@ public class BlockRenderer{ entity.drawLight(); }else if(tile.block().emitLight){ tile.block().drawEnvironmentLight(tile); - }else if(tile.floor().emitLight && !tile.block().solid && world.getDarkness(tile.x, tile.y) < 3){ //only draw floor light under non-solid blocks + }else if(tile.floor().emitLight && tile.block() == Blocks.air){ //only draw floor light under non-solid blocks tile.floor().drawEnvironmentLight(tile); } } } + } + static class BlockQuadtree extends QuadTree{ + public BlockQuadtree(Rect bounds){ + super(bounds); + } + + @Override + public void hitbox(Tile tile){ + var block = tile.block(); + tmp.setCentered(tile.worldx() + block.offset, tile.worldy() + block.offset, block.clipSize, block.clipSize); + } + + @Override + protected QuadTree newChild(Rect rect){ + return new BlockQuadtree(rect); + } + } + + static class FloorQuadtree extends QuadTree{ + + public FloorQuadtree(Rect bounds){ + super(bounds); + } + + @Override + public void hitbox(Tile tile){ + var floor = tile.floor(); + tmp.setCentered(tile.worldx(), tile.worldy(), floor.clipSize, floor.clipSize); + } + + @Override + protected QuadTree newChild(Rect rect){ + return new FloorQuadtree(rect); + } } } diff --git a/core/src/mindustry/graphics/CacheLayer.java b/core/src/mindustry/graphics/CacheLayer.java index 01ef5e5bff..31b8074196 100644 --- a/core/src/mindustry/graphics/CacheLayer.java +++ b/core/src/mindustry/graphics/CacheLayer.java @@ -6,92 +6,80 @@ import arc.graphics.gl.*; import static mindustry.Vars.*; -public enum CacheLayer{ - water{ - @Override - public void begin(){ - beginShader(); - } +public class CacheLayer{ + public static CacheLayer - @Override - public void end(){ - endShader(Shaders.water); - } - }, - mud{ - @Override - public void begin(){ - beginShader(); - } + water, mud, tar, slag, space, normal, walls; - @Override - public void end(){ - endShader(Shaders.mud); - } - }, - tar{ - @Override - public void begin(){ - beginShader(); - } + public static CacheLayer[] all = {}; - @Override - public void end(){ - endShader(Shaders.tar); - } - }, - slag{ - @Override - public void begin(){ - beginShader(); - } + public int id; - @Override - public void end(){ - endShader(Shaders.slag); - } - }, - space{ - @Override - public void begin(){ - beginShader(); - } + /** Register a new CacheLayer. */ + public static void add(CacheLayer... layers){ + int newSize = all.length + layers.length; + var prev = all; + //reallocate the array and copy everything over; performance matters very little here anyway + all = new CacheLayer[newSize]; + System.arraycopy(prev, 0, all, 0, prev.length); + System.arraycopy(layers, 0, all, prev.length, layers.length); - @Override - public void end(){ - endShader(Shaders.space); + for(int i = 0; i < all.length; i++){ + all[i].id = i; } - }, - normal, - walls; + } - public static final CacheLayer[] all = values(); + /** Loads default cache layers. */ + public static void init(){ + add( + water = new ShaderLayer(Shaders.water), + mud = new ShaderLayer(Shaders.mud), + tar = new ShaderLayer(Shaders.tar), + slag = new ShaderLayer(Shaders.slag), + space = new ShaderLayer(Shaders.space), + normal = new CacheLayer(), + walls = new CacheLayer() + ); + } + /** Called before the cache layer begins rendering. Begin FBOs here. */ public void begin(){ } + /** Called after the cache layer ends rendering. Blit FBOs here. */ public void end(){ } - void beginShader(){ - if(!Core.settings.getBool("animatedwater")) return; + public static class ShaderLayer extends CacheLayer{ + public Shader shader; - renderer.blocks.floor.endc(); - renderer.effectBuffer.begin(); - Core.graphics.clear(Color.clear); - renderer.blocks.floor.beginc(); - } + public ShaderLayer(Shader shader){ + //shader will be null on headless backend, but that's ok + this.shader = shader; + } - void endShader(Shader shader){ - if(!Core.settings.getBool("animatedwater")) return; + @Override + public void begin(){ + if(!Core.settings.getBool("animatedwater")) return; - renderer.blocks.floor.endc(); - renderer.effectBuffer.end(); + renderer.blocks.floor.endc(); + renderer.effectBuffer.begin(); + Core.graphics.clear(Color.clear); + renderer.blocks.floor.beginc(); + } - renderer.effectBuffer.blit(shader); + @Override + public void end(){ + if(!Core.settings.getBool("animatedwater")) return; - renderer.blocks.floor.beginc(); + renderer.blocks.floor.endc(); + renderer.effectBuffer.end(); + + renderer.effectBuffer.blit(shader); + + renderer.blocks.floor.beginc(); + } } } diff --git a/core/src/mindustry/graphics/Drawf.java b/core/src/mindustry/graphics/Drawf.java index 32576bf55a..7d8b2be9d5 100644 --- a/core/src/mindustry/graphics/Drawf.java +++ b/core/src/mindustry/graphics/Drawf.java @@ -10,13 +10,21 @@ import mindustry.*; import mindustry.ctype.*; import mindustry.game.*; import mindustry.gen.*; -import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; public class Drawf{ + public static void dashLine(Color color, float x, float y, float x2, float y2){ + int segments = (int)(Math.max(Math.abs(x - x2), Math.abs(y - y2)) / tilesize * 2); + Lines.stroke(3f, Pal.gray); + Lines.dashLine(x, y, x2, y2, segments); + Lines.stroke(1f, color); + Lines.dashLine(x, y, x2, y2, segments); + Draw.reset(); + } + public static void target(float x, float y, float rad, Color color){ target(x, y, rad, 1, color); } @@ -96,7 +104,7 @@ public class Drawf{ public static void shadow(float x, float y, float rad, float alpha){ Draw.color(0, 0, 0, 0.4f * alpha); - Draw.rect("circle-shadow", x, y, rad, rad); + Draw.rect("circle-shadow", x, y, rad * Draw.xscl, rad * Draw.yscl); Draw.color(); } @@ -223,7 +231,7 @@ public class Drawf{ } public static void construct(Building t, UnlockableContent content, float rotation, float progress, float speed, float time){ - construct(t, content.icon(Cicon.full), rotation, progress, speed, time); + construct(t, content.fullIcon, rotation, progress, speed, time); } public static void construct(float x, float y, TextureRegion region, float rotation, float progress, float speed, float time){ diff --git a/core/src/mindustry/graphics/EnvRenderers.java b/core/src/mindustry/graphics/EnvRenderers.java new file mode 100644 index 0000000000..bc669b56ef --- /dev/null +++ b/core/src/mindustry/graphics/EnvRenderers.java @@ -0,0 +1,8 @@ +package mindustry.graphics; + +public class EnvRenderers{ + + public static void init(){ + + } +} diff --git a/core/src/mindustry/graphics/FloorRenderer.java b/core/src/mindustry/graphics/FloorRenderer.java index 900384e1e1..8d837b5dad 100644 --- a/core/src/mindustry/graphics/FloorRenderer.java +++ b/core/src/mindustry/graphics/FloorRenderer.java @@ -3,6 +3,7 @@ package mindustry.graphics; import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.graphics.gl.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; @@ -12,29 +13,93 @@ import mindustry.game.EventType.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; -import java.util.*; - import static mindustry.Vars.*; -public class FloorRenderer implements Disposable{ - private static final int chunksize = mobile ? 16 : 32, chunkunits = chunksize * tilesize; +/** + * general implementation: + * + * caching: + * 1. create fixed-size float array fpr rendering into + * 2. for each chunk, cache each layer into buffer; record layer boundary indices (alternatively, create mesh per layer for fast recache) + * 3. create mesh for this chunk based on buffer size, copy buffer into mesh + * + * rendering: + * 1. iterate through visible chunks + * 2. activate the shader vertex attributes beforehand + * 3. bind each mesh individually, draw it + * + * */ +public class FloorRenderer{ + private static final VertexAttribute[] attributes = {VertexAttribute.position, VertexAttribute.color, VertexAttribute.texCoords}; + private static final int + chunksize = 30, //todo 32? + chunkunits = chunksize * tilesize, + vertexSize = 2 + 1 + 2, + spriteSize = vertexSize * 4, + maxSprites = chunksize * chunksize * 9; private static final float pad = tilesize/2f; + //if true, chunks are rendered on-demand; this causes small lag spikes and is generally not needed for most maps + private static final boolean dynamic = false; - private int[][][] cache; - private MultiCacheBatch cbatch; + private float[] vertices = new float[maxSprites * vertexSize * 4]; + private short[] indices = new short[maxSprites * 6]; + private int vidx; + private FloorRenderBatch batch = new FloorRenderBatch(); + private Shader shader; + private Texture texture; + private TextureRegion error; + + private Mesh[][][] cache; private IntSet drawnLayerSet = new IntSet(); private IntSet recacheSet = new IntSet(); private IntSeq drawnLayers = new IntSeq(); private ObjectSet used = new ObjectSet<>(); public FloorRenderer(){ + short j = 0; + for(int i = 0; i < indices.length; i += 6, j += 4){ + indices[i] = j; + indices[i + 1] = (short)(j + 1); + indices[i + 2] = (short)(j + 2); + indices[i + 3] = (short)(j + 2); + indices[i + 4] = (short)(j + 3); + indices[i + 5] = j; + } + + shader = new Shader( + """ + attribute vec4 a_position; + attribute vec4 a_color; + attribute vec2 a_texCoord0; + uniform mat4 u_projectionViewMatrix; + varying vec4 v_color; + varying vec2 v_texCoords; + + void main(){ + v_color = a_color; + v_color.a = v_color.a * (255.0/254.0); + v_texCoords = a_texCoord0; + gl_Position = u_projectionViewMatrix * a_position; + } + """, + """ + varying vec4 v_color; + varying vec2 v_texCoords; + uniform sampler2D u_texture; + + void main(){ + gl_FragColor = v_color * texture2D(u_texture, v_texCoords); + } + """); + Events.on(WorldLoadEvent.class, event -> clearTiles()); } - /**Queues up a cache change for a tile. Only runs in render loop. */ + /** Queues up a cache change for a tile. Only runs in render loop. */ public void recacheTile(Tile tile){ - //currently a no-op - //recacheSet.add(Point2.pack(tile.x / chunksize, tile.y / chunksize)); + //TODO will be faster it the position also specified the layer to be recached + //recaching all layers may not be necessary + recacheSet.add(Point2.pack(tile.x / chunksize, tile.y / chunksize)); } public void drawFloor(){ @@ -44,6 +109,8 @@ public class FloorRenderer implements Disposable{ Camera camera = Core.camera; + float pad = tilesize/2f; + int minx = (int)((camera.position.x - camera.width/2f - pad) / chunkunits), miny = (int)((camera.position.y - camera.height/2f - pad) / chunkunits), @@ -61,11 +128,15 @@ public class FloorRenderer implements Disposable{ if(!Structs.inBounds(x, y, cache)) continue; - int[] chunk = cache[x][y]; + if(cache[x][y].length == 0){ + cacheChunk(x, y); + } + + Mesh[] chunk = cache[x][y]; //loop through all layers, and add layer index if it exists for(int i = 0; i < layers; i++){ - if(chunk[i] != -1 && i != CacheLayer.walls.ordinal()){ + if(chunk[i] != null && i != CacheLayer.walls.id){ drawnLayerSet.add(i); } } @@ -92,11 +163,30 @@ public class FloorRenderer implements Disposable{ } public void beginc(){ - cbatch.beginDraw(); + shader.bind(); + shader.setUniformMatrix4("u_projectionViewMatrix", Core.camera.mat); + shader.setUniformi("u_texture", 0); + + //only ever use the base environment texture + //TODO show error texture for anything else + texture.bind(0); + + //enable all mesh attributes + for(VertexAttribute attribute : attributes){ + shader.enableVertexAttribute(attribute.alias); + } } public void endc(){ - cbatch.endDraw(); + + //disable all mesh attributes + for(VertexAttribute attribute : attributes){ + shader.disableVertexAttribute(attribute.alias); + } + + //unbind last buffer + Gl.bindBuffer(Gl.arrayBuffer, 0); + Gl.bindBuffer(Gl.elementArrayBuffer, 0); } public void checkChanges(){ @@ -118,8 +208,8 @@ public class FloorRenderer implements Disposable{ } Draw.flush(); - cbatch.setProjection(Core.camera.mat); - cbatch.beginDraw(); + + beginc(); Gl.enable(Gl.blend); } @@ -129,7 +219,7 @@ public class FloorRenderer implements Disposable{ return; } - cbatch.endDraw(); + endc(); } public void drawLayer(CacheLayer layer){ @@ -150,13 +240,37 @@ public class FloorRenderer implements Disposable{ for(int x = minx; x <= maxx; x++){ for(int y = miny; y <= maxy; y++){ - if(!Structs.inBounds(x, y, cache)){ + if(!Structs.inBounds(x, y, cache) || cache[x][y].length == 0){ continue; } - int[] chunk = cache[x][y]; - if(chunk[layer.ordinal()] == -1) continue; - cbatch.drawCache(chunk[layer.ordinal()]); + var mesh = cache[x][y][layer.id]; + + if(mesh != null){ + + //this *must* be a vertexbufferobject, so cast it and render it directly + if(mesh.vertices instanceof VertexBufferObject vbo && mesh.indices instanceof IndexBufferObject ibo){ + + //bindi the buffer and update its contents, but do not unnecessarily enable all the attributes again + vbo.bind(); + //set up vertex attribute pointers for this specific VBO + int offset = 0; + for(VertexAttribute attribute : attributes){ + int location = shader.getAttributeLocation(attribute.alias); + int aoffset = offset; + offset += attribute.size; + if(location < 0) continue; + + shader.setVertexAttribute(location, attribute.components, attribute.type, attribute.normalized, vertexSize * 4, aoffset); + } + + ibo.bind(); + + mesh.vertices.render(mesh.indices, Gl.triangles, 0, mesh.getNumIndices()); + }else{ + throw new ArcRuntimeException("Non-VBO meshes are not supported for caches."); + } + } } } @@ -165,7 +279,6 @@ public class FloorRenderer implements Disposable{ private void cacheChunk(int cx, int cy){ used.clear(); - int[] chunk = cache[cx][cy]; for(int tilex = cx * chunksize; tilex < (cx + 1) * chunksize && tilex < world.width(); tilex++){ for(int tiley = cy * chunksize; tiley < (cy + 1) * chunksize && tiley < world.height(); tiley++){ @@ -179,21 +292,29 @@ public class FloorRenderer implements Disposable{ } } + if(cache[cx][cy].length == 0){ + cache[cx][cy] = new Mesh[CacheLayer.all.length]; + } + + var meshes = cache[cx][cy]; + + for(CacheLayer layer : CacheLayer.all){ + if(meshes[layer.id] != null){ + meshes[layer.id].dispose(); + } + meshes[layer.id] = null; + } + for(CacheLayer layer : used){ - cacheChunkLayer(cx, cy, chunk, layer); + meshes[layer.id] = cacheChunkLayer(cx, cy, layer); } } - private void cacheChunkLayer(int cx, int cy, int[] chunk, CacheLayer layer){ - Batch current = Core.batch; - Core.batch = cbatch; + private Mesh cacheChunkLayer(int cx, int cy, CacheLayer layer){ + vidx = 0; - //begin a new cache - if(chunk[layer.ordinal()] == -1){ - cbatch.beginCache(); - }else{ - cbatch.beginCache(chunk[layer.ordinal()]); - } + Batch current = Core.batch; + Core.batch = batch; for(int tilex = cx * chunksize; tilex < (cx + 1) * chunksize; tilex++){ for(int tiley = cy * chunksize; tiley < (cy + 1) * chunksize; tiley++){ @@ -217,36 +338,173 @@ public class FloorRenderer implements Disposable{ } Core.batch = current; - chunk[layer.ordinal()] = cbatch.endCache(); + + int floats = vidx; + //every 4 vertices need 6 indices + int vertCount = floats / vertexSize, indCount = vertCount * 6/4; + + Mesh mesh = new Mesh(true, vertCount, indCount, attributes); + mesh.setAutoBind(false); + mesh.setVertices(vertices, 0, vidx); + mesh.setIndices(indices, 0, indCount); + + return mesh; } public void clearTiles(){ - if(cbatch != null) cbatch.dispose(); - - recacheSet.clear(); - int chunksx = Mathf.ceil((float)(world.width()) / chunksize), - chunksy = Mathf.ceil((float)(world.height()) / chunksize); - cache = new int[chunksx][chunksy][CacheLayer.all.length]; - cbatch = new MultiCacheBatch(chunksize * chunksize * 9); - - Time.mark(); - - for(int x = 0; x < chunksx; x++){ - for(int y = 0; y < chunksy; y++){ - Arrays.fill(cache[x][y], -1); - - cacheChunk(x, y); + //dispose all old meshes + if(cache != null){ + for(var x : cache){ + for(var y : x){ + for(var mesh : y){ + if(mesh != null){ + mesh.dispose(); + } + } + } } } - Log.debug("Time to cache: @", Time.elapsed()); + recacheSet.clear(); + int chunksx = Mathf.ceil((float)(world.width()) / chunksize), chunksy = Mathf.ceil((float)(world.height()) / chunksize); + cache = new Mesh[chunksx][chunksy][dynamic ? 0 : CacheLayer.all.length]; + + texture = Core.atlas.find("grass1").texture; + error = Core.atlas.find("env-error"); + //not supported due to internal access + Mesh.useVAO = false; + + //pre-cache chunks + if(!dynamic){ + Time.mark(); + + for(int x = 0; x < chunksx; x++){ + for(int y = 0; y < chunksy; y++){ + cacheChunk(x, y); + } + } + + Log.debug("Time to cache: @", Time.elapsed()); + } } - @Override - public void dispose(){ - if(cbatch != null){ - cbatch.dispose(); - cbatch = null; + class FloorRenderBatch extends Batch{ + + @Override + protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){ + + //substitute invalid regions with error + if(region.texture != texture && region != error){ + draw(error, x, y, originX, originY, width, height, rotation); + return; + } + + float[] verts = vertices; + int idx = vidx; + vidx += spriteSize; + + if(!Mathf.zero(rotation)){ + //bottom left and top right corner points relative to origin + float worldOriginX = x + originX; + float worldOriginY = y + originY; + float fx = -originX; + float fy = -originY; + float fx2 = width - originX; + float fy2 = height - originY; + + // rotate + float cos = Mathf.cosDeg(rotation); + float sin = Mathf.sinDeg(rotation); + + float x1 = cos * fx - sin * fy + worldOriginX; + float y1 = sin * fx + cos * fy + worldOriginY; + float x2 = cos * fx - sin * fy2 + worldOriginX; + float y2 = sin * fx + cos * fy2 + worldOriginY; + float x3 = cos * fx2 - sin * fy2 + worldOriginX; + float y3 = sin * fx2 + cos * fy2 + worldOriginY; + float x4 = x1 + (x3 - x2); + float y4 = y3 - (y2 - y1); + + float u = region.u; + float v = region.v2; + float u2 = region.u2; + float v2 = region.v; + + float color = this.colorPacked; + + verts[idx] = x1; + verts[idx + 1] = y1; + verts[idx + 2] = color; + verts[idx + 3] = u; + verts[idx + 4] = v; + + verts[idx + 5] = x2; + verts[idx + 6] = y2; + verts[idx + 7] = color; + verts[idx + 8] = u; + verts[idx + 9] = v2; + + verts[idx + 10] = x3; + verts[idx + 11] = y3; + verts[idx + 12] = color; + verts[idx + 13] = u2; + verts[idx + 14] = v2; + + verts[idx + 15] = x4; + verts[idx + 16] = y4; + verts[idx + 17] = color; + verts[idx + 18] = u2; + verts[idx + 19] = v; + }else{ + float fx2 = x + width; + float fy2 = y + height; + float u = region.u; + float v = region.v2; + float u2 = region.u2; + float v2 = region.v; + + float color = this.colorPacked; + + verts[idx] = x; + verts[idx + 1] = y; + verts[idx + 2] = color; + verts[idx + 3] = u; + verts[idx + 4] = v; + + verts[idx + 5] = x; + verts[idx + 6] = fy2; + verts[idx + 7] = color; + verts[idx + 8] = u; + verts[idx + 9] = v2; + + verts[idx + 10] = fx2; + verts[idx + 11] = fy2; + verts[idx + 12] = color; + verts[idx + 13] = u2; + verts[idx + 14] = v2; + + verts[idx + 15] = fx2; + verts[idx + 16] = y; + verts[idx + 17] = color; + verts[idx + 18] = u2; + verts[idx + 19] = v; + } + + } + + @Override + public void flush(){ + + } + + @Override + public void setShader(Shader shader, boolean apply){ + throw new IllegalArgumentException("cache shader unsupported"); + } + + @Override + protected void draw(Texture texture, float[] spriteVertices, int offset, int count){ + throw new IllegalArgumentException("cache vertices unsupported"); } } } diff --git a/core/src/mindustry/graphics/LightRenderer.java b/core/src/mindustry/graphics/LightRenderer.java index ebf1cf9187..acf0583e02 100644 --- a/core/src/mindustry/graphics/LightRenderer.java +++ b/core/src/mindustry/graphics/LightRenderer.java @@ -19,6 +19,9 @@ public class LightRenderer{ private float[] vertices = new float[24]; private FrameBuffer buffer = new FrameBuffer(); private Seq lights = new Seq<>(); + private Seq circles = new Seq<>(CircleLight.class); + private int circleIndex = 0; + private TextureRegion circleRegion; public void add(Runnable run){ if(!enabled()) return; @@ -27,14 +30,17 @@ public class LightRenderer{ } public void add(float x, float y, float radius, Color color, float opacity){ - if(!enabled()) return; + if(!enabled() || radius <= 0f) return; - float res = color.toFloatBits(); - add(() -> { - Draw.color(res); - Draw.alpha(opacity); - Draw.rect("circle-shadow", x, y, radius * 2, radius * 2); - }); + float res = Color.toFloatBits(color.r, color.g, color.b, opacity); + + if(circles.size <= circleIndex) circles.add(new CircleLight()); + + //pool circles to prevent runaway GC usage from lambda capturing + var light = circles.items[circleIndex]; + light.set(x, y, res, radius); + + circleIndex ++; } public void add(float x, float y, TextureRegion region, Color color, float opacity){ @@ -170,7 +176,7 @@ public class LightRenderer{ } public boolean enabled(){ - return state.rules.lighting && state.rules.ambientLight.a > 0.00001f; + return state.rules.lighting && state.rules.ambientLight.a > 0.0001f; } public void draw(){ @@ -179,6 +185,8 @@ public class LightRenderer{ return; } + if(circleRegion == null) circleRegion = Core.atlas.find("circle-shadow"); + buffer.resize(Core.graphics.getWidth()/scaling, Core.graphics.getHeight()/scaling); Draw.color(); @@ -188,6 +196,11 @@ public class LightRenderer{ for(Runnable run : lights){ run.run(); } + for(int i = 0; i < circleIndex; i++){ + var cir = circles.items[i]; + Draw.color(cir.color); + Draw.rect(circleRegion, cir.x, cir.y, cir.radius * 2, cir.radius * 2); + } Draw.reset(); buffer.end(); Gl.blendEquationSeparate(Gl.funcAdd, Gl.funcAdd); @@ -197,5 +210,17 @@ public class LightRenderer{ buffer.blit(Shaders.light); lights.clear(); + circleIndex = 0; + } + + static class CircleLight{ + float x, y, color, radius; + + public void set(float x, float y, float color, float radius){ + this.x = x; + this.y = y; + this.color = color; + this.radius = radius; + } } } diff --git a/core/src/mindustry/graphics/LoadRenderer.java b/core/src/mindustry/graphics/LoadRenderer.java index 8a314b00df..c15e948285 100644 --- a/core/src/mindustry/graphics/LoadRenderer.java +++ b/core/src/mindustry/graphics/LoadRenderer.java @@ -37,6 +37,7 @@ public class LoadRenderer implements Disposable{ private int lastLength = -1; private FxProcessor fx; private WindowedMean renderTimes = new WindowedMean(20); + private BloomFilter bloom; private long lastFrameTime; { @@ -49,7 +50,7 @@ public class LoadRenderer implements Disposable{ //vignetting is probably too much //fx.addEffect(new VignettingFilter(false)); - fx.addEffect(new BloomFilter()); + fx.addEffect(bloom = new BloomFilter()); bars = new Bar[]{ new Bar("s_proc#", OS.cores / 16f, OS.cores < 4), @@ -69,6 +70,7 @@ public class LoadRenderer implements Disposable{ public void dispose(){ mesh.dispose(); fx.dispose(); + bloom.dispose(); } public void draw(){ diff --git a/core/src/mindustry/graphics/MenuRenderer.java b/core/src/mindustry/graphics/MenuRenderer.java index b44b6cc5e7..dbbf75f06a 100644 --- a/core/src/mindustry/graphics/MenuRenderer.java +++ b/core/src/mindustry/graphics/MenuRenderer.java @@ -12,7 +12,6 @@ import arc.util.*; import arc.util.noise.*; import mindustry.content.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; @@ -30,7 +29,7 @@ public class MenuRenderer implements Disposable{ private float time = 0f; private float flyerRot = 45f; private int flyers = Mathf.chance(0.2) ? Mathf.random(35) : Mathf.random(15); - private UnitType flyerType = Structs.select(UnitTypes.flare, UnitTypes.flare, UnitTypes.horizon, UnitTypes.mono, UnitTypes.poly, UnitTypes.mega, UnitTypes.zenith); + private UnitType flyerType = content.units().select(u -> u.hitSize <= 20f && u.flying && u.region.found()).random(); public MenuRenderer(){ Time.mark(); @@ -42,7 +41,7 @@ public class MenuRenderer implements Disposable{ private void generate(){ world.beginMapLoad(); Tiles tiles = world.resize(width, height); - Seq ores = content.blocks().select(b -> b instanceof OreBlock); + Seq ores = content.blocks().select(b -> b instanceof OreBlock && !(b instanceof WallOreBlock)); shadows = new FrameBuffer(width, height); int offset = Mathf.random(100000); Simplex s1 = new Simplex(offset); @@ -239,7 +238,7 @@ public class MenuRenderer implements Disposable{ private void drawFlyers(){ Draw.color(0f, 0f, 0f, 0.4f); - TextureRegion icon = flyerType.icon(Cicon.full); + TextureRegion icon = flyerType.fullIcon; float size = Math.max(icon.width, icon.height) * Draw.scl * 1.6f; @@ -275,10 +274,12 @@ public class MenuRenderer implements Disposable{ float offset = -100f; for(int i = 0; i < flyers; i++){ - Tmp.v1.trns(flyerRot, time * (2f + flyerType.speed)); + Tmp.v1.trns(flyerRot, time * (flyerType.speed)); - cons.get((Mathf.randomSeedRange(i, range) + Tmp.v1.x + Mathf.absin(time + Mathf.randomSeedRange(i + 2, 500), 10f, 3.4f) + offset) % (tw + Mathf.randomSeed(i + 5, 0, 500)), - (Mathf.randomSeedRange(i + 1, range) + Tmp.v1.y + Mathf.absin(time + Mathf.randomSeedRange(i + 3, 500), 10f, 3.4f) + offset) % th); + cons.get( + (Mathf.randomSeedRange(i, range) + Tmp.v1.x + Mathf.absin(time + Mathf.randomSeedRange(i + 2, 500), 10f, 3.4f) + offset) % (tw + Mathf.randomSeed(i + 5, 0, 500)), + (Mathf.randomSeedRange(i + 1, range) + Tmp.v1.y + Mathf.absin(time + Mathf.randomSeedRange(i + 3, 500), 10f, 3.4f) + offset) % th + ); } } diff --git a/core/src/mindustry/graphics/MinimapRenderer.java b/core/src/mindustry/graphics/MinimapRenderer.java index 4cc044753e..a77face3f4 100644 --- a/core/src/mindustry/graphics/MinimapRenderer.java +++ b/core/src/mindustry/graphics/MinimapRenderer.java @@ -2,7 +2,6 @@ package mindustry.graphics; import arc.*; import arc.graphics.*; -import arc.graphics.Pixmap.*; import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; @@ -36,6 +35,7 @@ public class MinimapRenderer{ //make sure to call on the graphics thread Events.on(TileChangeEvent.class, event -> { + //TODO don't update when the minimap is off? if(!ui.editor.isShown()){ update(event.tile); } @@ -69,7 +69,7 @@ public class MinimapRenderer{ texture.dispose(); } setZoom(4f); - pixmap = new Pixmap(world.width(), world.height(), Format.rgba8888); + pixmap = new Pixmap(world.width(), world.height()); texture = new Texture(pixmap); region = new TextureRegion(texture); } @@ -96,7 +96,7 @@ public class MinimapRenderer{ Draw.mixcol(unit.team().color, 1f); float scale = Scl.scl(1f) / 2f * scaling * 32f; - var region = unit.type.icon(Cicon.full); + var region = unit.type.fullIcon; Draw.rect(region, x + rx, y + ry, scale, scale * (float)region.height / region.width, unit.rotation() - 90); Draw.reset(); } @@ -136,7 +136,7 @@ public class MinimapRenderer{ public void updateAll(){ for(Tile tile : world.tiles){ - pixmap.draw(tile.x, pixmap.getHeight() - 1 - tile.y, colorFor(tile)); + pixmap.set(tile.x, pixmap.height - 1 - tile.y, colorFor(tile)); } texture.draw(pixmap); } @@ -144,10 +144,10 @@ public class MinimapRenderer{ public void update(Tile tile){ if(world.isGenerating() || !state.isGame()) return; - int color = colorFor(world.tile(tile.x, tile.y)); - pixmap.draw(tile.x, pixmap.getHeight() - 1 - tile.y, color); + int color = colorFor(tile); + pixmap.set(tile.x, pixmap.height - 1 - tile.y, color); - Pixmaps.drawPixel(texture, tile.x, pixmap.getHeight() - 1 - tile.y, color); + Pixmaps.drawPixel(texture, tile.x, pixmap.height - 1 - tile.y, color); } public void updateUnitArray(){ diff --git a/core/src/mindustry/graphics/MultiPacker.java b/core/src/mindustry/graphics/MultiPacker.java index ac8277b078..eb823ea991 100644 --- a/core/src/mindustry/graphics/MultiPacker.java +++ b/core/src/mindustry/graphics/MultiPacker.java @@ -1,21 +1,40 @@ package mindustry.graphics; import arc.graphics.*; -import arc.graphics.Pixmap.*; import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.util.*; +import mindustry.*; public class MultiPacker implements Disposable{ private PixmapPacker[] packers = new PixmapPacker[PageType.all.length]; public MultiPacker(){ for(int i = 0; i < packers.length; i++){ - int pageSize = 2048; - packers[i] = new PixmapPacker(pageSize, pageSize, Format.rgba8888, 2, true); + packers[i] = new PixmapPacker(Math.min(Vars.maxTextureSize, PageType.all[i].width), Math.min(Vars.maxTextureSize, PageType.all[i].height), 2, true); } } + @Nullable + public PixmapRegion get(String name){ + for(var packer : packers){ + var region = packer.getRegion(name); + if(region != null){ + return region; + } + } + return null; + } + + public boolean has(String name){ + for(var page : PageType.all){ + if(packers[page.ordinal()].getRect(name) != null){ + return true; + } + } + return false; + } + public boolean has(PageType type, String name){ return packers[type.ordinal()].getRect(name) != null; } @@ -24,6 +43,10 @@ public class MultiPacker implements Disposable{ packers[type.ordinal()].pack(name, region); } + public void add(PageType type, String name, PixmapRegion region, int[] splits, int[] pads){ + packers[type.ordinal()].pack(name, region, splits, pads); + } + public void add(PageType type, String name, Pixmap pix){ packers[type.ordinal()].pack(name, pix); } @@ -50,11 +73,27 @@ public class MultiPacker implements Disposable{ //zone page (sprites4.png) - zone previews //ui page (sprites5.png) - content icons, white icons and UI elements public enum PageType{ - main, + main(4096), environment, - editor, - ui; + editor(4096, 2048), + rubble, + ui(4096); public static final PageType[] all = values(); + + public int width = 2048, height = 2048; + + PageType(int defaultSize){ + this.width = this.height = defaultSize; + } + + PageType(int width, int height){ + this.width = width; + this.height = height; + } + + PageType(){ + + } } } diff --git a/core/src/mindustry/graphics/OverlayRenderer.java b/core/src/mindustry/graphics/OverlayRenderer.java index d811af6fbc..5279de9e4d 100644 --- a/core/src/mindustry/graphics/OverlayRenderer.java +++ b/core/src/mindustry/graphics/OverlayRenderer.java @@ -8,6 +8,7 @@ import arc.math.geom.*; import arc.util.*; import mindustry.*; import mindustry.ai.types.*; +import mindustry.entities.*; import mindustry.gen.*; import mindustry.input.*; import mindustry.ui.*; @@ -21,7 +22,7 @@ public class OverlayRenderer{ private static final Rect rect = new Rect(); private float buildFade, unitFade; - private Unit lastSelect; + private Sized lastSelect; public void drawBottom(){ InputHandler input = control.input; @@ -72,27 +73,31 @@ public class OverlayRenderer{ InputHandler input = control.input; - Unit select = input.selectedUnit(); + + Sized select = input.selectedUnit(); + if(select == null) select = input.selectedControlBuild(); if(!Core.input.keyDown(Binding.control)) select = null; + unitFade = Mathf.lerpDelta(unitFade, Mathf.num(select != null), 0.1f); if(select != null) lastSelect = select; if(select == null) select = lastSelect; - if(select != null && select.isAI()){ + if(select != null && (!(select instanceof Unitc u) || u.isAI())){ Draw.mixcol(Pal.accent, 1f); Draw.alpha(unitFade); + Building build = (select instanceof BlockUnitc b ? b.tile() : select instanceof Building b ? b : null); - if(select instanceof BlockUnitc){ + if(build != null){ //special selection for block "units" - Fill.square(select.x, select.y, ((BlockUnitc)select).tile().block.size * tilesize/2f); - }else{ - Draw.rect(select.type.icon(Cicon.full), select.x(), select.y(), select.rotation() - 90); + Fill.square(build.x, build.y, build.block.size * tilesize/2f); + }else if(select instanceof Unit u){ + Draw.rect(u.type.fullIcon, u.x, u.y, u.rotation - 90); } for(int i = 0; i < 4; i++){ float rot = i * 90f + 45f + (-Time.time) % 360f; float length = select.hitSize() * 1.5f + (unitFade * 2.5f); - Draw.rect("select-arrow", select.x + Angles.trnsx(rot, length), select.y + Angles.trnsy(rot, length), length / 1.9f, length / 1.9f, rot - 135f); + Draw.rect("select-arrow", select.getX() + Angles.trnsx(rot, length), select.getY() + Angles.trnsy(rot, length), length / 1.9f, length / 1.9f, rot - 135f); } Draw.reset(); @@ -159,7 +164,8 @@ public class OverlayRenderer{ input.drawOverSelect(); - if(ui.hudfrag.blockfrag.hover() instanceof Unit unit && unit.controller() instanceof LogicAI ai && ai.controller instanceof Building build && build.isValid()){ + if(ui.hudfrag.blockfrag.hover() instanceof Unit unit && unit.controller() instanceof LogicAI ai && ai.controller != null && ai.controller.isValid()){ + var build = ai.controller; Drawf.square(build.x, build.y, build.block.size * tilesize/2f + 2f); if(!unit.within(build, unit.hitSize * 2f)){ Drawf.arrow(unit.x, unit.y, build.x, build.y, unit.hitSize *2f, 4f); @@ -170,7 +176,7 @@ public class OverlayRenderer{ if(input.isDroppingItem()){ Vec2 v = Core.input.mouseWorld(input.getMouseX(), input.getMouseY()); float size = 8; - Draw.rect(player.unit().item().icon(Cicon.medium), v.x, v.y, size, size); + Draw.rect(player.unit().item().fullIcon, v.x, v.y, size, size); Draw.color(Pal.accent); Lines.circle(v.x, v.y, 6 + Mathf.absin(Time.time, 5f, 1f)); Draw.reset(); diff --git a/core/src/mindustry/graphics/Pal.java b/core/src/mindustry/graphics/Pal.java index 1b2b10ff94..012e5a0aff 100644 --- a/core/src/mindustry/graphics/Pal.java +++ b/core/src/mindustry/graphics/Pal.java @@ -14,10 +14,12 @@ public class Pal{ sapBullet = Color.valueOf("bf92f9"), sapBulletBack = Color.valueOf("6d56bf"), + reactorPurple = Color.valueOf("bf92f9"), + reactorPurple2 = Color.valueOf("8a73c6"), + spore = Color.valueOf("7457ce"), shield = Color.valueOf("ffd37f").a(0.7f), - shieldIn = Color.black.cpy().a(0f), bulletYellow = Color.valueOf("fff8e8"), bulletYellowBack = Color.valueOf("f9c27a"), diff --git a/core/src/mindustry/graphics/ScorchGenerator.java b/core/src/mindustry/graphics/ScorchGenerator.java deleted file mode 100644 index b7dfd15439..0000000000 --- a/core/src/mindustry/graphics/ScorchGenerator.java +++ /dev/null @@ -1,31 +0,0 @@ -package mindustry.graphics; - -import arc.graphics.*; -import arc.math.*; -import arc.util.noise.*; - -/** Generates a scorch pixmap based on parameters. Thread safe, unless multiple scorch generators are running in parallel. */ -public class ScorchGenerator{ - private static final Simplex sim = new Simplex(); - - public int size = 80, seed = 0, color = Color.white.rgba(); - public double scale = 18, pow = 2, octaves = 4, pers = 0.4, add = 2, nscl = 4.5f; - - public Pixmap generate(){ - Pixmap pix = new Pixmap(size, size); - sim.setSeed(seed); - - pix.each((x, y) -> { - double dst = Mathf.dst(x, y, size/2, size/2) / (size / 2f); - double scaled = Math.abs(dst - 0.5f) * 5f + add; - scaled -= noise(Angles.angle(x, y, size/2, size/2))*nscl; - if(scaled < 1.5f) pix.draw(x, y, color); - }); - - return pix; - } - - private double noise(float angle){ - return Math.pow(sim.octaveNoise2D(octaves, pers, 1 / scale, Angles.trnsx(angle, size/2f) + size/2f, Angles.trnsy(angle, size/2f) + size/2f), pow); - } -} diff --git a/core/src/mindustry/graphics/Shaders.java b/core/src/mindustry/graphics/Shaders.java index 8aa304065f..75d61a2178 100644 --- a/core/src/mindustry/graphics/Shaders.java +++ b/core/src/mindustry/graphics/Shaders.java @@ -21,7 +21,7 @@ public class Shaders{ public static UnitBuildShader build; public static DarknessShader darkness; public static LightShader light; - public static SurfaceShader water, mud, tar, slag, space; + public static SurfaceShader water, mud, tar, slag, space, caustics; public static PlanetShader planet; public static PlanetGridShader planetGrid; public static AtmosphereShader atmosphere; @@ -48,6 +48,12 @@ public class Shaders{ tar = new SurfaceShader("tar"); slag = new SurfaceShader("slag"); space = new SpaceShader("space"); + caustics = new SurfaceShader("caustics"){ + @Override + public String textureName(){ + return "caustics"; + } + }; planet = new PlanetShader(); planetGrid = new PlanetGridShader(); atmosphere = new AtmosphereShader(); @@ -249,6 +255,8 @@ public class Shaders{ } public static class SurfaceShader extends Shader{ + Texture noiseTex; + public SurfaceShader(String frag){ super(getShaderFi("screenspace.vert"), getShaderFi(frag + ".frag")); loadNoise(); @@ -259,8 +267,12 @@ public class Shaders{ loadNoise(); } + public String textureName(){ + return "noise"; + } + public void loadNoise(){ - Core.assets.load("sprites/noise.png", Texture.class).loaded = t -> { + Core.assets.load("sprites/" + textureName() + ".png", Texture.class).loaded = t -> { ((Texture)t).setFilter(TextureFilter.linear); ((Texture)t).setWrap(TextureWrap.repeat); }; @@ -273,7 +285,11 @@ public class Shaders{ setUniformf("u_time", Time.time); if(hasUniform("u_noise")){ - Core.assets.get("sprites/noise.png", Texture.class).bind(1); + if(noiseTex == null){ + noiseTex = Core.assets.get("sprites/" + textureName() + ".png", Texture.class); + } + + noiseTex.bind(1); renderer.effectBuffer.getTexture().bind(0); setUniformi("u_noise", 1); diff --git a/core/src/mindustry/graphics/Trail.java b/core/src/mindustry/graphics/Trail.java index fd5e36eb25..fc998e64ea 100644 --- a/core/src/mindustry/graphics/Trail.java +++ b/core/src/mindustry/graphics/Trail.java @@ -3,51 +3,101 @@ package mindustry.graphics; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; -import arc.math.geom.*; import arc.struct.*; -import arc.util.pooling.*; +import arc.util.*; public class Trail{ public int length; - private final Seq points; - private float lastX = -1, lastY = -1; + private final FloatSeq points; + private float lastX = -1, lastY = -1, lastAngle = -1, counter = 0f; public Trail(int length){ this.length = length; - points = new Seq<>(length); + points = new FloatSeq(length*3); + } + + public Trail copy(){ + Trail out = new Trail(length); + out.points.addAll(points); + out.lastX = lastX; + out.lastY = lastY; + out.lastAngle = lastAngle; + return out; } public void clear(){ points.clear(); } + public int size(){ + return points.size/3; + } + + public void drawCap(Color color, float width){ + if(points.size > 0){ + Draw.color(color); + float[] items = points.items; + int i = points.size - 3; + float x1 = items[i], y1 = items[i + 1], w1 = items[i + 2], w = w1 * width / (points.size/3) * i/3f * 2f; + Draw.rect("hcircle", x1, y1, w, w, -Mathf.radDeg * lastAngle + 180f); + Draw.reset(); + } + } + public void draw(Color color, float width){ Draw.color(color); + float[] items = points.items; + float lx = lastX, ly = lastY, lastAngle = this.lastAngle; - for(int i = 0; i < points.size - 1; i++){ - Vec3 c = points.get(i); - Vec3 n = points.get(i + 1); - float size = width / length; + for(int i = 0; i < points.size - 3; i+= 3){ + float x1 = items[i], y1 = items[i + 1], w1 = items[i + 2], + x2 = items[i + 3], y2 = items[i + 4], w2 = items[i + 5]; + float size = width / (points.size/3); + float z1 = lastAngle; + float z2 = -Angles.angleRad(x2, y2, lx, ly); - float cx = Mathf.sin(c.z) * i * size, cy = Mathf.cos(c.z) * i * size, nx = Mathf.sin(n.z) * (i + 1) * size, ny = Mathf.cos(n.z) * (i + 1) * size; - Fill.quad(c.x - cx, c.y - cy, c.x + cx, c.y + cy, n.x + nx, n.y + ny, n.x - nx, n.y - ny); + float cx = Mathf.sin(z1) * i/3f * size * w1, cy = Mathf.cos(z1) * i/3f * size * w1, + nx = Mathf.sin(z2) * (i/3f + 1) * size * w2, ny = Mathf.cos(z2) * (i/3f + 1) * size * w2; + Fill.quad(x1 - cx, y1 - cy, x1 + cx, y1 + cy, x2 + nx, y2 + ny, x2 - nx, y2 - ny); + + lastAngle = z2; + lx = x2; + ly = y2; } Draw.reset(); } - public void update(float x, float y){ - if(points.size > length){ - Pools.free(points.first()); - points.remove(0); + /** Removes the last point from the trail at intervals. */ + public void shorten(){ + if((counter += Time.delta) >= 0.99f){ + if(points.size >= 3){ + points.removeRange(0, 2); + } } + } - float angle = -Angles.angle(x, y, lastX, lastY); + /** Adds a new point to the trail at intervals. */ + public void update(float x, float y){ + update(x, y, 1f); + } - points.add(Pools.obtain(Vec3.class, Vec3::new).set(x, y, (angle) * Mathf.degRad)); + /** Adds a new point to the trail at intervals. */ + public void update(float x, float y, float width){ + if((counter += Time.delta) >= 0.99f){ + if(points.size > length*3){ + points.removeRange(0, 2); + } - lastX = x; - lastY = y; + lastAngle = -Angles.angleRad(x, y, lastX, lastY); + + points.add(x, y, width); + + lastX = x; + lastY = y; + + counter = 0f; + } } } diff --git a/core/src/mindustry/graphics/g3d/PlanetRenderer.java b/core/src/mindustry/graphics/g3d/PlanetRenderer.java index 7e84e7a8bb..3401ba8b3d 100644 --- a/core/src/mindustry/graphics/g3d/PlanetRenderer.java +++ b/core/src/mindustry/graphics/g3d/PlanetRenderer.java @@ -94,8 +94,12 @@ public class PlanetRenderer implements Disposable{ cam.position.setZero(); cam.update(); + Gl.depthMask(false); + skybox.render(cam.combined); + Gl.depthMask(true); + cam.position.set(lastPos); cam.update(); @@ -172,7 +176,7 @@ public class PlanetRenderer implements Disposable{ } public void renderOrbit(Planet planet){ - if(planet.parent == null || !planet.visible()) return; + if(planet.parent == null || !planet.visible() || orbitAlpha <= 0.02f) return; Vec3 center = planet.parent.position; float radius = planet.orbitRadius; @@ -182,6 +186,8 @@ public class PlanetRenderer implements Disposable{ } public void renderSectors(Planet planet){ + if(orbitAlpha <= 0.02f) return; + //apply transformed position batch.proj().mul(planet.getTransform(mat)); @@ -203,6 +209,7 @@ public class PlanetRenderer implements Disposable{ public void drawArc(Planet planet, Vec3 a, Vec3 b){ drawArc(planet, a, b, Pal.accent, Color.clear, 1f); } + public void drawArc(Planet planet, Vec3 a, Vec3 b, Color from, Color to, float length){ drawArc(planet, a, b, from, to, length, 80f, 25); } diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 5c2961bc06..3746dae72e 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -162,7 +162,7 @@ public class DesktopInput extends InputHandler{ } lineRequests.each(this::drawOverRequest); }else if(isPlacing()){ - if(block.rotate){ + if(block.rotate && block.drawArrow){ drawArrow(block, cursorX, cursorY, rotation); } Draw.color(); @@ -225,17 +225,20 @@ public class DesktopInput extends InputHandler{ if(!scene.hasMouse()){ if(Core.input.keyDown(Binding.control) && Core.input.keyTap(Binding.select)){ Unit on = selectedUnit(); + var build = selectedControlBuild(); if(on != null){ Call.unitControl(player, on); shouldShoot = false; + }else if(build != null){ + Call.buildingControlSelect(player, build); } } } - if(!player.dead() && !state.isPaused() && !(Core.scene.getKeyboardFocus() instanceof TextField)){ + if(!player.dead() && !state.isPaused() && !scene.hasField()){ updateMovement(player.unit()); - if(Core.input.keyDown(Binding.respawn) && !player.unit().spawnedByCore() && !scene.hasField()){ + if(Core.input.keyDown(Binding.respawn)){ Call.unitClear(player); controlledType = null; } @@ -641,15 +644,17 @@ public class DesktopInput extends InputHandler{ unit.moveAt(movement); }else{ unit.moveAt(Tmp.v2.trns(unit.rotation, movement.len())); + + //problem: actual unit rotation is controlled by velocity, but velocity is 1) unpredictable and 2) can be set to 0 if(!movement.isZero()){ - unit.vel.rotateTo(movement.angle(), unit.type.rotateSpeed * Math.max(Time.delta, 1)); + unit.rotation = Angles.moveToward(unit.rotation, movement.angle(), unit.type.rotateSpeed * Math.max(Time.delta, 1)); } } unit.aim(unit.type.faceTarget ? Core.input.mouseWorld() : Tmp.v1.trns(unit.rotation, Core.input.mouseWorld().dst(unit)).add(unit.x, unit.y)); unit.controlWeapons(true, player.shooting && !boosted); - player.boosting = Core.input.keyDown(Binding.boost) && !movement.isZero(); + player.boosting = Core.input.keyDown(Binding.boost); player.mouseX = unit.aimX(); player.mouseY = unit.aimY(); diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 874ade09e7..ebabd2523f 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -34,7 +34,6 @@ import mindustry.world.blocks.ConstructBlock.*; import mindustry.world.blocks.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.meta.*; import java.util.*; @@ -44,6 +43,7 @@ import static mindustry.Vars.*; public abstract class InputHandler implements InputProcessor, GestureListener{ /** Used for dropping items. */ final static float playerSelectRange = mobile ? 17f : 11f; + final static IntSeq removed = new IntSeq(); /** Maximum line length. */ final static int maxLength = 100; final static Rect r1 = new Rect(), r2 = new Rect(); @@ -128,6 +128,27 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } } + @Remote(called = Loc.both, targets = Loc.both, forward = true, unreliable = true) + public static void deletePlans(Player player, int[] positions){ + if(netServer.admins.allowAction(player, ActionType.removePlanned, a -> a.plans = positions)){ + + var it = state.teams.get(player.team()).blocks.iterator(); + //O(n^2) search here; no way around it + outer: + while(it.hasNext()){ + BlockPlan req = it.next(); + + for(int pos : positions){ + if(req.x == Point2.x(pos) && req.y == Point2.y(pos)){ + it.remove(); + continue outer; + } + } + } + + } + } + public static void createItemTransfer(Item item, int amount, float x, float y, Position to, Runnable done){ Fx.itemTransfer.at(x, y, amount, item.color, to); if(done != null){ @@ -331,6 +352,20 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ Events.fire(new TapEvent(player, tile)); } + @Remote(targets = Loc.both, called = Loc.both, forward = true) + public static void buildingControlSelect(Player player, Building build){ + if(player == null || build == null || player.dead()) return; + + //make sure player is allowed to control the building + if(net.server() && !netServer.admins.allowAction(player, ActionType.buildSelect, action -> action.tile = build.tile)){ + throw new ValidateException(player, "Player cannot control a building."); + } + + if(player.team() == build.team && build.canControlSelect(player)){ + build.onControlSelect(player); + } + } + @Remote(targets = Loc.both, called = Loc.both, forward = true) public static void unitControl(Player player, @Nullable Unit unit){ if(player == null) return; @@ -341,16 +376,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } //clear player unit when they possess a core - if(unit instanceof BlockUnitc block && block.tile() instanceof CoreBuild build){ - Fx.spawn.at(player); - if(net.client()){ - control.input.controlledType = null; - } - - player.clearUnit(); - player.deathTimer = Player.deathDelay + 1f; - build.requestSpawn(player); - }else if(unit == null){ //just clear the unit (is this used?) + if(unit == null){ //just clear the unit (is this used?) player.clearUnit(); //make sure it's AI controlled, so players can't overwrite each other }else if(unit.isAI() && unit.team == player.team() && !unit.dead){ @@ -369,8 +395,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ @Remote(targets = Loc.both, called = Loc.both, forward = true) public static void unitClear(Player player){ - //no free core teleports? - if(player == null || !player.dead() && player.unit().spawnedByCore) return; + if(player == null) return; Fx.spawn.at(player); player.clearUnit(); @@ -379,19 +404,19 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ @Remote(targets = Loc.both, called = Loc.server, forward = true) public static void unitCommand(Player player){ - if(player == null || player.dead() || !(player.unit() instanceof Commanderc commander)) return; + if(player == null || player.dead() || (player.unit() == null)) return; //make sure player is allowed to make the command if(net.server() && !netServer.admins.allowAction(player, ActionType.command, action -> {})){ throw new ValidateException(player, "Player cannot command a unit."); } - if(commander.isCommanding()){ - commander.clearCommand(); - }else if(player.unit().type.commandLimit > 0 && player.unit().type.commandRadius > 0){ + if(player.unit().isCommanding()){ + player.unit().clearCommand(); + }else if(player.unit().type.commandLimit > 0){ //TODO try out some other formations - commander.commandNearby(new CircleFormation()); + player.unit().commandNearby(new CircleFormation()); Fx.commandSend.at(player, player.unit().type.commandRadius); } @@ -530,7 +555,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ public boolean requestMatches(BuildPlan request){ Tile tile = world.tile(request.x, request.y); - return tile != null && tile.build instanceof ConstructBuild cons && cons.cblock == request.block; + return tile != null && tile.build instanceof ConstructBuild cons && cons.current == request.block; } public void drawBreaking(int x, int y){ @@ -571,6 +596,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ int ox = schemOriginX(), oy = schemOriginY(); requests.each(req -> { + if(req.breaking) return; + req.pointConfig(p -> { int cx = p.x, cy = p.y; int lx = cx; @@ -605,6 +632,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ int origin = (x ? schemOriginX() : schemOriginY()) * tilesize; requests.each(req -> { + if(req.breaking) return; + float value = -((x ? req.x : req.y) * tilesize - origin + req.block.offset) + origin; if(x){ @@ -840,15 +869,23 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } } + removed.clear(); + //remove blocks to rebuild Iterator broken = state.teams.get(player.team()).blocks.iterator(); while(broken.hasNext()){ BlockPlan req = broken.next(); Block block = content.block(req.block); if(block.bounds(req.x, req.y, Tmp.r2).overlaps(Tmp.r1)){ + removed.add(Point2.pack(req.x, req.y)); broken.remove(); } } + + //TODO array may be too large? + if(removed.size > 0 && net.active()){ + Call.deletePlans(player, removed.toArray()); + } } protected void updateLine(int x1, int y1, int x2, int y2){ @@ -1041,6 +1078,14 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ return null; } + public @Nullable Building selectedControlBuild(){ + Building build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); + if(build != null && !player.dead() && build.canControlSelect(player) && build.team == player.team()){ + return build; + } + return null; + } + public void remove(){ Core.input.removeProcessor(this); frag.remove(); @@ -1218,13 +1263,15 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ line.x = point.x; line.y = point.y; if(!overrideLineRotation || diagonal){ + int result = baseRotation; if(next != null){ - line.rotation = Tile.relativeTo(point.x, point.y, next.x, next.y); + result = Tile.relativeTo(point.x, point.y, next.x, next.y); }else if(block.conveyorPlacement && i > 0){ Point2 prev = points.get(i - 1); - line.rotation = Tile.relativeTo(prev.x, prev.y, point.x, point.y); - }else{ - line.rotation = baseRotation; + result = Tile.relativeTo(prev.x, prev.y, point.x, point.y); + } + if(result != -1){ + line.rotation = result; } }else{ line.rotation = rotation; diff --git a/core/src/mindustry/input/MobileInput.java b/core/src/mindustry/input/MobileInput.java index 6958e3fa0b..d037a0d351 100644 --- a/core/src/mindustry/input/MobileInput.java +++ b/core/src/mindustry/input/MobileInput.java @@ -59,20 +59,22 @@ public class MobileInput extends InputHandler implements GestureListener{ /** Current place mode. */ public PlaceMode mode = none; /** Whether no recipe was available when switching to break mode. */ - public Block lastBlock; + public @Nullable Block lastBlock; /** Last placed request. Used for drawing block overlay. */ - public BuildPlan lastPlaced; + public @Nullable BuildPlan lastPlaced; /** Down tracking for panning. */ public boolean down = false; /** Whether manual shooting (point with finger) is enabled. */ public boolean manualShooting = false; /** Current thing being shot at. */ - public Teamc target; + public @Nullable Teamc target; /** Payload target being moved to. Can be a position (for dropping), or a unit/block. */ - public Position payloadTarget; + public @Nullable Position payloadTarget; /** Unit last tapped, or null if last tap was not on a unit. */ - public Unit unitTapped; + public @Nullable Unit unitTapped; + /** Control building last tapped. */ + public @Nullable Building buildingTapped; //region utility methods @@ -362,7 +364,7 @@ public class MobileInput extends InputHandler implements GestureListener{ } //draw last placed request - if(!request.breaking && request == lastPlaced && request.block != null){ + if(!request.breaking && request == lastPlaced && request.block != null && request.block.drawArrow){ Draw.mixcol(); request.block.drawPlace(tile.x, tile.y, rotation, validPlace(tile.x, tile.y, request.block, rotation)); } @@ -530,7 +532,7 @@ public class MobileInput extends InputHandler implements GestureListener{ }else{ Building build = world.buildWorld(pos.x, pos.y); - if(build != null && build.team == player.team() && pay.canPickup(build)){ + if(build != null && build.team == player.team() && (pay.canPickup(build) || build.getPayload() != null && pay.canPickupPayload(build.getPayload()))){ payloadTarget = build; }else if(pay.hasPayload()){ //drop off at position @@ -612,6 +614,8 @@ public class MobileInput extends InputHandler implements GestureListener{ //control a unit/block detected on first tap of double-tap if(unitTapped != null){ Call.unitControl(player, unitTapped); + }else if(buildingTapped != null){ + Call.buildingControlSelect(player, buildingTapped); }else if(!tryBeginMine(cursor)){ tileTapped(linked.build); } @@ -620,6 +624,7 @@ public class MobileInput extends InputHandler implements GestureListener{ } unitTapped = selectedUnit(); + buildingTapped = selectedControlBuild(); //prevent mining if placing/breaking blocks if(!tryStopMine() && !canTapPlayer(worldx, worldy) && !tileTapped(linked.build) && mode == none && !Core.settings.getBool("doubletapmine")){ tryBeginMine(cursor); @@ -885,7 +890,7 @@ public class MobileInput extends InputHandler implements GestureListener{ if(payloadTarget instanceof Vec2 && pay.hasPayload()){ //vec -> dropping something tryDropPayload(); - }else if(payloadTarget instanceof Building build && pay.canPickup(build)){ + }else if(payloadTarget instanceof Building build && build.team == unit.team){ //building -> picking building up Call.requestBuildPayload(player, build); }else if(payloadTarget instanceof Unit other && pay.canPickup(other)){ diff --git a/core/src/mindustry/input/Placement.java b/core/src/mindustry/input/Placement.java index 99091fc7ef..f4afb96683 100644 --- a/core/src/mindustry/input/Placement.java +++ b/core/src/mindustry/input/Placement.java @@ -169,6 +169,62 @@ public class Placement{ plans.set(result); } + public static void calculateDuctBridges(Seq plans, DuctBridge bridge){ + //check for orthogonal placement + unlocked state + if(!(plans.first().x == plans.peek().x || plans.first().y == plans.peek().y) || !bridge.unlockedNow()){ + return; + } + + Boolf placeable = plan -> (plan.placeable(player.team())) || + (plan.tile() != null && plan.tile().block() == plan.block); //don't count the same block as inaccessible + + var result = plans1.clear(); + var team = player.team(); + var rot = plans.first().rotation; + + outer: + for(int i = 0; i < plans.size;){ + var cur = plans.get(i); + result.add(cur); + + //gap found + if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){ + + //find the closest valid position within range + for(int j = i + 1; j < plans.size; j++){ + var other = plans.get(j); + + //out of range now, set to current position and keep scanning forward for next occurrence + if(!bridge.positionsValid(cur.x, cur.y, other.x, other.y)){ + //add 'missed' conveyors + for(int k = i + 1; k < j; k++){ + result.add(plans.get(k)); + } + i = j; + continue outer; + }else if(other.placeable(team)){ + //found a link, assign bridges + cur.block = bridge; + other.block = bridge; + + i = j; + continue outer; + } + } + + //if it got here, that means nothing was found. this likely means there's a bunch of stuff at the end; add it and bail out + for(int j = i + 1; j < plans.size; j++){ + result.add(plans.get(j)); + } + break; + }else{ + i ++; + } + } + + plans.set(result); + } + private static float tileHeuristic(Tile tile, Tile other){ Block block = control.input.block; diff --git a/core/src/mindustry/io/JsonIO.java b/core/src/mindustry/io/JsonIO.java index d6ef8f8e17..ea15b3969c 100644 --- a/core/src/mindustry/io/JsonIO.java +++ b/core/src/mindustry/io/JsonIO.java @@ -192,11 +192,12 @@ public class JsonIO{ json.setSerializer(UnlockableContent.class, new Serializer<>(){ @Override public void write(Json json, UnlockableContent object, Class knownType){ - json.writeValue(object.name); + json.writeValue(object == null ? null : object.name); } @Override public UnlockableContent read(Json json, JsonValue jsonData, Class type){ + if(jsonData.isNull()) return null; String str = jsonData.asString(); Item item = Vars.content.getByName(ContentType.item, str); Liquid liquid = Vars.content.getByName(ContentType.liquid, str); diff --git a/core/src/mindustry/io/MapIO.java b/core/src/mindustry/io/MapIO.java index c484524709..d7c6c8a49f 100644 --- a/core/src/mindustry/io/MapIO.java +++ b/core/src/mindustry/io/MapIO.java @@ -2,7 +2,6 @@ package mindustry.io; import arc.files.*; import arc.graphics.*; -import arc.graphics.Pixmap.*; import arc.math.geom.*; import arc.struct.*; import arc.util.io.*; @@ -84,8 +83,8 @@ public class MapIO{ int c = colorFor(block(), Blocks.air, Blocks.air, team()); if(c != black){ - walls.draw(x, floors.getHeight() - 1 - y, c); - floors.draw(x, floors.getHeight() - 1 - y + 1, shade); + walls.setRaw(x, floors.height - 1 - y, c); + floors.set(x, floors.height - 1 - y + 1, shade); } } }; @@ -112,7 +111,7 @@ public class MapIO{ for(int dx = 0; dx < size; dx++){ for(int dy = 0; dy < size; dy++){ int drawx = tile.x + dx + offsetx, drawy = tile.y + dy + offsety; - walls.draw(drawx, floors.getHeight() - 1 - drawy, c); + walls.set(drawx, floors.height - 1 - drawy, c); } } @@ -132,9 +131,9 @@ public class MapIO{ @Override public Tile create(int x, int y, int floorID, int overlayID, int wallID){ if(overlayID != 0){ - floors.draw(x, floors.getHeight() - 1 - y, colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict)); + floors.set(x, floors.height - 1 - y, colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict)); }else{ - floors.draw(x, floors.getHeight() - 1 - y, colorFor(Blocks.air, content.block(floorID), Blocks.air, Team.derelict)); + floors.set(x, floors.height - 1 - y, colorFor(Blocks.air, content.block(floorID), Blocks.air, Team.derelict)); } if(content.block(overlayID) == Blocks.spawn){ map.spawns ++; @@ -143,7 +142,7 @@ public class MapIO{ } })); - floors.drawPixmap(walls, 0, 0); + floors.draw(walls, true); walls.dispose(); return floors; }finally{ @@ -152,11 +151,11 @@ public class MapIO{ } public static Pixmap generatePreview(Tiles tiles){ - Pixmap pixmap = new Pixmap(tiles.width, tiles.height, Format.rgba8888); - for(int x = 0; x < pixmap.getWidth(); x++){ - for(int y = 0; y < pixmap.getHeight(); y++){ + Pixmap pixmap = new Pixmap(tiles.width, tiles.height); + for(int x = 0; x < pixmap.width; x++){ + for(int y = 0; y < pixmap.height; y++){ Tile tile = tiles.getn(x, y); - pixmap.draw(x, pixmap.getHeight() - 1 - y, colorFor(tile.block(), tile.floor(), tile.overlay(), tile.team())); + pixmap.set(x, pixmap.height - 1 - y, colorFor(tile.block(), tile.floor(), tile.overlay(), tile.team())); } } return pixmap; @@ -175,14 +174,14 @@ public class MapIO{ //while synthetic blocks are possible, most of their data is lost, so in order to avoid questions like //"why is there air under my drill" and "why are all my conveyors facing right", they are disabled int color = tile.block().hasColor && !tile.block().synthetic() ? tile.block().mapColor.rgba() : tile.floor().mapColor.rgba(); - pix.draw(tile.x, tiles.height - 1 - tile.y, color); + pix.set(tile.x, tiles.height - 1 - tile.y, color); } return pix; } public static void readImage(Pixmap pixmap, Tiles tiles){ for(Tile tile : tiles){ - int color = pixmap.getPixel(tile.x, pixmap.getHeight() - 1 - tile.y); + int color = pixmap.get(tile.x, pixmap.height - 1 - tile.y); Block block = ColorMapper.get(color); if(block.isFloor()){ diff --git a/core/src/mindustry/io/SaveFileReader.java b/core/src/mindustry/io/SaveFileReader.java index 69e1ea67cc..178980207f 100644 --- a/core/src/mindustry/io/SaveFileReader.java +++ b/core/src/mindustry/io/SaveFileReader.java @@ -102,8 +102,8 @@ public abstract class SaveFileReader{ if(!isByte){ output.writeInt(length); }else{ - if(length > Short.MAX_VALUE){ - throw new IOException("Byte write length exceeded: " + length + " > " + Short.MAX_VALUE); + if(length > 65535){ + throw new IOException("Byte write length exceeded: " + length + " > 65535"); } output.writeShort(length); } diff --git a/core/src/mindustry/io/SaveIO.java b/core/src/mindustry/io/SaveIO.java index 8b56ef2a61..e6d04d5ddb 100644 --- a/core/src/mindustry/io/SaveIO.java +++ b/core/src/mindustry/io/SaveIO.java @@ -18,10 +18,10 @@ import java.util.zip.*; import static mindustry.Vars.*; public class SaveIO{ - /** Format header. This is the string 'MSAV' in ASCII. */ - public static final byte[] header = {77, 83, 65, 86}; + /** Save format header. */ + public static final byte[] header = {'M', 'S', 'A', 'V'}; public static final IntMap versions = new IntMap<>(); - public static final Seq versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4()); + public static final Seq versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5()); static{ for(SaveVersion version : versionArray){ diff --git a/core/src/mindustry/io/SavePreviewLoader.java b/core/src/mindustry/io/SavePreviewLoader.java index bf01424a11..46f51216b1 100644 --- a/core/src/mindustry/io/SavePreviewLoader.java +++ b/core/src/mindustry/io/SavePreviewLoader.java @@ -1,14 +1,14 @@ package mindustry.io; +import arc.*; import arc.assets.*; import arc.assets.loaders.*; -import arc.assets.loaders.resolvers.*; import arc.files.*; public class SavePreviewLoader extends TextureLoader{ public SavePreviewLoader(){ - super(new AbsoluteFileHandleResolver()); + super(Core.files::absolute); } @Override diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 58d9d77d53..90fd35a59d 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -1,6 +1,7 @@ package mindustry.io; import arc.*; +import arc.func.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; @@ -12,10 +13,11 @@ import mindustry.ctype.*; import mindustry.game.*; import mindustry.game.Teams.*; import mindustry.gen.*; -import mindustry.maps.*; +import mindustry.maps.Map; import mindustry.world.*; import java.io.*; +import java.util.*; import static mindustry.Vars.*; @@ -24,6 +26,9 @@ public abstract class SaveVersion extends SaveFileReader{ //HACK stores the last read build of the save file, valid after read meta call protected int lastReadBuild; + //stores entity mappings for use after readEntityMapping + //if null, fall back to EntityMapping's values + protected @Nullable Prov[] entityMapping; public SaveVersion(int version){ this.version = version; @@ -284,7 +289,7 @@ public abstract class SaveVersion extends SaveFileReader{ } } - public void writeEntities(DataOutput stream) throws IOException{ + public void writeTeamBlocks(DataOutput stream) throws IOException{ //write team data with entities. Seq data = state.teams.getActive().copy(); if(!data.contains(Team.sharded.data())) data.add(Team.sharded.data()); @@ -300,7 +305,9 @@ public abstract class SaveVersion extends SaveFileReader{ TypeIO.writeObject(Writes.get(stream), block.config); } } + } + public void writeWorldEntities(DataOutput stream) throws IOException{ stream.writeInt(Groups.all.count(Entityc::serialize)); for(Entityc entity : Groups.all){ if(!entity.serialize()) continue; @@ -312,7 +319,21 @@ public abstract class SaveVersion extends SaveFileReader{ } } - public void readEntities(DataInput stream) throws IOException{ + public void writeEntityMapping(DataOutput stream) throws IOException{ + stream.writeShort(EntityMapping.customIdMap.size); + for(var entry : EntityMapping.customIdMap.entries()){ + stream.writeShort(entry.key); + stream.writeUTF(entry.value); + } + } + + public void writeEntities(DataOutput stream) throws IOException{ + writeEntityMapping(stream); + writeTeamBlocks(stream); + writeWorldEntities(stream); + } + + public void readTeamBlocks(DataInput stream) throws IOException{ int teamc = stream.readInt(); for(int i = 0; i < teamc; i++){ @@ -333,23 +354,48 @@ public abstract class SaveVersion extends SaveFileReader{ } } } + } + + public void readWorldEntities(DataInput stream) throws IOException{ + //entityMapping is null in older save versions, so use the default + Prov[] mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping; int amount = stream.readInt(); for(int j = 0; j < amount; j++){ readChunk(stream, true, in -> { byte typeid = in.readByte(); - if(EntityMapping.map(typeid) == null){ + if(mapping[typeid] == null){ in.skipBytes(lastRegionLength - 1); return; } - Entityc entity = (Entityc)EntityMapping.map(typeid).get(); + Entityc entity = (Entityc)mapping[typeid].get(); entity.read(Reads.get(in)); entity.add(); }); } } + public void readEntityMapping(DataInput stream) throws IOException{ + //copy entityMapping for further mutation; will be used in readWorldEntities + entityMapping = Arrays.copyOf(EntityMapping.idMap, EntityMapping.idMap.length); + + short amount = stream.readShort(); + for(int i = 0; i < amount; i++){ + //everything that corresponded to this ID in this save goes by this name + //so replace the prov in the current mapping with the one found with this name + short id = stream.readShort(); + String name = stream.readUTF(); + entityMapping[id] = EntityMapping.map(name); + } + } + + public void readEntities(DataInput stream) throws IOException{ + readEntityMapping(stream); + readTeamBlocks(stream); + readWorldEntities(stream); + } + public void readContentHeader(DataInput stream) throws IOException{ byte mapped = stream.readByte(); diff --git a/core/src/mindustry/io/TypeIO.java b/core/src/mindustry/io/TypeIO.java index 6e1ee0bd36..f070c3446b 100644 --- a/core/src/mindustry/io/TypeIO.java +++ b/core/src/mindustry/io/TypeIO.java @@ -5,7 +5,6 @@ import arc.math.geom.*; import arc.struct.*; import arc.util.*; import arc.util.io.*; -import arc.util.pooling.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -29,7 +28,7 @@ import java.nio.*; import static mindustry.Vars.*; -/** Class for specifying read/write methods for code generation. */ +/** Class for specifying read/write methods for code generation. All IO MUST be thread safe!*/ @SuppressWarnings("unused") @TypeIOHandler public class TypeIO{ @@ -392,13 +391,13 @@ public class TypeIO{ return new Vec2(read.f(), read.f()); } - public static void writeStatuse(Writes write, StatusEntry entry){ + public static void writeStatus(Writes write, StatusEntry entry){ write.s(entry.effect.id); write.f(entry.time); } - public static StatusEntry readStatuse(Reads read){ - return Pools.obtain(StatusEntry.class, StatusEntry::new).set(content.getByID(ContentType.status, read.s()), read.f()); + public static StatusEntry readStatus(Reads read){ + return new StatusEntry().set(content.getByID(ContentType.status, read.s()), read.f()); } public static void writeItems(Writes write, ItemStack stack){ diff --git a/core/src/mindustry/io/versions/Save4.java b/core/src/mindustry/io/versions/Save4.java index 4c3198cc35..c75268592c 100644 --- a/core/src/mindustry/io/versions/Save4.java +++ b/core/src/mindustry/io/versions/Save4.java @@ -2,9 +2,25 @@ package mindustry.io.versions; import mindustry.io.*; +import java.io.*; + +/** This version only writes entities, no entity ID mappings. */ public class Save4 extends SaveVersion{ public Save4(){ super(4); } + + @Override + public void writeEntities(DataOutput stream) throws IOException{ + writeTeamBlocks(stream); + writeWorldEntities(stream); + } + + @Override + public void readEntities(DataInput stream) throws IOException{ + readTeamBlocks(stream); + readWorldEntities(stream); + } + } diff --git a/core/src/mindustry/io/versions/Save5.java b/core/src/mindustry/io/versions/Save5.java new file mode 100644 index 0000000000..98cdbad1dd --- /dev/null +++ b/core/src/mindustry/io/versions/Save5.java @@ -0,0 +1,10 @@ +package mindustry.io.versions; + +import mindustry.io.*; + +public class Save5 extends SaveVersion{ + + public Save5(){ + super(5); + } +} diff --git a/core/src/mindustry/logic/LAccess.java b/core/src/mindustry/logic/LAccess.java index 4f4517498a..baf4068707 100644 --- a/core/src/mindustry/logic/LAccess.java +++ b/core/src/mindustry/logic/LAccess.java @@ -21,6 +21,7 @@ public enum LAccess{ maxHealth, heat, efficiency, + progress, timescale, rotation, x, @@ -42,7 +43,6 @@ public enum LAccess{ controller, commanded, name, - config, payloadCount, payloadType, @@ -50,7 +50,7 @@ public enum LAccess{ enabled("to"), //"to" is standard for single parameter access shoot("x", "y", "shoot"), shootp(true, "unit", "shoot"), - configure(true, "to"), + config(true, "to"), color("r", "g", "b"); public final String[] params; diff --git a/core/src/mindustry/logic/LExecutor.java b/core/src/mindustry/logic/LExecutor.java index ddb498a69b..06661d83bc 100644 --- a/core/src/mindustry/logic/LExecutor.java +++ b/core/src/mindustry/logic/LExecutor.java @@ -541,7 +541,7 @@ public class LExecutor{ public void run(LExecutor exec){ Object obj = exec.obj(target); if(obj instanceof Building b && b.team == exec.team && exec.linkIds.contains(b.id)){ - if(type.isObj){ + if(type.isObj && exec.var(p1).isobj){ //TODO may break logic? b.control(type, exec.obj(p1), exec.num(p2), exec.num(p3), exec.num(p4)); }else{ b.control(type, exec.num(p1), exec.num(p2), exec.num(p3), exec.num(p4)); @@ -867,7 +867,7 @@ public class LExecutor{ } static int packSign(int value){ - return (Math.abs(value) & 0b011111111) | (value < 0 ? 0b1000000000 : 0); + return (Math.abs(value) & 0b0111111111) | (value < 0 ? 0b1000000000 : 0); } } diff --git a/core/src/mindustry/logic/LParser.java b/core/src/mindustry/logic/LParser.java index def8f98af2..d6e8b40e62 100644 --- a/core/src/mindustry/logic/LParser.java +++ b/core/src/mindustry/logic/LParser.java @@ -43,7 +43,7 @@ public class LParser{ String string(){ int from = pos; - while(pos++ < chars.length){ + while(++pos < chars.length){ var c = chars[pos]; if(c == '\n'){ error("Missing closing quote \" before end of line."); @@ -52,7 +52,7 @@ public class LParser{ } } - if(chars[pos] != '"') error("Missing closing quote \" before end of file."); + if(pos >= chars.length || chars[pos] != '"') error("Missing closing quote \" before end of file."); return new String(chars, from, ++pos - from); } @@ -128,6 +128,11 @@ public class LParser{ tokens[1] = "-1"; } + for(int i = 1; i < tok; i++){ + if(tokens[i].equals("@configure")) tokens[i] = "@config"; + if(tokens[i].equals("configure")) tokens[i] = "config"; + } + LStatement st; try{ @@ -165,7 +170,7 @@ public class LParser{ while(pos < chars.length && line < LExecutor.maxInstructions){ switch(chars[pos]){ - case '\n', ' ' -> pos ++; //skip newlines and spaces + case '\n', ';', ' ' -> pos ++; //skip newlines and spaces case '\r' -> pos += 2; //skip the newline after the \r default -> statement(); } diff --git a/core/src/mindustry/logic/LStatement.java b/core/src/mindustry/logic/LStatement.java index 638af52074..29c9c14677 100644 --- a/core/src/mindustry/logic/LStatement.java +++ b/core/src/mindustry/logic/LStatement.java @@ -45,8 +45,47 @@ public abstract class LStatement{ tooltip(label, text); } + protected String sanitize(String value){ + if(value.length() == 0){ + return ""; + }else if(value.length() == 1){ + if(value.charAt(0) == '"' || value.charAt(0) == ';' || value.charAt(0) == ' '){ + return "invalid"; + } + }else{ + StringBuilder res = new StringBuilder(value.length()); + if(value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"'){ + res.append('\"'); + //strip out extra quotes + for(int i = 1; i < value.length() - 1; i++){ + if(value.charAt(i) == '"'){ + res.append('\''); + }else{ + res.append(value.charAt(i)); + } + } + res.append('\"'); + }else{ + //otherwise, strip out semicolons, spaces and quotes + for(int i = 0; i < value.length(); i++){ + char c = value.charAt(i); + res.append(switch(c){ + case ';' -> 's'; + case '"' -> '\''; + case ' ' -> '_'; + default -> c; + }); + } + } + + return res.toString(); + } + + return value; + } + protected Cell field(Table table, String value, Cons setter){ - return table.field(value, Styles.nodeField, setter) + return table.field(value, Styles.nodeField, s -> setter.get(sanitize(s))) .size(144f, 40f).pad(2f).color(table.color).maxTextLength(LAssembler.maxTokenLength).addInputDialog(); } @@ -144,7 +183,7 @@ public abstract class LStatement{ public void afterRead(){} public void write(StringBuilder builder){ - LogicIO.write(this,builder); + LogicIO.write(this, builder); } public void setupUI(){ diff --git a/core/src/mindustry/logic/LStatements.java b/core/src/mindustry/logic/LStatements.java index c93ac23448..9b78de37f8 100644 --- a/core/src/mindustry/logic/LStatements.java +++ b/core/src/mindustry/logic/LStatements.java @@ -15,6 +15,7 @@ import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; import static mindustry.logic.LCanvas.*; import static mindustry.world.blocks.logic.LogicDisplay.*; @@ -484,7 +485,7 @@ public class LStatements{ int c = 0; for(Item item : Vars.content.items()){ if(!item.unlockedNow()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { stype("@" + item.name); hide.run(); }).size(40f); @@ -498,7 +499,7 @@ public class LStatements{ int c = 0; for(Liquid item : Vars.content.liquids()){ if(!item.unlockedNow()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { stype("@" + item.name); hide.run(); }).size(40f); @@ -785,11 +786,11 @@ public class LStatements{ int c = 0; for(UnitType item : Vars.content.units()){ if(!item.unlockedNow() || item.isHidden()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { type = "@" + item.name; field.setText(type); hide.run(); - }).size(40f).get().resizeImage(Cicon.small.size); + }).size(40f); if(++c % 6 == 0) i.row(); } @@ -948,11 +949,11 @@ public class LStatements{ int c = 0; for(Item item : Vars.content.items()){ if(!item.unlockedNow()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { ore = "@" + item.name; rebuild(table); hide.run(); - }).size(40f).get().resizeImage(Cicon.small.size); + }).size(40f); if(++c % 6 == 0) i.row(); } diff --git a/core/src/mindustry/maps/Map.java b/core/src/mindustry/maps/Map.java index f1cc24d5f4..0abe2aaffa 100644 --- a/core/src/mindustry/maps/Map.java +++ b/core/src/mindustry/maps/Map.java @@ -147,7 +147,7 @@ public class Map implements Comparable, Publishable{ public void addSteamID(String id){ tags.put("steamid", id); - ui.editor.editor.tags.put("steamid", id); + editor.tags.put("steamid", id); try{ ui.editor.save(); }catch(Exception e){ @@ -160,7 +160,7 @@ public class Map implements Comparable, Publishable{ public void removeSteamID(){ tags.remove("steamid"); - ui.editor.editor.tags.remove("steamid"); + editor.tags.remove("steamid"); try{ ui.editor.save(); }catch(Exception e){ @@ -204,7 +204,7 @@ public class Map implements Comparable, Publishable{ @Override public boolean prePublish(){ tags.put("author", player.name); - ui.editor.editor.tags.put("author", tags.get("author")); + editor.tags.put("author", tags.get("author")); ui.editor.save(); return true; diff --git a/core/src/mindustry/maps/MapPreviewLoader.java b/core/src/mindustry/maps/MapPreviewLoader.java index 6453c6f0b1..7419f7e9cb 100644 --- a/core/src/mindustry/maps/MapPreviewLoader.java +++ b/core/src/mindustry/maps/MapPreviewLoader.java @@ -1,8 +1,8 @@ package mindustry.maps; +import arc.*; import arc.assets.*; import arc.assets.loaders.*; -import arc.assets.loaders.resolvers.*; import arc.files.*; import arc.graphics.*; import arc.struct.*; @@ -13,7 +13,7 @@ import mindustry.ctype.*; public class MapPreviewLoader extends TextureLoader{ public MapPreviewLoader(){ - super(new AbsoluteFileHandleResolver()); + super(Core.files::absolute); } @Override diff --git a/core/src/mindustry/maps/Maps.java b/core/src/mindustry/maps/Maps.java index cd4020a07a..d7ea3b392d 100644 --- a/core/src/mindustry/maps/Maps.java +++ b/core/src/mindustry/maps/Maps.java @@ -217,7 +217,7 @@ public class Maps{ } Pixmap pix = MapIO.generatePreview(world.tiles); - executor.submit(() -> map.previewFile().writePNG(pix)); + executor.submit(() -> map.previewFile().writePng(pix)); writeCache(map); map.texture = new Texture(pix); @@ -409,7 +409,7 @@ public class Maps{ map.texture = new Texture(pix); executor.submit(() -> { try{ - map.previewFile().writePNG(pix); + map.previewFile().writePng(pix); writeCache(map); }catch(Exception e){ e.printStackTrace(); diff --git a/core/src/mindustry/maps/filters/BlendFilter.java b/core/src/mindustry/maps/filters/BlendFilter.java index fd3c1d9619..8aa0e7e067 100644 --- a/core/src/mindustry/maps/filters/BlendFilter.java +++ b/core/src/mindustry/maps/filters/BlendFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.math.*; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -26,6 +27,11 @@ public class BlendFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockSand; + } + @Override public void apply(){ if(in.floor == block || block == Blocks.air || in.floor == ignore) return; diff --git a/core/src/mindustry/maps/filters/ClearFilter.java b/core/src/mindustry/maps/filters/ClearFilter.java index 4da2564a79..59e1a5b53c 100644 --- a/core/src/mindustry/maps/filters/ClearFilter.java +++ b/core/src/mindustry/maps/filters/ClearFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -14,6 +15,11 @@ public class ClearFilter extends GenerateFilter{ return Structs.arr(new BlockOption("block", () -> block, b -> block = b, b -> oresOnly.get(b) || wallsOnly.get(b))); } + @Override + public char icon(){ + return Iconc.blockSnow; + } + @Override public void apply(){ diff --git a/core/src/mindustry/maps/filters/CoreSpawnFilter.java b/core/src/mindustry/maps/filters/CoreSpawnFilter.java index 6aa2db744b..84447ea1f3 100644 --- a/core/src/mindustry/maps/filters/CoreSpawnFilter.java +++ b/core/src/mindustry/maps/filters/CoreSpawnFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.struct.*; import arc.util.*; +import mindustry.gen.*; import mindustry.world.*; import mindustry.world.blocks.storage.*; @@ -19,6 +20,11 @@ public class CoreSpawnFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockCoreShard; + } + @Override public void apply(Tiles tiles, GenerateInput in){ IntSeq spawns = new IntSeq(); diff --git a/core/src/mindustry/maps/filters/DistortFilter.java b/core/src/mindustry/maps/filters/DistortFilter.java index daedca1765..4ae9c5a1d2 100644 --- a/core/src/mindustry/maps/filters/DistortFilter.java +++ b/core/src/mindustry/maps/filters/DistortFilter.java @@ -1,6 +1,7 @@ package mindustry.maps.filters; import arc.util.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -20,6 +21,11 @@ public class DistortFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockTendrils; + } + @Override public void apply(){ Tile tile = in.tile(in.x + noise(in.x, in.y, scl, mag) - mag / 2f, in.y + noise(in.x, in.y + o, scl, mag) - mag / 2f); diff --git a/core/src/mindustry/maps/filters/EnemySpawnFilter.java b/core/src/mindustry/maps/filters/EnemySpawnFilter.java index ac64761632..682d82debf 100644 --- a/core/src/mindustry/maps/filters/EnemySpawnFilter.java +++ b/core/src/mindustry/maps/filters/EnemySpawnFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.struct.*; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -17,6 +18,11 @@ public class EnemySpawnFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockSpawn; + } + @Override public void apply(Tiles tiles, GenerateInput in){ IntSeq spawns = new IntSeq(); diff --git a/core/src/mindustry/maps/filters/FilterOption.java b/core/src/mindustry/maps/filters/FilterOption.java index 1a867d8dee..f47d1c3a60 100644 --- a/core/src/mindustry/maps/filters/FilterOption.java +++ b/core/src/mindustry/maps/filters/FilterOption.java @@ -17,13 +17,13 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; public abstract class FilterOption{ - public static final Boolf floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full)); - public static final Boolf wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full)) && b.inEditor; - public static final Boolf floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full))); - public static final Boolf wallsOptional = b -> (b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full)))) && b.inEditor; - public static final Boolf wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full))) && b.inEditor; - public static final Boolf oresOnly = b -> b instanceof OverlayFloor && !headless && Core.atlas.isFound(b.icon(Cicon.full)); - public static final Boolf oresFloorsOptional = b -> (b instanceof Floor) && !headless && Core.atlas.isFound(b.icon(Cicon.full)); + public static final Boolf floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.fullIcon); + public static final Boolf wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.fullIcon) && b.inEditor; + public static final Boolf floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.fullIcon)); + public static final Boolf wallsOptional = b -> (b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.fullIcon))) && b.inEditor; + public static final Boolf wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.fullIcon)) && b.inEditor; + public static final Boolf oresOnly = b -> b instanceof OverlayFloor && !headless && Core.atlas.isFound(b.fullIcon); + public static final Boolf oresFloorsOptional = b -> (b instanceof Floor) && !headless && Core.atlas.isFound(b.fullIcon); public static final Boolf anyOptional = b -> (floorsOnly.get(b) || wallsOnly.get(b) || oresOnly.get(b) || b == Blocks.air) && b.inEditor; public abstract void build(Table table); @@ -89,15 +89,15 @@ public abstract class FilterOption{ @Override public void build(Table table){ - table.button(b -> b.image(supplier.get().icon(Cicon.small)).update(i -> ((TextureRegionDrawable)i.getDrawable()) - .setRegion(supplier.get() == Blocks.air ? Icon.none.getRegion() : supplier.get().icon(Cicon.small))).size(8 * 3), () -> { + table.button(b -> b.image(supplier.get().uiIcon).update(i -> ((TextureRegionDrawable)i.getDrawable()) + .setRegion(supplier.get() == Blocks.air ? Icon.none.getRegion() : supplier.get().uiIcon)).size(iconSmall), () -> { BaseDialog dialog = new BaseDialog(""); dialog.setFillParent(false); int i = 0; for(Block block : Vars.content.blocks()){ if(!filter.get(block)) continue; - dialog.cont.image(block == Blocks.air ? Icon.none.getRegion() : block.icon(Cicon.medium)).size(8 * 4).pad(3).get().clicked(() -> { + dialog.cont.image(block == Blocks.air ? Icon.none.getRegion() : block.uiIcon).size(iconMed).pad(3).get().clicked(() -> { consumer.get(block); dialog.hide(); changed.run(); diff --git a/core/src/mindustry/maps/filters/GenerateFilter.java b/core/src/mindustry/maps/filters/GenerateFilter.java index 07811c3baf..0887d4f135 100644 --- a/core/src/mindustry/maps/filters/GenerateFilter.java +++ b/core/src/mindustry/maps/filters/GenerateFilter.java @@ -82,6 +82,10 @@ public abstract class GenerateFilter{ return Core.bundle.get("filter." + c.getSimpleName().toLowerCase().replace("filter", ""), c.getSimpleName().replace("Filter", "")); } + public char icon(){ + return '\0'; + } + /** set the seed to a random number */ public void randomize(){ seed = Mathf.random(99999999); diff --git a/core/src/mindustry/maps/filters/MedianFilter.java b/core/src/mindustry/maps/filters/MedianFilter.java index 78a4df65d4..fa2c3e310b 100644 --- a/core/src/mindustry/maps/filters/MedianFilter.java +++ b/core/src/mindustry/maps/filters/MedianFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.math.*; import arc.struct.*; import arc.util.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -26,6 +27,11 @@ public class MedianFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockSporePine; + } + @Override public void apply(){ int rad = (int)radius; diff --git a/core/src/mindustry/maps/filters/MirrorFilter.java b/core/src/mindustry/maps/filters/MirrorFilter.java index d5642381ad..ed57af5138 100644 --- a/core/src/mindustry/maps/filters/MirrorFilter.java +++ b/core/src/mindustry/maps/filters/MirrorFilter.java @@ -6,6 +6,7 @@ import arc.math.geom.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.util.*; +import mindustry.gen.*; import mindustry.graphics.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -22,6 +23,11 @@ public class MirrorFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockMetalFloor5; + } + @Override protected void apply(){ v1.trnsExact(angle - 90, 1f); diff --git a/core/src/mindustry/maps/filters/NoiseFilter.java b/core/src/mindustry/maps/filters/NoiseFilter.java index abff26ceb8..acbf8162c6 100644 --- a/core/src/mindustry/maps/filters/NoiseFilter.java +++ b/core/src/mindustry/maps/filters/NoiseFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -23,6 +24,11 @@ public class NoiseFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockPebbles; + } + @Override public void apply(){ float noise = noise(in.x, in.y, scl, 1f, octaves, falloff); diff --git a/core/src/mindustry/maps/filters/OreFilter.java b/core/src/mindustry/maps/filters/OreFilter.java index 501d152d4f..5487ea0d59 100644 --- a/core/src/mindustry/maps/filters/OreFilter.java +++ b/core/src/mindustry/maps/filters/OreFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -22,6 +23,11 @@ public class OreFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockOreCopper; + } + @Override public void apply(){ float noise = noise(in.x, in.y, scl, 1f, octaves, falloff); diff --git a/core/src/mindustry/maps/filters/OreMedianFilter.java b/core/src/mindustry/maps/filters/OreMedianFilter.java index 27a10548af..cba1d56936 100644 --- a/core/src/mindustry/maps/filters/OreMedianFilter.java +++ b/core/src/mindustry/maps/filters/OreMedianFilter.java @@ -5,6 +5,7 @@ import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -27,6 +28,11 @@ public class OreMedianFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockOreLead; + } + @Override public void apply(){ if(in.overlay == Blocks.spawn) return; diff --git a/core/src/mindustry/maps/filters/RiverNoiseFilter.java b/core/src/mindustry/maps/filters/RiverNoiseFilter.java index 73ce77acb9..5019124c23 100644 --- a/core/src/mindustry/maps/filters/RiverNoiseFilter.java +++ b/core/src/mindustry/maps/filters/RiverNoiseFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -22,6 +23,11 @@ public class RiverNoiseFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockWater; + } + @Override public void apply(){ float noise = rnoise(in.x, in.y, scl, 1f); diff --git a/core/src/mindustry/maps/filters/ScatterFilter.java b/core/src/mindustry/maps/filters/ScatterFilter.java index 9cc809eeed..b766183087 100644 --- a/core/src/mindustry/maps/filters/ScatterFilter.java +++ b/core/src/mindustry/maps/filters/ScatterFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -20,6 +21,11 @@ public class ScatterFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockBoulder; + } + @Override public void apply(){ diff --git a/core/src/mindustry/maps/filters/SpawnPathFilter.java b/core/src/mindustry/maps/filters/SpawnPathFilter.java index 8384024186..ac49b13d31 100644 --- a/core/src/mindustry/maps/filters/SpawnPathFilter.java +++ b/core/src/mindustry/maps/filters/SpawnPathFilter.java @@ -6,6 +6,7 @@ import arc.util.*; import mindustry.*; import mindustry.ai.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; import mindustry.world.blocks.storage.*; @@ -23,6 +24,11 @@ public class SpawnPathFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockCommandCenter; + } + @Override public void apply(Tiles tiles, GenerateInput in){ Tile core = null; diff --git a/core/src/mindustry/maps/filters/TerrainFilter.java b/core/src/mindustry/maps/filters/TerrainFilter.java index 4747149448..f494d0c143 100644 --- a/core/src/mindustry/maps/filters/TerrainFilter.java +++ b/core/src/mindustry/maps/filters/TerrainFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.math.*; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -25,6 +26,11 @@ public class TerrainFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockStoneWall; + } + @Override public void apply(){ float noise = noise(in.x, in.y, scl, magnitude, octaves, falloff) + Mathf.dst((float)in.x / in.width, (float)in.y / in.height, 0.5f, 0.5f) * circleScl; diff --git a/core/src/mindustry/maps/generators/BaseGenerator.java b/core/src/mindustry/maps/generators/BaseGenerator.java index 02e02e8de2..636e386356 100644 --- a/core/src/mindustry/maps/generators/BaseGenerator.java +++ b/core/src/mindustry/maps/generators/BaseGenerator.java @@ -15,6 +15,7 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.defense.*; import mindustry.world.blocks.distribution.*; +import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.power.*; import mindustry.world.blocks.production.*; import mindustry.world.meta.*; @@ -103,7 +104,7 @@ public class BaseGenerator{ Tile o = tiles.get(tile.x + p.x, tile.y + p.y); //do not block payloads - if(o != null && (o.block() instanceof PayloadConveyor || o.block() instanceof PayloadAcceptor)){ + if(o != null && (o.block() instanceof PayloadConveyor || o.block() instanceof PayloadBlock)){ return; } } diff --git a/core/src/mindustry/maps/generators/BasicGenerator.java b/core/src/mindustry/maps/generators/BasicGenerator.java index 1ef6d31297..bee63cc9eb 100644 --- a/core/src/mindustry/maps/generators/BasicGenerator.java +++ b/core/src/mindustry/maps/generators/BasicGenerator.java @@ -121,7 +121,10 @@ public abstract class BasicGenerator implements WorldGenerator{ } public void tech(){ - Block[] blocks = {Blocks.darkPanel3}; + tech(Blocks.darkPanel3, Blocks.darkPanel4, Blocks.darkMetal); + } + + public void tech(Block floor1, Block floor2, Block wall){ int secSize = 20; pass((x, y) -> { if(!floor.asFloor().hasSurface()) return; @@ -130,14 +133,14 @@ public abstract class BasicGenerator implements WorldGenerator{ int sclx = x / secSize, scly = y / secSize; if(noise(sclx, scly, 0.2f, 1f) > 0.63f && noise(sclx, scly + 999, 200f, 1f) > 0.6f && (mx == 0 || my == 0 || mx == secSize - 1 || my == secSize - 1)){ if(Mathf.chance(noise(x + 0x231523, y, 40f, 1f))){ - floor = blocks[rand.random(0, blocks.length - 1)]; + floor = floor1; if(Mathf.dst(mx, my, secSize/2, secSize/2) > secSize/2f + 2){ - floor = Blocks.darkPanel4; + floor = floor2; } } if(block.solid && Mathf.chance(0.7)){ - block = Blocks.darkMetal; + block = wall; } } }); @@ -215,7 +218,9 @@ public abstract class BasicGenerator implements WorldGenerator{ read.set(write); } - tiles.each((x, y) -> tiles.get(x, y).setBlock(!read.get(x, y) ? Blocks.air : tiles.get(x, y).floor().wall)); + for(var t : tiles){ + t.setBlock(!read.get(t.x, t.y) ? Blocks.air : t.floor().wall); + } } protected float noise(float x, float y, double scl, double mag){ diff --git a/core/src/mindustry/maps/generators/PlanetGenerator.java b/core/src/mindustry/maps/generators/PlanetGenerator.java index d1cfe30bf5..830be018fb 100644 --- a/core/src/mindustry/maps/generators/PlanetGenerator.java +++ b/core/src/mindustry/maps/generators/PlanetGenerator.java @@ -2,12 +2,19 @@ package mindustry.maps.generators; import arc.math.geom.*; import arc.struct.*; +import arc.struct.ObjectIntMap.*; import arc.util.noise.*; +import mindustry.content.*; +import mindustry.ctype.*; +import mindustry.game.*; import mindustry.graphics.g3d.*; import mindustry.graphics.g3d.PlanetGrid.*; import mindustry.type.*; +import mindustry.type.Weather.*; import mindustry.world.*; +import static mindustry.Vars.*; + public abstract class PlanetGenerator extends BasicGenerator implements HexMesher{ protected IntSeq ints = new IntSeq(); protected Sector sector; @@ -43,6 +50,65 @@ public abstract class PlanetGenerator extends BasicGenerator implements HexMeshe } } + public void addWeather(Sector sector, Rules rules){ + + //apply weather based on terrain + ObjectIntMap floorc = new ObjectIntMap<>(); + ObjectSet content = new ObjectSet<>(); + + for(Tile tile : world.tiles){ + if(world.getDarkness(tile.x, tile.y) >= 3){ + continue; + } + + Liquid liquid = tile.floor().liquidDrop; + if(tile.floor().itemDrop != null) content.add(tile.floor().itemDrop); + if(tile.overlay().itemDrop != null) content.add(tile.overlay().itemDrop); + if(liquid != null) content.add(liquid); + + if(!tile.block().isStatic()){ + floorc.increment(tile.floor()); + if(tile.overlay() != Blocks.air){ + floorc.increment(tile.overlay()); + } + } + } + + //sort counts in descending order + Seq> entries = floorc.entries().toArray(); + entries.sort(e -> -e.value); + //remove all blocks occuring < 30 times - unimportant + entries.removeAll(e -> e.value < 30); + + Block[] floors = new Block[entries.size]; + for(int i = 0; i < entries.size; i++){ + floors[i] = entries.get(i).key; + } + + //TODO bad code + boolean hasSnow = floors.length > 0 && (floors[0].name.contains("ice") || floors[0].name.contains("snow")); + boolean hasRain = floors.length > 0 && !hasSnow && content.contains(Liquids.water) && !floors[0].name.contains("sand"); + boolean hasDesert = floors.length > 0 && !hasSnow && !hasRain && floors[0] == Blocks.sand; + boolean hasSpores = floors.length > 0 && (floors[0].name.contains("spore") || floors[0].name.contains("moss") || floors[0].name.contains("tainted")); + + if(hasSnow){ + rules.weather.add(new WeatherEntry(Weathers.snow)); + } + + if(hasRain){ + rules.weather.add(new WeatherEntry(Weathers.rain)); + rules.weather.add(new WeatherEntry(Weathers.fog)); + } + + if(hasDesert){ + rules.weather.add(new WeatherEntry(Weathers.sandstorm)); + } + + if(hasSpores){ + rules.weather.add(new WeatherEntry(Weathers.sporestorm)); + } + } + protected void genTile(Vec3 position, TileGen tile){ } @@ -53,6 +119,11 @@ public abstract class PlanetGenerator extends BasicGenerator implements HexMeshe return (float)noise.octaveNoise3D(octaves, falloff, 1f / scl, v.x, v.y, v.z) * (float)mag; } + /** @return the scaling factor for sector rects. */ + public float getSizeScl(){ + return 3200; + } + public void generate(Tiles tiles, Sector sec){ this.tiles = tiles; this.sector = sec; diff --git a/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java b/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java index 2459a7b2eb..b5f8be471a 100644 --- a/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java +++ b/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java @@ -115,7 +115,7 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{ tile.floor = getBlock(position); tile.block = tile.floor.asFloor().wall; - if(rid.getValue(position.x, position.y, position.z, 22) > 0.32){ + if(rid.getValue(position.x, position.y, position.z, 22) > 0.31){ tile.block = Blocks.air; } } @@ -162,12 +162,11 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{ } void connect(Room to){ - if(connected.contains(to)) return; + if(!connected.add(to)) return; - connected.add(to); - float nscl = rand.random(20f, 60f); - int stroke = rand.random(4, 12); - brush(pathfind(x, y, to.x, to.y, tile -> (tile.solid() ? 5f : 0f) + noise(tile.x, tile.y, 1, 1, 1f / nscl) * 60, Astar.manhattan), stroke); + float nscl = rand.random(100f, 140f); + int stroke = rand.random(3, 9); + brush(pathfind(x, y, to.x, to.y, tile -> (tile.solid() ? 5f : 0f) + noise(tile.x, tile.y, 2, 0.4, 1f / nscl) * 500, Astar.manhattan), stroke); } } @@ -219,7 +218,7 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{ for(int j = 0; j < enemySpawns; j++){ float enemyOffset = rand.range(60f); Tmp.v1.set(cx - width/2, cy - height/2).rotate(180f + enemyOffset).add(width/2, height/2); - Room espawn = new Room((int)Tmp.v1.x, (int)Tmp.v1.y, rand.random(8, 15)); + Room espawn = new Room((int)Tmp.v1.x, (int)Tmp.v1.y, rand.random(8, 16)); roomseq.add(espawn); enemies.add(espawn); } diff --git a/core/src/mindustry/maps/planet/TantrosPlanetGenerator.java b/core/src/mindustry/maps/planet/TantrosPlanetGenerator.java deleted file mode 100644 index d74f94e250..0000000000 --- a/core/src/mindustry/maps/planet/TantrosPlanetGenerator.java +++ /dev/null @@ -1,32 +0,0 @@ -package mindustry.maps.planet; - -import arc.graphics.*; -import arc.math.*; -import arc.math.geom.*; -import mindustry.content.*; -import mindustry.game.*; -import mindustry.maps.generators.*; - -public class TantrosPlanetGenerator extends PlanetGenerator{ - Color c1 = Color.valueOf("5057a6"), c2 = Color.valueOf("272766"), out = new Color(); - - @Override - public float getHeight(Vec3 position){ - return 0; - } - - @Override - public Color getColor(Vec3 position){ - float depth = (float)noise.octaveNoise3D(2, 0.56, 1.7f, position.x, position.y, position.z) / 2f; - return c1.write(out).lerp(c2, Mathf.clamp(Mathf.round(depth, 0.15f))).a(0.6f); - } - - @Override - protected void generate(){ - pass((x, y) -> { - floor = Blocks.deepwater; - }); - - Schematics.placeLaunchLoadout(width / 2, height / 2); - } -} diff --git a/core/src/mindustry/mod/ClassMap.java b/core/src/mindustry/mod/ClassMap.java index af000c2f96..0961e12999 100644 --- a/core/src/mindustry/mod/ClassMap.java +++ b/core/src/mindustry/mod/ClassMap.java @@ -1,20 +1,26 @@ package mindustry.mod; import arc.struct.*; +import mindustry.world.blocks.environment.*; + /** Generated class. Maps simple class names to concrete classes. For use in JSON mods. */ +@SuppressWarnings("deprecation") public class ClassMap{ public static final ObjectMap> classes = new ObjectMap<>(); static{ classes.put("BuilderAI", mindustry.ai.types.BuilderAI.class); + classes.put("DefenderAI", mindustry.ai.types.DefenderAI.class); classes.put("FlyingAI", mindustry.ai.types.FlyingAI.class); classes.put("FormationAI", mindustry.ai.types.FormationAI.class); classes.put("GroundAI", mindustry.ai.types.GroundAI.class); + classes.put("HugAI", mindustry.ai.types.HugAI.class); classes.put("LogicAI", mindustry.ai.types.LogicAI.class); classes.put("MinerAI", mindustry.ai.types.MinerAI.class); classes.put("RepairAI", mindustry.ai.types.RepairAI.class); classes.put("SuicideAI", mindustry.ai.types.SuicideAI.class); classes.put("Ability", mindustry.entities.abilities.Ability.class); + classes.put("EnergyFieldAbility", mindustry.entities.abilities.EnergyFieldAbility.class); classes.put("ForceFieldAbility", mindustry.entities.abilities.ForceFieldAbility.class); classes.put("MoveLightningAbility", mindustry.entities.abilities.MoveLightningAbility.class); classes.put("RepairFieldAbility", mindustry.entities.abilities.RepairFieldAbility.class); @@ -26,6 +32,7 @@ public class ClassMap{ classes.put("BombBulletType", mindustry.entities.bullet.BombBulletType.class); classes.put("BulletType", mindustry.entities.bullet.BulletType.class); classes.put("ContinuousLaserBulletType", mindustry.entities.bullet.ContinuousLaserBulletType.class); + classes.put("EmpBulletType", mindustry.entities.bullet.EmpBulletType.class); classes.put("FlakBulletType", mindustry.entities.bullet.FlakBulletType.class); classes.put("LaserBoltBulletType", mindustry.entities.bullet.LaserBoltBulletType.class); classes.put("LaserBulletType", mindustry.entities.bullet.LaserBulletType.class); @@ -37,6 +44,7 @@ public class ClassMap{ classes.put("RailBulletType", mindustry.entities.bullet.RailBulletType.class); classes.put("SapBulletType", mindustry.entities.bullet.SapBulletType.class); classes.put("ShrapnelBulletType", mindustry.entities.bullet.ShrapnelBulletType.class); + classes.put("ExplosionEffect", mindustry.entities.effect.ExplosionEffect.class); classes.put("MultiEffect", mindustry.entities.effect.MultiEffect.class); classes.put("ParticleEffect", mindustry.entities.effect.ParticleEffect.class); classes.put("WaveEffect", mindustry.entities.effect.WaveEffect.class); @@ -45,8 +53,36 @@ public class ClassMap{ classes.put("Produce", mindustry.game.Objectives.Produce.class); classes.put("Research", mindustry.game.Objectives.Research.class); classes.put("SectorComplete", mindustry.game.Objectives.SectorComplete.class); + classes.put("AmmoType", mindustry.type.AmmoType.class); + classes.put("AmmoTypes", mindustry.type.AmmoTypes.class); + classes.put("ItemAmmoType", mindustry.type.AmmoTypes.ItemAmmoType.class); + classes.put("PowerAmmoType", mindustry.type.AmmoTypes.PowerAmmoType.class); + classes.put("Category", mindustry.type.Category.class); + classes.put("ErrorContent", mindustry.type.ErrorContent.class); + classes.put("Item", mindustry.type.Item.class); + classes.put("ItemSeq", mindustry.type.ItemSeq.class); + classes.put("ItemStack", mindustry.type.ItemStack.class); + classes.put("Liquid", mindustry.type.Liquid.class); + classes.put("LiquidStack", mindustry.type.LiquidStack.class); + classes.put("Planet", mindustry.type.Planet.class); + classes.put("Publishable", mindustry.type.Publishable.class); + classes.put("Satellite", mindustry.type.Satellite.class); + classes.put("Sector", mindustry.type.Sector.class); + classes.put("SectorRect", mindustry.type.Sector.SectorRect.class); + classes.put("SectorPreset", mindustry.type.SectorPreset.class); + classes.put("StatusEffect", mindustry.type.StatusEffect.class); + classes.put("TransitionHandler", mindustry.type.StatusEffect.TransitionHandler.class); + classes.put("UnitType", mindustry.type.UnitType.class); + classes.put("Weapon", mindustry.type.Weapon.class); + classes.put("Weather", mindustry.type.Weather.class); + classes.put("WeatherEntry", mindustry.type.Weather.WeatherEntry.class); + classes.put("PointDefenseWeapon", mindustry.type.weapons.PointDefenseWeapon.class); + classes.put("RepairBeamWeapon", mindustry.type.weapons.RepairBeamWeapon.class); + classes.put("HealBeamMount", mindustry.type.weapons.RepairBeamWeapon.HealBeamMount.class); + classes.put("MagneticStorm", mindustry.type.weather.MagneticStorm.class); classes.put("ParticleWeather", mindustry.type.weather.ParticleWeather.class); classes.put("RainWeather", mindustry.type.weather.RainWeather.class); + classes.put("SolarFlare", mindustry.type.weather.SolarFlare.class); classes.put("Attributes", mindustry.world.blocks.Attributes.class); classes.put("Autotiler", mindustry.world.blocks.Autotiler.class); classes.put("AutotilerHolder", mindustry.world.blocks.Autotiler.AutotilerHolder.class); @@ -59,6 +95,8 @@ public class ClassMap{ classes.put("AcceleratorBuild", mindustry.world.blocks.campaign.Accelerator.AcceleratorBuild.class); classes.put("LaunchPad", mindustry.world.blocks.campaign.LaunchPad.class); classes.put("LaunchPadBuild", mindustry.world.blocks.campaign.LaunchPad.LaunchPadBuild.class); + classes.put("PayloadLaunchPad", mindustry.world.blocks.campaign.PayloadLaunchPad.class); + classes.put("PayloadLaunchPadBuild", mindustry.world.blocks.campaign.PayloadLaunchPad.PayloadLaunchPadBuild.class); classes.put("Door", mindustry.world.blocks.defense.Door.class); classes.put("DoorBuild", mindustry.world.blocks.defense.Door.DoorBuild.class); classes.put("ForceProjector", mindustry.world.blocks.defense.ForceProjector.class); @@ -76,6 +114,7 @@ public class ClassMap{ classes.put("BaseTurret", mindustry.world.blocks.defense.turrets.BaseTurret.class); classes.put("BaseTurretBuild", mindustry.world.blocks.defense.turrets.BaseTurret.BaseTurretBuild.class); classes.put("ItemTurret", mindustry.world.blocks.defense.turrets.ItemTurret.class); + classes.put("ItemEntry", mindustry.world.blocks.defense.turrets.ItemTurret.ItemEntry.class); classes.put("ItemTurretBuild", mindustry.world.blocks.defense.turrets.ItemTurret.ItemTurretBuild.class); classes.put("LaserTurret", mindustry.world.blocks.defense.turrets.LaserTurret.class); classes.put("LaserTurretBuild", mindustry.world.blocks.defense.turrets.LaserTurret.LaserTurretBuild.class); @@ -99,6 +138,12 @@ public class ClassMap{ classes.put("ChainedBuilding", mindustry.world.blocks.distribution.ChainedBuilding.class); classes.put("Conveyor", mindustry.world.blocks.distribution.Conveyor.class); classes.put("ConveyorBuild", mindustry.world.blocks.distribution.Conveyor.ConveyorBuild.class); + classes.put("Duct", mindustry.world.blocks.distribution.Duct.class); + classes.put("DuctBuild", mindustry.world.blocks.distribution.Duct.DuctBuild.class); + classes.put("DuctBridge", mindustry.world.blocks.distribution.DuctBridge.class); + classes.put("DuctBridgeBuild", mindustry.world.blocks.distribution.DuctBridge.DuctBridgeBuild.class); + classes.put("DuctRouter", mindustry.world.blocks.distribution.DuctRouter.class); + classes.put("DuctBuild", mindustry.world.blocks.distribution.DuctRouter.DuctBuild.class); classes.put("ExtendingItemBridge", mindustry.world.blocks.distribution.ExtendingItemBridge.class); classes.put("ExtendingItemBridgeBuild", mindustry.world.blocks.distribution.ExtendingItemBridge.ExtendingItemBridgeBuild.class); classes.put("ItemBridge", mindustry.world.blocks.distribution.ItemBridge.class); @@ -122,19 +167,22 @@ public class ClassMap{ classes.put("StackConveyor", mindustry.world.blocks.distribution.StackConveyor.class); classes.put("StackConveyorBuild", mindustry.world.blocks.distribution.StackConveyor.StackConveyorBuild.class); classes.put("AirBlock", mindustry.world.blocks.environment.AirBlock.class); - classes.put("Boulder", mindustry.world.blocks.environment.Boulder.class); classes.put("Cliff", mindustry.world.blocks.environment.Cliff.class); classes.put("DoubleOverlayFloor", mindustry.world.blocks.environment.DoubleOverlayFloor.class); classes.put("Floor", mindustry.world.blocks.environment.Floor.class); classes.put("OreBlock", mindustry.world.blocks.environment.OreBlock.class); classes.put("OverlayFloor", mindustry.world.blocks.environment.OverlayFloor.class); + classes.put("Prop", mindustry.world.blocks.environment.Prop.class); + classes.put("Bush", Bush.class); + classes.put("WavingProp", WavingProp.class); classes.put("ShallowLiquid", mindustry.world.blocks.environment.ShallowLiquid.class); classes.put("SpawnBlock", mindustry.world.blocks.environment.SpawnBlock.class); + classes.put("StaticClusterWall", StaticClusterWall.class); classes.put("StaticTree", mindustry.world.blocks.environment.StaticTree.class); classes.put("StaticWall", mindustry.world.blocks.environment.StaticWall.class); classes.put("TreeBlock", mindustry.world.blocks.environment.TreeBlock.class); - classes.put("BlockForge", mindustry.world.blocks.experimental.BlockForge.class); - classes.put("BlockForgeBuild", mindustry.world.blocks.experimental.BlockForge.BlockForgeBuild.class); + classes.put("WallOreBlock", mindustry.world.blocks.environment.WallOreBlock.class); + classes.put("WobbleProp", mindustry.world.blocks.environment.WobbleProp.class); classes.put("BlockLoader", mindustry.world.blocks.experimental.BlockLoader.class); classes.put("BlockLoaderBuild", mindustry.world.blocks.experimental.BlockLoader.BlockLoaderBuild.class); classes.put("BlockUnloader", mindustry.world.blocks.experimental.BlockUnloader.class); @@ -170,8 +218,26 @@ public class ClassMap{ classes.put("MessageBuild", mindustry.world.blocks.logic.MessageBlock.MessageBuild.class); classes.put("SwitchBlock", mindustry.world.blocks.logic.SwitchBlock.class); classes.put("SwitchBuild", mindustry.world.blocks.logic.SwitchBlock.SwitchBuild.class); + classes.put("BallisticSilo", mindustry.world.blocks.payloads.BallisticSilo.class); + classes.put("BallisticSiloBuild", mindustry.world.blocks.payloads.BallisticSilo.BallisticSiloBuild.class); + classes.put("BlockForge", mindustry.world.blocks.payloads.BlockForge.class); + classes.put("BlockForgeBuild", mindustry.world.blocks.payloads.BlockForge.BlockForgeBuild.class); + classes.put("BlockProducer", mindustry.world.blocks.payloads.BlockProducer.class); + classes.put("BlockProducerBuild", mindustry.world.blocks.payloads.BlockProducer.BlockProducerBuild.class); classes.put("BuildPayload", mindustry.world.blocks.payloads.BuildPayload.class); + classes.put("NuclearWarhead", mindustry.world.blocks.payloads.NuclearWarhead.class); + classes.put("NuclearWarheadBuild", mindustry.world.blocks.payloads.NuclearWarhead.NuclearWarheadBuild.class); classes.put("Payload", mindustry.world.blocks.payloads.Payload.class); + classes.put("PayloadBlock", mindustry.world.blocks.payloads.PayloadBlock.class); + classes.put("PayloadBlockBuild", mindustry.world.blocks.payloads.PayloadBlock.PayloadBlockBuild.class); + classes.put("PayloadMassDriver", mindustry.world.blocks.payloads.PayloadMassDriver.class); + classes.put("PayloadDriverBuild", mindustry.world.blocks.payloads.PayloadMassDriver.PayloadDriverBuild.class); + classes.put("PayloadDriverState", mindustry.world.blocks.payloads.PayloadMassDriver.PayloadDriverState.class); + classes.put("PayloadMassDriverData", mindustry.world.blocks.payloads.PayloadMassDriver.PayloadMassDriverData.class); + classes.put("PayloadSource", mindustry.world.blocks.payloads.PayloadSource.class); + classes.put("PayloadSourceBuild", mindustry.world.blocks.payloads.PayloadSource.PayloadSourceBuild.class); + classes.put("PayloadVoid", mindustry.world.blocks.payloads.PayloadVoid.class); + classes.put("BlockLoaderBuild", mindustry.world.blocks.payloads.PayloadVoid.BlockLoaderBuild.class); classes.put("UnitPayload", mindustry.world.blocks.payloads.UnitPayload.class); classes.put("Battery", mindustry.world.blocks.power.Battery.class); classes.put("BatteryBuild", mindustry.world.blocks.power.Battery.BatteryBuild.class); @@ -201,8 +267,12 @@ public class ClassMap{ classes.put("SolarGeneratorBuild", mindustry.world.blocks.power.SolarGenerator.SolarGeneratorBuild.class); classes.put("ThermalGenerator", mindustry.world.blocks.power.ThermalGenerator.class); classes.put("ThermalGeneratorBuild", mindustry.world.blocks.power.ThermalGenerator.ThermalGeneratorBuild.class); + classes.put("AttributeCrafter", mindustry.world.blocks.production.AttributeCrafter.class); + classes.put("AttributeCrafterBuild", mindustry.world.blocks.production.AttributeCrafter.AttributeCrafterBuild.class); classes.put("AttributeSmelter", mindustry.world.blocks.production.AttributeSmelter.class); classes.put("AttributeSmelterBuild", mindustry.world.blocks.production.AttributeSmelter.AttributeSmelterBuild.class); + classes.put("BeamDrill", mindustry.world.blocks.production.BeamDrill.class); + classes.put("BeamDrillBuild", mindustry.world.blocks.production.BeamDrill.BeamDrillBuild.class); classes.put("Cultivator", mindustry.world.blocks.production.Cultivator.class); classes.put("CultivatorBuild", mindustry.world.blocks.production.Cultivator.CultivatorBuild.class); classes.put("Drill", mindustry.world.blocks.production.Drill.class); @@ -223,6 +293,8 @@ public class ClassMap{ classes.put("PumpBuild", mindustry.world.blocks.production.Pump.PumpBuild.class); classes.put("Separator", mindustry.world.blocks.production.Separator.class); classes.put("SeparatorBuild", mindustry.world.blocks.production.Separator.SeparatorBuild.class); + classes.put("SingleBlockProducer", mindustry.world.blocks.production.SingleBlockProducer.class); + classes.put("SingleBlockProducerBuild", mindustry.world.blocks.production.SingleBlockProducer.SingleBlockProducerBuild.class); classes.put("SolidPump", mindustry.world.blocks.production.SolidPump.class); classes.put("SolidPumpBuild", mindustry.world.blocks.production.SolidPump.SolidPumpBuild.class); classes.put("ItemSource", mindustry.world.blocks.sandbox.ItemSource.class); @@ -256,10 +328,14 @@ public class ClassMap{ classes.put("UnitFactoryBuild", mindustry.world.blocks.units.UnitFactory.UnitFactoryBuild.class); classes.put("UnitPlan", mindustry.world.blocks.units.UnitFactory.UnitPlan.class); classes.put("DrawAnimation", mindustry.world.draw.DrawAnimation.class); + classes.put("DrawArcSmelter", mindustry.world.draw.DrawArcSmelter.class); classes.put("DrawBlock", mindustry.world.draw.DrawBlock.class); + classes.put("DrawCells", mindustry.world.draw.DrawCells.class); + classes.put("DrawCultivator", mindustry.world.draw.DrawCultivator.class); classes.put("DrawGlow", mindustry.world.draw.DrawGlow.class); classes.put("DrawMixer", mindustry.world.draw.DrawMixer.class); classes.put("DrawRotator", mindustry.world.draw.DrawRotator.class); + classes.put("DrawSmelter", mindustry.world.draw.DrawSmelter.class); classes.put("DrawWeave", mindustry.world.draw.DrawWeave.class); classes.put("Block", mindustry.world.Block.class); } diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index 11bd9d0416..720f5c2c02 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -26,6 +26,7 @@ import mindustry.entities.effect.*; import mindustry.game.*; import mindustry.game.Objectives.*; import mindustry.gen.*; +import mindustry.graphics.*; import mindustry.mod.Mods.*; import mindustry.type.*; import mindustry.type.weather.*; @@ -59,6 +60,7 @@ public class ContentParser{ return result; }); put(Interp.class, (type, data) -> field(Interp.class, data)); + put(CacheLayer.class, (type, data) -> field(CacheLayer.class, data)); put(Schematic.class, (type, data) -> { Object result = fieldOpt(Loadouts.class, data); if(result != null){ @@ -132,7 +134,9 @@ public class ContentParser{ return obj; }); put(Weapon.class, (type, data) -> { - Weapon weapon = new Weapon(); + var oc = resolve(data.getString("type", ""), Weapon.class); + data.remove("type"); + var weapon = make(oc); readFields(weapon, data); weapon.name = currentMod.name + "-" + weapon.name; return weapon; @@ -325,8 +329,17 @@ public class ContentParser{ return item; }, ContentType.item, parser(ContentType.item, Item::new), - ContentType.liquid, parser(ContentType.liquid, Liquid::new) - //ContentType.sector, parser(ContentType.sector, SectorPreset::new) + ContentType.liquid, parser(ContentType.liquid, Liquid::new), + ContentType.status, parser(ContentType.status, StatusEffect::new), + ContentType.sector, (TypeParser)(mod, name, value) -> { + if(value.isString()){ + return locate(ContentType.sector, name); + } + + if(!value.has("sector") || !value.get("sector").isNumber()) throw new RuntimeException("SectorPresets must have a sector number."); + + return new SectorPreset(name, locate(ContentType.planet, value.getString("planet", "serpulo")), value.getInt("sector")); + } ); private Prov unitType(JsonValue value){ @@ -714,14 +727,10 @@ public class ContentParser{ try{ return (Class)Class.forName(base); }catch(Exception ignored){ - //try to load from a mod's class loader - for(LoadedMod mod : mods.mods){ - if(mod.loader != null){ - try{ - return (Class)Class.forName(base, true, mod.loader); - }catch(Exception ignore){} - } - } + //try to use mod class loader + try{ + return (Class)Class.forName(base, true, mods.mainLoader()); + }catch(Exception ignore){} } } diff --git a/core/src/mindustry/mod/ModClassLoader.java b/core/src/mindustry/mod/ModClassLoader.java new file mode 100644 index 0000000000..74864cb81c --- /dev/null +++ b/core/src/mindustry/mod/ModClassLoader.java @@ -0,0 +1,35 @@ +package mindustry.mod; + +import arc.struct.*; + +public class ModClassLoader extends ClassLoader{ + private Seq children = new Seq<>(); + private volatile boolean inChild = false; + + public void addChild(ClassLoader child){ + children.add(child); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ + //always try the superclass first + try{ + return super.loadClass(name, resolve); + }catch(ClassNotFoundException error){ + //a child may try to delegate class loading to its parent, which is *this class loader* - do not let that happen + if(inChild) throw error; + int size = children.size; + //if it doesn't exist in the main class loader, try all the children + for(int i = 0; i < size; i++){ + try{ + inChild = true; + var out = children.get(i).loadClass(name); + inChild = false; + return out; + }catch(ClassNotFoundException ignored){ + } + } + throw error; + } + } +} diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index a79634a43e..073c30f1b6 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -11,6 +11,7 @@ import arc.graphics.g2d.TextureAtlas.*; import arc.scene.ui.*; import arc.struct.*; import arc.util.*; +import arc.util.async.*; import arc.util.io.*; import arc.util.serialization.*; import arc.util.serialization.Jval.*; @@ -29,6 +30,7 @@ import java.util.*; import static mindustry.Vars.*; public class Mods implements Loadable{ + private AsyncExecutor async = new AsyncExecutor(); private Json json = new Json(); private @Nullable Scripts scripts; private ContentParser parser = new ContentParser(); @@ -37,15 +39,21 @@ public class Mods implements Loadable{ private int totalSprites; private MultiPacker packer; + private ModClassLoader mainLoader = new ModClassLoader(); Seq mods = new Seq<>(); private ObjectMap, ModMeta> metas = new ObjectMap<>(); - private boolean requiresReload, createdAtlas; + private boolean requiresReload; public Mods(){ Events.on(ClientLoadEvent.class, e -> Core.app.post(this::checkWarnings)); } + /** @return the main class loader for all mods */ + public ClassLoader mainLoader(){ + return mainLoader; + } + /** Returns a file named 'config.json' in a special folder for the specified plugin. * Call this in init(). */ public Fi getConfig(Mod mod){ @@ -97,9 +105,7 @@ public class Mods implements Loadable{ Core.settings.put("mod-" + loaded.name + "-enabled", true); sortMods(); //try to load the mod's icon so it displays on import - Core.app.post(() -> { - loadIcon(loaded); - }); + Core.app.post(() -> loadIcon(loaded)); return loaded; }catch(IOException e){ dest.delete(); @@ -117,16 +123,37 @@ public class Mods implements Loadable{ Time.mark(); packer = new MultiPacker(); + //all packing tasks to await + var tasks = new Seq>(); eachEnabled(mod -> { Seq sprites = mod.root.child("sprites").findAll(f -> f.extension().equals("png")); Seq overrides = mod.root.child("sprites-override").findAll(f -> f.extension().equals("png")); - packSprites(sprites, mod, true); - packSprites(overrides, mod, false); + + packSprites(sprites, mod, true, tasks); + packSprites(overrides, mod, false, tasks); + Log.debug("Packed @ images for mod '@'.", sprites.size + overrides.size, mod.meta.name); totalSprites += sprites.size + overrides.size; }); + for(var result : tasks){ + try{ + var packRun = result.get(); + if(packRun != null){ //can be null for very strange reasons, ignore if that's the case + try{ + //actually pack the image + packRun.run(); + }catch(Exception e){ //the image can fail to fit in the spritesheet + Log.err("Failed to fit image into the spritesheet, skipping."); + Log.err(e); + } + } + }catch(Exception e){ //this means loading the image failed, log it and move on + Log.err(e); + } + } + Log.debug("Time to pack textures: @", Time.elapsed()); } @@ -148,23 +175,29 @@ public class Mods implements Loadable{ } } - private void packSprites(Seq sprites, LoadedMod mod, boolean prefix){ + private void packSprites(Seq sprites, LoadedMod mod, boolean prefix, Seq> tasks){ + boolean linear = Core.settings.getBool("linear"); + for(Fi file : sprites){ - try(InputStream stream = file.read()){ - byte[] bytes = Streams.copyBytes(stream, Math.max((int)file.length(), 512)); - Pixmap pixmap = new Pixmap(bytes, 0, bytes.length); - packer.add(getPage(file), (prefix ? mod.name + "-" : "") + file.nameWithoutExtension(), new PixmapRegion(pixmap)); - pixmap.dispose(); - }catch(IOException e){ - Core.app.post(() -> { - Log.err("Error packing images for mod: @", mod.meta.name); - Log.err(e); - if(!headless) ui.showException(e); - }); - break; - } + //read and bleed pixmaps in parallel + tasks.add(async.submit(() -> { + try{ + Pixmap pix = new Pixmap(file.readBytes()); + //only bleeds when linear filtering is on at startup + if(linear){ + Pixmaps.bleed(pix); + } + //this returns a *runnable* which actually packs the resulting pixmap; this has to be done synchronously outside the method + return () -> { + packer.add(getPage(file), (prefix ? mod.name + "-" : "") + file.nameWithoutExtension(), new PixmapRegion(pix)); + pix.dispose(); + }; + }catch(Exception e){ + //rethrow exception with details about the cause of failure + throw new Exception("Failed to load image " + file + " for mod " + mod.name, e); + } + })); } - totalSprites += sprites.size; } @Override @@ -176,32 +209,77 @@ public class Mods implements Loadable{ //get textures packed if(totalSprites > 0){ - if(!createdAtlas) Core.atlas = new TextureAtlas(Core.files.internal("sprites/sprites.atlas")); - createdAtlas = true; for(AtlasRegion region : Core.atlas.getRegions()){ + //TODO PageType completely breaks down with multiple pages. PageType type = getPage(region); if(!packer.has(type, region.name)){ - packer.add(type, region.name, Core.atlas.getPixmap(region)); + packer.add(type, region.name, Core.atlas.getPixmap(region), region.splits, region.pads); } } - TextureFilter filter = Core.settings.getBool("linear") ? TextureFilter.linear : TextureFilter.nearest; + Core.atlas.dispose(); - //flush so generators can use these sprites - packer.flush(filter, Core.atlas); + //dead shadow-atlas for getting regions, but not pixmaps + var shadow = Core.atlas; + //dummy texture atlas that returns the 'shadow' regions; used for mod loading + Core.atlas = new TextureAtlas(){ + + @Override + public AtlasRegion find(String name){ + var base = packer.get(name); + + if(base != null){ + var reg = new AtlasRegion(shadow.find(name).texture, base.x, base.y, base.width, base.height); + reg.name = name; + reg.pixmapRegion = base; + return reg; + } + + return shadow.find(name); + } + + @Override + public boolean isFound(TextureRegion region){ + return region != shadow.find("error"); + } + + @Override + public TextureRegion find(String name, TextureRegion def){ + return !has(name) ? def : find(name); + } + + @Override + public boolean has(String s){ + return shadow.has(s) || packer.get(s) != null; + } + + //return the *actual* pixmap regions, not the disposed ones. + @Override + public PixmapRegion getPixmap(AtlasRegion region){ + PixmapRegion out = packer.get(region.name); + //this should not happen in normal situations + if(out == null) return packer.get("error"); + return out; + } + }; + + TextureFilter filter = Core.settings.getBool("linear") ? TextureFilter.linear : TextureFilter.nearest; //generate new icons for(Seq arr : content.getContentMap()){ arr.each(c -> { if(c instanceof UnlockableContent u && c.minfo.mod != null){ u.load(); + u.loadIcon(); u.createIcons(packer); } }); } + //dispose old atlas data Core.atlas = packer.flush(filter, new TextureAtlas()); + Core.atlas.setErrorRegion("error"); Log.debug("Total pages: @", Core.atlas.getTextures().size); } @@ -217,6 +295,7 @@ public class Mods implements Loadable{ region.texture == Core.atlas.find("stone1").texture ? PageType.environment : region.texture == Core.atlas.find("clear-editor").texture ? PageType.editor : region.texture == Core.atlas.find("whiteui").texture ? PageType.ui : + region.texture == Core.atlas.find("rubble-1-0").texture ? PageType.rubble : PageType.main; } @@ -225,6 +304,7 @@ public class Mods implements Loadable{ return parent.equals("environment") ? PageType.environment : parent.equals("editor") ? PageType.editor : + parent.equals("rubble") ? PageType.editor : parent.equals("ui") || file.parent().parent().name().equals("ui") ? PageType.ui : PageType.main; } @@ -696,12 +776,12 @@ public class Mods implements Loadable{ Version.isAtLeast(meta.minGameVersion) && (meta.getMinMajor() >= 105 || headless) ){ - if(ios){ throw new IllegalArgumentException("Java class mods are not supported on iOS."); } - loader = platform.loadJar(sourceFile, mainClass); + loader = platform.loadJar(sourceFile, mainLoader); + mainLoader.addChild(loader); Class main = Class.forName(mainClass, true, loader); metas.put(main, meta); mainMod = (Mod)main.getDeclaredConstructor().newInstance(); diff --git a/core/src/mindustry/mod/Scripts.java b/core/src/mindustry/mod/Scripts.java index 1634a0bde3..0517d99311 100644 --- a/core/src/mindustry/mod/Scripts.java +++ b/core/src/mindustry/mod/Scripts.java @@ -25,12 +25,13 @@ public class Scripts implements Disposable{ private static final Seq blacklist = Seq.with(".net.", "java.net", "files", "reflect", "javax", "rhino", "file", "channels", "jdk", "runtime", "util.os", "rmi", "security", "org.", "sun.", "beans", "sql", "http", "exec", "compiler", "process", "system", ".awt", "socket", "classloader", "oracle", "invoke", "java.util.function", "java.util.stream", "org.", "mod.classmap"); - private static final Seq whitelist = Seq.with("mindustry.net", "netserver", "netclient", "com.sun.proxy.$proxy", "jdk.proxy1", "mindustry.gen.", - "mindustry.logic.", "mindustry.async.", "saveio", "systemcursor", "filetreeinitevent"); + private static final Seq whitelist = Seq.with("mindustry.net", "netserver", "netclient", "com.sun.proxy.$proxy", "jdk.proxy", "mindustry.gen.", + "mindustry.logic.", "mindustry.async.", "saveio", "systemcursor", "filetreeinitevent", "asyncexecutor"); private final Context context; private final Scriptable scope; private boolean errored; + LoadedMod currentMod = null; public static boolean allowClass(String type){ @@ -41,10 +42,6 @@ public class Scripts implements Disposable{ Time.mark(); context = Vars.platform.getScriptContext(); - context.setClassShutter(Scripts::allowClass); - context.getWrapFactory().setJavaPrimitiveWrap(false); - context.setLanguageVersion(Context.VERSION_ES6); - scope = new ImporterTopLevel(context); new RequireBuilder() diff --git a/core/src/mindustry/net/Administration.java b/core/src/mindustry/net/Administration.java index 5f78270421..306a6d8291 100644 --- a/core/src/mindustry/net/Administration.java +++ b/core/src/mindustry/net/Administration.java @@ -369,7 +369,7 @@ public class Administration{ ObjectSet result = new ObjectSet<>(); for(PlayerInfo info : playerInfo.values()){ - if(info.lastName.equalsIgnoreCase(name) || (info.names.contains(name, false)) + if(info.lastName.equalsIgnoreCase(name) || info.names.contains(name, false) || Strings.stripColors(Strings.stripColors(info.lastName)).equals(name) || info.ips.contains(name, false) || info.id.equals(name)){ result.add(info); @@ -617,6 +617,9 @@ public class Administration{ /** valid for unit-type events only, and even in that case may be null. */ public @Nullable Unit unit; + /** valid only for removePlanned events only; contains packed positions. */ + public @Nullable int[] plans; + public PlayerAction set(Player player, ActionType type, Tile tile){ this.player = player; this.type = type; @@ -641,11 +644,12 @@ public class Administration{ tile = null; block = null; unit = null; + plans = null; } } public enum ActionType{ - breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, command + breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, buildSelect, command, removePlanned } } diff --git a/core/src/mindustry/net/ArcNetProvider.java b/core/src/mindustry/net/ArcNetProvider.java index de88aabdac..a18c8d57de 100644 --- a/core/src/mindustry/net/ArcNetProvider.java +++ b/core/src/mindustry/net/ArcNetProvider.java @@ -2,14 +2,17 @@ package mindustry.net; import arc.*; import arc.func.*; +import arc.math.*; import arc.net.*; import arc.net.FrameworkMessage.*; import arc.struct.*; import arc.util.*; +import arc.util.Log.*; import arc.util.async.*; -import arc.util.pooling.*; +import arc.util.io.*; import mindustry.net.Net.*; import mindustry.net.Packets.*; +import net.jpountz.lz4.*; import java.io.*; import java.net.*; @@ -28,8 +31,15 @@ public class ArcNetProvider implements NetProvider{ final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); Thread serverThread; + private static final LZ4FastDecompressor decompressor = LZ4Factory.fastestInstance().fastDecompressor(); + private static final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor(); + public ArcNetProvider(){ - ArcNet.errorHandler = e -> Log.debug(Strings.getStackTrace(e)); + ArcNet.errorHandler = e -> { + if(Log.level == LogLevel.debug){ + Log.debug(Strings.getStackTrace(e)); + } + }; client = new Client(8192, 8192, new PacketSerializer()); client.setDiscoveryPacket(packetSupplier); @@ -56,11 +66,11 @@ public class ArcNetProvider implements NetProvider{ @Override public void received(Connection connection, Object object){ - if(object instanceof FrameworkMessage) return; + if(!(object instanceof Packet p)) return; Core.app.post(() -> { try{ - net.handleClientReceived(object); + net.handleClientReceived(p); }catch(Throwable e){ net.handleException(e); } @@ -111,13 +121,13 @@ public class ArcNetProvider implements NetProvider{ @Override public void received(Connection connection, Object object){ ArcConnection k = getByArcID(connection.getID()); - if(object instanceof FrameworkMessage || k == null) return; + if(!(object instanceof Packet pack) || k == null) return; Core.app.post(() -> { try{ - net.handleServerReceived(k, object); + net.handleServerReceived(k, pack); }catch(Throwable e){ - e.printStackTrace(); + Log.err(e); } }); } @@ -163,9 +173,9 @@ public class ArcNetProvider implements NetProvider{ } @Override - public void sendClient(Object object, SendMode mode){ + public void sendClient(Object object, boolean reliable){ try{ - if(mode == SendMode.tcp){ + if(reliable){ client.sendTCP(object); }else{ client.sendUDP(object); @@ -174,8 +184,6 @@ public class ArcNetProvider implements NetProvider{ }catch(BufferOverflowException | BufferUnderflowException e){ net.showError(e); } - - Pools.free(object); } @Override @@ -293,7 +301,7 @@ public class ArcNetProvider implements NetProvider{ //send an object so the receiving side knows how to handle the following chunks StreamBegin begin = new StreamBegin(); begin.total = stream.stream.available(); - begin.type = Registrator.getID(stream.getClass()); + begin.type = Net.getPacketId(stream); connection.sendTCP(begin); id = begin.id; } @@ -309,9 +317,9 @@ public class ArcNetProvider implements NetProvider{ } @Override - public void send(Object object, SendMode mode){ + public void send(Object object, boolean reliable){ try{ - if(mode == SendMode.tcp){ + if(reliable){ connection.sendTCP(object); }else{ connection.sendUDP(object); @@ -332,32 +340,129 @@ public class ArcNetProvider implements NetProvider{ } } - @SuppressWarnings("unchecked") public static class PacketSerializer implements NetSerializer{ + //for debugging total read/write speeds + private static final boolean debug = false; + + ThreadLocal decompressBuffer = new ThreadLocal<>(){ + @Override + protected ByteBuffer initialValue(){ + return ByteBuffer.allocate(32768); + } + }; + ThreadLocal reads = new ThreadLocal<>(){ + @Override + protected Reads initialValue(){ + return new Reads(new ByteBufferInput(decompressBuffer.get())); + } + }; + ThreadLocal writes = new ThreadLocal<>(){ + @Override + protected Writes initialValue(){ + return new Writes(new ByteBufferOutput(decompressBuffer.get())); + } + }; + + //for debugging network write counts + static WindowedMean upload = new WindowedMean(5), download = new WindowedMean(5); + static long lastUpload, lastDownload, uploadAccum, downloadAccum; + static int lastPos; @Override public Object read(ByteBuffer byteBuffer){ + if(debug){ + if(Time.timeSinceMillis(lastDownload) >= 1000){ + lastDownload = Time.millis(); + download.add(downloadAccum); + downloadAccum = 0; + Log.info("Download: @ b/s", download.mean()); + } + downloadAccum += byteBuffer.remaining(); + } + byte id = byteBuffer.get(); if(id == -2){ return readFramework(byteBuffer); }else{ - Packet packet = Pools.obtain((Class)Registrator.getByID(id).type, (Prov)Registrator.getByID(id).constructor); - packet.read(byteBuffer); + //read length int, followed by compressed lz4 data + //TODO not thread safe!!! + Packet packet = Net.newPacket(id); + var buffer = decompressBuffer.get(); + int length = byteBuffer.getShort() & 0xffff; + byte compression = byteBuffer.get(); + + //no compression, copy over buffer + if(compression == 0){ + buffer.position(0).limit(length); + buffer.put(byteBuffer.array(), byteBuffer.position(), length); + buffer.position(0); + packet.read(reads.get()); + //move read packets forward + byteBuffer.position(byteBuffer.position() + buffer.position()); + }else{ + //decompress otherwise + int read = decompressor.decompress(byteBuffer, byteBuffer.position(), buffer, 0, length); + + buffer.position(0); + buffer.limit(length); + packet.read(reads.get()); + //move buffer forward based on bytes read by decompressor + byteBuffer.position(byteBuffer.position() + read); + } + return packet; } } @Override public void write(ByteBuffer byteBuffer, Object o){ - if(o instanceof FrameworkMessage){ + if(debug){ + lastPos = byteBuffer.position(); + } + + //write raw buffer + if(o instanceof ByteBuffer raw){ + byteBuffer.put(raw); + }else if(o instanceof FrameworkMessage msg){ byteBuffer.put((byte)-2); //code for framework message - writeFramework(byteBuffer, (FrameworkMessage)o); + writeFramework(byteBuffer, msg); }else{ - if(!(o instanceof Packet)) throw new RuntimeException("All sent objects must implement be Packets! Class: " + o.getClass()); - byte id = Registrator.getID(o.getClass()); - if(id == -1) throw new RuntimeException("Unregistered class: " + o.getClass()); + if(!(o instanceof Packet pack)) throw new RuntimeException("All sent objects must implement be Packets! Class: " + o.getClass()); + byte id = Net.getPacketId(pack); byteBuffer.put(id); - ((Packet)o).write(byteBuffer); + + var temp = decompressBuffer.get(); + temp.position(0); + temp.limit(temp.capacity()); + pack.write(writes.get()); + + short length = (short)temp.position(); + + //write length, uncompressed + byteBuffer.putShort(length); + + //don't bother with small packets + if(length < 36 || pack instanceof StreamChunk){ + //write direct contents... + byteBuffer.put((byte)0); //0 = no compression + byteBuffer.put(temp.array(), 0, length); + }else{ + byteBuffer.put((byte)1); //1 = compression + //write compressed data; this does not modify position! + int written = compressor.compress(temp, 0, temp.position(), byteBuffer, byteBuffer.position(), byteBuffer.remaining()); + //skip to indicate the written, compressed data + byteBuffer.position(byteBuffer.position() + written); + } + } + + if(debug){ + if(Time.timeSinceMillis(lastUpload) >= 1000){ + lastUpload = Time.millis(); + upload.add(uploadAccum); + uploadAccum = 0; + Log.info("Upload: @ b/s", upload.mean()); + } + uploadAccum += byteBuffer.position() - lastPos; } } @@ -388,9 +493,9 @@ public class ArcNetProvider implements NetProvider{ p.isReply = buffer.get() == 1; return p; }else if(id == 1){ - return new DiscoverHost(); + return FrameworkMessage.discoverHost; }else if(id == 2){ - return new KeepAlive(); + return FrameworkMessage.keepAlive; }else if(id == 3){ RegisterUDP p = new RegisterUDP(); p.connectionID = buffer.getInt(); diff --git a/core/src/mindustry/net/Net.java b/core/src/mindustry/net/Net.java index 194c6dabbf..689834c07b 100644 --- a/core/src/mindustry/net/Net.java +++ b/core/src/mindustry/net/Net.java @@ -5,33 +5,62 @@ import arc.func.*; import arc.net.*; import arc.struct.*; import arc.util.*; -import arc.util.pooling.*; import mindustry.gen.*; import mindustry.net.Packets.*; import mindustry.net.Streamable.*; -import net.jpountz.lz4.*; import java.io.*; import java.nio.*; import java.nio.channels.*; +import static arc.util.Log.*; import static mindustry.Vars.*; @SuppressWarnings("unchecked") public class Net{ + private static Seq> packetProvs = new Seq<>(); + private static Seq> packetClasses = new Seq<>(); + private static ObjectIntMap> packetToId = new ObjectIntMap<>(); + private boolean server; private boolean active; private boolean clientLoaded; private @Nullable StreamBuilder currentStream; - private final Seq packetQueue = new Seq<>(); + private final Seq packetQueue = new Seq<>(); private final ObjectMap, Cons> clientListeners = new ObjectMap<>(); private final ObjectMap, Cons2> serverListeners = new ObjectMap<>(); private final IntMap streams = new IntMap<>(); private final NetProvider provider; - private final LZ4FastDecompressor decompressor = LZ4Factory.fastestInstance().fastDecompressor(); - private final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor(); + + static{ + registerPacket(StreamBegin::new); + registerPacket(StreamChunk::new); + registerPacket(WorldStream::new); + registerPacket(ConnectPacket::new); + + //register generated packet classes + Call.registerPackets(); + } + + /** Registers a new packet type for serialization. */ + public static void registerPacket(Prov cons){ + packetProvs.add(cons); + var t = cons.get(); + packetClasses.add(t.getClass()); + packetToId.put(t.getClass(), packetProvs.size - 1); + } + + public static byte getPacketId(Packet packet){ + int id = packetToId.get(packet.getClass(), -1); + if(id == -1) throw new ArcRuntimeException("Unknown packet type: " + packet.getClass()); + return (byte)id; + } + + public static T newPacket(byte id){ + return ((Prov)packetProvs.get(id & 0xff)).get(); + } public Net(NetProvider provider){ this.provider = provider; @@ -39,9 +68,9 @@ public class Net{ public void handleException(Throwable e){ if(e instanceof ArcNetException){ - Core.app.post(() -> showError(new IOException("mismatch"))); + Core.app.post(() -> showError(new IOException("mismatch", e))); }else if(e instanceof ClosedChannelException){ - Core.app.post(() -> showError(new IOException("alreadyconnected"))); + Core.app.post(() -> showError(new IOException("alreadyconnected", e))); }else{ Core.app.post(() -> showError(e)); } @@ -171,14 +200,6 @@ public class Net{ active = false; } - public byte[] compressSnapshot(byte[] input){ - return compressor.compress(input); - } - - public byte[] decompressSnapshot(byte[] input, int size){ - return decompressor.decompress(input, size); - } - /** * Starts discovering servers on a different thread. * Callback is run on the main Arc thread. @@ -195,21 +216,21 @@ public class Net{ } /** Send an object to all connected clients, or to the server if this is a client.*/ - public void send(Object object, SendMode mode){ + public void send(Object object, boolean reliable){ if(server){ for(NetConnection con : provider.getConnections()){ - con.send(object, mode); + con.send(object, reliable); } }else{ - provider.sendClient(object, mode); + provider.sendClient(object, reliable); } } /** Send an object to everyone EXCEPT a certain client. Server-side only.*/ - public void sendExcept(NetConnection except, Object object, SendMode mode){ + public void sendExcept(NetConnection except, Object object, boolean reliable){ for(NetConnection con : getConnections()){ if(con != except){ - con.send(object, mode); + con.send(object, reliable); } } } @@ -235,7 +256,7 @@ public class Net{ /** * Call to handle a packet being received for the client. */ - public void handleClientReceived(Object object){ + public void handleClientReceived(Packet object){ if(object instanceof StreamBegin b){ streams.put(b.id, currentStream = new StreamBuilder(b)); @@ -251,33 +272,43 @@ public class Net{ handleClientReceived(builder.build()); currentStream = null; } - }else if(clientListeners.get(object.getClass()) != null){ + }else{ + int p = object.getPriority(); - if(clientLoaded || ((object instanceof Packet) && ((Packet)object).isImportant())){ + if(clientLoaded || p == Packet.priorityHigh){ if(clientListeners.get(object.getClass()) != null){ clientListeners.get(object.getClass()).get(object); + }else{ + object.handleClient(); } - Pools.free(object); - }else if(!((object instanceof Packet) && ((Packet)object).isUnimportant())){ + }else if(p != Packet.priorityLow){ packetQueue.add(object); - }else{ - Pools.free(object); } - }else{ - Log.err("Unhandled packet type: '@'!", object); } } /** * Call to handle a packet being received for the server. */ - public void handleServerReceived(NetConnection connection, Object object){ - - if(serverListeners.get(object.getClass()) != null){ - serverListeners.get(object.getClass()).get(connection, object); - Pools.free(object); - }else{ - Log.err("Unhandled packet type: '@'!", object.getClass()); + public void handleServerReceived(NetConnection connection, Packet object){ + try{ + //handle object normally + if(serverListeners.get(object.getClass()) != null){ + serverListeners.get(object.getClass()).get(connection, object); + }else{ + object.handleServer(connection); + } + }catch(ValidateException e){ + //ignore invalid actions + debug("Validation failed for '@': @", e.player, e.getMessage()); + }catch(RuntimeException e){ + //ignore indirect ValidateException-s + if(e.getCause() instanceof ValidateException v){ + debug("Validation failed for '@': @", v.player, v.getMessage()); + }else{ + //rethrow if not ValidateException + throw e; + } } } @@ -315,17 +346,13 @@ public class Net{ active = false; } - public enum SendMode{ - tcp, udp - } - /** Networking implementation. */ public interface NetProvider{ /** Connect to a server. */ void connectClient(String ip, int port, Runnable success) throws IOException; /** Send an object to the server. */ - void sendClient(Object object, SendMode mode); + void sendClient(Object object, boolean reliable); /** Disconnect from the server. */ void disconnectClient(); @@ -355,4 +382,5 @@ public class Net{ closeServer(); } } + } diff --git a/core/src/mindustry/net/NetConnection.java b/core/src/mindustry/net/NetConnection.java index 0b1af2d769..a7bcc9b7fd 100644 --- a/core/src/mindustry/net/NetConnection.java +++ b/core/src/mindustry/net/NetConnection.java @@ -5,7 +5,6 @@ import arc.util.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.net.Administration.*; -import mindustry.net.Net.*; import mindustry.net.Packets.*; import java.io.*; @@ -40,7 +39,7 @@ public abstract class NetConnection{ public void kick(KickReason reason){ if(kicked) return; - Log.info("Kicking connection @; Reason: @", address, reason.name()); + Log.info("Kicking connection @ / @; Reason: @", address, uuid, reason.name()); if((reason == KickReason.kick || reason == KickReason.banned || reason == KickReason.vote)){ PlayerInfo info = netServer.admins.getInfo(uuid); @@ -65,7 +64,7 @@ public abstract class NetConnection{ public void kick(String reason, long kickDuration){ if(kicked) return; - Log.info("Kicking connection @; Reason: @", address, reason.replace("\n", " ")); + Log.info("Kicking connection @ / @; Reason: @", address, uuid, reason.replace("\n", " ")); netServer.admins.handleKicked(uuid, address, kickDuration); @@ -86,8 +85,8 @@ public abstract class NetConnection{ int cid; StreamBegin begin = new StreamBegin(); begin.total = stream.stream.available(); - begin.type = Registrator.getID(stream.getClass()); - send(begin, SendMode.tcp); + begin.type = Net.getPacketId(stream); + send(begin, true); cid = begin.id; while(stream.stream.available() > 0){ @@ -97,14 +96,14 @@ public abstract class NetConnection{ StreamChunk chunk = new StreamChunk(); chunk.id = cid; chunk.data = bytes; - send(chunk, SendMode.tcp); + send(chunk, true); } }catch(IOException e){ throw new RuntimeException(e); } } - public abstract void send(Object object, SendMode mode); + public abstract void send(Object object, boolean reliable); public abstract void close(); } diff --git a/core/src/mindustry/net/NetworkIO.java b/core/src/mindustry/net/NetworkIO.java index f4494e1c4f..9253f71181 100644 --- a/core/src/mindustry/net/NetworkIO.java +++ b/core/src/mindustry/net/NetworkIO.java @@ -46,6 +46,7 @@ public class NetworkIO{ SaveIO.getSaveWriter().writeContentHeader(stream); SaveIO.getSaveWriter().writeMap(stream); + SaveIO.getSaveWriter().writeTeamBlocks(stream); }catch(IOException e){ throw new RuntimeException(e); } @@ -70,6 +71,7 @@ public class NetworkIO{ SaveIO.getSaveWriter().readContentHeader(stream); SaveIO.getSaveWriter().readMap(stream, world.context); + SaveIO.getSaveWriter().readTeamBlocks(stream); }catch(IOException e){ throw new RuntimeException(e); }finally{ diff --git a/core/src/mindustry/net/Packet.java b/core/src/mindustry/net/Packet.java index 2aaa2a05c0..a6200547cc 100644 --- a/core/src/mindustry/net/Packet.java +++ b/core/src/mindustry/net/Packet.java @@ -1,19 +1,24 @@ package mindustry.net; -import arc.util.pooling.Pool.*; +import arc.util.io.*; -import java.nio.*; +public abstract class Packet{ + //these are constants because I don't want to bother making an enum to mirror the annotation enum -public interface Packet extends Poolable{ - default void read(ByteBuffer buffer){} - default void write(ByteBuffer buffer){} - default void reset(){} + /** Does not get handled unless client is connected. */ + public static final int priorityLow = 0; + /** Gets put in a queue and processed if not connected. */ + public static final int priorityNormal = 1; + /** Gets handled immediately, regardless of connection status. */ + public static final int priorityHigh = 2; - default boolean isImportant(){ - return false; + public void read(Reads read){} + public void write(Writes write){} + + public int getPriority(){ + return priorityNormal; } - default boolean isUnimportant(){ - return false; - } + public void handleClient(){} + public void handleServer(NetConnection con){} } diff --git a/core/src/mindustry/net/Packets.java b/core/src/mindustry/net/Packets.java index 779be43b03..a1cd13670c 100644 --- a/core/src/mindustry/net/Packets.java +++ b/core/src/mindustry/net/Packets.java @@ -2,13 +2,12 @@ package mindustry.net; import arc.*; import arc.struct.*; +import arc.util.*; import arc.util.io.*; import arc.util.serialization.*; import mindustry.core.*; import mindustry.io.*; -import java.io.*; -import java.nio.*; import java.util.zip.*; /** @@ -45,21 +44,23 @@ public class Packets{ kick, ban, trace, wave } - public static class Connect implements Packet{ + /** Generic client connection event. */ + public static class Connect extends Packet{ public String addressTCP; @Override - public boolean isImportant(){ - return true; + public int getPriority(){ + return priorityHigh; } } - public static class Disconnect implements Packet{ + /** Generic client disconnection event. */ + public static class Disconnect extends Packet{ public String reason; @Override - public boolean isImportant(){ - return true; + public int getPriority(){ + return priorityHigh; } } @@ -67,55 +68,8 @@ public class Packets{ } - public static class InvokePacket implements Packet{ - private static ReusableByteInStream bin; - private static Reads read = new Reads(new DataInputStream(bin = new ReusableByteInStream())); - - public byte type, priority; - - public byte[] bytes; - public int length; - - @Override - public void read(ByteBuffer buffer){ - type = buffer.get(); - priority = buffer.get(); - short writeLength = buffer.getShort(); - bytes = new byte[writeLength]; - buffer.get(bytes); - } - - @Override - public void write(ByteBuffer buffer){ - buffer.put(type); - buffer.put(priority); - buffer.putShort((short)length); - buffer.put(bytes, 0, length); - } - - @Override - public void reset(){ - priority = 0; - } - - @Override - public boolean isImportant(){ - return priority == 1; - } - - @Override - public boolean isUnimportant(){ - return priority == 2; - } - - public Reads reader(){ - bin.setBytes(bytes); - return read; - } - } - /** Marks the beginning of a stream. */ - public static class StreamBegin implements Packet{ + public static class StreamBegin extends Packet{ private static int lastid; public int id = lastid++; @@ -123,40 +77,39 @@ public class Packets{ public byte type; @Override - public void write(ByteBuffer buffer){ - buffer.putInt(id); - buffer.putInt(total); - buffer.put(type); + public void write(Writes buffer){ + buffer.i(id); + buffer.i(total); + buffer.b(type); } @Override - public void read(ByteBuffer buffer){ - id = buffer.getInt(); - total = buffer.getInt(); - type = buffer.get(); + public void read(Reads buffer){ + id = buffer.i(); + total = buffer.i(); + type = buffer.b(); } } - public static class StreamChunk implements Packet{ + public static class StreamChunk extends Packet{ public int id; public byte[] data; @Override - public void write(ByteBuffer buffer){ - buffer.putInt(id); - buffer.putShort((short)data.length); - buffer.put(data); + public void write(Writes buffer){ + buffer.i(id); + buffer.s((short)data.length); + buffer.b(data); } @Override - public void read(ByteBuffer buffer){ - id = buffer.getInt(); - data = new byte[buffer.getShort()]; - buffer.get(data); + public void read(Reads buffer){ + id = buffer.i(); + data = buffer.b(buffer.s()); } } - public static class ConnectPacket implements Packet{ + public static class ConnectPacket extends Packet{ public int version; public String versionType; public Seq mods; @@ -165,40 +118,41 @@ public class Packets{ public int color; @Override - public void write(ByteBuffer buffer){ - buffer.putInt(Version.build); + public void write(Writes buffer){ + buffer.i(Version.build); TypeIO.writeString(buffer, versionType); TypeIO.writeString(buffer, name); TypeIO.writeString(buffer, locale); TypeIO.writeString(buffer, usid); byte[] b = Base64Coder.decode(uuid); - buffer.put(b); + buffer.b(b); CRC32 crc = new CRC32(); crc.update(Base64Coder.decode(uuid), 0, b.length); - buffer.putLong(crc.getValue()); + buffer.l(crc.getValue()); - buffer.put(mobile ? (byte)1 : 0); - buffer.putInt(color); - buffer.put((byte)mods.size); + Log.info("CRC value sent: @", Long.toHexString(crc.getValue())); + + buffer.b(mobile ? (byte)1 : 0); + buffer.i(color); + buffer.b((byte)mods.size); for(int i = 0; i < mods.size; i++){ TypeIO.writeString(buffer, mods.get(i)); } } @Override - public void read(ByteBuffer buffer){ - version = buffer.getInt(); + public void read(Reads buffer){ + version = buffer.i(); versionType = TypeIO.readString(buffer); name = TypeIO.readString(buffer); locale = TypeIO.readString(buffer); usid = TypeIO.readString(buffer); - byte[] idbytes = new byte[16]; - buffer.get(idbytes); + byte[] idbytes = buffer.b(16); uuid = new String(Base64Coder.encode(idbytes)); - mobile = buffer.get() == 1; - color = buffer.getInt(); - int totalMods = buffer.get(); + mobile = buffer.b() == 1; + color = buffer.i(); + int totalMods = buffer.b(); mods = new Seq<>(totalMods); for(int i = 0; i < totalMods; i++){ mods.add(TypeIO.readString(buffer)); diff --git a/core/src/mindustry/net/Registrator.java b/core/src/mindustry/net/Registrator.java deleted file mode 100644 index 2280d016e0..0000000000 --- a/core/src/mindustry/net/Registrator.java +++ /dev/null @@ -1,45 +0,0 @@ -package mindustry.net; - -import arc.func.*; -import arc.struct.*; -import mindustry.net.Packets.*; - -public class Registrator{ - private static final ClassEntry[] classes = { - new ClassEntry(StreamBegin.class, StreamBegin::new), - new ClassEntry(StreamChunk.class, StreamChunk::new), - new ClassEntry(WorldStream.class, WorldStream::new), - new ClassEntry(ConnectPacket.class, ConnectPacket::new), - new ClassEntry(InvokePacket.class, InvokePacket::new) - }; - private static final ObjectIntMap> ids = new ObjectIntMap<>(); - - static{ - if(classes.length > 127) throw new RuntimeException("Can't have more than 127 registered classes!"); - for(int i = 0; i < classes.length; i++){ - ids.put(classes[i].type, i); - } - } - - public static ClassEntry getByID(byte id){ - return classes[id]; - } - - public static byte getID(Class type){ - return (byte)ids.get(type, -1); - } - - public static ClassEntry[] getClasses(){ - return classes; - } - - public static class ClassEntry{ - public final Class type; - public final Prov constructor; - - public ClassEntry(Class type, Prov constructor){ - this.type = type; - this.constructor = constructor; - } - } -} diff --git a/core/src/mindustry/net/Streamable.java b/core/src/mindustry/net/Streamable.java index 52ce5d36d8..c150fc26f3 100644 --- a/core/src/mindustry/net/Streamable.java +++ b/core/src/mindustry/net/Streamable.java @@ -4,12 +4,12 @@ import mindustry.net.Packets.*; import java.io.*; -public class Streamable implements Packet{ +public class Streamable extends Packet{ public transient ByteArrayInputStream stream; @Override - public boolean isImportant(){ - return true; + public int getPriority(){ + return priorityHigh; } public static class StreamBuilder{ @@ -37,7 +37,7 @@ public class Streamable implements Packet{ } public Streamable build(){ - Streamable s = (Streamable)Registrator.getByID(type).constructor.get(); + Streamable s = Net.newPacket(type); s.stream = new ByteArrayInputStream(stream.toByteArray()); return s; } diff --git a/desktop/src/mindustry/desktop/steam/SAchievement.java b/core/src/mindustry/service/Achievement.java similarity index 83% rename from desktop/src/mindustry/desktop/steam/SAchievement.java rename to core/src/mindustry/service/Achievement.java index 073ff23942..857baca118 100644 --- a/desktop/src/mindustry/desktop/steam/SAchievement.java +++ b/core/src/mindustry/service/Achievement.java @@ -1,6 +1,8 @@ -package mindustry.desktop.steam; +package mindustry.service; -public enum SAchievement{ +import static mindustry.Vars.*; + +public enum Achievement{ kill1kEnemies(SStat.unitsDestroyed, 1000), kill100kEnemies(SStat.unitsDestroyed, 100_000), launch100kItems(SStat.itemsLaunched, 100_000), @@ -62,22 +64,24 @@ public enum SAchievement{ private final SStat stat; private final int statGoal; - public static final SAchievement[] all = values(); + private boolean completed = false; + + public static final Achievement[] all = values(); /** Creates an achievement that is triggered when this stat reaches a number.*/ - SAchievement(SStat stat, int goal){ + Achievement(SStat stat, int goal){ this.stat = stat; this.statGoal = goal; } - SAchievement(){ + Achievement(){ this(null, 0); } public void complete(){ if(!isAchieved()){ - SVars.stats.stats.setAchievement(name()); - SVars.stats.stats.storeStats(); + service.completeAchievement(name()); + service.storeStats(); } } @@ -88,6 +92,9 @@ public enum SAchievement{ } public boolean isAchieved(){ - return SVars.stats.stats.isAchieved(name(), false); + if(completed){ + return true; + } + return (completed = service.isAchieved(name())); } } diff --git a/core/src/mindustry/service/GameService.java b/core/src/mindustry/service/GameService.java new file mode 100644 index 0000000000..97b4cd002b --- /dev/null +++ b/core/src/mindustry/service/GameService.java @@ -0,0 +1,358 @@ +package mindustry.service; + +import arc.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.content.*; +import mindustry.ctype.*; +import mindustry.entities.units.*; +import mindustry.game.EventType.*; +import mindustry.game.SectorInfo.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.distribution.*; + +import static mindustry.Vars.*; +import static mindustry.service.Achievement.*; + +/** + * Interface for handling game service across multiple platforms. + * + * This includes: + * - Desktop (Steam) + * - iOS (Game Center) + * - Android (Google Play Games) + * + * The default implementation does nothing. + * */ +public class GameService{ + private ObjectSet blocksBuilt = new ObjectSet<>(), unitsBuilt = new ObjectSet<>(); + private ObjectSet t5s = new ObjectSet<>(); + private IntSet checked = new IntSet(); + + /** Begin listening for new achievement events, once the game service is activated. This can be called at any time, but only once. */ + public void init(){ + if(clientLoaded){ + registerEvents(); + }else{ + Events.on(ClientLoadEvent.class, e -> registerEvents()); + } + } + + public boolean enabled(){ + return false; + } + + public void completeAchievement(String name){ + + } + + public boolean isAchieved(String name){ + return false; + } + + public int getStat(String name, int def){ + return def; + } + + public void setStat(String name, int amount){ + + } + + public void storeStats(){ + + } + + private void registerEvents(){ + unitsBuilt = Core.settings.getJson("units-built" , ObjectSet.class, String.class, ObjectSet::new); + blocksBuilt = Core.settings.getJson("blocks-built" , ObjectSet.class, String.class, ObjectSet::new); + t5s = ObjectSet.with(UnitTypes.omura, UnitTypes.reign, UnitTypes.toxopid, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.corvus); + + //periodically check for various conditions + float updateInterval = 2f; + Timer.schedule(this::checkUpdate, updateInterval, updateInterval); + + if(Items.thorium.unlocked()) obtainThorium.complete(); + if(Items.titanium.unlocked()) obtainTitanium.complete(); + if(!content.sectors().contains(UnlockableContent::locked)){ + unlockAllZones.complete(); + } + + Events.on(UnitDestroyEvent.class, e -> { + if(campaign()){ + if(e.unit.team != Vars.player.team()){ + SStat.unitsDestroyed.add(); + + if(e.unit.isBoss()){ + SStat.bossesDefeated.add(); + } + } + } + }); + + Events.on(TurnEvent.class, e -> { + float total = 0; + for(Planet planet : content.planets()){ + for(Sector sec : planet.sectors){ + if(sec.hasBase()){ + for(ExportStat v : sec.info.production.values()){ + if(v.mean > 0) total += v.mean * 60; + } + } + } + } + + SStat.maxProduction.max(Math.round(total)); + }); + + Events.run(Trigger.newGame, () -> Core.app.post(() -> { + if(campaign() && player.core() != null && player.core().items.total() >= 10 * 1000){ + drop10kitems.complete(); + } + })); + + Events.on(CommandIssueEvent.class, e -> { + if(campaign() && e.command == UnitCommand.attack){ + issueAttackCommand.complete(); + } + }); + + Events.on(BlockBuildEndEvent.class, e -> { + if(campaign() && e.unit != null && e.unit.isLocal() && !e.breaking){ + SStat.blocksBuilt.add(); + + if(e.tile.block() == Blocks.router && e.tile.build.proximity().contains(t -> t.block == Blocks.router)){ + chainRouters.complete(); + } + + if(e.tile.block() == Blocks.groundFactory){ + buildGroundFactory.complete(); + } + + if(blocksBuilt.add(e.tile.block().name)){ + if(blocksBuilt.contains("meltdown") && blocksBuilt.contains("spectre") && blocksBuilt.contains("foreshadow")){ + buildMeltdownSpectre.complete(); + } + + save(); + } + + if(!circleConveyor.isAchieved() && e.tile.block() instanceof Conveyor){ + checked.clear(); + check: { + Tile current = e.tile; + for(int i = 0; i < 4; i++){ + checked.add(current.pos()); + if(current.build == null) break check; + Tile next = current.nearby(current.build.rotation); + if(next != null && next.block() instanceof Conveyor){ + current = next; + }else{ + break check; + } + } + + if(current == e.tile && checked.size == 4){ + circleConveyor.complete(); + } + } + } + } + }); + + Events.on(UnitCreateEvent.class, e -> { + if(campaign()){ + if(unitsBuilt.add(e.unit.type.name)){ + SStat.unitTypesBuilt.set(content.units().count(u -> unitsBuilt.contains(u.name) && !u.isHidden())); + } + + if(t5s.contains(e.unit.type)){ + buildT5.complete(); + } + } + }); + + Events.on(UnitControlEvent.class, e -> { + if(e.unit instanceof BlockUnitc && ((BlockUnitc)e.unit).tile().block == Blocks.router){ + becomeRouter.complete(); + } + }); + + Events.on(SchematicCreateEvent.class, e -> { + SStat.schematicsCreated.add(); + }); + + Events.on(BlockDestroyEvent.class, e -> { + if(campaign() && e.tile.team() != player.team()){ + SStat.blocksDestroyed.add(); + } + }); + + Events.on(MapMakeEvent.class, e -> SStat.mapsMade.add()); + + Events.on(MapPublishEvent.class, e -> SStat.mapsPublished.add()); + + Events.on(UnlockEvent.class, e -> { + if(e.content == Items.thorium) obtainThorium.complete(); + if(e.content == Items.titanium) obtainTitanium.complete(); + if(e.content instanceof SectorPreset && !content.sectors().contains(s -> s.locked())){ + unlockAllZones.complete(); + } + }); + + Events.run(Trigger.openWiki, openWiki::complete); + + Events.run(Trigger.exclusionDeath, dieExclusion::complete); + + Events.on(UnitDrownEvent.class, e -> { + if(campaign() && e.unit.isPlayer()){ + drown.complete(); + } + }); + + trigger(Trigger.acceleratorUse, useAccelerator); + + trigger(Trigger.impactPower, powerupImpactReactor); + + trigger(Trigger.flameAmmo, useFlameAmmo); + + trigger(Trigger.turretCool, coolTurret); + + trigger(Trigger.suicideBomb, suicideBomb); + + Events.run(Trigger.enablePixelation, enablePixelation::complete); + + Events.run(Trigger.thoriumReactorOverheat, () -> { + if(campaign()){ + SStat.reactorsOverheated.add(); + } + }); + + trigger(Trigger.shock, shockWetEnemy); + + trigger(Trigger.phaseDeflectHit, killEnemyPhaseWall); + + Events.on(LaunchItemEvent.class, e -> { + if(campaign()){ + launchItemPad.complete(); + } + }); + + Events.on(PickupEvent.class, e -> { + if(e.carrier.isPlayer() && campaign() && e.unit != null && t5s.contains(e.unit.type)){ + pickupT5.complete(); + } + }); + + Events.on(UnitCreateEvent.class, e -> { + if(campaign() && e.unit.team() == player.team()){ + SStat.unitsBuilt.add(); + } + }); + + Events.on(SectorLaunchEvent.class, e -> { + SStat.timesLaunched.add(); + }); + + Events.on(LaunchItemEvent.class, e -> { + SStat.itemsLaunched.add(e.stack.amount); + }); + + Events.on(WaveEvent.class, e -> { + if(campaign()){ + SStat.maxWavesSurvived.max(Vars.state.wave); + + if(state.stats.buildingsBuilt == 0 && state.wave >= 10){ + survive10WavesNoBlocks.complete(); + } + } + }); + + Events.on(PlayerJoin.class, e -> { + if(Vars.net.server()){ + SStat.maxPlayersServer.max(Groups.player.size()); + } + }); + + Runnable checkUnlocks = () -> { + if(Blocks.router.unlocked()) researchRouter.complete(); + + if(!TechTree.all.contains(t -> t.content.locked())){ + researchAll.complete(); + } + }; + + //check unlocked stuff on load as well + Events.on(ResearchEvent.class, e -> checkUnlocks.run()); + Events.on(UnlockEvent.class, e -> checkUnlocks.run()); + Events.on(ClientLoadEvent.class, e -> checkUnlocks.run()); + + Events.on(WinEvent.class, e -> { + if(state.rules.pvp){ + SStat.pvpsWon.add(); + } + }); + + Events.on(SectorCaptureEvent.class, e -> { + if(e.sector.isBeingPlayed() || net.client()){ + if(Vars.state.wave <= 5 && state.rules.attackMode){ + defeatAttack5Waves.complete(); + } + + if(state.stats.buildingsDestroyed == 0){ + captureNoBlocksBroken.complete(); + } + } + + if(Vars.state.rules.attackMode){ + SStat.attacksWon.add(); + } + + if(!e.sector.isBeingPlayed() && !net.client()){ + captureBackground.complete(); + } + + if(!e.sector.planet.sectors.contains(s -> !s.hasBase())){ + captureAllSectors.complete(); + } + + SStat.sectorsControlled.set(e.sector.planet.sectors.count(Sector::hasBase)); + }); + } + + private void checkUpdate(){ + if(campaign()){ + SStat.maxUnitActive.max(Groups.unit.count(t -> t.team == player.team())); + + if(Groups.unit.count(u -> u.type == UnitTypes.poly && u.team == player.team()) >= 10){ + active10Polys.complete(); + } + + for(Building entity : player.team().cores()){ + if(!content.items().contains(i -> entity.items.get(i) < entity.block.itemCapacity)){ + fillCoreAllCampaign.complete(); + break; + } + } + } + } + + private void save(){ + Core.settings.putJson("units-built" , String.class, unitsBuilt); + Core.settings.putJson("blocks-built" , String.class, blocksBuilt); + } + + private void trigger(Trigger trigger, Achievement ach){ + Events.run(trigger, () -> { + if(campaign()){ + ach.complete(); + } + }); + } + + private boolean campaign(){ + return Vars.state.isCampaign(); + } +} diff --git a/desktop/src/mindustry/desktop/steam/SStat.java b/core/src/mindustry/service/SStat.java similarity index 76% rename from desktop/src/mindustry/desktop/steam/SStat.java rename to core/src/mindustry/service/SStat.java index 0086f68785..d099b83bda 100644 --- a/desktop/src/mindustry/desktop/steam/SStat.java +++ b/core/src/mindustry/service/SStat.java @@ -1,4 +1,6 @@ -package mindustry.desktop.steam; +package mindustry.service; + +import static mindustry.Vars.*; public enum SStat{ unitsDestroyed, @@ -23,7 +25,7 @@ public enum SStat{ ; public int get(){ - return SVars.stats.stats.getStatI(name(), 0); + return service.getStat(name(), 0); } public void max(int amount){ @@ -33,10 +35,10 @@ public enum SStat{ } public void set(int amount){ - SVars.stats.stats.setStatI(name(), amount); - SVars.stats.onUpdate(); + service.setStat(name(), amount); + service.storeStats(); - for(SAchievement a : SAchievement.all){ + for(Achievement a : Achievement.all){ a.checkCompletion(); } } diff --git a/core/src/mindustry/type/Item.java b/core/src/mindustry/type/Item.java index 24dd790d37..b4fbc2cdd5 100644 --- a/core/src/mindustry/type/Item.java +++ b/core/src/mindustry/type/Item.java @@ -13,9 +13,9 @@ public class Item extends UnlockableContent{ /** how explosive this item is. */ public float explosiveness = 0f; - /** flammability above 0.3 makes this eleigible for item burners. */ + /** flammability above 0.3 makes this eligible for item burners. */ public float flammability = 0f; - /** how radioactive this item is. 0=none, 1=chernobyl ground zero */ + /** how radioactive this item is. */ public float radioactivity; /** how electrically potent this item is. */ public float charge = 0f; @@ -58,6 +58,6 @@ public class Item extends UnlockableContent{ /** Allocates a new array containing all items that generate ores. */ public static Seq getAllOres(){ - return content.blocks().select(b -> b instanceof OreBlock).map(b -> ((Floor)b).itemDrop); + return content.blocks().select(b -> b instanceof OreBlock).map(b -> b.itemDrop); } } diff --git a/core/src/mindustry/type/ItemStack.java b/core/src/mindustry/type/ItemStack.java index 26cffe97b4..ec15207760 100644 --- a/core/src/mindustry/type/ItemStack.java +++ b/core/src/mindustry/type/ItemStack.java @@ -37,7 +37,7 @@ public class ItemStack implements Comparable{ } public static ItemStack[] mult(ItemStack[] stacks, float amount){ - ItemStack[] copy = new ItemStack[stacks.length]; + var copy = new ItemStack[stacks.length]; for(int i = 0; i < copy.length; i++){ copy[i] = new ItemStack(stacks[i].item, Mathf.round(stacks[i].amount * amount)); } @@ -45,7 +45,7 @@ public class ItemStack implements Comparable{ } public static ItemStack[] with(Object... items){ - ItemStack[] stacks = new ItemStack[items.length / 2]; + var stacks = new ItemStack[items.length / 2]; for(int i = 0; i < items.length; i += 2){ stacks[i / 2] = new ItemStack((Item)items[i], ((Number)items[i + 1]).intValue()); } diff --git a/core/src/mindustry/type/Planet.java b/core/src/mindustry/type/Planet.java index 40d571eec7..60ef7f0d9b 100644 --- a/core/src/mindustry/type/Planet.java +++ b/core/src/mindustry/type/Planet.java @@ -18,7 +18,7 @@ import static mindustry.Vars.*; public class Planet extends UnlockableContent{ /** Default spacing between planet orbits in world units. */ - private static final float orbitSpacing = 9f; + private static final float orbitSpacing = 11f; /** intersect() temp var. */ private static final Vec3 intersectResult = new Vec3(); /** Mesh used for rendering. Created on load() - will be null on the server! */ @@ -152,7 +152,7 @@ public class Planet extends UnlockableContent{ public float getRotation(){ //tidally locked planets always face toward parents if(tidalLock){ - return getOrbitAngle(); + return -getOrbitAngle() + 90; } //random offset for more variability float offset = Mathf.randomSeed(id+1, 360); @@ -202,8 +202,18 @@ public class Planet extends UnlockableContent{ return mat.setToTranslation(position).rotate(Vec3.Y, getRotation()); } + /** Regenerates the planet mesh. For debugging only. */ + public void reloadMesh(){ + if(mesh != null){ + mesh.dispose(); + } + mesh = meshLoader.get(); + } + @Override public void load(){ + super.load(); + mesh = meshLoader.get(); } diff --git a/core/src/mindustry/type/Satellite.java b/core/src/mindustry/type/Satellite.java index a30a44c136..986d53a01a 100644 --- a/core/src/mindustry/type/Satellite.java +++ b/core/src/mindustry/type/Satellite.java @@ -1,8 +1,9 @@ package mindustry.type; +//TODO /** Any object that is orbiting a planet. */ public class Satellite{ - public Planet planet; + public transient Planet planet; public Satellite(Planet orbiting){ this.planet = orbiting; diff --git a/core/src/mindustry/type/Sector.java b/core/src/mindustry/type/Sector.java index dda4cc6a6c..7aea305426 100644 --- a/core/src/mindustry/type/Sector.java +++ b/core/src/mindustry/type/Sector.java @@ -123,7 +123,7 @@ public class Sector{ @Nullable public TextureRegion icon(){ - return info.icon == null ? null : Fonts.getLargeIcon(info.icon); + return info.contentIcon != null ? info.contentIcon.uiIcon : info.icon == null ? null : Fonts.getLargeIcon(info.icon); } public boolean isCaptured(){ @@ -148,7 +148,8 @@ public class Sector{ /** @return the sector size, in tiles */ public int getSize(){ - int res = (int)(rect.radius * 3200); + if(planet.generator == null) return 1; + int res = (int)(rect.radius * planet.generator.getSizeScl()); return res % 2 == 0 ? res : res + 1; } diff --git a/core/src/mindustry/type/SectorPreset.java b/core/src/mindustry/type/SectorPreset.java index 997e014c63..3aa2743dc2 100644 --- a/core/src/mindustry/type/SectorPreset.java +++ b/core/src/mindustry/type/SectorPreset.java @@ -1,12 +1,10 @@ package mindustry.type; import arc.func.*; -import arc.graphics.g2d.*; import mindustry.ctype.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.maps.generators.*; -import mindustry.ui.*; public class SectorPreset extends UnlockableContent{ public FileMapGenerator generator; @@ -18,6 +16,7 @@ public class SectorPreset extends UnlockableContent{ public boolean useAI = true; /** Difficulty, 0-10. */ public float difficulty; + public float startWaveTimeMultiplier = 2f; public boolean addStartingItems = false; public SectorPreset(String name, Planet planet, int sector){ @@ -32,8 +31,10 @@ public class SectorPreset extends UnlockableContent{ } @Override - public TextureRegion icon(Cicon c){ - return Icon.terrain.getRegion(); + public void loadIcon(){ + if(Icon.terrain != null){ + uiIcon = fullIcon = Icon.terrain.getRegion(); + } } @Override diff --git a/core/src/mindustry/type/StatusEffect.java b/core/src/mindustry/type/StatusEffect.java index c7d825c188..c173492ab6 100644 --- a/core/src/mindustry/type/StatusEffect.java +++ b/core/src/mindustry/type/StatusEffect.java @@ -10,7 +10,6 @@ import mindustry.ctype.*; import mindustry.entities.*; import mindustry.entities.units.*; import mindustry.gen.*; -import mindustry.ui.*; import mindustry.world.meta.*; public class StatusEffect extends UnlockableContent{ @@ -38,6 +37,8 @@ public class StatusEffect extends UnlockableContent{ public boolean permanent; /** If true, this effect will only react with other effects and cannot be applied. */ public boolean reactive; + /** Whether to show this effect in the database. */ + public boolean show = true; /** Tint color of effect. */ public Color color = Color.white.cpy(); /** Effect that happens randomly on top of the affected unit. */ @@ -47,8 +48,7 @@ public class StatusEffect extends UnlockableContent{ /** Called on init. */ protected Runnable initblock = () -> {}; - public ObjectSet affinities = new ObjectSet<>(); - public ObjectSet opposites = new ObjectSet<>(); + public ObjectSet affinities = new ObjectSet<>(), opposites = new ObjectSet<>(); public StatusEffect(String name){ super(name); @@ -65,7 +65,7 @@ public class StatusEffect extends UnlockableContent{ @Override public boolean isHidden(){ - return localizedName.equals(name); + return localizedName.equals(name) || !show; } @Override @@ -105,11 +105,6 @@ public class StatusEffect extends UnlockableContent{ } - @Override - public Cicon prefDatabaseIcon(){ - return Cicon.large; - } - @Override public boolean showUnlock(){ return false; @@ -131,7 +126,6 @@ public class StatusEffect extends UnlockableContent{ protected void trans(StatusEffect effect, TransitionHandler handler){ transitions.put(effect, handler); - effect.transitions.put(this, handler); } protected void affinity(StatusEffect effect, TransitionHandler handler){ @@ -141,20 +135,23 @@ public class StatusEffect extends UnlockableContent{ } protected void opposite(StatusEffect... effect){ - opposites.addAll(effect); - for(StatusEffect sup : effect){ - sup.opposites.add(this); - trans(sup, (unit, time, newTime, result) -> { - time -= newTime * 0.5f; - if(time > 0){ - result.set(this, time); - return; - } - result.set(sup, newTime); - }); + for(var other : effect){ + handleOpposite(other); + other.handleOpposite(this); } } + protected void handleOpposite(StatusEffect other){ + opposites.add(other); + trans(other, (unit, result, time) -> { + result.time -= time * 0.5f; + if(result.time <= 0f){ + result.time = time; + result.effect = other; + } + }); + } + public void draw(Unit unit){ } @@ -166,16 +163,16 @@ public class StatusEffect extends UnlockableContent{ /** * Called when transitioning between two status effects. * @param to The state to transition to - * @param time The current status effect time - * @param newTime The time that the new status effect will last + * @param time The applies status effect time + * @return whether a reaction occurred */ - public StatusEntry getTransition(Unit unit, StatusEffect to, float time, float newTime, StatusEntry result){ - if(transitions.containsKey(to)){ - transitions.get(to).handle(unit, time, newTime, result); - return result; + public boolean applyTransition(Unit unit, StatusEffect to, StatusEntry entry, float time){ + var trans = transitions.get(to); + if(trans != null){ + trans.handle(unit, entry, time); + return true; } - - return result.set(to, newTime); + return false; } @Override @@ -189,6 +186,6 @@ public class StatusEffect extends UnlockableContent{ } public interface TransitionHandler{ - void handle(Unit unit, float time, float newTime, StatusEntry result); + void handle(Unit unit, StatusEntry current, float time); } } diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index 713c9cdf52..14ced69a2d 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -5,13 +5,13 @@ import arc.audio.*; import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.graphics.g2d.TextureAtlas.*; import arc.math.*; import arc.math.geom.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; -import mindustry.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -23,6 +23,7 @@ import mindustry.entities.units.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.graphics.MultiPacker.*; import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; @@ -30,18 +31,26 @@ import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.units.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; +//TODO document public class UnitType extends UnlockableContent{ - public static final float shadowTX = -12, shadowTY = -13, outlineSpace = 0.01f; + public static final float shadowTX = -12, shadowTY = -13; private static final Vec2 legOffset = new Vec2(); /** If true, the unit is always at elevation 1. */ public boolean flying; public Prov constructor; public Prov defaultController = () -> !flying ? new GroundAI() : new FlyingAI(); + + /** Environmental flags that are *all* required for this unit to function. 0 = any environment */ + public int envRequired = 0; + /** The environment flags that this unit can function in. If the env matches any of these, it will be enabled. */ + public int envEnabled = Env.terrestrial; + /** The environment flags that this unit *cannot* function in. If the env matches any of these, it will be *disabled*. */ + public int envDisabled = 0; + public float speed = 1.1f, boostMultiplier = 1f, rotateSpeed = 5f, baseRotateSpeed = 5f; public float drag = 0.3f, accel = 0.5f, landShake = 0f, rippleScale = 1f, riseSpeed = 0.08f, fallSpeed = 0.018f; public float health = 200f, range = -1, miningRange = 70f, armor = 0f, maxRange = -1f; @@ -65,6 +74,10 @@ public class UnitType extends UnlockableContent{ public Seq abilities = new Seq<>(); public BlockFlag targetFlag = BlockFlag.generator; + public Color outlineColor = Pal.darkerMetal; + public int outlineRadius = 3; + public boolean outlines = true; + public int legCount = 4, legGroupSize = 2; public float legLength = 10f, legSpeed = 0.1f, legTrns = 1f, legBaseOffset = 0f, legMoveSpace = 1f, legExtension = 0, legPairOffset = 0, legLengthScl = 1f, kinematicScl = 1f, maxStretch = 1.75f; public float legSplashDamage = 0f, legSplashRange = 5; @@ -95,13 +108,14 @@ public class UnitType extends UnlockableContent{ public float strafePenalty = 0.5f; public float hitSize = 6f; public float itemOffsetY = 3f; - public float lightRadius = 60f, lightOpacity = 0.6f; + public float lightRadius = -1f, lightOpacity = 0.6f; public Color lightColor = Pal.powerLight; public boolean drawCell = true, drawItems = true, drawShields = true; public int trailLength = 3; public float trailX = 4f, trailY = -3f, trailScl = 1f; /** Whether the unit can heal blocks. Initialized in init() */ public boolean canHeal = false; + /** If true, all weapons will attack the same target. */ public boolean singleTarget = false; public ObjectSet immunities = new ObjectSet<>(); @@ -117,7 +131,7 @@ public class UnitType extends UnlockableContent{ public UnitType(String name){ super(name); - constructor = EntityMapping.map(name); + constructor = EntityMapping.map(this.name); } public UnitController createController(){ @@ -166,7 +180,7 @@ public class UnitType extends UnlockableContent{ public void display(Unit unit, Table table){ table.table(t -> { t.left(); - t.add(new Image(icon(Cicon.medium))).size(8 * 4).scaling(Scaling.fit); + t.add(new Image(uiIcon)).size(iconMed).scaling(Scaling.fit); t.labelWrap(localizedName).left().width(190f).padLeft(5); }).growX().left(); table.row(); @@ -210,6 +224,11 @@ public class UnitType extends UnlockableContent{ table.row(); } + /** @return whether this block supports a specific environment. */ + public boolean supportsEnv(int env){ + return (envEnabled & env) != 0 && (envDisabled & env) == 0 && (envRequired == 0 || (envRequired & env) == envRequired); + } + @Override public void getDependencies(Cons cons){ //units require reconstructors being researched @@ -236,6 +255,7 @@ public class UnitType extends UnlockableContent{ stats.add(Stat.health, health); stats.add(Stat.armor, armor); stats.add(Stat.speed, speed); + stats.add(Stat.size, hitSize / tilesize, StatUnit.blocksSquared); stats.add(Stat.itemCapacity, itemCapacity); stats.add(Stat.range, (int)(maxRange / tilesize), StatUnit.blocks); stats.add(Stat.commandLimit, commandLimit); @@ -258,7 +278,7 @@ public class UnitType extends UnlockableContent{ if(mineTier >= 1){ stats.addPercent(Stat.mineSpeed, mineSpeed); - stats.add(Stat.mineTier, new BlockFilterValue(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= mineTier && !f.playerUnmineable)); + stats.add(Stat.mineTier, StatValues.blocks(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= mineTier && !f.playerUnmineable)); } if(buildSpeed > 0){ stats.addPercent(Stat.buildSpeed, buildSpeed); @@ -268,7 +288,7 @@ public class UnitType extends UnlockableContent{ } if(weapons.any()){ - stats.add(Stat.weapons, new WeaponListValue(this, weapons)); + stats.add(Stat.weapons, StatValues.weapons(this, weapons)); } } @@ -286,27 +306,34 @@ public class UnitType extends UnlockableContent{ immunities.add(StatusEffects.wet); } + if(lightRadius == -1){ + lightRadius = Math.max(60f, hitSize * 2.3f); + } + + clipSize = Math.max(clipSize, lightRadius * 1.1f); singleTarget = weapons.size <= 1; if(itemCapacity < 0){ itemCapacity = Math.max(Mathf.round((int)(hitSize * 4.3), 10), 10); } + //assume slight range margin + float margin = 4f; + //set up default range if(range < 0){ range = Float.MAX_VALUE; for(Weapon weapon : weapons){ - range = Math.min(range, weapon.bullet.range() + hitSize / 2f); - maxRange = Math.max(maxRange, weapon.bullet.range() + hitSize / 2f); + range = Math.min(range, weapon.bullet.range() - margin); + maxRange = Math.max(maxRange, weapon.bullet.range() - margin); } } if(maxRange < 0){ - maxRange = 0f; - maxRange = Math.max(maxRange, range); + maxRange = Math.max(0f, range); for(Weapon weapon : weapons){ - maxRange = Math.max(maxRange, weapon.bullet.range() + hitSize / 2f); + maxRange = Math.max(maxRange, weapon.bullet.range() - margin); } } @@ -363,7 +390,7 @@ public class UnitType extends UnlockableContent{ //calculate estimated DPS for one target based on weapons if(dpsEstimate < 0){ - dpsEstimate = weapons.sumf(w -> (w.bullet.estimateDPS() / w.reload) * w.shots * 60f); + dpsEstimate = weapons.sumf(Weapon::dps); //suicide enemy if(weapons.contains(w -> w.bullet.killShooter)){ @@ -376,6 +403,8 @@ public class UnitType extends UnlockableContent{ @CallSuper @Override public void load(){ + super.load(); + weapons.each(Weapon::load); region = Core.atlas.find(name); legRegion = Core.atlas.find(name + "-leg"); @@ -387,12 +416,43 @@ public class UnitType extends UnlockableContent{ cellRegion = Core.atlas.find(name + "-cell", Core.atlas.find("power-cell")); softShadowRegion = Core.atlas.find("circle-shadow"); outlineRegion = Core.atlas.find(name + "-outline"); - shadowRegion = icon(Cicon.full); + shadowRegion = fullIcon; wreckRegions = new TextureRegion[3]; for(int i = 0; i < wreckRegions.length; i++){ wreckRegions[i] = Core.atlas.find(name + "-wreck" + i); } + + clipSize = Math.max(region.width * 2f, clipSize); + } + + private void makeOutline(MultiPacker packer, TextureRegion region){ + if(region instanceof AtlasRegion at && region.found()){ + String name = at.name; + if(!packer.has(name + "-outline")){ + PixmapRegion base = Core.atlas.getPixmap(region); + var result = Pixmaps.outline(base, outlineColor, outlineRadius); + if(Core.settings.getBool("linear")){ + Pixmaps.bleed(result); + } + packer.add(PageType.main, name + "-outline", result); + } + } + } + + @Override + public void createIcons(MultiPacker packer){ + super.createIcons(packer); + + //currently does not create outlines for legs or base regions due to older mods having them outlined by default + if(outlines){ + makeOutline(packer, region); + for(Weapon weapon : weapons){ + if(!weapon.name.isEmpty()){ + makeOutline(packer, weapon.region); + } + } + } } @Override @@ -475,11 +535,10 @@ public class UnitType extends UnlockableContent{ drawSoftShadow(unit); - Draw.z(z - outlineSpace); + Draw.z(z); drawOutline(unit); - - Draw.z(z); + drawWeaponOutlines(unit); if(engineSize > 0) drawEngine(unit); drawBody(unit); if(drawCell) drawCell(unit); @@ -516,7 +575,10 @@ public class UnitType extends UnlockableContent{ public void drawShield(Unit unit){ float alpha = unit.shieldAlpha(); float radius = unit.hitSize() * 1.3f; - Fill.light(unit.x, unit.y, Lines.circleVertices(radius), radius, Tmp.c1.set(Pal.shieldIn), Tmp.c2.set(Pal.shield).lerp(Color.white, Mathf.clamp(unit.hitTime() / 2f)).a(Pal.shield.a * alpha)); + Fill.light(unit.x, unit.y, Lines.circleVertices(radius), radius, + Color.clear, + Tmp.c2.set(unit.team.color).lerp(Color.white, Mathf.clamp(unit.hitTime() / 2f)).a(0.7f * alpha) + ); } public void drawControl(Unit unit){ @@ -539,7 +601,7 @@ public class UnitType extends UnlockableContent{ Draw.color(0, 0, 0, 0.4f); float rad = 1.6f; float size = Math.max(region.width, region.height) * Draw.scl; - Draw.rect(softShadowRegion, unit, size * rad, size * rad); + Draw.rect(softShadowRegion, unit, size * rad * Draw.xscl, size * rad * Draw.yscl); Draw.color(); } @@ -551,7 +613,7 @@ public class UnitType extends UnlockableContent{ float size = (itemSize + Mathf.absin(Time.time, 5f, 1f)) * unit.itemTime; Draw.mixcol(Pal.accent, Mathf.absin(Time.time, 5f, 0.5f)); - Draw.rect(unit.item().icon(Cicon.medium), + Draw.rect(unit.item().fullIcon, unit.x + Angles.trnsx(unit.rotation + 180f, itemOffsetY), unit.y + Angles.trnsy(unit.rotation + 180f, itemOffsetY), size, size, unit.rotation); @@ -606,58 +668,38 @@ public class UnitType extends UnlockableContent{ applyColor(unit); for(WeaponMount mount : unit.mounts){ - Weapon weapon = mount.weapon; + mount.weapon.draw(unit, mount); + } - float rotation = unit.rotation - 90; - float weaponRotation = rotation + (weapon.rotate ? mount.rotation : 0); - float recoil = -((mount.reload) / weapon.reload * weapon.recoil); - float wx = unit.x + Angles.trnsx(rotation, weapon.x, weapon.y) + Angles.trnsx(weaponRotation, 0, recoil), - wy = unit.y + Angles.trnsy(rotation, weapon.x, weapon.y) + Angles.trnsy(weaponRotation, 0, recoil); + Draw.reset(); + } - if(weapon.shadow > 0){ - Drawf.shadow(wx, wy, weapon.shadow); - } + public void drawWeaponOutlines(Unit unit){ + applyColor(unit); + applyOutlineColor(unit); - if(weapon.outlineRegion.found()){ - float z = Draw.z(); - if(!weapon.top) Draw.z(z - outlineSpace); - - Draw.rect(weapon.outlineRegion, - wx, wy, - weapon.outlineRegion.width * Draw.scl * -Mathf.sign(weapon.flipSprite), - weapon.region.height * Draw.scl, - weaponRotation); - - Draw.z(z); - } - - Draw.rect(weapon.region, - wx, wy, - weapon.region.width * Draw.scl * -Mathf.sign(weapon.flipSprite), - weapon.region.height * Draw.scl, - weaponRotation); - - if(weapon.heatRegion.found() && mount.heat > 0){ - Draw.color(weapon.heatColor, mount.heat); - Draw.blend(Blending.additive); - Draw.rect(weapon.heatRegion, - wx, wy, - weapon.heatRegion.width * Draw.scl * -Mathf.sign(weapon.flipSprite), - weapon.heatRegion.height * Draw.scl, - weaponRotation); - Draw.blend(); - Draw.color(); + for(WeaponMount mount : unit.mounts){ + if(!mount.weapon.top){ + mount.weapon.drawOutline(unit, mount); } } Draw.reset(); } + public void applyOutlineColor(Unit unit){ + if(unit.isBoss()){ + Draw.mixcol(unit.team.color, Mathf.absin(7f, 1f)); + } + } + public void drawOutline(Unit unit){ Draw.reset(); if(Core.atlas.isFound(outlineRegion)){ + applyOutlineColor(unit); Draw.rect(outlineRegion, unit.x, unit.y, unit.rotation - 90); + Draw.reset(); } } @@ -678,7 +720,8 @@ public class UnitType extends UnlockableContent{ } public Color cellColor(Unit unit){ - return Tmp.c1.set(Color.black).lerp(unit.team.color, unit.healthf() + Mathf.absin(Time.time, Math.max(unit.healthf() * 5f, 1f), 1f - unit.healthf())); + float f = Mathf.clamp(unit.healthf()); + return Tmp.c1.set(Color.black).lerp(unit.team.color, f + Mathf.absin(Time.time, Math.max(f * 5f, 1f), 1f - f)); } public void drawLight(Unit unit){ @@ -786,7 +829,9 @@ public class UnitType extends UnlockableContent{ public void applyColor(Unit unit){ Draw.color(); - Draw.mixcol(Color.white, unit.hitTime); + Tmp.c1.set(Color.white).lerp(Pal.heal, Mathf.clamp(unit.healTime - unit.hitTime)); + Draw.mixcol(Tmp.c1, Math.max(unit.hitTime, Mathf.clamp(unit.healTime))); + if(unit.drownTime > 0 && unit.floorOn().isDeep()){ Draw.mixcol(unit.floorOn().mapColor, unit.drownTime * 0.8f); } diff --git a/core/src/mindustry/type/Weapon.java b/core/src/mindustry/type/Weapon.java index af5f63050b..1389bd238a 100644 --- a/core/src/mindustry/type/Weapon.java +++ b/core/src/mindustry/type/Weapon.java @@ -2,21 +2,34 @@ package mindustry.type; import arc.*; import arc.audio.*; +import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import mindustry.audio.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.entities.bullet.*; +import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.graphics.*; +import static mindustry.Vars.*; + public class Weapon implements Cloneable{ + /** temporary weapon sequence number */ + static int sequenceNum = 0; + /** displayed weapon region */ public String name = ""; /** bullet shot */ - public BulletType bullet; + public BulletType bullet = Bullets.standardCopper; /** shell ejection effect */ public Effect ejectEffect = Fx.none; + /** whether to consume ammo when ammo is enabled in rules */ + public boolean useAmmo = true; /** whether to create a flipped copy of this weapon upon initialization. default: true */ public boolean mirror = true; /** whether to flip the weapon's sprite when rendering */ @@ -29,6 +42,14 @@ public class Weapon implements Cloneable{ public boolean top = true; /** whether to hold the bullet in place while firing */ public boolean continuous; + /** whether this weapon can be aimed manually by players */ + public boolean controllable = true; + /** whether to automatically target relevant units in update(); only works when controllable = false. */ + public boolean autoTarget = false; + /** whether to perform target trajectory prediction */ + public boolean predictTarget = true; + /** ticks to wait in-between targets */ + public float targetInterval = 40f, targetSwitchInterval = 70f; /** rotation speed of weapon when rotation is enabled, in degrees/t*/ public float rotateSpeed = 20f; /** weapon reload in frames */ @@ -85,6 +106,8 @@ public class Weapon implements Cloneable{ public Color heatColor = Pal.turretHeat; /** status effect applied when shooting */ public StatusEffect shootStatus = StatusEffects.none; + /** type of weapon mount to be used */ + public Func mountType = WeaponMount::new; /** status effect duration when shot */ public float shootStatusDuration = 60f * 5f; @@ -96,6 +119,238 @@ public class Weapon implements Cloneable{ this(""); } + public float dps(){ + return (bullet.estimateDPS() / reload) * shots * 60f; + } + + //TODO copy-pasted code + public void drawOutline(Unit unit, WeaponMount mount){ + float + rotation = unit.rotation - 90, + weaponRotation = rotation + (rotate ? mount.rotation : 0), + recoil = -((mount.reload) / reload * this.recoil), + wx = unit.x + Angles.trnsx(rotation, x, y) + Angles.trnsx(weaponRotation, 0, recoil), + wy = unit.y + Angles.trnsy(rotation, x, y) + Angles.trnsy(weaponRotation, 0, recoil); + + if(outlineRegion.found()){ + Draw.rect(outlineRegion, + wx, wy, + outlineRegion.width * Draw.scl * -Mathf.sign(flipSprite), + region.height * Draw.scl, + weaponRotation); + } + } + + public void draw(Unit unit, WeaponMount mount){ + float + rotation = unit.rotation - 90, + weaponRotation = rotation + (rotate ? mount.rotation : 0), + recoil = -((mount.reload) / reload * this.recoil), + wx = unit.x + Angles.trnsx(rotation, x, y) + Angles.trnsx(weaponRotation, 0, recoil), + wy = unit.y + Angles.trnsy(rotation, x, y) + Angles.trnsy(weaponRotation, 0, recoil); + + if(shadow > 0){ + Drawf.shadow(wx, wy, shadow); + } + + if(outlineRegion.found() && top){ + Draw.rect(outlineRegion, + wx, wy, + outlineRegion.width * Draw.scl * -Mathf.sign(flipSprite), + region.height * Draw.scl, + weaponRotation); + } + + Draw.rect(region, + wx, wy, + region.width * Draw.scl * -Mathf.sign(flipSprite), + region.height * Draw.scl, + weaponRotation); + + if(heatRegion.found() && mount.heat > 0){ + Draw.color(heatColor, mount.heat); + Draw.blend(Blending.additive); + Draw.rect(heatRegion, + wx, wy, + heatRegion.width * Draw.scl * -Mathf.sign(flipSprite), + heatRegion.height * Draw.scl, + weaponRotation); + Draw.blend(); + Draw.color(); + } + } + + public void update(Unit unit, WeaponMount mount){ + boolean can = unit.canShoot(); + mount.reload = Math.max(mount.reload - Time.delta * unit.reloadMultiplier, 0); + + float + weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : 0), + mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), + mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y), + bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX, this.shootY), + bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX, this.shootY), + shootAngle = rotate ? weaponRotation + 90 : Angles.angle(bulletX, bulletY, mount.aimX, mount.aimY) + (unit.rotation - unit.angleTo(mount.aimX, mount.aimY)); + + //find a new target + if(!controllable && autoTarget){ + if((mount.retarget -= Time.delta) <= 0f){ + mount.target = findTarget(unit, mountX, mountY, bullet.range(), bullet.collidesAir, bullet.collidesGround); + mount.retarget = mount.target == null ? targetInterval : targetSwitchInterval; + } + + if(mount.target != null && checkTarget(unit, mount.target, mountX, mountY, bullet.range())){ + mount.target = null; + } + + boolean shoot = false; + + if(mount.target != null){ + shoot = mount.target.within(mountX, mountY, bullet.range() + Math.abs(shootY) + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && can; + + if(predictTarget){ + Vec2 to = Predict.intercept(unit, mount.target, bullet.speed); + mount.aimX = to.x; + mount.aimY = to.y; + }else{ + mount.aimX = mount.target.x(); + mount.aimY = mount.target.y(); + } + } + + mount.shoot = mount.rotate = shoot; + + //note that shooting state is not affected, as these cannot be controlled + //logic will return shooting as false even if these return true, which is fine + } + + //update continuous state + if(continuous && mount.bullet != null){ + if(!mount.bullet.isAdded() || mount.bullet.time >= mount.bullet.lifetime || mount.bullet.type != bullet){ + mount.bullet = null; + }else{ + mount.bullet.rotation(weaponRotation + 90); + mount.bullet.set(bulletX, bulletY); + mount.reload = reload; + unit.vel.add(Tmp.v1.trns(unit.rotation + 180f, mount.bullet.type.recoil)); + if(shootSound != Sounds.none && !headless){ + if(mount.sound == null) mount.sound = new SoundLoop(shootSound, 1f); + mount.sound.update(x, y, true); + } + } + }else{ + //heat decreases when not firing + mount.heat = Math.max(mount.heat - Time.delta * unit.reloadMultiplier / mount.weapon.cooldownTime, 0); + + if(mount.sound != null){ + mount.sound.update(bulletX, bulletY, false); + } + } + + //flip weapon shoot side for alternating weapons at half reload + if(otherSide != -1 && alternate && mount.side == flipSprite && + mount.reload + Time.delta * unit.reloadMultiplier > reload/2f && mount.reload <= reload/2f){ + unit.mounts[otherSide].side = !unit.mounts[otherSide].side; + mount.side = !mount.side; + } + + //rotate if applicable + if(rotate && (mount.rotate || mount.shoot) && can){ + float axisX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), + axisY = unit.y + Angles.trnsy(unit.rotation - 90, x, y); + + mount.targetRotation = Angles.angle(axisX, axisY, mount.aimX, mount.aimY) - unit.rotation; + mount.rotation = Angles.moveToward(mount.rotation, mount.targetRotation, rotateSpeed * Time.delta); + }else if(!rotate){ + mount.rotation = 0; + mount.targetRotation = unit.angleTo(mount.aimX, mount.aimY); + } + + //shoot if applicable + if(mount.shoot && //must be shooting + can && //must be able to shoot + (!useAmmo || unit.ammo > 0 || !state.rules.unitAmmo || unit.team.rules().infiniteAmmo) && //check ammo + (!alternate || mount.side == flipSprite) && + //TODO checking for velocity this way isn't entirely correct + (unit.vel.len() >= mount.weapon.minShootVelocity || (net.active() && !unit.isLocal())) && //check velocity requirements + mount.reload <= 0.0001f && //reload has to be 0 + Angles.within(rotate ? mount.rotation : unit.rotation, mount.targetRotation, mount.weapon.shootCone) //has to be within the cone + ){ + shoot(unit, mount, bulletX, bulletY, mount.aimX, mount.aimY, mountX, mountY, shootAngle, Mathf.sign(x)); + + mount.reload = reload; + + if(useAmmo){ + unit.ammo--; + if(unit.ammo < 0) unit.ammo = 0; + } + } + } + + protected Teamc findTarget(Unit unit, float x, float y, float range, boolean air, boolean ground){ + return Units.closestTarget(unit.team, x, y, range + Math.abs(shootY), u -> u.checkTarget(air, ground), t -> ground); + } + + protected boolean checkTarget(Unit unit, Teamc target, float x, float y, float range){ + return Units.invalidateTarget(target, unit.team, x, y, range + Math.abs(shootY)); + } + + protected void shoot(Unit unit, WeaponMount mount, float shootX, float shootY, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ + float baseX = unit.x, baseY = unit.y; + boolean delay = firstShotDelay + shotDelay > 0f; + + (delay ? chargeSound : continuous ? Sounds.none : shootSound).at(shootX, shootY, Mathf.random(soundPitchMin, soundPitchMax)); + + BulletType ammo = bullet; + float lifeScl = ammo.scaleVelocity ? Mathf.clamp(Mathf.dst(shootX, shootY, aimX, aimY) / ammo.range()) : 1f; + + sequenceNum = 0; + if(delay){ + Angles.shotgun(shots, spacing, rotation, f -> { + Time.run(sequenceNum * shotDelay + firstShotDelay, () -> { + if(!unit.isAdded()) return; + mount.bullet = bullet(unit, shootX + unit.x - baseX, shootY + unit.y - baseY, f + Mathf.range(inaccuracy), lifeScl); + }); + sequenceNum++; + }); + }else{ + Angles.shotgun(shots, spacing, rotation, f -> mount.bullet = bullet(unit, shootX, shootY, f + Mathf.range(inaccuracy), lifeScl)); + } + + boolean parentize = ammo.keepVelocity; + + if(delay){ + Time.run(firstShotDelay, () -> { + if(!unit.isAdded()) return; + + unit.vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); + Effect.shake(shake, shake, shootX, shootY); + mount.heat = 1f; + if(!continuous){ + shootSound.at(shootX, shootY, Mathf.random(soundPitchMin, soundPitchMax)); + } + }); + }else{ + unit.vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); + Effect.shake(shake, shake, shootX, shootY); + mount.heat = 1f; + } + + ejectEffect.at(mountX, mountY, rotation * side); + ammo.shootEffect.at(shootX, shootY, rotation, parentize ? unit : null); + ammo.smokeEffect.at(shootX, shootY, rotation, parentize ? unit : null); + unit.apply(shootStatus, shootStatusDuration); + } + + protected Bullet bullet(Unit unit, float shootX, float shootY, float angle, float lifescl){ + float xr = Mathf.range(xRand); + + return bullet.create(unit, unit.team, + shootX + Angles.trnsx(angle, 0, xr), + shootY + Angles.trnsy(angle, 0, xr), + angle, (1f - velocityRnd) + Mathf.random(velocityRnd), lifescl); + } + public Weapon copy(){ try{ return (Weapon)clone(); diff --git a/core/src/mindustry/type/Weather.java b/core/src/mindustry/type/Weather.java index e30b6ab0e8..fe4c0d3aa6 100644 --- a/core/src/mindustry/type/Weather.java +++ b/core/src/mindustry/type/Weather.java @@ -20,6 +20,9 @@ import mindustry.world.blocks.*; import static mindustry.Vars.*; public class Weather extends UnlockableContent{ + /** Global random variable used for rendering. */ + public static final Rand rand = new Rand(); + /** Default duration of this weather event in ticks. */ public float duration = 10f * Time.toMinutes; public float opacityMultiplier = 1f; @@ -29,7 +32,6 @@ public class Weather extends UnlockableContent{ public float soundVolOscMag = 0f, soundVolOscScl = 20f; //internals - public Rand rand = new Rand(); public Prov type = WeatherState::create; public StatusEffect status = StatusEffects.none; public float statusDuration = 60f * 2; @@ -108,7 +110,7 @@ public class Weather extends UnlockableContent{ } - public void drawParticles(TextureRegion region, Color color, + public static void drawParticles(TextureRegion region, Color color, float sizeMin, float sizeMax, float density, float intensity, float opacity, float windx, float windy, @@ -147,7 +149,8 @@ public class Weather extends UnlockableContent{ } } - public void drawRain(float sizeMin, float sizeMax, float xspeed, float yspeed, float density, float intensity, float stroke, Color color){ + public static void drawRain(float sizeMin, float sizeMax, float xspeed, float yspeed, float density, float intensity, float stroke, Color color){ + rand.setSeed(0); float padding = sizeMax*0.9f; Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale()); @@ -180,12 +183,13 @@ public class Weather extends UnlockableContent{ } } - public void drawSplashes(TextureRegion[] splashes, float padding, float density, float intensity, float opacity, float timeScale, float stroke, Color color, Liquid splasher){ + public static void drawSplashes(TextureRegion[] splashes, float padding, float density, float intensity, float opacity, float timeScale, float stroke, Color color, Liquid splasher){ Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale()); Tmp.r1.grow(padding); Core.camera.bounds(Tmp.r2); int total = (int)(Tmp.r1.area() / density * intensity) / 2; Lines.stroke(stroke); + rand.setSeed(0); float t = Time.time / timeScale; @@ -226,7 +230,21 @@ public class Weather extends UnlockableContent{ } } - public void drawNoise(Texture noise, Color color, float noisescl, float opacity, float baseSpeed, float intensity, float vwindx, float vwindy, float offset){ + public static void drawNoiseLayers(Texture noise, Color color, float noisescl, float opacity, float baseSpeed, float intensity, float vwindx, float vwindy, + int layers, float layerSpeedM , float layerAlphaM, float layerSclM, float layerColorM){ + float sspeed = 1f, sscl = 1f, salpha = 1f, offset = 0f; + Color col = Tmp.c1.set(color); + for(int i = 0; i < layers; i++){ + drawNoise(noise, col, noisescl * sscl, salpha * opacity, sspeed * baseSpeed, intensity, vwindx, vwindy, offset); + sspeed *= layerSpeedM; + salpha *= layerAlphaM; + sscl *= layerSclM; + offset += 0.29f; + col.mul(layerColorM); + } + } + + public static void drawNoise(Texture noise, Color color, float noisescl, float opacity, float baseSpeed, float intensity, float vwindx, float vwindy, float offset){ Draw.alpha(opacity); Draw.tint(color); @@ -325,14 +343,12 @@ public class Weather extends UnlockableContent{ public void draw(){ if(renderer.weatherAlpha() > 0.0001f && renderer.drawWeather && Core.settings.getBool("showweather")){ Draw.draw(Layer.weather, () -> { - weather.rand.setSeed(0); Draw.alpha(renderer.weatherAlpha() * opacity * weather.opacityMultiplier); weather.drawOver(self()); Draw.reset(); }); Draw.draw(Layer.debris, () -> { - weather.rand.setSeed(0); Draw.alpha(renderer.weatherAlpha() * opacity * weather.opacityMultiplier); weather.drawUnder(self()); Draw.reset(); diff --git a/core/src/mindustry/type/weapons/PointDefenseWeapon.java b/core/src/mindustry/type/weapons/PointDefenseWeapon.java new file mode 100644 index 0000000000..7df83e1148 --- /dev/null +++ b/core/src/mindustry/type/weapons/PointDefenseWeapon.java @@ -0,0 +1,62 @@ +package mindustry.type.weapons; + +import arc.graphics.*; +import arc.math.*; +import arc.math.geom.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; + +/** + * Note that this requires several things: + * - A bullet with positive maxRange + * - A bullet with positive damage + * - Rotation + * */ +public class PointDefenseWeapon extends Weapon{ + public Color color = Color.white; + public Effect beamEffect = Fx.pointBeam; + + public PointDefenseWeapon(String name){ + super(name); + } + + public PointDefenseWeapon(){ + } + + { + predictTarget = false; + autoTarget = true; + controllable = false; + rotate = true; + useAmmo = false; + } + + @Override + protected Teamc findTarget(Unit unit, float x, float y, float range, boolean air, boolean ground){ + return Groups.bullet.intersect(x - range, y - range, range*2, range*2).min(b -> b.team != unit.team && b.type().hittable, b -> b.dst2(x, y)); + } + + @Override + protected boolean checkTarget(Unit unit, Teamc target, float x, float y, float range){ + return !(target.within(unit, range) && target.team() != unit.team && target instanceof Bullet bullet && bullet.type != null && bullet.type.hittable); + } + + @Override + protected void shoot(Unit unit, WeaponMount mount, float shootX, float shootY, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ + if(!(mount.target instanceof Bullet target)) return; + + if(target.damage() > bullet.damage){ + target.damage(target.damage() - bullet.damage); + }else{ + target.remove(); + } + + beamEffect.at(shootX, shootY, rotation, color, new Vec2().set(target)); + bullet.shootEffect.at(shootX, shootY, rotation, color); + bullet.hitEffect.at(target.x, target.y, color); + shootSound.at(shootX, shootY, Mathf.random(0.9f, 1.1f)); + } +} diff --git a/core/src/mindustry/type/weapons/RepairBeamWeapon.java b/core/src/mindustry/type/weapons/RepairBeamWeapon.java new file mode 100644 index 0000000000..c2938207f0 --- /dev/null +++ b/core/src/mindustry/type/weapons/RepairBeamWeapon.java @@ -0,0 +1,121 @@ +package mindustry.type.weapons; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.blocks.units.*; + +/** + * Note that this weapon requires a bullet with a positive maxRange. + * Rotation must be set to true. Fixed repair points are not supported. + * */ +public class RepairBeamWeapon extends Weapon{ + public boolean targetBuildings = false; + + public float repairSpeed = 0.3f; + public float beamWidth = 1f; + public float pulseRadius = 6f; + public float pulseStroke = 2f; + + public TextureRegion laser, laserEnd, laserTop, laserTopEnd; + + public Color laserColor = Color.valueOf("98ffa9"), laserTopColor = Color.white.cpy(); + + public RepairBeamWeapon(String name){ + super(name); + } + + public RepairBeamWeapon(){ + } + + { + //must be >0 to prevent various bugs + reload = 1f; + predictTarget = false; + autoTarget = true; + controllable = false; + rotate = true; + useAmmo = false; + mountType = HealBeamMount::new; + recoil = 0f; + } + + @Override + public float dps(){ + return 0f; + } + + @Override + public void load(){ + super.load(); + + laser = Core.atlas.find("laser-white"); + laserEnd = Core.atlas.find("laser-white-end"); + laserTop = Core.atlas.find("laser-top"); + laserTopEnd = Core.atlas.find("laser-top-end"); + } + + @Override + protected Teamc findTarget(Unit unit, float x, float y, float range, boolean air, boolean ground){ + var out = Units.closest(unit.team, x, y, range, u -> u != unit && u.damaged()); + if(out != null || !targetBuildings) return out; + //TODO maybe buildings shouldn't be allowed at all + return Units.findAllyTile(unit.team, x, y, range, Building::damaged); + } + + @Override + protected boolean checkTarget(Unit unit, Teamc target, float x, float y, float range){ + return !(target.within(unit, range + unit.hitSize/2f) && target.team() == unit.team && target instanceof Healthc u && u.damaged() && u.isValid()); + } + + @Override + protected void shoot(Unit unit, WeaponMount mount, float shootX, float shootY, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ + //does nothing, shooting is handled in update() + } + + @Override + public void update(Unit unit, WeaponMount mount){ + super.update(unit, mount); + + HealBeamMount heal = (HealBeamMount)mount; + heal.strength = Mathf.lerpDelta(heal.strength, Mathf.num(mount.target != null), 0.2f); + + if(mount.target instanceof Healthc u){ + u.heal(repairSpeed * heal.strength * Time.delta); + } + } + + @Override + public void draw(Unit unit, WeaponMount mount){ + super.draw(unit, mount); + + HealBeamMount heal = (HealBeamMount)mount; + + float + weaponRotation = unit.rotation - 90, + wx = unit.x + Angles.trnsx(weaponRotation, x, y), + wy = unit.y + Angles.trnsy(weaponRotation, x, y); + + float z = Draw.z(); + RepairPoint.drawBeam(wx, wy, unit.rotation + mount.rotation, shootY, unit.id, mount.target == null ? null : (Sized)mount.target, unit.team, heal.strength, + pulseStroke, pulseRadius, beamWidth, heal.lastEnd, heal.offset, laserColor, laserTopColor, + laser, laserEnd, laserTop, laserTopEnd); + Draw.z(z); + } + + public static class HealBeamMount extends WeaponMount{ + public Vec2 offset = new Vec2(), lastEnd = new Vec2(); + public float strength; + + public HealBeamMount(Weapon weapon){ + super(weapon); + } + } +} diff --git a/core/src/mindustry/type/weather/MagneticStorm.java b/core/src/mindustry/type/weather/MagneticStorm.java new file mode 100644 index 0000000000..c1d45b2529 --- /dev/null +++ b/core/src/mindustry/type/weather/MagneticStorm.java @@ -0,0 +1,11 @@ +package mindustry.type.weather; + +import mindustry.type.*; + +//TODO +public class MagneticStorm extends Weather{ + + public MagneticStorm(String name){ + super(name); + } +} diff --git a/core/src/mindustry/type/weather/SolarFlare.java b/core/src/mindustry/type/weather/SolarFlare.java new file mode 100644 index 0000000000..47694d0e65 --- /dev/null +++ b/core/src/mindustry/type/weather/SolarFlare.java @@ -0,0 +1,11 @@ +package mindustry.type.weather; + +import mindustry.type.*; + +//TODO +public class SolarFlare extends Weather{ + + public SolarFlare(String name){ + super(name); + } +} diff --git a/core/src/mindustry/ui/Bar.java b/core/src/mindustry/ui/Bar.java index 8de089041a..6bab3a146d 100644 --- a/core/src/mindustry/ui/Bar.java +++ b/core/src/mindustry/ui/Bar.java @@ -8,6 +8,7 @@ import arc.math.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.style.*; +import arc.scene.ui.layout.*; import arc.util.pooling.*; import mindustry.gen.*; @@ -15,9 +16,9 @@ public class Bar extends Element{ private static Rect scissor = new Rect(); private Floatp fraction; - private String name = ""; - private float value, lastValue, blink; - private Color blinkColor = new Color(); + private CharSequence name = ""; + private float value, lastValue, blink, outlineRadius; + private Color blinkColor = new Color(), outlineColor = new Color(); public Bar(String name, Color color, Floatp fraction){ this.fraction = fraction; @@ -27,7 +28,7 @@ public class Bar extends Element{ setColor(color); } - public Bar(Prov name, Prov color, Floatp fraction){ + public Bar(Prov name, Prov color, Floatp fraction){ this.fraction = fraction; try{ lastValue = value = Mathf.clamp(fraction.get()); @@ -61,6 +62,12 @@ public class Bar extends Element{ update(() -> this.name = name.get()); } + public Bar outline(Color color, float stroke){ + outlineColor.set(color); + outlineRadius = Scl.scl(stroke); + return this; + } + public Bar blink(Color color){ blinkColor.set(color); return this; @@ -94,6 +101,11 @@ public class Bar extends Element{ Drawable bar = Tex.bar; + if(outlineRadius > 0){ + Draw.color(outlineColor); + bar.draw(x - outlineRadius, y - outlineRadius, width + outlineRadius*2, height + outlineRadius*2); + } + Draw.colorl(0.1f); bar.draw(x, y, width, height); Draw.color(color, blinkColor, blink); diff --git a/core/src/mindustry/ui/Cicon.java b/core/src/mindustry/ui/Cicon.java index 2fc95294fa..846b7d136f 100644 --- a/core/src/mindustry/ui/Cicon.java +++ b/core/src/mindustry/ui/Cicon.java @@ -1,23 +1,12 @@ package mindustry.ui; -import java.util.*; - -/** Defines sizes of a content's preview icon. */ +/** Use content icon fields directly instead. This will be removed. */ +@Deprecated public enum Cicon{ - /** Full size. */ - full(0), - tiny(8 * 2), - small(8 * 3), - medium(8 * 4), - large(8 * 5), - xlarge(8 * 6); + tiny, small, medium, large, xlarge, full; + @Deprecated + public final int size = 32; public static final Cicon[] all = values(); - public static final Cicon[] scaled = Arrays.copyOfRange(all, 1, all.length); - - public final int size; - - Cicon(int size){ - this.size = size; - } + public static final Cicon[] scaled = values(); } diff --git a/core/src/mindustry/ui/CoreItemsDisplay.java b/core/src/mindustry/ui/CoreItemsDisplay.java index 319c0213fd..5bfcdc51ee 100644 --- a/core/src/mindustry/ui/CoreItemsDisplay.java +++ b/core/src/mindustry/ui/CoreItemsDisplay.java @@ -41,9 +41,9 @@ public class CoreItemsDisplay extends Table{ for(Item item : content.items()){ if(usedItems.contains(item)){ - image(item.icon(Cicon.small)).padRight(3); + image(item.uiIcon).size(iconSmall).padRight(3).tooltip(t -> t.background(Styles.black6).margin(4f).add(item.localizedName).style(Styles.outlineLabel)); //TODO leaks garbage - label(() -> core == null ? "0" : UI.formatAmount(core.items.get(item))).padRight(3).left(); + label(() -> core == null ? "0" : UI.formatAmount(core.items.get(item))).padRight(3).minWidth(52f).left(); if(++i % 4 == 0){ row(); diff --git a/core/src/mindustry/ui/Fonts.java b/core/src/mindustry/ui/Fonts.java index 6bd969fe4b..3c6c1b175c 100644 --- a/core/src/mindustry/ui/Fonts.java +++ b/core/src/mindustry/ui/Fonts.java @@ -3,14 +3,11 @@ package mindustry.ui; import arc.*; import arc.Graphics.Cursor.*; import arc.assets.*; -import arc.assets.loaders.*; -import arc.assets.loaders.resolvers.*; import arc.files.*; import arc.freetype.*; import arc.freetype.FreeTypeFontGenerator.*; import arc.freetype.FreetypeFontLoader.*; import arc.graphics.*; -import arc.graphics.Pixmap.*; import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.graphics.g2d.Font.*; @@ -56,6 +53,10 @@ public class Fonts{ return stringIcons.get(content, ""); } + public static boolean hasUnicodeStr(String content){ + return stringIcons.containsKey(content); + } + /** Called from a static context to make the cursor appear immediately upon startup.*/ public static void loadSystemCursors(){ SystemCursor.arrow.set(Core.graphics.newCursor("cursor", cursorScale())); @@ -160,10 +161,9 @@ public class Fonts{ public static void loadDefaultFont(){ int max = Gl.getInt(Gl.maxTextureSize); - UI.packer = new PixmapPacker(max >= 4096 ? 4096 : 2048, 2048, Format.rgba8888, 2, true); - FileHandleResolver resolver = new InternalFileHandleResolver(); - Core.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver)); - Core.assets.setLoader(Font.class, null, new FreetypeFontLoader(resolver){ + UI.packer = new PixmapPacker(max >= 4096 ? 4096 : 2048, 2048, 2, true); + Core.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(Core.files::internal)); + Core.assets.setLoader(Font.class, null, new FreetypeFontLoader(Core.files::internal){ ObjectSet scaled = new ObjectSet<>(); @Override @@ -197,7 +197,7 @@ public class Fonts{ size = 18; }})).loaded = f -> { Fonts.tech = (Font)f; - ((Font)f).getData().down *= 1.5f; + Fonts.tech.getData().down *= 1.5f; }; } @@ -214,13 +214,16 @@ public class Fonts{ for(AtlasRegion region : regions){ //get new pack rect page.setDirty(false); - Rect rect = UI.packer.pack(region.name + (region.splits != null ? ".9" : ""), atlas.getPixmap(region)); + Rect rect = UI.packer.pack(region.name, atlas.getPixmap(region), region.splits, region.pads); + //set new texture region.texture = UI.packer.getPages().first().getTexture(); //set its new position region.set((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height); //add old texture atlas.getTextures().add(region.texture); + //clear it + region.pixmapRegion = null; } //remove old texture, it will no longer be used diff --git a/core/src/mindustry/ui/ItemDisplay.java b/core/src/mindustry/ui/ItemDisplay.java index d0cedc5ba1..faf2f3cc27 100644 --- a/core/src/mindustry/ui/ItemDisplay.java +++ b/core/src/mindustry/ui/ItemDisplay.java @@ -1,8 +1,12 @@ package mindustry.ui; +import arc.graphics.*; import arc.scene.ui.layout.*; +import arc.util.*; import mindustry.type.*; +import mindustry.world.meta.*; +//TODO replace with static methods? /** An item image with text. */ public class ItemDisplay extends Table{ public final Item item; @@ -23,4 +27,14 @@ public class ItemDisplay extends Table{ public ItemDisplay(Item item, int amount){ this(item, amount, true); } + + /** Displays the item with a "/sec" qualifier based on the time period, in ticks. */ + public ItemDisplay(Item item, int amount, float timePeriod, boolean showName){ + add(new ItemImage(item.uiIcon, amount)); + add(Strings.autoFixed(amount / (timePeriod / 60f), 2) + StatUnit.perSecond.localized()).padLeft(2).padRight(5).color(Color.lightGray).style(Styles.outlineLabel); + if(showName) add(item.localizedName).padLeft(4 + amount > 99 ? 4 : 0); + + this.item = item; + this.amount = amount; + } } diff --git a/core/src/mindustry/ui/ItemImage.java b/core/src/mindustry/ui/ItemImage.java index 437360829e..e0a4ce0b17 100644 --- a/core/src/mindustry/ui/ItemImage.java +++ b/core/src/mindustry/ui/ItemImage.java @@ -33,7 +33,7 @@ public class ItemImage extends Stack{ add(new Table(o -> { o.left(); - o.add(new Image(stack.item.icon(Cicon.medium))).size(32f); + o.add(new Image(stack.item.uiIcon)).size(32f); })); if(stack.amount != 0){ diff --git a/core/src/mindustry/ui/ItemsDisplay.java b/core/src/mindustry/ui/ItemsDisplay.java index a6dec71bc7..375aaffe57 100644 --- a/core/src/mindustry/ui/ItemsDisplay.java +++ b/core/src/mindustry/ui/ItemsDisplay.java @@ -41,7 +41,7 @@ public class ItemsDisplay extends Table{ if(!items.has(item)) continue; Label label = t.add(UI.formatAmount(items.get(item))).left().get(); - t.image(item.icon(Cicon.small)).size(8 * 3).padLeft(4).padRight(4); + t.image(item.uiIcon).size(8 * 3).padLeft(4).padRight(4); t.add(item.localizedName).color(Color.lightGray).left(); t.row(); diff --git a/core/src/mindustry/ui/LiquidDisplay.java b/core/src/mindustry/ui/LiquidDisplay.java index 0a3d7b1716..416a865336 100644 --- a/core/src/mindustry/ui/LiquidDisplay.java +++ b/core/src/mindustry/ui/LiquidDisplay.java @@ -7,6 +7,8 @@ import arc.util.*; import mindustry.type.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + /** An ItemDisplay, but for liquids. */ public class LiquidDisplay extends Table{ public final Liquid liquid; @@ -19,14 +21,14 @@ public class LiquidDisplay extends Table{ this.perSecond = perSecond; add(new Stack(){{ - add(new Image(liquid.icon(Cicon.medium))); + add(new Image(liquid.uiIcon)); if(amount != 0){ Table t = new Table().left().bottom(); t.add(Strings.autoFixed(amount, 2)).style(Styles.outlineLabel); add(t); } - }}).size(8 * 4).padRight(3 + (amount != 0 && Strings.autoFixed(amount, 2).length() > 2 ? 8 : 0)); + }}).size(iconMed).padRight(3 + (amount != 0 && Strings.autoFixed(amount, 2).length() > 2 ? 8 : 0)); if(perSecond){ add(StatUnit.perSecond.localized()).padLeft(2).padRight(5).color(Color.lightGray).style(Styles.outlineLabel); diff --git a/core/src/mindustry/ui/Styles.java b/core/src/mindustry/ui/Styles.java index ff234bf4c4..90ffbaae89 100644 --- a/core/src/mindustry/ui/Styles.java +++ b/core/src/mindustry/ui/Styles.java @@ -9,15 +9,16 @@ import arc.scene.ui.Button.*; import arc.scene.ui.CheckBox.*; import arc.scene.ui.Dialog.*; import arc.scene.ui.ImageButton.*; -import arc.scene.ui.KeybindDialog.*; import arc.scene.ui.Label.*; import arc.scene.ui.ScrollPane.*; import arc.scene.ui.Slider.*; import arc.scene.ui.TextButton.*; import arc.scene.ui.TextField.*; +import arc.scene.ui.TreeElement.*; import mindustry.annotations.Annotations.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.ui.dialogs.*; import static mindustry.gen.Tex.*; @@ -28,13 +29,14 @@ public class Styles{ public static ButtonStyle defaultb, waveb, modsb; public static TextButtonStyle defaultt, squaret, nodet, cleart, discordt, nonet, infot, clearPartialt, clearTogglet, logicTogglet, 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; + public static ScrollPaneStyle defaultPane, horizontalPane, smallPane, nonePane; + public static KeybindDialog.KeybindDialogStyle defaultKeybindDialog; public static SliderStyle defaultSlider, vSlider; public static LabelStyle defaultLabel, outlineLabel, techLabel; public static TextFieldStyle defaultField, nodeField, areaField, nodeArea; public static CheckBoxStyle defaultCheck; public static DialogStyle defaultDialog, fullDialog; + public static TreeStyle defaultTree; public static void load(){ black = whiteui.tint(0f, 0f, 0f, 1f); @@ -214,6 +216,7 @@ public class Styles{ }}; emptyi = new ImageButtonStyle(){{ imageDownColor = Pal.accent; + imageOverColor = Color.lightGray; imageUpColor = Color.white; }}; emptytogglei = new ImageButtonStyle(){{ @@ -306,8 +309,9 @@ public class Styles{ vScroll = clear; vScrollKnob = scrollKnobVerticalThin; }}; + nonePane = new ScrollPaneStyle(); - defaultKeybindDialog = new KeybindDialogStyle(){{ + defaultKeybindDialog = new KeybindDialog.KeybindDialogStyle(){{ keyColor = Pal.accent; keyNameColor = Color.white; controllerColor = Color.lightGray; @@ -411,6 +415,13 @@ public class Styles{ background = windowEmpty; titleFontColor = Pal.accent; }}; + + defaultTree = new TreeStyle(){{ + plus = Icon.downOpen; + minus = Icon.upOpen; + background = black5; + over = flatOver; + }}; } private static Drawable createFlatDown(){ diff --git a/core/src/mindustry/ui/dialogs/BaseDialog.java b/core/src/mindustry/ui/dialogs/BaseDialog.java index b99b164ca4..42fb7ac7ff 100644 --- a/core/src/mindustry/ui/dialogs/BaseDialog.java +++ b/core/src/mindustry/ui/dialogs/BaseDialog.java @@ -4,7 +4,6 @@ import arc.*; import arc.scene.ui.*; import arc.util.*; import mindustry.core.GameState.*; -import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; @@ -44,12 +43,7 @@ public class BaseDialog extends Dialog{ } protected void onResize(Runnable run){ - Events.on(ResizeEvent.class, event -> { - if(isShown() && Core.scene.getDialog() == this){ - run.run(); - updateScrollFocus(); - } - }); + resized(run); } public void addCloseListener(){ diff --git a/core/src/mindustry/ui/dialogs/ContentInfoDialog.java b/core/src/mindustry/ui/dialogs/ContentInfoDialog.java index 1d03d360f3..82939448c3 100644 --- a/core/src/mindustry/ui/dialogs/ContentInfoDialog.java +++ b/core/src/mindustry/ui/dialogs/ContentInfoDialog.java @@ -8,6 +8,8 @@ import mindustry.ctype.*; import mindustry.graphics.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + public class ContentInfoDialog extends BaseDialog{ public ContentInfoDialog(){ @@ -26,9 +28,7 @@ public class ContentInfoDialog extends BaseDialog{ content.checkStats(); table.table(title1 -> { - var size = content.prefDatabaseIcon(); - - title1.image(content.icon(size)).size(size.size).scaling(Scaling.fit); + title1.image(content.uiIcon).size(iconXLarge).scaling(Scaling.fit); title1.add("[accent]" + content.localizedName).padLeft(5); }); diff --git a/core/src/mindustry/ui/dialogs/ControlsDialog.java b/core/src/mindustry/ui/dialogs/ControlsDialog.java deleted file mode 100644 index 400d5f086a..0000000000 --- a/core/src/mindustry/ui/dialogs/ControlsDialog.java +++ /dev/null @@ -1,26 +0,0 @@ -package mindustry.ui.dialogs; - -import arc.input.*; -import arc.scene.ui.*; -import arc.util.*; -import mindustry.gen.*; -import mindustry.graphics.*; - -public class ControlsDialog extends KeybindDialog{ - - public ControlsDialog(){ - setFillParent(true); - title.setAlignment(Align.center); - titleTable.row(); - titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent); - } - - @Override - public void addCloseButton(){ - buttons.button("@back", Icon.left, this::hide).size(210f, 64f); - - keyDown(key -> { - if(key == KeyCode.escape || key == KeyCode.back) hide(); - }); - } -} diff --git a/core/src/mindustry/ui/dialogs/CustomGameDialog.java b/core/src/mindustry/ui/dialogs/CustomGameDialog.java index efe5a157ea..1e9055a8e9 100644 --- a/core/src/mindustry/ui/dialogs/CustomGameDialog.java +++ b/core/src/mindustry/ui/dialogs/CustomGameDialog.java @@ -4,6 +4,7 @@ import arc.*; import arc.graphics.g2d.*; import arc.scene.style.*; import arc.scene.ui.*; +import arc.scene.ui.ImageButton.*; import arc.scene.ui.layout.*; import arc.util.*; import mindustry.*; @@ -31,14 +32,20 @@ public class CustomGameDialog extends BaseDialog{ cont.clear(); Table maps = new Table(); - maps.marginRight(14); - maps.marginBottom(55f); + maps.marginBottom(55f).marginRight(-20f); ScrollPane pane = new ScrollPane(maps); pane.setFadeScrollBars(false); int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(210)), 1); float images = 146f; + ImageButtonStyle style = new ImageButtonStyle(){{ + up = Styles.none; + down = Styles.flatOver; + over = Styles.flatOver; + disabled = Styles.none; + }}; + int i = 0; maps.defaults().width(170).fillY().top().pad(4f); for(Map map : Vars.maps.all()){ @@ -47,7 +54,7 @@ public class CustomGameDialog extends BaseDialog{ maps.row(); } - ImageButton image = new ImageButton(new TextureRegion(map.safeTexture()), Styles.cleari); + ImageButton image = new ImageButton(new TextureRegion(map.safeTexture()), style); image.margin(5); image.top(); @@ -86,6 +93,6 @@ public class CustomGameDialog extends BaseDialog{ maps.add("@maps.none").pad(50); } - cont.add(pane).uniformX(); + cont.add(pane).grow(); } } diff --git a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java index 94ea454d21..bf150bcd6a 100644 --- a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java +++ b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java @@ -72,7 +72,7 @@ public class CustomRulesDialog extends BaseDialog{ for(Block block : array){ t.table(Tex.underline, b -> { b.left().margin(4f); - b.image(block.icon(Cicon.medium)).size(Cicon.medium.size).padRight(3); + b.image(block.uiIcon).size(iconMed).padRight(3); b.add(block.localizedName).color(Color.lightGray).padLeft(3).growX().left().wrap(); b.button(Icon.cancel, Styles.clearPartiali, () -> { @@ -94,11 +94,11 @@ public class CustomRulesDialog extends BaseDialog{ int[] i = {0}; content.blocks().each(b -> !rules.bannedBlocks.contains(b) && b.canBeBuilt(), b -> { int cols = mobile && Core.graphics.isPortrait() ? 4 : 12; - t.button(new TextureRegionDrawable(b.icon(Cicon.medium)), Styles.cleari, () -> { + t.button(new TextureRegionDrawable(b.uiIcon), Styles.cleari, iconMed, () -> { rules.bannedBlocks.add(b); rebuildBanned(); dialog.hide(); - }).size(60f).get().resizeImage(Cicon.medium.size); + }).size(60f); if(++i[0] % cols == 0){ t.row(); @@ -149,7 +149,7 @@ public class CustomRulesDialog extends BaseDialog{ number("@rules.blockdamagemultiplier", f -> rules.blockDamageMultiplier = f, () -> rules.blockDamageMultiplier); main.button("@configure", - () -> loadoutDialog.show(Blocks.coreShard.itemCapacity, rules.loadout, + () -> loadoutDialog.show(999999, rules.loadout, i -> true, () -> rules.loadout.clear().add(new ItemStack(Items.copper, 100)), () -> {}, () -> {} @@ -169,6 +169,7 @@ public class CustomRulesDialog extends BaseDialog{ title("@rules.title.enemy"); check("@rules.attack", b -> rules.attackMode = b, () -> rules.attackMode); check("@rules.buildai", b -> rules.teams.get(rules.waveTeam).ai = rules.teams.get(rules.waveTeam).infiniteResources = b, () -> rules.teams.get(rules.waveTeam).ai); + check("@rules.corecapture", b -> rules.coreCapture = b, () -> rules.coreCapture); number("@rules.enemycorebuildradius", f -> rules.enemyCoreBuildRadius = f * tilesize, () -> Math.min(rules.enemyCoreBuildRadius / tilesize, 200)); title("@rules.title.environment"); diff --git a/core/src/mindustry/ui/dialogs/DatabaseDialog.java b/core/src/mindustry/ui/dialogs/DatabaseDialog.java index e936c418fc..5c2aa76d17 100644 --- a/core/src/mindustry/ui/dialogs/DatabaseDialog.java +++ b/core/src/mindustry/ui/dialogs/DatabaseDialog.java @@ -56,7 +56,7 @@ public class DatabaseDialog extends BaseDialog{ for(int i = 0; i < array.size; i++){ UnlockableContent unlock = (UnlockableContent)array.get(i); - Image image = unlocked(unlock) ? new Image(unlock.icon(Cicon.medium)).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray); + Image image = unlocked(unlock) ? new Image(unlock.uiIcon).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray); list.add(image).size(8 * 4).pad(3); ClickListener listener = new ClickListener(); image.addListener(listener); diff --git a/core/src/mindustry/ui/dialogs/GameOverDialog.java b/core/src/mindustry/ui/dialogs/GameOverDialog.java index 8c69756a3c..083e49c116 100644 --- a/core/src/mindustry/ui/dialogs/GameOverDialog.java +++ b/core/src/mindustry/ui/dialogs/GameOverDialog.java @@ -66,7 +66,7 @@ public class GameOverDialog extends BaseDialog{ if(state.stats.itemsDelivered.get(item, 0) > 0){ t.table(items -> { items.add(" [lightgray]" + state.stats.itemsDelivered.get(item, 0)); - items.image(item.icon(Cicon.small)).size(8 * 3).pad(4); + items.image(item.uiIcon).size(8 * 3).pad(4); }).left().row(); } } diff --git a/core/src/mindustry/ui/dialogs/HostDialog.java b/core/src/mindustry/ui/dialogs/HostDialog.java index 682f0a010f..05cd5375ce 100644 --- a/core/src/mindustry/ui/dialogs/HostDialog.java +++ b/core/src/mindustry/ui/dialogs/HostDialog.java @@ -64,7 +64,7 @@ public class HostDialog extends BaseDialog{ Time.runTask(5f, () -> { try{ net.host(Vars.port); - player.admin(true); + player.admin = true; if(steam){ Core.app.post(() -> Core.settings.getBoolOnce("steampublic3", () -> { diff --git a/core/src/mindustry/ui/dialogs/JoinDialog.java b/core/src/mindustry/ui/dialogs/JoinDialog.java index 2077d5e091..636a91d212 100644 --- a/core/src/mindustry/ui/dialogs/JoinDialog.java +++ b/core/src/mindustry/ui/dialogs/JoinDialog.java @@ -50,9 +50,7 @@ public class JoinDialog extends BaseDialog{ addCloseButton(); buttons.add().growX().width(-1); - if(!steam){ - buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f).width(-1); - } + if(!steam) buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f); add = new BaseDialog("@joingame.title"); add.cont.add("@joingame.ip").padRight(5f).left(); @@ -97,8 +95,12 @@ public class JoinDialog extends BaseDialog{ }); onResize(() -> { - setup(); - refreshAll(); + //only refresh on resize when the minimum dimension is smaller than the maximum preferred width + //this means that refreshes on resize will only happen for small phones that need the list to fit in portrait mode + if(Math.min(Core.graphics.getWidth(), Core.graphics.getHeight()) / Scl.scl() * 0.9f < 500f){ + setup(); + refreshAll(); + } }); } @@ -402,6 +404,7 @@ public class JoinDialog extends BaseDialog{ global.background(null); float w = targetWidth(); + //TODO looks bad container.button(b -> buildServer(host, b), Styles.cleart, () -> { Events.fire(new ClientPreConnectEvent(host)); if(!Core.settings.getBool("server-disclaimer", false)){ diff --git a/core/src/mindustry/ui/dialogs/KeybindDialog.java b/core/src/mindustry/ui/dialogs/KeybindDialog.java new file mode 100644 index 0000000000..4674c8f60e --- /dev/null +++ b/core/src/mindustry/ui/dialogs/KeybindDialog.java @@ -0,0 +1,262 @@ +package mindustry.ui.dialogs; + +import arc.*; +import arc.KeyBinds.*; +import arc.graphics.*; +import arc.input.*; +import arc.input.InputDevice.*; +import arc.scene.event.*; +import arc.scene.style.*; +import arc.scene.ui.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import mindustry.gen.*; +import mindustry.graphics.*; + +import static arc.Core.*; + +public class KeybindDialog extends Dialog{ + protected KeybindDialogStyle style; + protected Section section; + protected KeyBind rebindKey = null; + protected boolean rebindAxis = false; + protected boolean rebindMin = true; + protected KeyCode minKey = null; + protected Dialog rebindDialog; + protected ObjectIntMap
sectionControls = new ObjectIntMap<>(); + + public KeybindDialog(){ + super(bundle.get("keybind.title", "Rebind Keys")); + KeybindDialog.this.style = scene.getStyle(KeybindDialogStyle.class); + KeybindDialog.this.setup(); + addCloseButton(); + setFillParent(true); + title.setAlignment(Align.center); + titleTable.row(); + titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent); + } + + @Override + public void addCloseButton(){ + buttons.button("@back", Icon.left, this::hide).size(210f, 64f); + + keyDown(key -> { + if(key == KeyCode.escape || key == KeyCode.back) hide(); + }); + } + + public void setStyle(KeybindDialogStyle style){ + this.style = style; + setup(); + } + + private void setup(){ + cont.clear(); + + Section[] sections = Core.keybinds.getSections(); + + Stack stack = new Stack(); + ButtonGroup group = new ButtonGroup<>(); + ScrollPane pane = new ScrollPane(stack); + pane.setFadeScrollBars(false); + this.section = sections[0]; + + for(Section section : sections){ + if(!sectionControls.containsKey(section)) + sectionControls.put(section, input.getDevices().indexOf(section.device, true)); + + if(sectionControls.get(section, 0) >= input.getDevices().size){ + sectionControls.put(section, 0); + section.device = input.getDevices().get(0); + } + + if(sections.length != 1){ + //TODO toggle style + TextButton button = new TextButton(bundle.get("section." + section.name + ".name", Strings.capitalize(section.name))/*, "toggle"*/); + if(section.equals(this.section)) + button.toggle(); + + button.clicked(() -> this.section = section); + + group.add(button); + cont.add(button).fill(); + } + + Table table = new Table(); + + Label device = new Label("Keyboard"); + //device.setColor(style.controllerColor); + device.setAlignment(Align.center); + + Seq devices = input.getDevices(); + + Table stable = new Table(); + + stable.button("<", () -> { + int i = sectionControls.get(section, 0); + if(i - 1 >= 0){ + sectionControls.put(section, i - 1); + section.device = devices.get(i - 1); + setup(); + } + }).disabled(sectionControls.get(section, 0) - 1 < 0).size(40); + + stable.add(device).minWidth(device.getMinWidth() + 60); + + device.setText(input.getDevices().get(sectionControls.get(section, 0)).name()); + + stable.button(">", () -> { + int i = sectionControls.get(section, 0); + + if(i + 1 < devices.size){ + sectionControls.put(section, i + 1); + section.device = devices.get(i + 1); + setup(); + } + }).disabled(sectionControls.get(section, 0) + 1 >= devices.size).size(40); + + table.add(stable).colspan(4); + + table.row(); + table.add().height(10); + table.row(); + if(section.device.type() == DeviceType.controller){ + table.table(info -> info.add("Controller Type: [#" + style.controllerColor.toString().toUpperCase() + "]" + + Strings.capitalize(section.device.name())).left()); + } + table.row(); + + String lastCategory = null; + + for(KeyBind keybind : keybinds.getKeybinds()){ + if(lastCategory != keybind.category() && keybind.category() != null){ + table.add(bundle.get("category." + keybind.category() + ".name", Strings.capitalize(keybind.category()))).color(Color.gray).colspan(4).pad(10).padBottom(4).row(); + table.image().color(Color.gray).fillX().height(3).pad(6).colspan(4).padTop(0).padBottom(10).row(); + lastCategory = keybind.category(); + } + + Axis axis = keybinds.get(section, keybind); + + if(keybind.defaultValue(section.device.type()) instanceof Axis){ + table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), style.keyNameColor).left().padRight(40).padLeft(8); + + if(axis.key != null){ + table.add(axis.key.toString(), style.keyColor).left().minWidth(90).padRight(20); + }else{ + Table axt = new Table(); + axt.left(); + axt.labelWrap(axis.min.toString() + " [red]/[] " + axis.max.toString()).color(style.keyColor).width(140f).padRight(5); + table.add(axt).left().minWidth(90).padRight(20); + } + + table.button(bundle.get("settings.rebind", "Rebind"), () -> { + rebindAxis = true; + rebindMin = true; + openDialog(section, keybind); + }).width(130f); + }else{ + table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), + style.keyNameColor).left().padRight(40).padLeft(8); + table.add(keybinds.get(section, keybind).key.toString(), + style.keyColor).left().minWidth(90).padRight(20); + + table.button(bundle.get("settings.rebind", "Rebind"), () -> { + rebindAxis = false; + rebindMin = false; + openDialog(section, keybind); + }).width(130f); + } + table.button(bundle.get("settings.resetKey", "Reset"), () -> { + keybinds.resetToDefault(section, keybind); + setup(); + }).width(130f); + table.row(); + } + + table.visible(() -> this.section.equals(section)); + + table.button(bundle.get("settings.reset", "Reset to Defaults"), () -> { + keybinds.resetToDefaults(); + setup(); + }).colspan(4).padTop(4).fill(); + + stack.add(table); + } + + cont.row(); + + cont.add(pane).growX().colspan(sections.length); + + } + + void rebind(Section section, KeyBind bind, KeyCode newKey){ + if(rebindKey == null) return; + rebindDialog.hide(); + boolean isAxis = bind.defaultValue(section.device.type()) instanceof Axis; + + if(isAxis){ + if(newKey.axis || !rebindMin){ + section.binds.get(section.device.type(), OrderedMap::new).put(rebindKey, newKey.axis ? new Axis(newKey) : new Axis(minKey, newKey)); + } + }else{ + section.binds.get(section.device.type(), OrderedMap::new).put(rebindKey, new Axis(newKey)); + } + + if(rebindAxis && isAxis && rebindMin && !newKey.axis){ + rebindMin = false; + minKey = newKey; + openDialog(section, rebindKey); + }else{ + rebindKey = null; + rebindAxis = false; + setup(); + } + } + + private void openDialog(Section section, KeyBind name){ + rebindDialog = new Dialog(rebindAxis ? bundle.get("keybind.press.axis", "Press an axis or key...") : bundle.get("keybind.press", "Press a key...")); + + rebindKey = name; + + rebindDialog.titleTable.getCells().first().pad(4); + + if(section.device.type() == DeviceType.keyboard){ + rebindDialog.keyDown(i -> setup()); + + rebindDialog.addListener(new InputListener(){ + @Override + public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ + if(Core.app.isAndroid()) return false; + rebind(section, name, button); + return false; + } + + @Override + public boolean keyDown(InputEvent event, KeyCode keycode){ + rebindDialog.hide(); + if(keycode == KeyCode.escape) return false; + rebind(section, name, keycode); + return false; + } + + @Override + public boolean scrolled(InputEvent event, float x, float y, float amountX, float amountY){ + if(!rebindAxis) return false; + rebindDialog.hide(); + rebind(section, name, KeyCode.scroll); + return false; + } + }); + } + + rebindDialog.show(); + Time.runTask(1f, () -> getScene().setScrollFocus(rebindDialog)); + } + + public static class KeybindDialogStyle extends Style{ + public Color keyColor = Color.white; + public Color keyNameColor = Color.white; + public Color controllerColor = Color.white; + } +} diff --git a/core/src/mindustry/ui/dialogs/LanguageDialog.java b/core/src/mindustry/ui/dialogs/LanguageDialog.java index 41519c32fd..6504bbe60c 100644 --- a/core/src/mindustry/ui/dialogs/LanguageDialog.java +++ b/core/src/mindustry/ui/dialogs/LanguageDialog.java @@ -13,9 +13,41 @@ import static mindustry.Vars.*; public class LanguageDialog extends BaseDialog{ private Locale lastLocale; - private ObjectMap displayNames = ObjectMap.of( - Locale.TRADITIONAL_CHINESE, "正體中文", - Locale.SIMPLIFIED_CHINESE, "简体中文" + private ObjectMap displayNames = ObjectMap.of( + "in_ID", "Bahasa Indonesia (Indonesia)", + "da", "Dansk", + "de", "Deutsch", + "et", "Eesti", + "en", "English", + "es", "Español", + "eu", "Euskara", + "fil", "Filipino", + "fr", "Français", + "it", "Italiano", + "lt", "Lietuvių", + "hu", "Magyar", + "nl", "Nederlands", + "nl_BE", "Nederlands (België)", + "pl", "Polski", + "pt_BR", "Português (Brasil)", + "pt_PT", "Português (Portugal)", + "ro", "Română", + "fi", "Suomi", + "sv", "Svenska", + "vi", "Tiếng Việt", + "tk", "Türkmen dili", + "tr", "Türkçe", + "cs", "Čeština", + "be", "Беларуская", + "bg", "Български", + "ru", "Русский", + "uk_UA", "Українська (Україна)", + "th", "ไทย", + "zh_CN", "简体中文", + "zh_TW", "正體中文", + "ja", "日本語", + "ko", "한국어", + "router", "router" ); public LanguageDialog(){ @@ -33,7 +65,7 @@ public class LanguageDialog extends BaseDialog{ ButtonGroup group = new ButtonGroup<>(); for(Locale loc : locales){ - TextButton button = new TextButton(Strings.capitalize(displayNames.get(loc, loc.getDisplayName(loc))), Styles.clearTogglet); + TextButton button = new TextButton(displayNames.get(loc.toString(), loc.getDisplayName(Locale.ROOT)), Styles.clearTogglet); button.clicked(() -> { if(getLocale().equals(loc)) return; Core.settings.put("locale", loc.toString()); diff --git a/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java b/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java index b9e05308e3..d91ae8530a 100644 --- a/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java +++ b/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java @@ -64,7 +64,7 @@ public class LaunchLoadoutDialog extends BaseDialog{ ItemSeq launches = universe.getLaunchResources(); for(ItemStack s : total){ - table.image(s.item.icon(Cicon.small)).left().size(Cicon.small.size); + table.image(s.item.uiIcon).left().size(iconSmall); int as = schems.get(s.item), al = launches.get(s.item); String amountStr = (al + as) + "[gray] (" + (al + " + " + as + ")"); diff --git a/core/src/mindustry/ui/dialogs/LoadoutDialog.java b/core/src/mindustry/ui/dialogs/LoadoutDialog.java index 311e4d2ea7..eff8f24164 100644 --- a/core/src/mindustry/ui/dialogs/LoadoutDialog.java +++ b/core/src/mindustry/ui/dialogs/LoadoutDialog.java @@ -111,7 +111,7 @@ public class LoadoutDialog extends BaseDialog{ ui.showInfo(Core.bundle.format("configure.invalid", capacity)); })).size(bsize); - t.image(stack.item.icon(Cicon.small)).size(8 * 3).padRight(4).padLeft(4); + t.image(stack.item.uiIcon).size(8 * 3).padRight(4).padLeft(4); t.label(() -> stack.amount + "").left().width(90f); }).pad(2).left().fillX(); diff --git a/core/src/mindustry/ui/dialogs/MapsDialog.java b/core/src/mindustry/ui/dialogs/MapsDialog.java index e0783b7965..ee25c0ae28 100644 --- a/core/src/mindustry/ui/dialogs/MapsDialog.java +++ b/core/src/mindustry/ui/dialogs/MapsDialog.java @@ -49,7 +49,7 @@ public class MapsDialog extends BaseDialog{ Runnable show = () -> ui.loadAnd(() -> { hide(); ui.editor.show(); - ui.editor.editor.tags.put("name", text); + editor.tags.put("name", text); Events.fire(new MapMakeEvent()); }); diff --git a/core/src/mindustry/ui/dialogs/ModsDialog.java b/core/src/mindustry/ui/dialogs/ModsDialog.java index 0328422bb0..de974ce8d8 100644 --- a/core/src/mindustry/ui/dialogs/ModsDialog.java +++ b/core/src/mindustry/ui/dialogs/ModsDialog.java @@ -147,11 +147,12 @@ public class ModsDialog extends BaseDialog{ } void setup(){ + boolean squish = Core.graphics.isPortrait(); float h = 110f; - float w = mobile ? 440f : 524f; + float w = squish ? 410f : 524f; cont.clear(); - cont.defaults().width(mobile ? 500 : 560f).pad(4); + cont.defaults().width(squish ? 480 : 560f).pad(4); cont.add("@mod.reloadrequired").visible(mods::requiresReload).center().get().setAlignment(Align.center); cont.row(); @@ -365,7 +366,7 @@ public class ModsDialog extends BaseDialog{ d.cont.pane(cs -> { int i = 0; for(UnlockableContent c : all){ - cs.button(new TextureRegionDrawable(c.icon(Cicon.medium)), Styles.cleari, Cicon.medium.size, () -> { + cs.button(new TextureRegionDrawable(c.uiIcon), Styles.cleari, iconMed, () -> { ui.content.show(c); }).size(50f).with(im -> { var click = im.getClickListener(); diff --git a/core/src/mindustry/ui/dialogs/PausedDialog.java b/core/src/mindustry/ui/dialogs/PausedDialog.java index 5e9b5d1666..1226d59324 100644 --- a/core/src/mindustry/ui/dialogs/PausedDialog.java +++ b/core/src/mindustry/ui/dialogs/PausedDialog.java @@ -93,12 +93,19 @@ public class PausedDialog extends BaseDialog{ } void showQuitConfirm(){ - ui.showConfirm("@confirm", "@quit.confirm", () -> { + Runnable quit = () -> { wasClient = net.client(); if(net.client()) netClient.disconnectQuietly(); runExitSave(); hide(); - }); + }; + + if(confirmExit){ + ui.showConfirm("@confirm", "@quit.confirm", quit); + }else{ + quit.run(); + } + } public void runExitSave(){ diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index 18d9a8c706..1e671c43b1 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -10,6 +10,7 @@ import arc.math.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.event.*; +import arc.scene.style.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; @@ -36,6 +37,13 @@ import static mindustry.graphics.g3d.PlanetRenderer.*; import static mindustry.ui.dialogs.PlanetDialog.Mode.*; public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ + static final String[] defaultIcons = { + "effect", "power", "logic", "units", "liquid", "production", "defense", "turret", "distribution", "crafting", + "settings", "cancel", "zoom", "ok", "star", "home", "pencil", "up", "down", "left", "right", + "hammer", "warning", "tree", "admin", "map", "modePvp", "terrain", + "modeSurvival", "commandRally", "commandAttack", + }; + //if true, enables launching anywhere for testing public static boolean debugSelect = false; public static float sectorShowDuration = 60f * 2.4f; @@ -61,6 +69,8 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ super("", Styles.fullDialog); shouldPause = true; + planets.planet = content.getByName(ContentType.planet, Core.settings.getString("lastplanet", "serpulo")); + if(planets.planet == null) planets.planet = Planets.serpulo; keyDown(key -> { if(key == KeyCode.escape || key == KeyCode.back || key == Core.keybinds.get(Binding.planet_map).key){ @@ -163,12 +173,13 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ launchSector = state.getSector(); presetShow = 0f; showed = false; + listener = s -> {}; newPresets.clear(); //announce new presets for(SectorPreset preset : content.sectors()){ - if(preset.unlocked() && !preset.alwaysUnlocked && !preset.sector.info.shown && !preset.sector.hasBase()){ + if(preset.unlocked() && !preset.alwaysUnlocked && !preset.sector.info.shown && !preset.sector.hasBase() && preset.planet == planets.planet){ newPresets.add(preset.sector); preset.sector.info.shown = true; preset.sector.saveInfo(); @@ -229,6 +240,24 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ dialog.add("@sectors.captured"); } + //TODO + public void showPlanetLaunch(Sector sector, Cons listener){ + selected = null; + hovered = null; + launching = false; + this.listener = listener; + launchSector = sector; + + //update view to sector + zoom = 1f; + planets.zoom = 1f; + selectAlpha = 0f; + + mode = planetLaunch; + + super.show(); + } + public void showSelect(Sector sector, Cons listener){ selected = null; hovered = null; @@ -253,7 +282,9 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ boolean canSelect(Sector sector){ if(mode == select) return sector.hasBase(); - if(sector.hasBase()) return true; + //cannot launch to existing sector w/ accelerator + if(mode == planetLaunch) return !sector.hasBase(); + if(sector.hasBase() || sector.id == sector.planet.startSector) return true; //preset sectors can only be selected once unlocked if(sector.preset != null){ TechNode node = sector.preset.node(); @@ -369,7 +400,8 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(icon != null){ planets.drawPlane(sec, () -> { - Draw.color(color, selectAlpha); + //use white for content icons + Draw.color(preficon == icon && sec.info.contentIcon != null ? Color.white : color, selectAlpha); Draw.rect(icon, 0, 0, iw, iw * icon.height / icon.width); }); } @@ -395,6 +427,12 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ Draw.reset(); } + boolean selectable(Planet planet){ + //TODO what if any sector is selectable? + if(mode == planetLaunch) return launchSector != null && planet != launchSector.planet; + return planet == planets.planet || planet.alwaysUnlocked || planet.sectors.contains(Sector::hasBase); + } + void setup(){ zoom = planets.zoom = 1f; selectAlpha = 1f; @@ -447,7 +485,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ //planet selection new Table(t -> { t.right(); - if(content.planets().count(p -> p.accessible) > 1){ + if(content.planets().count(this::selectable) > 1){ t.table(Styles.black6, pt -> { pt.add("@planets").color(Pal.accent); pt.row(); @@ -455,11 +493,12 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ pt.row(); for(int i = 0; i < content.planets().size; i++){ Planet planet = content.planets().get(i); - if(planet.accessible){ + if(selectable(planet)){ pt.button(planet.localizedName, Styles.clearTogglet, () -> { selected = null; launchSector = null; renderer.planets.planet = planet; + Core.settings.put("lastplanet", planet.name); }).width(200).height(40).growX().update(bb -> bb.setChecked(renderer.planets.planet == planet)); pt.row(); } @@ -516,7 +555,11 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(hovered != null){ StringBuilder tx = hoverLabel.getText(); if(!canSelect(hovered)){ - tx.append("[gray]").append(Iconc.lock).append(" ").append(Core.bundle.get("locked")); + if(mode == planetLaunch){ + tx.append("[gray]").append(Iconc.cancel); + }else{ + tx.append("[gray]").append(Iconc.lock).append(" ").append(Core.bundle.get("locked")); + } }else{ tx.append("[accent][[ [white]").append(hovered.name()).append("[accent] ]"); } @@ -574,7 +617,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ stats.each((item, stat) -> { int total = (int)(stat.mean * 60 * scl); if(total > 1){ - t.image(item.icon(Cicon.small)).padRight(3); + t.image(item.uiIcon).padRight(3); t.add(UI.formatAmount(total) + " " + Core.bundle.get("unit.perminute")).color(Color.lightGray).padRight(3); if(++i[0] % 3 == 0){ t.row(); @@ -604,7 +647,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ c.add("@sectors.resources").left().row(); c.table(t -> { for(UnlockableContent uc : sector.info.resources){ - t.image(uc.icon(Cicon.small)).padRight(3).size(Cicon.small.size); + t.image(uc.uiIcon).padRight(3).size(iconSmall); } }).padLeft(10f).left().row(); } @@ -628,7 +671,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ int i = 0; for(ItemStack stack : items){ - res.image(stack.item.icon(Cicon.small)).padRight(3); + res.image(stack.item.uiIcon).padRight(3); res.add(UI.formatAmount(Math.max(stack.amount, 0))).color(Color.lightGray); if(++i % 4 == 0){ res.row(); @@ -671,41 +714,69 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ }).size(40f).padLeft(4); } - var icon = Icon.icons.get(sector.info.icon + "Small"); + var icon = sector.info.contentIcon != null ? + new TextureRegionDrawable(sector.info.contentIcon.uiIcon) : + Icon.icons.get(sector.info.icon + "Small"); - title.button(icon == null ? Icon.noneSmall : icon, Styles.clearPartiali, () -> { + title.button(icon == null ? Icon.noneSmall : icon, Styles.clearPartiali, iconSmall, () -> { new Dialog(""){{ closeOnBack(); setFillParent(true); cont.pane(t -> { - t.marginRight(19f); - t.defaults().size(48f); + resized(true, () -> { + t.clearChildren(); + t.marginRight(19f); + t.defaults().size(48f); - t.button(Icon.none, Styles.clearTogglei, () -> { - sector.info.icon = null; - sector.saveInfo(); - hide(); - updateSelected(); - }).checked(sector.info.icon == null); - - int i = 1; - for(var entry : Icon.icons.entries()){ - if(entry.key.endsWith("Small") || entry.key.contains("none")) continue; - String key = entry.key; - - t.button(entry.value, Styles.cleari, () -> { - sector.info.icon = key; + t.button(Icon.none, Styles.clearTogglei, () -> { + sector.info.icon = null; sector.saveInfo(); hide(); updateSelected(); - }).checked(entry.key.equals(sector.info.icon)); + }).checked(sector.info.icon == null); - if(++i % 8 == 0) t.row(); - } + int cols = (int)Math.min(20, Core.graphics.getWidth() / 52f); + + int i = 1; + for(var key : defaultIcons){ + var value = Icon.icons.get(key); + + t.button(value, Styles.cleari, () -> { + sector.info.icon = key; + sector.info.contentIcon = null; + sector.saveInfo(); + hide(); + updateSelected(); + }).checked(key.equals(sector.info.icon)); + + if(++i % cols == 0) t.row(); + } + + for(ContentType ctype : defaultContentIcons){ + t.row(); + t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent); + t.row(); + + i = 0; + for(UnlockableContent u : content.getBy(ctype).as()){ + if(!u.isHidden() && u.unlocked()){ + t.button(new TextureRegionDrawable(u.uiIcon), Styles.cleari, iconMed, () -> { + sector.info.icon = null; + sector.info.contentIcon = u; + sector.saveInfo(); + hide(); + updateSelected(); + }).checked(sector.info.contentIcon == u); + + if(++i % cols == 0) t.row(); + } + } + } + }); }); buttons.button("@back", Icon.left, this::hide).size(210f, 64f); }}.show(); - }).size(40f); + }).size(40f).tooltip("@sector.changeicon"); }).row(); stable.image().color(Pal.accent).fillX().height(3f).pad(3f).row(); @@ -752,7 +823,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ t.add("@sectors.resources").padRight(4); for(UnlockableContent c : sector.info.resources){ if(c == null) continue; //apparently this is possible. - t.image(c.icon(Cicon.small)).padRight(3).size(Cicon.small.size); + t.image(c.uiIcon).padRight(3).size(iconSmall); } }).padLeft(10f).fillX().row(); } @@ -825,7 +896,12 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(mode == look && !sector.hasBase()){ shouldHide = false; Sector from = findLauncher(sector); - if(from == null){ + if(from == null || mode == planetLaunch){ + //TODO use the standard nucleus core schematic. + if(mode == planetLaunch){ + listener.get(sector); + } + //clear loadout information, so only the basic loadout gets used universe.clearLoadoutInfo(); //free launch. @@ -857,6 +933,8 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ /** Look around for existing sectors. Can only deploy. */ look, /** Select a sector for some purpose. */ - select + select, + /** Launch between planets. */ + planetLaunch } } diff --git a/core/src/mindustry/ui/dialogs/ResearchDialog.java b/core/src/mindustry/ui/dialogs/ResearchDialog.java index f78d98e77b..dbe7caa19f 100644 --- a/core/src/mindustry/ui/dialogs/ResearchDialog.java +++ b/core/src/mindustry/ui/dialogs/ResearchDialog.java @@ -109,6 +109,9 @@ public class ResearchDialog extends BaseDialog{ checkNodes(root); treeLayout(); + view.hoverNode = null; + view.infoTable.remove(); + view.infoTable.clear(); }); hidden(ui.planet::setup); @@ -292,7 +295,7 @@ public class ResearchDialog extends BaseDialog{ infoTable.touchable = Touchable.enabled; for(TechTreeNode node : nodes){ - ImageButton button = new ImageButton(node.node.content.icon(Cicon.medium), Styles.nodei); + ImageButton button = new ImageButton(node.node.content.uiIcon, Styles.nodei); button.visible(() -> node.visible); button.clicked(() -> { if(moved) return; @@ -339,7 +342,7 @@ public class ResearchDialog extends BaseDialog{ button.setPosition(node.x + panX + width / 2f, node.y + panY + height / 2f + offset, Align.center); button.getStyle().up = !locked(node.node) ? Tex.buttonOver : !selectable(node.node) || !canSpend(node.node) ? Tex.buttonRed : Tex.button; - ((TextureRegionDrawable)button.getStyle().imageUp).setRegion(node.selectable ? node.node.content.icon(Cicon.medium) : Icon.lock.getRegion()); + ((TextureRegionDrawable)button.getStyle().imageUp).setRegion(node.selectable ? node.node.content.uiIcon : Icon.lock.getRegion()); button.getImage().setColor(!locked(node.node) ? Color.white : node.selectable ? Color.gray : Pal.gray); button.getImage().setScaling(Scaling.bounded); }); @@ -528,7 +531,7 @@ public class ResearchDialog extends BaseDialog{ int reqAmount = req.amount - completed.amount; list.left(); - list.image(req.item.icon(Cicon.small)).size(8 * 3).padRight(3); + list.image(req.item.uiIcon).size(8 * 3).padRight(3); list.add(req.item.localizedName).color(Color.lightGray); Label label = list.label(() -> " " + UI.formatAmount(Math.min(items.get(req.item), reqAmount)) + " / " diff --git a/core/src/mindustry/ui/dialogs/SchematicsDialog.java b/core/src/mindustry/ui/dialogs/SchematicsDialog.java index 44b54df434..a4616af643 100644 --- a/core/src/mindustry/ui/dialogs/SchematicsDialog.java +++ b/core/src/mindustry/ui/dialogs/SchematicsDialog.java @@ -1,6 +1,7 @@ package mindustry.ui.dialogs; import arc.*; +import arc.func.*; import arc.graphics.*; import arc.graphics.Texture.*; import arc.graphics.g2d.*; @@ -11,8 +12,11 @@ import arc.scene.ui.*; import arc.scene.ui.ImageButton.*; import arc.scene.ui.TextButton.*; import arc.scene.ui.layout.*; +import arc.scene.utils.*; +import arc.struct.*; import arc.util.*; import mindustry.*; +import mindustry.ctype.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; @@ -20,18 +24,27 @@ import mindustry.input.*; import mindustry.type.*; import mindustry.ui.*; +import java.util.regex.*; + import static mindustry.Vars.*; public class SchematicsDialog extends BaseDialog{ + private static final float tagh = 42f; private SchematicInfoDialog info = new SchematicInfoDialog(); private Schematic firstSchematic; private String search = ""; private TextField searchField; + private Runnable rebuildPane = () -> {}, rebuildTags = () -> {}; + private Pattern ignoreSymbols = Pattern.compile("[`~!@#$%^&*()-_=+{}|;:'\",<.>/?]"); + private Seq tags, selectedTags = new Seq<>(); + private boolean checkedTags; public SchematicsDialog(){ super("@schematics"); Core.assets.load("sprites/schematic-background.png", Texture.class).loaded = t -> ((Texture)t).setWrap(TextureWrap.repeat); + tags = Core.settings.getJson("schematic-tags", Seq.class, String.class, Seq::new); + shouldPause = true; addCloseButton(); buttons.button("@schematic.import", Icon.download, this::showImport); @@ -40,8 +53,12 @@ public class SchematicsDialog extends BaseDialog{ } void setup(){ + if(!checkedTags){ + checkTags(); + checkedTags = true; + } + search = ""; - Runnable[] rebuildPane = {null}; cont.top(); cont.clear(); @@ -51,15 +68,46 @@ public class SchematicsDialog extends BaseDialog{ s.image(Icon.zoom); searchField = s.field(search, res -> { search = res; - rebuildPane[0].run(); + rebuildPane.run(); }).growX().get(); }).fillX().padBottom(4); cont.row(); + cont.table(in -> { + in.left(); + in.add("@schematic.tags").padRight(4); + + //tags (no scroll pane visible) + in.pane(Styles.nonePane, t -> { + rebuildTags = () -> { + t.clearChildren(); + t.left(); + + t.defaults().pad(2).height(tagh); + for(var tag : tags){ + t.button(tag, Styles.togglet, () -> { + if(selectedTags.contains(tag)){ + selectedTags.remove(tag); + }else{ + selectedTags.add(tag); + } + rebuildPane.run(); + }).checked(selectedTags.contains(tag)).with(c -> c.getLabel().setWrap(false)); + } + }; + rebuildTags.run(); + }).fillX().height(tagh).get().setScrollingDisabled(false, true); + + in.button(Icon.pencilSmall, () -> { + showAllTags(); + }).size(tagh).pad(2).tooltip("@schematic.edittags"); + }).height(tagh).fillX(); + + cont.row(); + cont.pane(t -> { t.top(); - t.margin(20f); t.update(() -> { if(Core.input.keyTap(Binding.chat) && Core.scene.getKeyboardFocus() == searchField && firstSchematic != null){ @@ -72,18 +120,20 @@ public class SchematicsDialog extends BaseDialog{ } }); - rebuildPane[0] = () -> { + rebuildPane = () -> { int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1); t.clear(); int i = 0; - String regex = "[`~!@#$%^&*()-_=+{}|;:'\",<.>/?]"; - String searchString = search.toLowerCase().replaceAll(regex, " "); + String searchString = ignoreSymbols.matcher(search.toLowerCase()).replaceAll(""); firstSchematic = null; for(Schematic s : schematics.all()){ - if(!search.isEmpty() && !s.name().toLowerCase().replaceAll(regex, " ").contains(searchString)) continue; + //make sure *tags* fit + if(selectedTags.any() && !s.labels.containsAll(selectedTags)) continue; + //make sure search fits + if(!search.isEmpty() && !ignoreSymbols.matcher(s.name().toLowerCase()).replaceAll("").contains(searchString)) continue; if(firstSchematic == null) firstSchematic = s; Button[] sel = {null}; @@ -106,20 +156,27 @@ public class SchematicsDialog extends BaseDialog{ buttons.button(Icon.pencil, style, () -> { new Dialog("@schematic.rename"){{ + setFillParent(true); + + cont.margin(30); + + cont.add("@schematic.tags").padRight(6f); + cont.table(tags -> buildTags(s, tags, false)).maxWidth(400f).fillX().left().row(); + cont.margin(30).add("@name").padRight(6f); - TextField nameField = cont.field(s.name(), null).size(400f, 55f).addInputDialog().get(); + TextField nameField = cont.field(s.name(), null).size(400f, 55f).addInputDialog().left().get(); cont.row(); cont.margin(30).add("@editor.description").padRight(6f); - TextField descripionField = cont.area(s.description(), Styles.areaField, t -> {}).size(400f, 140f).addInputDialog().get(); + TextField descField = cont.area(s.description(), Styles.areaField, t -> {}).size(400f, 140f).left().addInputDialog().get(); Runnable accept = () -> { s.tags.put("name", nameField.getText()); - s.tags.put("description", descripionField.getText()); + s.tags.put("description", descField.getText()); s.save(); hide(); - rebuildPane[0].run(); + rebuildPane.run(); }; buttons.defaults().size(120, 54).pad(4); @@ -127,7 +184,7 @@ public class SchematicsDialog extends BaseDialog{ buttons.button("@cancel", this::hide); keyDown(KeyCode.enter, () -> { - if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descripionField){ + if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descField){ accept.run(); } }); @@ -146,7 +203,7 @@ public class SchematicsDialog extends BaseDialog{ }else{ ui.showConfirm("@confirm", "@schematic.delete.confirm", () -> { schematics.remove(s); - rebuildPane[0].run(); + rebuildPane.run(); }); } }); @@ -188,8 +245,8 @@ public class SchematicsDialog extends BaseDialog{ } }; - rebuildPane[0].run(); - }).get().setScrollingDisabled(true, false); + rebuildPane.run(); + }).grow().get().setScrollingDisabled(true, false); } public void showInfo(Schematic schematic){ @@ -212,6 +269,7 @@ public class SchematicsDialog extends BaseDialog{ schematics.add(s); setup(); ui.showInfoFade("@schematic.saved"); + checkTags(s); showInfo(s); }catch(Throwable e){ ui.showException(e); @@ -227,6 +285,7 @@ public class SchematicsDialog extends BaseDialog{ schematics.add(s); setup(); showInfo(s); + checkTags(s); }catch(Exception e){ ui.showException(e); } @@ -281,6 +340,315 @@ public class SchematicsDialog extends BaseDialog{ Core.scene.setKeyboardFocus(searchField); } + + //adds all new tags to the global list of tags + //alternatively, unknown tags could be discarded on import? + void checkTags(){ + ObjectSet encountered = new ObjectSet<>(); + encountered.addAll(tags); + for(Schematic s : schematics.all()){ + for(var tag : s.labels){ + if(encountered.add(tag)){ + tags.add(tag); + } + } + } + } + + //adds any new tags found to the global tag list + //TODO remove tags from it instead? + void checkTags(Schematic s){ + boolean any = false; + for(var tag : s.labels){ + if(!tags.contains(tag)){ + tags.add(tag); + any = true; + } + } + if(any){ + rebuildTags.run(); + } + } + + void tagsChanged(){ + rebuildTags.run(); + if(selectedTags.any()){ + rebuildPane.run(); + } + + Core.settings.putJson("schematic-tags", String.class, tags); + } + + void addTag(Schematic s, String tag){ + s.labels.add(tag); + s.save(); + tagsChanged(); + } + + void removeTag(Schematic s, String tag){ + s.labels.remove(tag); + s.save(); + tagsChanged(); + } + + //shows a dialog for creating a new tag + void showNewTag(Cons result){ + ui.showTextInput("@schematic.addtag", "", "", out -> { + if(tags.contains(out)){ + ui.showInfo("@schematic.tagexists"); + }else{ + tags.add(out); + tagsChanged(); + result.get(out); + } + }); + } + + void showNewIconTag(Cons cons){ + new Dialog(){{ + closeOnBack(); + setFillParent(true); + + cont.pane(t -> { + resized(true, () -> { + t.clearChildren(); + t.marginRight(19f); + t.defaults().size(48f); + + int cols = (int)Math.min(20, Core.graphics.getWidth() / 52f); + + /* + int i = 0; + for(var key : defaultIcons){ + var value = Icon.icons.get(key); + + t.button(value, Styles.cleari, () -> { + sector.info.icon = key; + sector.info.contentIcon = null; + sector.saveInfo(); + hide(); + updateSelected(); + }).checked(key.equals(sector.info.icon)); + + if(++i % cols == 0) t.row(); + }*/ + + int i = 0; + + for(ContentType ctype : defaultContentIcons){ + t.row(); + t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent); + t.row(); + + i = 0; + for(UnlockableContent u : content.getBy(ctype).as()){ + if(!u.isHidden() && u.unlockedNow() && u.hasEmoji() && !tags.contains(u.emoji())){ + t.button(new TextureRegionDrawable(u.uiIcon), Styles.cleari, iconMed, () -> { + String out = u.emoji() + ""; + + tags.add(out); + tagsChanged(); + cons.get(out); + + hide(); + }); + + if(++i % cols == 0) t.row(); + } + } + } + }); + }); + buttons.button("@back", Icon.left, this::hide).size(210f, 64f); + }}.show(); + } + + void showAllTags(){ + var dialog = new BaseDialog("@schematic.edittags"); + dialog.addCloseButton(); + Runnable[] rebuild = {null}; + dialog.cont.pane(p -> { + rebuild[0] = () -> { + p.clearChildren(); + p.defaults().fillX().left(); + + float sum = 0f; + Table current = new Table().left(); + + for(var tag : tags){ + + var next = new Table(Tex.button, n -> { + n.add(tag).padRight(4); + + n.add().growX(); + n.defaults().size(30f); + + //delete + n.button(Icon.cancelSmall, Styles.emptyi, () -> { + ui.showConfirm("@schematic.tagdelconfirm", () -> { + for(Schematic s : schematics.all()){ + if(s.labels.any()){ + s.labels.remove(tag); + s.save(); + } + } + selectedTags.remove(tag); + tags.remove(tag); + tagsChanged(); + rebuildPane.run(); + rebuild[0].run(); + }); + }); + //rename + n.button(Icon.pencilSmall, Styles.emptyi, () -> { + ui.showTextInput("@schematic.renametag", "@name", tag, result -> { + //same tag, nothing was renamed + if(result.equals(tag)) return; + + if(tags.contains(result)){ + ui.showInfo("@schematic.tagexists"); + }else{ + for(Schematic s : schematics.all()){ + if(s.labels.any()){ + s.labels.replace(tag, result); + s.save(); + } + } + selectedTags.replace(tag, result); + tags.replace(tag, result); + tagsChanged(); + rebuild[0].run(); + } + }); + }); + //move + n.button(Icon.upSmall, Styles.emptyi, () -> { + int idx = tags.indexOf(tag); + if(idx > 0){ + tags.swap(idx, idx - 1); + tagsChanged(); + rebuild[0].run(); + } + }); + }); + + next.pack(); + float w = next.getPrefWidth() + Scl.scl(6f); + + if(w + sum >= Core.graphics.getWidth() * (Core.graphics.isPortrait() ? 1f : 0.8f)){ + p.add(current).row(); + current = new Table(); + current.left(); + current.add(next).height(tagh).pad(2); + sum = 0; + }else{ + current.add(next).height(tagh).pad(2); + } + + sum += w; + } + + if(sum > 0){ + p.add(current).row(); + } + + p.table(t -> { + t.left().defaults().fillX().height(tagh).pad(2); + + t.button("@schematic.texttag", Icon.add, () -> showNewTag(res -> rebuild[0].run())).wrapLabel(false).get().getLabelCell().padLeft(5); + t.button("@schematic.icontag", Icon.add, () -> showNewIconTag(res -> rebuild[0].run())).wrapLabel(false).get().getLabelCell().padLeft(5); + }); + + }; + + resized(true, rebuild[0]); + }); + dialog.show(); + } + + void buildTags(Schematic schem, Table t){ + buildTags(schem, t, true); + } + + void buildTags(Schematic schem, Table t, boolean name){ + t.clearChildren(); + t.left(); + + //sort by order in the main target array. the complexity of this is probably awful + schem.labels.sort(s -> tags.indexOf(s)); + + if(name) t.add("@schematic.tags").padRight(4); + t.pane(s -> { + s.left(); + s.defaults().pad(3).height(tagh); + for(var tag : schem.labels){ + s.table(Tex.button, i -> { + i.add(tag).padRight(4).height(tagh).labelAlign(Align.center); + i.button(Icon.cancelSmall, Styles.emptyi, () -> { + removeTag(schem, tag); + buildTags(schem, t, name); + }).size(tagh).padRight(-9f).padLeft(-9f); + }); + } + + }).fillX().left().height(tagh).get().setScrollingDisabled(false, true); + + t.button(Icon.addSmall, () -> { + var dialog = new BaseDialog("@schematic.addtag"); + dialog.addCloseButton(); + dialog.cont.pane(p -> resized(true, () -> { + p.clearChildren(); + + float sum = 0f; + Table current = new Table().left(); + for(var tag : tags){ + if(schem.labels.contains(tag)) continue; + + var next = Elem.newButton(tag, () -> { + addTag(schem, tag); + buildTags(schem, t, name); + dialog.hide(); + }); + next.getLabel().setWrap(false); + + next.pack(); + float w = next.getPrefWidth() + Scl.scl(6f); + + if(w + sum >= Core.graphics.getWidth() * (Core.graphics.isPortrait() ? 1f : 0.8f)){ + p.add(current).row(); + current = new Table(); + current.left(); + current.add(next).height(tagh).pad(2); + sum = 0; + }else{ + current.add(next).height(tagh).pad(2); + } + + sum += w; + } + + if(sum > 0){ + p.add(current).row(); + } + + Cons handleTag = res -> { + dialog.hide(); + addTag(schem, res); + buildTags(schem, t, name); + }; + + p.row(); + + p.table(v -> { + v.left().defaults().fillX().height(tagh).pad(2); + v.button("@schematic.texttag", Icon.add, () -> showNewTag(handleTag)).wrapLabel(false).get().getLabelCell().padLeft(4); + v.button("@schematic.icontag", Icon.add, () -> showNewIconTag(handleTag)).wrapLabel(false).get().getLabelCell().padLeft(4); + }); + })); + dialog.show(); + }).size(tagh).tooltip("@schematic.addtag"); + } + @Override public Dialog show(){ super.show(); @@ -298,6 +666,7 @@ public class SchematicsDialog extends BaseDialog{ public Color borderColor = Pal.gray; private Schematic schematic; + private Texture lastTexture; boolean set; public SchematicImage(Schematic s){ @@ -320,6 +689,8 @@ public class SchematicsDialog extends BaseDialog{ if(!set){ Core.app.post(this::setPreview); set = true; + }else if(lastTexture != null && lastTexture.isDisposed()){ + set = wasSet = false; } Texture background = Core.assets.get("sprites/schematic-background.png", Texture.class); @@ -346,13 +717,13 @@ public class SchematicsDialog extends BaseDialog{ } private void setPreview(){ - TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(schematics.getPreview(schematic))); + TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(lastTexture = schematics.getPreview(schematic))); setDrawable(draw); setScaling(Scaling.fit); } } - public static class SchematicInfoDialog extends BaseDialog{ + public class SchematicInfoDialog extends BaseDialog{ SchematicInfoDialog(){ super(""); @@ -366,6 +737,8 @@ public class SchematicsDialog extends BaseDialog{ cont.add(Core.bundle.format("schematic.info", schem.width, schem.height, schem.tiles.size)).color(Color.lightGray); cont.row(); + cont.table(tags -> buildTags(schem, tags)).fillX().left().row(); + cont.row(); cont.add(new SchematicImage(schem)).maxSize(800f); cont.row(); @@ -373,7 +746,7 @@ public class SchematicsDialog extends BaseDialog{ cont.table(r -> { int i = 0; for(ItemStack s : arr){ - r.image(s.item.icon(Cicon.small)).left(); + r.image(s.item.uiIcon).left().size(iconMed); r.label(() -> { Building core = player.core(); if(core == null || state.rules.infiniteResources || core.items.has(s.item, s.amount)) return "[lightgray]" + s.amount + ""; diff --git a/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java b/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java index 5290b6c5aa..2b89f153aa 100644 --- a/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java +++ b/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java @@ -2,11 +2,10 @@ package mindustry.ui.dialogs; import arc.*; import arc.files.*; +import arc.func.*; import arc.graphics.*; import arc.graphics.Texture.*; import arc.input.*; -import arc.scene.*; -import arc.scene.event.*; import arc.scene.ui.*; import arc.scene.ui.TextButton.*; import arc.scene.ui.layout.*; @@ -31,10 +30,11 @@ import static arc.Core.*; import static mindustry.Vars.net; import static mindustry.Vars.*; -public class SettingsMenuDialog extends SettingsDialog{ +public class SettingsMenuDialog extends Dialog{ public SettingsTable graphics; public SettingsTable game; public SettingsTable sound; + public SettingsTable main; private Table prefs; private Table menu; @@ -42,6 +42,11 @@ public class SettingsMenuDialog extends SettingsDialog{ private boolean wasPaused; public SettingsMenuDialog(){ + super(bundle.get("settings", "Settings")); + addCloseButton(); + + cont.add(main = new SettingsTable()); + hidden(() -> { Sounds.back.play(); if(state.isGame()){ @@ -60,6 +65,15 @@ public class SettingsMenuDialog extends SettingsDialog{ rebuildMenu(); }); + Events.on(ResizeEvent.class, event -> { + if(isShown() && Core.scene.getDialog() == this){ + graphics.rebuild(); + sound.rebuild(); + game.rebuild(); + updateScrollFocus(); + } + }); + setFillParent(true); title.setAlignment(Align.center); titleTable.row(); @@ -222,29 +236,8 @@ public class SettingsMenuDialog extends SettingsDialog{ }).marginLeft(4); }); - ScrollPane pane = new ScrollPane(prefs); - pane.addCaptureListener(new InputListener(){ - @Override - public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ - Element actor = pane.hit(x, y, true); - if(actor instanceof Slider){ - pane.setFlickScroll(false); - return true; - } - - return super.touchDown(event, x, y, pointer, button); - } - - @Override - public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){ - pane.setFlickScroll(true); - super.touchUp(event, x, y, pointer, button); - } - }); - pane.setFadeScrollBars(false); - row(); - add(pane).grow().top(); + pane(prefs).grow().top(); row(); add(buttons).fillX(); @@ -296,9 +289,13 @@ public class SettingsMenuDialog extends SettingsDialog{ game.screenshakePref(); if(mobile){ game.checkPref("autotarget", true); - game.checkPref("keyboard", false, val -> control.setInput(val ? new DesktopInput() : new MobileInput())); + game.checkPref("keyboard", false, val -> { + control.setInput(val ? new DesktopInput() : new MobileInput()); + input.setUseKeyboard(val); + }); if(Core.settings.getBool("keyboard")){ control.setInput(new DesktopInput()); + input.setUseKeyboard(true); } } //the issue with touchscreen support on desktop is that: @@ -346,10 +343,11 @@ public class SettingsMenuDialog extends SettingsDialog{ } } + int[] lastUiScale = {settings.getInt("uiscale", 100)}; + graphics.sliderPref("uiscale", 100, 25, 300, 25, s -> { - if(ui.settings != null){ - Core.settings.put("uiscalechanged", true); - } + //if the user changed their UI scale, but then put it back, don't consider it 'changed' + Core.settings.put("uiscalechanged", s != lastUiScale[0]); return s + "%"; }); graphics.sliderPref("fpscap", 240, 15, 245, 5, s -> (s > 240 ? Core.bundle.get("setting.fpscap.none") : Core.bundle.format("setting.fpscap.text", s))); @@ -412,15 +410,12 @@ public class SettingsMenuDialog extends SettingsDialog{ graphics.checkPref("indicators", true); graphics.checkPref("showweather", true); graphics.checkPref("animatedwater", true); + if(Shaders.shield != null){ graphics.checkPref("animatedshields", !mobile); } - //if(!ios){ - graphics.checkPref("bloom", true, val -> renderer.toggleBloom(val)); - //}else{ - // Core.settings.put("bloom", false); - //} + graphics.checkPref("bloom", true, val -> renderer.toggleBloom(val)); graphics.checkPref("pixelate", false, val -> { if(val){ @@ -538,4 +533,174 @@ public class SettingsMenuDialog extends SettingsDialog{ } }); } + + public interface StringProcessor{ + String get(int i); + } + + public static class SettingsTable extends Table{ + protected Seq list = new Seq<>(); + + public SettingsTable(){ + left(); + } + + public Seq getSettings(){ + return list; + } + + public void pref(Setting setting){ + list.add(setting); + rebuild(); + } + + public void screenshakePref(){ + sliderPref("screenshake", bundle.get("setting.screenshake.name", "Screen Shake"), 4, 0, 8, i -> (i / 4f) + "x"); + } + + public SliderSetting sliderPref(String name, String title, int def, int min, int max, StringProcessor s){ + return sliderPref(name, title, def, min, max, 1, s); + } + + public SliderSetting sliderPref(String name, String title, int def, int min, int max, int step, StringProcessor s){ + SliderSetting res; + list.add(res = new SliderSetting(name, title, def, min, max, step, s)); + settings.defaults(name, def); + rebuild(); + return res; + } + + public SliderSetting sliderPref(String name, int def, int min, int max, StringProcessor s){ + return sliderPref(name, def, min, max, 1, s); + } + + public SliderSetting sliderPref(String name, int def, int min, int max, int step, StringProcessor s){ + SliderSetting res; + list.add(res = new SliderSetting(name, bundle.get("setting." + name + ".name"), def, min, max, step, s)); + settings.defaults(name, def); + rebuild(); + return res; + } + + public void checkPref(String name, String title, boolean def){ + list.add(new CheckSetting(name, title, def, null)); + settings.defaults(name, def); + rebuild(); + } + + public void checkPref(String name, String title, boolean def, Boolc changed){ + list.add(new CheckSetting(name, title, def, changed)); + settings.defaults(name, def); + rebuild(); + } + + /** Localized title. */ + public void checkPref(String name, boolean def){ + list.add(new CheckSetting(name, bundle.get("setting." + name + ".name"), def, null)); + settings.defaults(name, def); + rebuild(); + } + + /** Localized title. */ + public void checkPref(String name, boolean def, Boolc changed){ + list.add(new CheckSetting(name, bundle.get("setting." + name + ".name"), def, changed)); + settings.defaults(name, def); + rebuild(); + } + + void rebuild(){ + clearChildren(); + + for(Setting setting : list){ + setting.add(this); + } + + button(bundle.get("settings.reset", "Reset to Defaults"), () -> { + for(Setting setting : list){ + if(setting.name == null || setting.title == null) continue; + settings.put(setting.name, settings.getDefault(setting.name)); + } + rebuild(); + }).margin(14).width(240f).pad(6); + } + + public abstract static class Setting{ + public String name; + public String title; + + public abstract void add(SettingsTable table); + } + + public static class CheckSetting extends Setting{ + boolean def; + Boolc changed; + + CheckSetting(String name, String title, boolean def, Boolc changed){ + this.name = name; + this.title = title; + this.def = def; + this.changed = changed; + } + + @Override + public void add(SettingsTable table){ + CheckBox box = new CheckBox(title); + + box.update(() -> box.setChecked(settings.getBool(name))); + + box.changed(() -> { + settings.put(name, box.isChecked()); + if(changed != null){ + changed.get(box.isChecked()); + } + }); + + box.left(); + table.add(box).left().padTop(3f); + table.row(); + } + } + + public static class SliderSetting extends Setting{ + int def, min, max, step; + StringProcessor sp; + + SliderSetting(String name, String title, int def, int min, int max, int step, StringProcessor s){ + this.name = name; + this.title = title; + this.def = def; + this.min = min; + this.max = max; + this.step = step; + this.sp = s; + } + + @Override + public void add(SettingsTable table){ + Slider slider = new Slider(min, max, step, false); + + slider.setValue(settings.getInt(name)); + + Label label = new Label(title); + slider.changed(() -> { + settings.put(name, (int)slider.getValue()); + label.setText(title + ": " + sp.get((int)slider.getValue())); + }); + + slider.change(); + + table.table(t -> { + t.left().defaults().left(); + t.add(label).minWidth(label.getPrefWidth() / Scl.scl(1f) + 50); + if(Core.graphics.isPortrait()){ + t.row(); + } + t.add(slider).width(180); + }).left().padTop(3); + + table.row(); + } + } + + } } diff --git a/core/src/mindustry/ui/fragments/BlockInventoryFragment.java b/core/src/mindustry/ui/fragments/BlockInventoryFragment.java index b6b2729e2b..e63acaac4e 100644 --- a/core/src/mindustry/ui/fragments/BlockInventoryFragment.java +++ b/core/src/mindustry/ui/fragments/BlockInventoryFragment.java @@ -17,7 +17,6 @@ import arc.util.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.type.*; -import mindustry.ui.*; import java.util.*; @@ -147,7 +146,7 @@ public class BlockInventoryFragment extends Fragment{ HandCursorListener l = new HandCursorListener(); l.enabled = canPick; - Element image = itemImage(item.icon(Cicon.xlarge), () -> { + Element image = itemImage(item.uiIcon, () -> { if(tile == null || !tile.isValid()){ return ""; } diff --git a/core/src/mindustry/ui/fragments/HintsFragment.java b/core/src/mindustry/ui/fragments/HintsFragment.java index 9a89b21a91..f4f677e720 100644 --- a/core/src/mindustry/ui/fragments/HintsFragment.java +++ b/core/src/mindustry/ui/fragments/HintsFragment.java @@ -156,6 +156,7 @@ public class HintsFragment extends Fragment{ schematicSelect(visibleDesktop, () -> ui.hints.placedBlocks.contains(Blocks.router), () -> Core.input.keyRelease(Binding.schematic_select) || Core.input.keyTap(Binding.pick)), conveyorPathfind(() -> control.input.block == Blocks.titaniumConveyor, () -> Core.input.keyRelease(Binding.diagonal_placement) || (mobile && Core.settings.getBool("swapdiagonal"))), boost(visibleDesktop, () -> !player.dead() && player.unit().type.canBoost, () -> Core.input.keyDown(Binding.boost)), + blockInfo(() -> true, () -> ui.content.isShown()), command(() -> state.rules.defaultTeam.data().units.size > 3 && !net.active(), () -> player.unit().isCommanding()), payloadPickup(() -> !player.unit().dead && player.unit() instanceof Payloadc p && p.payloads().isEmpty(), () -> player.unit() instanceof Payloadc p && p.payloads().any()), payloadDrop(() -> !player.unit().dead && player.unit() instanceof Payloadc p && p.payloads().any(), () -> player.unit() instanceof Payloadc p && p.payloads().isEmpty()), diff --git a/core/src/mindustry/ui/fragments/HudFragment.java b/core/src/mindustry/ui/fragments/HudFragment.java index 0884707e9e..704cf0ec61 100644 --- a/core/src/mindustry/ui/fragments/HudFragment.java +++ b/core/src/mindustry/ui/fragments/HudFragment.java @@ -17,6 +17,7 @@ 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.*; @@ -30,7 +31,7 @@ import mindustry.ui.*; import static mindustry.Vars.*; public class HudFragment extends Fragment{ - private static final float dsize = 65f; + private static final float dsize = 65f, pauseHeight = 36f; public final PlacementFragment blockfrag = new PlacementFragment(); public boolean shown = true; @@ -60,7 +61,7 @@ public class HudFragment extends Fragment{ //increments at which to warn about incoming guardian if(diff == 1 || diff == 2 || diff == 5 || diff == 10){ - showToast(Icon.warning, Core.bundle.format("wave.guardianwarn" + (diff == 1 ? ".one" : ""), diff)); + showToast(Icon.warning, group.type.emoji() + " " + Core.bundle.format("wave.guardianwarn" + (diff == 1 ? ".one" : ""), diff)); } break outer; @@ -90,7 +91,9 @@ public class HudFragment extends Fragment{ parent.fill(t -> { t.name = "paused"; t.top().visible(() -> state.isPaused() && shown).touchable = Touchable.disabled; - t.table(Styles.black5, top -> top.label(() -> state.gameOver && state.isCampaign() ? "@sector.curlost" : "@paused").style(Styles.outlineLabel).pad(8f)).growX(); + t.table(Styles.black6, top -> top.label(() -> netServer.isWaitingForPlayers() ? "@waiting.players" : 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 }); //minimap + position @@ -208,10 +211,7 @@ public class HudFragment extends Fragment{ wavesMain.row(); - wavesMain.table(Tex.button, t -> t.margin(10f).add(new Bar("boss.health", Pal.health, () -> state.boss() == null ? 0f : state.boss().healthf()).blink(Color.white)) - .grow()).fillX().visible(() -> state.rules.waves && state.boss() != null).height(60f).name("boss"); - - wavesMain.row(); + addInfoTable(wavesMain.table().width(dsize * 5f + 4f).left().get()); editorMain.name = "editor"; editorMain.table(Tex.buttonEdge4, t -> { @@ -244,6 +244,7 @@ public class HudFragment extends Fragment{ 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"); @@ -257,68 +258,83 @@ public class HudFragment extends Fragment{ } info.row(); - info.label(() -> ping.get(netClient.getPing())).visible(net::client).left().style(Styles.outlineLabel).name("ping"); + 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 items + //core info parent.fill(t -> { - t.name = "coreitems"; - t.top().add(coreItems); - t.visible(() -> Core.settings.getBool("coreitems") && !mobile && !state.isPaused() && shown); + t.top(); + + t.name = "coreinfo"; + + t.collapser(v -> v.add().height(pauseHeight), () -> state.isPaused()).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() || !state.teams.get(player.team()).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).padTop(7); }); //spawner warning parent.fill(t -> { t.name = "nearpoint"; t.touchable = Touchable.disabled; - t.table(Styles.black, c -> c.add("@nearpoint") + 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)))) - .get().setAlignment(Align.center, Align.center)) + .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; }); - parent.fill(t -> { - t.name = "waiting"; - t.visible(() -> netServer.isWaitingForPlayers()); - t.table(Tex.button, c -> c.add("@waiting.players")); - }); - - //'core is under attack' table - parent.fill(t -> { - t.name = "coreattack"; - t.touchable = Touchable.disabled; - float notifDuration = 240f; - float[] coreAttackTime = {0}; - float[] coreAttackOpacity = {0}; - - Events.run(Trigger.teamCoreDamage, () -> { - coreAttackTime[0] = notifDuration; - }); - - t.top().visible(() -> { - if(!shown) return false; - if(state.isMenu() || !state.teams.get(player.team()).hasCore()){ - coreAttackTime[0] = 0f; - return false; - } - - t.color.a = coreAttackOpacity[0]; - if(coreAttackTime[0] > 0){ - coreAttackOpacity[0] = Mathf.lerpDelta(coreAttackOpacity[0], 1f, 0.1f); - }else{ - coreAttackOpacity[0] = Mathf.lerpDelta(coreAttackOpacity[0], 0f, 0.1f); - } - - coreAttackTime[0] -= Time.delta; - - return coreAttackOpacity[0] > 0; - }); - t.table(Tex.button, top -> top.add("@coreattack").pad(2) - .update(label -> label.color.set(Color.orange).lerp(Color.scarlet, Mathf.absin(Time.time, 2f, 1f)))).touchable(Touchable.disabled); - }); - //'saving' indicator parent.fill(t -> { t.name = "saving"; @@ -343,7 +359,6 @@ public class HudFragment extends Fragment{ //TODO DEBUG: rate table if(false) parent.fill(t -> { - t.name = "rates"; t.bottom().left(); t.table(Styles.black6, c -> { Bits used = new Bits(content.items().size); @@ -353,7 +368,7 @@ public class HudFragment extends Fragment{ for(Item item : content.items()){ if(state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1){ - c.image(item.icon(Cicon.small)); + c.image(item.uiIcon); c.label(() -> (int)state.rules.sector.info.getExport(item) + " /s").color(Color.lightGray); c.row(); } @@ -412,6 +427,10 @@ public class HudFragment extends Fragment{ } 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(() -> { @@ -424,7 +443,8 @@ public class HudFragment extends Fragment{ } }); table.margin(12); - table.image(icon).pad(3); + 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(); @@ -462,7 +482,7 @@ public class HudFragment extends Fragment{ Table in = new Table(); //create texture stack for displaying - Image image = new Image(content.icon(Cicon.xlarge)); + Image image = new Image(content.uiIcon); image.setScaling(Scaling.fit); in.add(image).size(8 * 6).pad(2); @@ -516,7 +536,7 @@ public class HudFragment extends Fragment{ //if there's space, add it if(esize < cap){ - Image image = new Image(content.icon(Cicon.medium)); + Image image = new Image(content.uiIcon); image.setScaling(Scaling.fit); lastUnlockLayout.add(image); @@ -758,6 +778,12 @@ public class HudFragment extends Fragment{ table.row(); + return table; + } + + private void addInfoTable(Table table){ + table.left(); + var count = new float[]{-1}; table.table().update(t -> { if(player.unit() instanceof Payloadc payload){ @@ -770,8 +796,29 @@ public class HudFragment extends Fragment{ t.clear(); } }).growX().visible(() -> player.unit() instanceof Payloadc p && p.payloadUsed() > 0).colspan(2); + table.row(); - return table; + 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(){ diff --git a/core/src/mindustry/ui/fragments/PlacementFragment.java b/core/src/mindustry/ui/fragments/PlacementFragment.java index cf51957878..b81f6a34f8 100644 --- a/core/src/mindustry/ui/fragments/PlacementFragment.java +++ b/core/src/mindustry/ui/fragments/PlacementFragment.java @@ -98,7 +98,7 @@ public class PlacementFragment extends Fragment{ if(Core.input.keyTap(Binding.pick) && player.isBuilder()){ //mouse eyedropper select var build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); - Block tryRecipe = build == null ? null : build instanceof ConstructBuild c ? c.cblock : build.block; + Block tryRecipe = build == null ? null : build instanceof ConstructBuild c ? c.current : build.block; Object tryConfig = build == null || !build.block.copyConfig ? null : build.config(); for(BuildPlan req : player.unit().plans()){ @@ -224,7 +224,7 @@ public class PlacementFragment extends Fragment{ blockTable.row(); } - ImageButton button = blockTable.button(new TextureRegionDrawable(block.icon(Cicon.medium)), Styles.selecti, () -> { + ImageButton button = blockTable.button(new TextureRegionDrawable(block.uiIcon), Styles.selecti, () -> { if(unlocked(block)){ if(Core.input.keyDown(KeyCode.shiftLeft) && Fonts.getUnicode(block.name) != 0){ Core.app.setClipboardText((char)Fonts.getUnicode(block.name) + ""); @@ -235,7 +235,7 @@ public class PlacementFragment extends Fragment{ } } }).size(46f).group(group).name("block-" + block.name).get(); - button.resizeImage(Cicon.medium.size); + button.resizeImage(iconMed); button.update(() -> { //color unplacable things gray Building core = player.core(); @@ -309,7 +309,7 @@ public class PlacementFragment extends Fragment{ } final String keyComboFinal = keyCombo; header.left(); - header.add(new Image(displayBlock.icon(Cicon.medium))).size(8 * 4); + header.add(new Image(displayBlock.uiIcon)).size(8 * 4); header.labelWrap(() -> !unlocked(displayBlock) ? Core.bundle.get("block.unknown") : displayBlock.localizedName + keyComboFinal) .left().width(190f).padLeft(5); header.add().growX(); @@ -328,7 +328,7 @@ public class PlacementFragment extends Fragment{ for(ItemStack stack : displayBlock.requirements){ req.table(line -> { line.left(); - line.image(stack.item.icon(Cicon.small)).size(8 * 2); + line.image(stack.item.uiIcon).size(8 * 2); line.add(stack.item.localizedName).maxWidth(140f).fillX().color(Color.lightGray).padLeft(2).left().get().setEllipsis(true); line.labelWrap(() -> { Building core = player.core(); @@ -349,7 +349,7 @@ public class PlacementFragment extends Fragment{ topTable.row(); topTable.table(b -> { b.image(Icon.cancel).padRight(2).color(Color.scarlet); - b.add(!player.isBuilder() ? "@unit.nobuild" : "@banned").width(190f).wrap(); + b.add(!player.isBuilder() ? "@unit.nobuild" : !displayBlock.supportsEnv(state.rules.environment) ? "@unsupported.environment" : "@banned").width(190f).wrap(); b.left(); }).padTop(2).left(); } @@ -471,7 +471,7 @@ public class PlacementFragment extends Fragment{ } //if the tile has a drop, display the drop - if(hoverTile.drop() != null){ + if(hoverTile.drop() != null || hoverTile.wallDrop() != null){ return hoverTile; } } diff --git a/core/src/mindustry/world/Block.java b/core/src/mindustry/world/Block.java index 1d8e1db2a9..8c480ea545 100644 --- a/core/src/mindustry/world/Block.java +++ b/core/src/mindustry/world/Block.java @@ -29,7 +29,6 @@ import mindustry.world.blocks.environment.*; import mindustry.world.blocks.power.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import java.lang.reflect.*; import java.util.*; @@ -45,6 +44,7 @@ public class Block extends UnlockableContent{ public boolean consumesPower = true; public boolean outputsPower = false; public boolean outputsPayload = false; + public boolean acceptsPayload = false; public boolean outputFacing = true; public boolean acceptsItems = false; @@ -77,6 +77,8 @@ public class Block extends UnlockableContent{ public boolean solidifes; /** whether this is rotateable */ public boolean rotate; + /** whether to draw a rotation arrow - this does not apply to lines of blocks */ + public boolean drawArrow = true; /** for static blocks only: if true, tile data() is saved in world data. */ public boolean saveData; /** whether you can break this with rightclick */ @@ -105,6 +107,8 @@ public class Block extends UnlockableContent{ public boolean noUpdateDisabled = false; /** Whether to use this block's color in the minimap. Only used for overlays. */ public boolean useColor = true; + /** item that drops from this block, used for drills */ + public @Nullable Item itemDrop = null; /** tile entity health */ public int health = -1; /** base block explosiveness */ @@ -115,8 +119,12 @@ public class Block extends UnlockableContent{ public int size = 1; /** multiblock offset */ public float offset = 0f; - /** Whether to draw this block in the expanded draw range. */ + /** Deprecated for removal: Use clipSize instead. + * This does nothing, and is kept to preserve compatibility with v6 mods. */ + @Deprecated public boolean expanded = false; + /** Clipping size of this block. Should be as large as the block will draw. */ + public float clipSize = -1f; /** Max of timers used. */ public int timers = 0; /** Cache layer. Only used for 'cached' rendering. */ @@ -144,6 +152,12 @@ public class Block extends UnlockableContent{ public boolean consumesTap; /** Whether to draw the glow of the liquid for this block, if it has one. */ public boolean drawLiquidLight = true; + /** Environmental flags that are *all* required for this block to function. 0 = any environment */ + public int envRequired = 0; + /** The environment flags that this block can function in. If the env matches any of these, it will be enabled. */ + public int envEnabled = Env.terrestrial; + /** The environment flags that this block *cannot* function in. If the env matches any of these, it will be *disabled*. */ + public int envDisabled = 0; /** Whether to periodically sync this block across the network. */ public boolean sync; /** Whether this block uses conveyor-type placement mode. */ @@ -167,7 +181,9 @@ public class Block extends UnlockableContent{ public Color outlineColor = Color.valueOf("404049"); /** Whether any icon region has an outline added. */ public boolean outlineIcon = false; - /** Which of the icon regions gets the outline added. */ + /** Outline icon radius. */ + public int outlineRadius = 4; + /** Which of the icon regions gets the outline added. Uses last icon if <= 0. */ public int outlinedIcon = -1; /** Whether this block has a shadow under it. */ public boolean hasShadow = true; @@ -179,7 +195,7 @@ public class Block extends UnlockableContent{ public Color lightColor = Color.white.cpy(); /** * Whether this environmental block passively emits light. - * Not valid for non-environmental blocks. */ + * Does not change behavior for non-environmental blocks, but still updates clipSize. */ public boolean emitLight = false; /** Radius of the light emitted by this block. */ public float lightRadius = 60f; @@ -198,19 +214,23 @@ public class Block extends UnlockableContent{ public ItemStack[] requirements = {}; /** Category in place menu. */ public Category category = Category.distribution; - /** Cost of building this block; do not modify directly! */ - public float buildCost; + /** Time to build this block in ticks; do not modify directly! */ + public float buildCost = 20f; /** Whether this block is visible and can currently be built. */ public BuildVisibility buildVisibility = BuildVisibility.hidden; /** Multiplier for speed of building this block. */ public float buildCostMultiplier = 1f; /** Build completion at which deconstruction finishes. */ public float deconstructThreshold = 0f; + /** If true, this block deconstructs immediately. Instant deconstruction implies no resource refund. */ + public boolean instantDeconstruct = false; + /** Effect for breaking the block. Passes size as rotation. */ + public Effect breakEffect = Fx.breakBlock; /** Multiplier for cost of research in tech tree. */ public float researchCostMultiplier = 1; /** Whether this block has instant transfer.*/ public boolean instantTransfer = false; - /** Whether you can rotate this block with Keybind rotateplaced + Scroll Wheel. */ + /** Whether you can rotate this block after it is placed. */ public boolean quickRotate = true; /** Main subclass. Non-anonymous. */ public @Nullable Class subclass; @@ -318,7 +338,7 @@ public class Block extends UnlockableContent{ } public TextureRegion getDisplayIcon(Tile tile){ - return tile.build == null ? icon(Cicon.medium) : tile.build.getDisplayIcon(); + return tile.build == null ? uiIcon : tile.build.getDisplayIcon(); } public String getDisplayName(Tile tile){ @@ -363,7 +383,7 @@ public class Block extends UnlockableContent{ if(canBeBuilt()){ stats.add(Stat.buildTime, buildCost / 60, StatUnit.seconds); - stats.add(Stat.buildCost, new ItemListValue(false, requirements)); + stats.add(Stat.buildCost, StatValues.items(false, requirements)); } if(instantTransfer){ @@ -405,7 +425,9 @@ public class Block extends UnlockableContent{ bars.add("items", entity -> new Bar(() -> Core.bundle.format("bar.items", entity.items.total()), () -> Pal.items, () -> (float)entity.items.total() / itemCapacity)); } - if(flags.contains(BlockFlag.unitModifier)) stats.add(Stat.maxUnits, (unitCapModifier < 0 ? "-" : "+") + Math.abs(unitCapModifier)); + if(unitCapModifier != 0){ + stats.add(Stat.maxUnits, (unitCapModifier < 0 ? "-" : "+") + Math.abs(unitCapModifier)); + } } public boolean canReplace(Block other){ @@ -470,7 +492,7 @@ public class Block extends UnlockableContent{ } public TextureRegion getRequestRegion(BuildPlan req, Eachable list){ - return icon(Cicon.full); + return fullIcon; } public void drawRequestConfig(BuildPlan req, Eachable list){ @@ -554,6 +576,11 @@ public class Block extends UnlockableContent{ return editorVariantRegions; } + /** @return special icons to outline and save with an -outline variant. Vanilla only. */ + public TextureRegion[] makeIconRegions(){ + return new TextureRegion[0]; + } + protected TextureRegion[] icons(){ //use team region in vanilla team blocks return teamRegion.found() && minfo.mod == null ? new TextureRegion[]{region, teamRegions[Team.sharded.id]} : new TextureRegion[]{region}; @@ -564,7 +591,7 @@ public class Block extends UnlockableContent{ } public TextureRegion[] variantRegions(){ - return variantRegions == null ? (variantRegions = new TextureRegion[]{icon(Cicon.full)}) : variantRegions; + return variantRegions == null ? (variantRegions = new TextureRegion[]{fullIcon}) : variantRegions; } public boolean hasBuilding(){ @@ -588,7 +615,12 @@ public class Block extends UnlockableContent{ } public boolean isPlaceable(){ - return isVisible() && (!state.rules.bannedBlocks.contains(this) || state.rules.editor); + return isVisible() && (!state.rules.bannedBlocks.contains(this) || state.rules.editor) && supportsEnv(state.rules.environment); + } + + /** @return whether this block supports a specific environment. */ + public boolean supportsEnv(int env){ + return (envEnabled & env) != 0 && (envDisabled & env) == 0 && (envRequired == 0 || (envRequired & env) == envRequired); } /** Called when building of this block begins. */ @@ -737,16 +769,25 @@ public class Block extends UnlockableContent{ health = size * size * 40; } + clipSize = Math.max(clipSize, size * tilesize); + + if(emitLight){ + clipSize = Math.max(clipSize, lightRadius * 2f); + } + if(group == BlockGroup.transportation || consumes.has(ConsumeType.item) || category == Category.distribution){ acceptsItems = true; } offset = ((size + 1) % 2) * tilesize / 2f; - buildCost = 0f; - for(ItemStack stack : requirements){ - buildCost += stack.amount * stack.item.cost; + if(requirements.length > 0){ + buildCost = 0f; + for(ItemStack stack : requirements){ + buildCost += stack.amount * stack.item.cost; + } } + buildCost *= buildCostMultiplier; if(consumes.has(ConsumeType.power)) hasPower = true; @@ -772,9 +813,10 @@ public class Block extends UnlockableContent{ } } - @CallSuper @Override public void load(){ + super.load(); + region = Core.atlas.find(name); ContentRegions.loadRegions(this); @@ -795,62 +837,41 @@ public class Block extends UnlockableContent{ public void createIcons(MultiPacker packer){ super.createIcons(packer); - packer.add(PageType.editor, name + "-icon-editor", Core.atlas.getPixmap((AtlasRegion)icon(Cicon.full))); - if(!synthetic()){ - PixmapRegion image = Core.atlas.getPixmap((AtlasRegion)icon(Cicon.full)); - mapColor.set(image.getPixel(image.width/2, image.height/2)); + PixmapRegion image = Core.atlas.getPixmap(fullIcon); + mapColor.set(image.get(image.width/2, image.height/2)); } - getGeneratedIcons(); - Pixmap last = null; + var gen = icons(); + if(outlineIcon){ - final int radius = 4; - PixmapRegion region = Core.atlas.getPixmap(getGeneratedIcons()[outlinedIcon >= 0 ? outlinedIcon : getGeneratedIcons().length -1]); - Pixmap out = new Pixmap(region.width, region.height); - Color color = new Color(); - for(int x = 0; x < region.width; x++){ - for(int y = 0; y < region.height; y++){ - - region.getPixel(x, y, color); - out.draw(x, y, color); - if(color.a < 1f){ - boolean found = false; - outer: - for(int rx = -radius; rx <= radius; rx++){ - for(int ry = -radius; ry <= radius; ry++){ - if(Structs.inBounds(rx + x, ry + y, region.width, region.height) && Mathf.within(rx, ry, radius) && color.set(region.getPixel(rx + x, ry + y)).a > 0.01f){ - found = true; - break outer; - } - } - } - if(found){ - out.draw(x, y, outlineColor); - } - } - } + PixmapRegion region = Core.atlas.getPixmap(gen[outlinedIcon >= 0 ? outlinedIcon : gen.length -1]); + Pixmap out = last = Pixmaps.outline(region, outlineColor, outlineRadius); + if(Core.settings.getBool("linear")){ + Pixmaps.bleed(out); } - last = out; - packer.add(PageType.main, name, out); } - if(generatedIcons.length > 1){ - Pixmap base = Core.atlas.getPixmap(generatedIcons[0]).crop(); - for(int i = 1; i < generatedIcons.length; i++){ - if(i == generatedIcons.length - 1 && last != null){ - base.drawPixmap(last); + var editorBase = Core.atlas.getPixmap(fullIcon); + + if(gen.length > 1){ + Pixmap base = Core.atlas.getPixmap(gen[0]).crop(); + for(int i = 1; i < gen.length; i++){ + if(i == gen.length - 1 && last != null){ + base.draw(last, 0, 0, true); }else{ - base.draw(Core.atlas.getPixmap(generatedIcons[i])); + base.draw(Core.atlas.getPixmap(gen[i]), true); } } packer.add(PageType.main, "block-" + name + "-full", base); - generatedIcons = null; - Arrays.fill(cicons, null); + + editorBase = new PixmapRegion(base); } + + packer.add(PageType.editor, name + "-icon-editor", editorBase); } } diff --git a/core/src/mindustry/world/Build.java b/core/src/mindustry/world/Build.java index f3eb5a40e4..b873546340 100644 --- a/core/src/mindustry/world/Build.java +++ b/core/src/mindustry/world/Build.java @@ -35,6 +35,13 @@ public class Build{ int rotation = tile.build != null ? tile.build.rotation : 0; Block previous = tile.block(); + + //instantly deconstruct if necessary + if(previous.instantDeconstruct){ + ConstructBlock.deconstructFinish(tile, previous, unit); + return; + } + Block sub = ConstructBlock.get(previous.size); Seq prevBuild = new Seq<>(1); @@ -75,6 +82,14 @@ public class Build{ return; } + //break all props in the way + tile.getLinkedTilesAs(result, out -> { + if(out.block != Blocks.air && out.block.alwaysReplace){ + out.block.breakEffect.at(out.drawx(), out.drawy(), out.block.size, out.block.mapColor); + out.remove(); + } + }); + Block previous = tile.block(); Block sub = ConstructBlock.get(result.size); Seq prevBuild = new Seq<>(9); @@ -159,7 +174,7 @@ public class Build{ //this could be buggy and abuse-able, so I'm not enabling it yet //note that this requires a change in BuilderComp as well //(type == check.block() && check.centerX() == x && check.centerY() == y && check.build != null && check.build.health < check.build.maxHealth - 0.0001f) || - (check.build instanceof ConstructBuild build && build.cblock == type && check.centerX() == tile.x && check.centerY() == tile.y)) && //same type in construction + (check.build instanceof ConstructBuild build && build.current == type && check.centerX() == tile.x && check.centerY() == tile.y)) && //same type in construction type.bounds(tile.x, tile.y, Tmp.r1).grow(0.01f).contains(check.block.bounds(check.centerX(), check.centerY(), Tmp.r2))) || //no replacement (type.requiresWater && check.floor().liquidDrop != Liquids.water) //requires water but none found ) return false; diff --git a/core/src/mindustry/world/Tile.java b/core/src/mindustry/world/Tile.java index 79abb65ea3..b231cd2079 100644 --- a/core/src/mindustry/world/Tile.java +++ b/core/src/mindustry/world/Tile.java @@ -1,5 +1,6 @@ package mindustry.world; +import arc.*; import arc.func.*; import arc.math.*; import arc.math.geom.*; @@ -11,6 +12,7 @@ import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.game.*; +import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; @@ -19,7 +21,9 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; public class Tile implements Position, QuadTreeObject, Displayable{ - static final ObjectSet tileSet = new ObjectSet<>(); + private static final TileChangeEvent tileChange = new TileChangeEvent(); + private static final TilePreChangeEvent preChange = new TilePreChangeEvent(); + private static final ObjectSet tileSet = new ObjectSet<>(); /** Extra data for very specific blocks. */ public byte data; @@ -108,16 +112,18 @@ public class Tile implements Position, QuadTreeObject, Displayable{ * Takes flammability of floor liquid into account. */ public float getFlammability(){ - if(!block.hasItems){ - if(floor.liquidDrop != null && !block.solid){ - return floor.liquidDrop.flammability; - } + if(block == Blocks.air){ + if(floor.liquidDrop != null) return floor.liquidDrop.flammability; return 0; }else if(build != null){ - float result = build.items.sum((item, amount) -> item.flammability * amount); + float result = 0f; + + if(block.hasItems){ + result += build.items.sum((item, amount) -> item.flammability * amount) / block.itemCapacity * Mathf.clamp(block.itemCapacity / 2.4f, 1f, 3f); + } if(block.hasLiquids){ - result += build.liquids.sum((liquid, amount) -> liquid.flammability * amount / 3f); + result += build.liquids.sum((liquid, amount) -> liquid.flammability * amount / 1.6f) / block.liquidCapacity * Mathf.clamp(block.liquidCapacity / 30f, 1f, 2f); } return result; @@ -166,6 +172,7 @@ public class Tile implements Position, QuadTreeObject, Displayable{ return build == null ? Team.derelict : build.team; } + /** Do not call unless you know what you are doing! This does not update the indexer! */ public void setTeam(Team team){ if(build != null){ build.team(team); @@ -197,10 +204,12 @@ public class Tile implements Position, QuadTreeObject, Displayable{ if(type.isStatic() || this.block.isStatic()){ recache(); + recacheWall(); } - this.block = type; preChanged(); + + this.block = type; changeBuild(team, entityprov, (byte)Mathf.mod(rotation, 4)); if(build != null){ @@ -287,10 +296,17 @@ public class Tile implements Position, QuadTreeObject, Displayable{ circle(radius, (x, y) -> cons.get(world.rawTile(x, y))); } + public void recacheWall(){ + if(!headless && !world.isGenerating()){ + renderer.blocks.recacheWall(this); + } + } + public void recache(){ if(!headless && !world.isGenerating()){ renderer.blocks.floor.recacheTile(this); renderer.minimap.update(this); + //update neighbor tiles as well for(int i = 0; i < 8; i++){ Tile other = world.tile(x + Geometry.d8[i].x, y + Geometry.d8[i].y); if(other != null){ @@ -487,6 +503,13 @@ public class Tile implements Position, QuadTreeObject, Displayable{ return overlay == Blocks.air || overlay.itemDrop == null ? floor.itemDrop : overlay.itemDrop; } + public @Nullable Item wallDrop(){ + return block.solid ? + block.itemDrop != null ? block.itemDrop : + overlay instanceof WallOreBlock ? overlay.itemDrop : + null : null; + } + public int staticDarkness(){ return block.solid && block.fillsTile && !block.synthetic() ? data : 0; } @@ -497,6 +520,10 @@ public class Tile implements Position, QuadTreeObject, Displayable{ } protected void preChanged(){ + if(!world.isGenerating()){ + Events.fire(preChange.set(this)); + } + if(build != null){ //only call removed() for the center block - this only gets called once. build.onRemoved(); @@ -514,6 +541,9 @@ public class Tile implements Position, QuadTreeObject, Displayable{ if(other != null){ //reset entity and block *manually* - thus, preChanged() will not be called anywhere else, for multiblocks if(other != this){ //do not remove own entity so it can be processed in changed() + //manually call pre-change event for other tile + Events.fire(preChange.set(other)); + other.build = null; other.block = Blocks.air; @@ -525,12 +555,6 @@ public class Tile implements Position, QuadTreeObject, Displayable{ } } } - - - //recache when static blocks get changed - if(block.isStatic()){ - recache(); - } } protected void changeBuild(Team team, Prov entityprov, int rotation){ @@ -584,12 +608,18 @@ public class Tile implements Position, QuadTreeObject, Displayable{ } protected void fireChanged(){ - world.notifyChanged(this); + if(!world.isGenerating()){ + Events.fire(tileChange.set(this)); + } } @Override public void display(Table table){ - Block toDisplay = overlay.itemDrop != null ? overlay : floor; + + Block toDisplay = + block.itemDrop != null ? block : + overlay.itemDrop != null || wallDrop() != null ? overlay : + floor; table.table(t -> { t.left(); @@ -635,8 +665,7 @@ public class Tile implements Position, QuadTreeObject, Displayable{ @Remote(called = Loc.server) public static void setTeam(Building build, Team team){ if(build != null){ - build.team = team; - indexer.updateIndices(build.tile); + build.changeTeam(team); } } diff --git a/core/src/mindustry/world/blocks/Attributes.java b/core/src/mindustry/world/blocks/Attributes.java index 38a39bd2e3..25dd3b51bb 100644 --- a/core/src/mindustry/world/blocks/Attributes.java +++ b/core/src/mindustry/world/blocks/Attributes.java @@ -7,27 +7,31 @@ import mindustry.world.meta.*; import java.util.*; public class Attributes implements JsonSerializable{ - private final float[] arr = new float[Attribute.all.length]; + private float[] arr = new float[Attribute.all.length]; public void clear(){ Arrays.fill(arr, 0); } public float get(Attribute attr){ - return arr[attr.ordinal()]; + check(); + return arr[attr.id]; } public void set(Attribute attr, float value){ - arr[attr.ordinal()] = value; + check(); + arr[attr.id] = value; } public void add(Attributes other){ + check(); for(int i = 0; i < arr.length; i++){ arr[i] += other.arr[i]; } } public void add(Attributes other, float scl){ + check(); for(int i = 0; i < arr.length; i++){ arr[i] += other.arr[i] * scl; } @@ -35,17 +39,23 @@ public class Attributes implements JsonSerializable{ @Override public void write(Json json){ + check(); for(Attribute at : Attribute.all){ - if(arr[at.ordinal()] != 0){ - json.writeValue(at.name(), arr[at.ordinal()]); + if(arr[at.id] != 0){ + json.writeValue(at.name, arr[at.id]); } } } @Override public void read(Json json, JsonValue data){ + check(); for(Attribute at : Attribute.all){ - arr[at.ordinal()] = data.getFloat(at.name(), 0); + arr[at.id] = data.getFloat(at.name, 0); } } + + private void check(){ + if(arr.length != Attribute.all.length) arr = new float[Attribute.all.length]; + } } diff --git a/core/src/mindustry/world/blocks/ConstructBlock.java b/core/src/mindustry/world/blocks/ConstructBlock.java index 112569dd71..5e31f6df00 100644 --- a/core/src/mindustry/world/blocks/ConstructBlock.java +++ b/core/src/mindustry/world/blocks/ConstructBlock.java @@ -16,8 +16,8 @@ import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.modules.*; @@ -54,7 +54,7 @@ public class ConstructBlock extends Block{ @Remote(called = Loc.server) public static void deconstructFinish(Tile tile, Block block, Unit builder){ Team team = tile.team(); - Fx.breakBlock.at(tile.drawx(), tile.drawy(), block.size); + block.breakEffect.at(tile.drawx(), tile.drawy(), block.size, block.mapColor); Events.fire(new BlockBuildEndEvent(tile, builder, team, true, null)); tile.remove(); if(shouldPlay()) Sounds.breaks.at(tile, calcPitch(false)); @@ -133,43 +133,36 @@ public class ConstructBlock extends Block{ } public class ConstructBuild extends Building{ - /** - * The recipe of the block that is being constructed. - * If there is no recipe for this block, as is the case with rocks, 'previous' is used. - */ - public @Nullable Block cblock; + /** The recipe of the block that is being (de)constructed. Never null. */ + public Block current = Blocks.air; + /** The block that used to be here. Never null. */ + public Block previous = Blocks.air; + /** Buildings that previously occupied this location. */ public @Nullable Seq prevBuild; public float progress = 0; public float buildCost; - /** - * The block that used to be here. - * If a non-recipe block is being deconstructed, this is the block that is being deconstructed. - */ - public Block previous; public @Nullable Object lastConfig; + public @Nullable Unit lastBuilder; public boolean wasConstructing, activeDeconstruct; public float constructColor; - @Nullable - public Unit lastBuilder; - private float[] accumulator; private float[] totalAccumulator; @Override public String getDisplayName(){ - return Core.bundle.format("block.constructing", cblock == null ? previous.localizedName : cblock.localizedName); + return Core.bundle.format("block.constructing", current.localizedName); } @Override public TextureRegion getDisplayIcon(){ - return (cblock == null ? previous : cblock).icon(Cicon.full); + return current.fullIcon; } @Override public boolean checkSolid(){ - return (cblock != null && cblock.solid) || previous == null || previous.solid; + return current.solid || previous.solid; } @Override @@ -180,14 +173,20 @@ public class ConstructBlock extends Block{ @Override public void tapped(){ //if the target is constructable, begin constructing - if(cblock != null){ + if(current.isPlaceable()){ if(control.input.buildWasAutoPaused && !control.input.isBuilding && player.isBuilder()){ control.input.isBuilding = true; } - player.unit().addBuild(new BuildPlan(tile.x, tile.y, rotation, cblock, lastConfig), false); + player.unit().addBuild(new BuildPlan(tile.x, tile.y, rotation, current, lastConfig), false); } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(progress); + return super.sense(sensor); + } + @Override public void onDestroyed(){ Fx.blockExplosionSmoke.at(tile); @@ -199,29 +198,33 @@ public class ConstructBlock extends Block{ @Override public void updateTile(){ + //auto-remove air blocks + if(current == Blocks.air){ + remove(); + } + constructColor = Mathf.lerpDelta(constructColor, activeDeconstruct ? 1f : 0f, 0.2f); activeDeconstruct = false; } @Override public void draw(){ - if(!(previous == null || cblock == null || previous == cblock) && Core.atlas.isFound(previous.icon(Cicon.full))){ - Draw.rect(previous.icon(Cicon.full), x, y, previous.rotate ? rotdeg() : 0); + //do not draw air + if(current == Blocks.air) return; + + if(previous != current && previous != Blocks.air && previous.fullIcon.found()){ + Draw.rect(previous.fullIcon, x, y, previous.rotate ? rotdeg() : 0); } Draw.draw(Layer.blockBuilding, () -> { Draw.color(Pal.accent, Pal.remove, constructColor); - Block target = cblock == null ? previous : cblock; + for(TextureRegion region : current.getGeneratedIcons()){ + Shaders.blockbuild.region = region; + Shaders.blockbuild.progress = progress; - if(target != null){ - for(TextureRegion region : target.getGeneratedIcons()){ - Shaders.blockbuild.region = region; - Shaders.blockbuild.progress = progress; - - Draw.rect(region, x, y, target.rotate ? rotdeg() : 0); - Draw.flush(); - } + Draw.rect(region, x, y, current.rotate ? rotdeg() : 0); + Draw.flush(); } Draw.color(); @@ -231,10 +234,6 @@ public class ConstructBlock extends Block{ public void construct(Unit builder, @Nullable Building core, float amount, Object config){ wasConstructing = true; activeDeconstruct = false; - if(cblock == null){ - kill(); - return; - } if(builder.isPlayer()){ lastBuilder = builder; @@ -242,14 +241,14 @@ public class ConstructBlock extends Block{ lastConfig = config; - if(cblock.requirements.length != accumulator.length || totalAccumulator.length != cblock.requirements.length){ - setConstruct(previous, cblock); + if(current.requirements.length != accumulator.length || totalAccumulator.length != current.requirements.length){ + setConstruct(previous, current); } float maxProgress = core == null || team.rules().infiniteResources ? amount : checkRequired(core.items, amount, false); - for(int i = 0; i < cblock.requirements.length; i++){ - int reqamount = Math.round(state.rules.buildCostMultiplier * cblock.requirements[i].amount); + for(int i = 0; i < current.requirements.length; i++){ + int reqamount = Math.round(state.rules.buildCostMultiplier * current.requirements[i].amount); accumulator[i] += Math.min(reqamount * maxProgress, reqamount - totalAccumulator[i] + 0.00001f); //add min amount progressed to the accumulator totalAccumulator[i] = Math.min(totalAccumulator[i] + reqamount * maxProgress, reqamount); } @@ -260,16 +259,17 @@ public class ConstructBlock extends Block{ if(progress >= 1f || state.rules.infiniteResources){ if(lastBuilder == null) lastBuilder = builder; - constructed(tile, cblock, lastBuilder, (byte)rotation, builder.team, config); + constructed(tile, current, lastBuilder, (byte)rotation, builder.team, config); } } - public void deconstruct(Unit builder, @Nullable Building core, float amount){ + public void deconstruct(Unit builder, @Nullable CoreBuild core, float amount){ //reset accumulated resources when switching modes if(wasConstructing){ Arrays.fill(accumulator, 0); Arrays.fill(totalAccumulator, 0); } + wasConstructing = false; activeDeconstruct = true; float deconstructMultiplier = state.rules.deconstructRefundMultiplier; @@ -278,55 +278,53 @@ public class ConstructBlock extends Block{ lastBuilder = builder; } - if(cblock != null){ - ItemStack[] requirements = cblock.requirements; - if(requirements.length != accumulator.length || totalAccumulator.length != requirements.length){ - setDeconstruct(cblock); - } + ItemStack[] requirements = current.requirements; + if(requirements.length != accumulator.length || totalAccumulator.length != requirements.length){ + setDeconstruct(current); + } - //make sure you take into account that you can't deconstruct more than there is deconstructed - float clampedAmount = Math.min(amount, progress); + //make sure you take into account that you can't deconstruct more than there is deconstructed + float clampedAmount = Math.min(amount, progress); - for(int i = 0; i < requirements.length; i++){ - int reqamount = Math.round(state.rules.buildCostMultiplier * requirements[i].amount); - accumulator[i] += Math.min(clampedAmount * deconstructMultiplier * reqamount, deconstructMultiplier * reqamount - totalAccumulator[i]); //add scaled amount progressed to the accumulator - totalAccumulator[i] = Math.min(totalAccumulator[i] + reqamount * clampedAmount * deconstructMultiplier, reqamount); + for(int i = 0; i < requirements.length; i++){ + int reqamount = Math.round(state.rules.buildCostMultiplier * requirements[i].amount); + accumulator[i] += Math.min(clampedAmount * deconstructMultiplier * reqamount, deconstructMultiplier * reqamount - totalAccumulator[i]); //add scaled amount progressed to the accumulator + totalAccumulator[i] = Math.min(totalAccumulator[i] + reqamount * clampedAmount * deconstructMultiplier, reqamount); - int accumulated = (int)(accumulator[i]); //get amount + int accumulated = (int)(accumulator[i]); //get amount - if(clampedAmount > 0 && accumulated > 0){ //if it's positive, add it to the core - if(core != null && requirements[i].item.unlockedNow()){ //only accept items that are unlocked - int accepting = Math.min(accumulated, ((CoreBuild)core).storageCapacity - core.items.get(requirements[i].item)); - //transfer items directly, as this is not production. - core.items.add(requirements[i].item, accepting); - accumulator[i] -= accepting; - }else{ - accumulator[i] -= accumulated; - } + if(clampedAmount > 0 && accumulated > 0){ //if it's positive, add it to the core + if(core != null && requirements[i].item.unlockedNow()){ //only accept items that are unlocked + int accepting = Math.min(accumulated, core.storageCapacity - core.items.get(requirements[i].item)); + //transfer items directly, as this is not production. + core.items.add(requirements[i].item, accepting); + accumulator[i] -= accepting; + }else{ + accumulator[i] -= accumulated; } } } progress = Mathf.clamp(progress - amount); - if(progress <= (previous == null ? 0 : previous.deconstructThreshold) || state.rules.infiniteResources){ + if(progress <= current.deconstructThreshold || state.rules.infiniteResources){ if(lastBuilder == null) lastBuilder = builder; - Call.deconstructFinish(tile, this.cblock == null ? previous : this.cblock, lastBuilder); + Call.deconstructFinish(tile, this.current, lastBuilder); } } private float checkRequired(ItemModule inventory, float amount, boolean remove){ float maxProgress = amount; - for(int i = 0; i < cblock.requirements.length; i++){ - int sclamount = Math.round(state.rules.buildCostMultiplier * cblock.requirements[i].amount); + for(int i = 0; i < current.requirements.length; i++){ + int sclamount = Math.round(state.rules.buildCostMultiplier * current.requirements[i].amount); int required = (int)(accumulator[i]); //calculate items that are required now - if(inventory.get(cblock.requirements[i].item) == 0 && sclamount != 0){ + if(inventory.get(current.requirements[i].item) == 0 && sclamount != 0){ maxProgress = 0f; }else if(required > 0){ //if this amount is positive... //calculate how many items it can actually use - int maxUse = Math.min(required, inventory.get(cblock.requirements[i].item)); + int maxUse = Math.min(required, inventory.get(current.requirements[i].item)); //get this as a fraction float fraction = maxUse / (float)required; @@ -337,7 +335,7 @@ public class ConstructBlock extends Block{ //remove stuff that is actually used if(remove){ - inventory.remove(cblock.requirements[i].item, maxUse); + inventory.remove(current.requirements[i].item, maxUse); } } //else, no items are required yet, so just keep going @@ -351,13 +349,15 @@ public class ConstructBlock extends Block{ } public void setConstruct(Block previous, Block block){ + if(block == null) return; + this.constructColor = 0f; this.wasConstructing = true; - this.cblock = block; + this.current = block; this.previous = previous; + this.buildCost = block.buildCost * state.rules.buildCostMultiplier; this.accumulator = new float[block.requirements.length]; this.totalAccumulator = new float[block.requirements.length]; - this.buildCost = block.buildCost * state.rules.buildCostMultiplier; } public void setDeconstruct(Block previous){ @@ -367,12 +367,8 @@ public class ConstructBlock extends Block{ this.wasConstructing = false; this.previous = previous; this.progress = 1f; - if(previous.buildCost >= 0.01f){ - this.cblock = previous; - this.buildCost = previous.buildCost * state.rules.buildCostMultiplier; - }else{ - this.buildCost = 20f; //default no-requirement build cost is 20 - } + this.current = previous; + this.buildCost = previous.buildCost * state.rules.buildCostMultiplier; this.accumulator = new float[previous.requirements.length]; this.totalAccumulator = new float[previous.requirements.length]; } @@ -381,8 +377,8 @@ public class ConstructBlock extends Block{ public void write(Writes write){ super.write(write); write.f(progress); - write.s(previous == null ? -1 : previous.id); - write.s(cblock == null ? -1 : cblock.id); + write.s(previous.id); + write.s(current.id); if(accumulator == null){ write.b(-1); @@ -413,13 +409,12 @@ public class ConstructBlock extends Block{ } if(pid != -1) previous = content.block(pid); - if(rid != -1) cblock = content.block(rid); + if(rid != -1) current = content.block(rid); - if(cblock != null){ - buildCost = cblock.buildCost * state.rules.buildCostMultiplier; - }else{ - buildCost = 20f; - } + if(previous == null) previous = Blocks.air; + if(current == null) current = Blocks.air; + + buildCost = current.buildCost * state.rules.buildCostMultiplier; } } } diff --git a/core/src/mindustry/world/blocks/ItemSelection.java b/core/src/mindustry/world/blocks/ItemSelection.java index f2c0b73084..523fb01325 100644 --- a/core/src/mindustry/world/blocks/ItemSelection.java +++ b/core/src/mindustry/world/blocks/ItemSelection.java @@ -34,7 +34,7 @@ public class ItemSelection{ if(closeSelect) control.input.frag.config.hideConfig(); }).group(group).get(); button.changed(() -> consumer.get(button.isChecked() ? item : null)); - button.getStyle().imageUp = new TextureRegionDrawable(item.icon(Cicon.small)); + button.getStyle().imageUp = new TextureRegionDrawable(item.uiIcon); button.update(() -> button.setChecked(holder.get() == item)); if(i++ % 4 == 3){ diff --git a/core/src/mindustry/world/blocks/campaign/Accelerator.java b/core/src/mindustry/world/blocks/campaign/Accelerator.java index 7160c6291a..6ca4ee3f0a 100644 --- a/core/src/mindustry/world/blocks/campaign/Accelerator.java +++ b/core/src/mindustry/world/blocks/campaign/Accelerator.java @@ -109,7 +109,10 @@ public class Accelerator extends Block{ if(!state.isCampaign() || !consValid()) return; - ui.showInfo("@indev.campaign"); + ui.planet.showPlanetLaunch(state.rules.sector, sector -> { + //TODO cutscene, etc... + consume(); + }); Events.fire(Trigger.acceleratorUse); } diff --git a/core/src/mindustry/world/blocks/campaign/LaunchPad.java b/core/src/mindustry/world/blocks/campaign/LaunchPad.java index f8f3ce53b0..03f7343044 100644 --- a/core/src/mindustry/world/blocks/campaign/LaunchPad.java +++ b/core/src/mindustry/world/blocks/campaign/LaunchPad.java @@ -1,9 +1,9 @@ package mindustry.world.blocks.campaign; import arc.*; -import arc.audio.*; import arc.Graphics.*; import arc.Graphics.Cursor.*; +import arc.audio.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; @@ -11,12 +11,14 @@ import arc.math.geom.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; +import arc.util.io.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.*; @@ -26,9 +28,8 @@ import mindustry.world.meta.*; import static mindustry.Vars.*; public class LaunchPad extends Block{ - public final int timerLaunch = timers++; /** Time inbetween launches. */ - public float launchTime; + public float launchTime = 1f; public Sound launchSound = Sounds.none; public @Load("@-light") TextureRegion lightRegion; @@ -42,6 +43,7 @@ public class LaunchPad extends Block{ update = true; configurable = true; drawDisabled = false; + flags = EnumSet.of(BlockFlag.launchPad); } @Override @@ -64,6 +66,7 @@ public class LaunchPad extends Block{ } public class LaunchPadBuild extends Building{ + public float launchCounter; @Override public Cursor getCursor(){ @@ -81,6 +84,12 @@ public class LaunchPad extends Block{ return true; } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(launchCounter / launchTime); + return super.sense(sensor); + } + @Override public void draw(){ super.draw(); @@ -89,7 +98,7 @@ public class LaunchPad extends Block{ if(lightRegion.found()){ Draw.color(lightColor); - float progress = Math.min((float)items.total() / itemCapacity, timer.getTime(timerLaunch) / (launchTime / timeScale)); + float progress = Math.min((float)items.total() / itemCapacity, launchCounter / launchTime); int steps = 3; float step = 1f; @@ -106,7 +115,7 @@ public class LaunchPad extends Block{ Draw.reset(); } - float cooldown = Mathf.clamp(timer.getTime(timerLaunch) / (90f / timeScale)); + float cooldown = Mathf.clamp(launchCounter / (90f)); Draw.mixcol(lightColor, 1f - cooldown); @@ -125,7 +134,7 @@ public class LaunchPad extends Block{ if(!state.isCampaign()) return; //launch when full and base conditions are met - if(items.total() >= itemCapacity && efficiency() >= 1f && timer(timerLaunch, launchTime / timeScale)){ + if(items.total() >= itemCapacity && efficiency() >= 1f && (launchCounter += edelta()) >= launchTime){ launchSound.at(x, y); LaunchPayload entity = LaunchPayload.create(); items.each((item, amount) -> entity.stacks.add(new ItemStack(item, amount))); @@ -136,6 +145,7 @@ public class LaunchPad extends Block{ Fx.launchPod.at(this); items.clear(); Effect.shake(3f, 3f, this); + launchCounter = 0f; } } @@ -171,6 +181,25 @@ public class LaunchPad extends Block{ deselect(); }).size(40f); } + + @Override + public byte version(){ + return 1; + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(launchCounter); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + if(revision >= 1){ + launchCounter = read.f(); + } + } } @EntityDef(LaunchPayloadc.class) diff --git a/core/src/mindustry/world/blocks/campaign/PayloadLaunchPad.java b/core/src/mindustry/world/blocks/campaign/PayloadLaunchPad.java new file mode 100644 index 0000000000..e5acad5bf7 --- /dev/null +++ b/core/src/mindustry/world/blocks/campaign/PayloadLaunchPad.java @@ -0,0 +1,280 @@ +package mindustry.world.blocks.campaign; + +import arc.*; +import arc.Graphics.*; +import arc.Graphics.Cursor.*; +import arc.audio.*; +import arc.graphics.*; +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.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.logic.*; +import mindustry.type.*; +import mindustry.ui.*; +import mindustry.world.blocks.payloads.*; +import mindustry.world.consumers.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +//TODO very much unfinished +public class PayloadLaunchPad extends PayloadBlock{ + /** Time between launches. */ + public float launchTime; + public Sound launchSound = Sounds.none; + + public @Load("@-light") TextureRegion lightRegion; + public @Load(value = "@-pod", fallback = "launchpod") TextureRegion podRegion; + public Color lightColor = Color.valueOf("eab678"); + + public PayloadLaunchPad(String name){ + super(name); + solid = true; + update = true; + configurable = true; + drawDisabled = false; + flags = EnumSet.of(BlockFlag.launchPad); + acceptsPayload = true; + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.launchTime, launchTime / 60f, StatUnit.seconds); + } + + public class PayloadLaunchPadBuild extends PayloadBlockBuild{ + public float launchCounter; + + @Override + public Cursor getCursor(){ + return !state.isCampaign() || net.client() ? SystemCursor.arrow : super.getCursor(); + } + + //cannot be disabled + @Override + public float efficiency(){ + return power != null && (block.consumes.has(ConsumeType.power) && !block.consumes.getPower().buffered) ? power.status : 1f; + } + + @Override + public boolean shouldConsume(){ + return true; + } + + @Override + public void draw(){ + super.draw(); + + if(!state.isCampaign()) return; + + if(lightRegion.found()){ + Draw.color(lightColor); + float progress = launchCounter / launchTime; + int steps = 3; + float step = 1f; + + for(int i = 0; i < 4; i++){ + for(int j = 0; j < steps; j++){ + float alpha = Mathf.curve(progress, (float)j / steps, (j+1f) / steps); + float offset = -(j - 1f) * step; + + Draw.color(Pal.metalGrayDark, lightColor, alpha); + Draw.rect(lightRegion, x + Geometry.d8edge(i).x * offset, y + Geometry.d8edge(i).y * offset, i * 90); + } + } + + Draw.reset(); + } + + drawPayload(); + + float cooldown = Mathf.clamp(launchCounter / (90f)); + + Draw.mixcol(lightColor, 1f - cooldown); + + Draw.rect(podRegion, x, y); + + Draw.reset(); + } + + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(launchCounter / launchTime); + return super.sense(sensor); + } + + @Override + public void updateTile(){ + if(!state.isCampaign()) return; + + //launch when full and base conditions are met + if(payload != null && moveInPayload() && efficiency() >= 1f && (launchCounter += edelta()) >= launchTime){ + launchSound.at(x, y); + LargeLaunchPayload entity = LargeLaunchPayload.create(); + entity.payload = payload; + entity.set(this); + entity.lifetime(120f); + entity.team(team); + entity.add(); + Fx.launchPod.at(this); + payload = null; + Effect.shake(3f, 3f, this); + launchCounter = 0f; + } + } + + @Override + public void display(Table table){ + super.display(table); + + if(!state.isCampaign()) return; + + table.row(); + table.label(() -> { + Sector dest = state.rules.sector == null ? null : state.rules.sector.info.getRealDestination(); + + return Core.bundle.format("launch.destination", + dest == null ? Core.bundle.get("sectors.nonelaunch") : + "[accent]" + dest.name()); + }).pad(4).wrap().width(200f).left(); + } + + @Override + public void buildConfiguration(Table table){ + if(!state.isCampaign() || net.client()){ + deselect(); + return; + } + + table.button(Icon.upOpen, Styles.clearTransi, () -> { + ui.planet.showSelect(state.rules.sector, other -> { + if(state.isCampaign()){ + state.rules.sector.info.destination = other; + } + }); + deselect(); + }).size(40f); + } + + @Override + public byte version(){ + return 1; + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(launchCounter); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + if(revision >= 1){ + launchCounter = read.f(); + } + } + } + + @EntityDef(LargeLaunchPayloadc.class) + @Component(base = true) + static abstract class LargeLaunchPayloadComp implements Drawc, Timedc, Teamc{ + @Import float x,y; + + Payload payload; + transient Interval in = new Interval(); + + @Override + public void draw(){ + float alpha = fout(Interp.pow5Out); + float scale = (1f - alpha) * 1.3f + 1f; + float cx = cx(), cy = cy(); + float rotation = fin() * (130f + Mathf.randomSeedRange(id(), 50f)); + + Draw.z(Layer.effect + 0.001f); + + Draw.color(Pal.engine); + + float rad = 0.2f + fslope(); + + Fill.light(cx, cy, 10, 25f * (rad + scale-1f), Tmp.c2.set(Pal.engine).a(alpha), Tmp.c1.set(Pal.engine).a(0f)); + + Draw.alpha(alpha); + for(int i = 0; i < 4; i++){ + Drawf.tri(cx, cy, 6f, 40f * (rad + scale-1f), i * 90f + rotation); + } + + Draw.color(); + + Draw.z(Layer.weather - 1); + + TextureRegion region = blockOn() instanceof mindustry.world.blocks.campaign.PayloadLaunchPad p ? p.podRegion : Core.atlas.find("launchpod"); + float rw = region.width * Draw.scl * scale, rh = region.height * Draw.scl * scale; + + Draw.alpha(alpha); + Draw.rect(region, cx, cy, rw, rh, rotation); + + Tmp.v1.trns(225f, fin(Interp.pow3In) * 250f); + + Draw.z(Layer.flyingUnit + 1); + Draw.color(0, 0, 0, 0.22f * alpha); + Draw.rect(region, cx + Tmp.v1.x, cy + Tmp.v1.y, rw, rh, rotation); + + Draw.reset(); + } + + float cx(){ + return x + fin(Interp.pow2In) * (12f + Mathf.randomSeedRange(id() + 3, 4f)); + } + + float cy(){ + return y + fin(Interp.pow5In) * (100f + Mathf.randomSeedRange(id() + 2, 30f)); + } + + @Override + public void update(){ + float r = 3f; + if(in.get(4f - fin()*2f)){ + Fx.rocketSmoke.at(cx() + Mathf.range(r), cy() + Mathf.range(r), fin()); + } + } + + @Override + public void remove(){ + if(!state.isCampaign()) return; + + Sector destsec = state.rules.sector.info.getRealDestination(); + + //actually launch the items upon removal + if(team() == state.rules.defaultTeam){ + if(destsec != null && (destsec != state.rules.sector || net.client())){ + /* + ItemSeq dest = new ItemSeq(); + + for(ItemStack stack : stacks){ + dest.add(stack); + + //update export + state.rules.sector.info.handleItemExport(stack); + Events.fire(new LaunchItemEvent(stack)); + } + + if(!net.client()){ + destsec.addItems(dest); + }*/ + } + } + } + } +} diff --git a/core/src/mindustry/world/blocks/defense/ForceProjector.java b/core/src/mindustry/world/blocks/defense/ForceProjector.java index 8a11429eec..5c464f59c9 100644 --- a/core/src/mindustry/world/blocks/defense/ForceProjector.java +++ b/core/src/mindustry/world/blocks/defense/ForceProjector.java @@ -31,7 +31,6 @@ public class ForceProjector extends Block{ public float cooldownNormal = 1.75f; public float cooldownLiquid = 1.5f; public float cooldownBrokenBase = 0.35f; - public float basePowerDraw = 0.2f; public @Load("@-top") TextureRegion topRegion; static ForceBuild paramEntity; @@ -40,7 +39,7 @@ public class ForceProjector extends Block{ trait.absorb(); Fx.absorb.at(trait); paramEntity.hit = 1f; - paramEntity.buildup += trait.damage() * paramEntity.warmup; + paramEntity.buildup += trait.damage(); } }; @@ -57,6 +56,12 @@ public class ForceProjector extends Block{ consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.1f)).boost().update(false); } + @Override + public void init(){ + clipSize = Math.max(clipSize, (radius + phaseRadiusBoost + 3f) * 2f); + super.init(); + } + @Override public void setBars(){ super.setBars(); @@ -70,10 +75,10 @@ public class ForceProjector extends Block{ @Override public void setStats(){ + stats.timePeriod = phaseUseTime; super.setStats(); stats.add(Stat.shieldHealth, shieldHealth, StatUnit.none); stats.add(Stat.cooldownTime, (int) (shieldHealth / cooldownBrokenBase / 60f), StatUnit.seconds); - stats.add(Stat.powerUse, basePowerDraw * 60f, StatUnit.powerSecond); stats.add(Stat.boostEffect, phaseRadiusBoost / tilesize, StatUnit.blocks); stats.add(Stat.boostEffect, phaseShieldBoost, StatUnit.shieldHealth); } @@ -94,22 +99,12 @@ public class ForceProjector extends Block{ public class ForceBuild extends Building implements Ranged{ public boolean broken = true; public float buildup, radscl, hit, warmup, phaseHeat; - public ForceDraw drawer; @Override public float range(){ return realRadius(); } - @Override - public void created(){ - super.created(); - drawer = ForceDraw.create(); - drawer.build = this; - drawer.set(x, y); - drawer.add(); - } - @Override public boolean shouldAmbientSound(){ return !broken && realRadius() > 1f; @@ -120,7 +115,6 @@ public class ForceProjector extends Block{ float radius = realRadius(); if(!broken && radius > 1f) Fx.forceShrink.at(x, y, radius, team.color); super.onRemoved(); - drawer.remove(); } @Override @@ -188,10 +182,6 @@ public class ForceProjector extends Block{ public void draw(){ super.draw(); - if(drawer != null){ - drawer.set(x, y); - } - if(buildup > 0f){ Draw.alpha(buildup / shieldHealth * 0.75f); Draw.blend(Blending.additive); @@ -199,6 +189,8 @@ public class ForceProjector extends Block{ Draw.blend(); Draw.reset(); } + + drawShield(); } public void drawShield(){ @@ -244,21 +236,4 @@ public class ForceProjector extends Block{ phaseHeat = read.f(); } } - - @EntityDef(value = {ForceDrawc.class}, serialize = false) - @Component(base = true) - abstract class ForceDrawComp implements Drawc{ - transient ForceBuild build; - - @Override - public void draw(){ - build.drawShield(); - } - - @Replace - @Override - public float clipSize(){ - return build.realRadius() * 3f; - } - } } diff --git a/core/src/mindustry/world/blocks/defense/MendProjector.java b/core/src/mindustry/world/blocks/defense/MendProjector.java index 6fb362f7d3..6fbf281918 100644 --- a/core/src/mindustry/world/blocks/defense/MendProjector.java +++ b/core/src/mindustry/world/blocks/defense/MendProjector.java @@ -43,6 +43,7 @@ public class MendProjector extends Block{ @Override public void setStats(){ + stats.timePeriod = useTime; super.setStats(); stats.add(Stat.repairTime, (int)(100f / healPercent * reload / 60f), StatUnit.seconds); @@ -90,11 +91,17 @@ public class MendProjector extends Block{ indexer.eachBlock(this, realRange, other -> other.damaged(), other -> { other.heal(other.maxHealth() * (healPercent + phaseHeat * phaseBoost) / 100f * efficiency()); - Fx.healBlockFull.at(other.x, other.y, other.block.size, Tmp.c1.set(baseColor).lerp(phaseColor, phaseHeat)); + Fx.healBlockFull.at(other.x, other.y, other.block.size, baseColor); }); } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(charge / reload); + return super.sense(sensor); + } + @Override public void drawSelect(){ float realRange = range + phaseHeat * phaseRangeBoost; diff --git a/core/src/mindustry/world/blocks/defense/OverdriveProjector.java b/core/src/mindustry/world/blocks/defense/OverdriveProjector.java index 499165f1bc..d11ef77db5 100644 --- a/core/src/mindustry/world/blocks/defense/OverdriveProjector.java +++ b/core/src/mindustry/world/blocks/defense/OverdriveProjector.java @@ -57,6 +57,7 @@ public class OverdriveProjector extends Block{ @Override public void setStats(){ + stats.timePeriod = useTime; super.setStats(); stats.add(Stat.speedIncrease, (int)(100f * speedBoost), StatUnit.percent); diff --git a/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java b/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java index 38237b8e40..58d0aed084 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java @@ -14,7 +14,6 @@ import mindustry.type.*; import mindustry.ui.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -28,7 +27,21 @@ public class ItemTurret extends Turret{ /** Initializes accepted ammo map. Format: [item1, bullet1, item2, bullet2...] */ public void ammo(Object... objects){ - ammoTypes = OrderedMap.of(objects); + ammoTypes = ObjectMap.of(objects); + } + + /** Makes copies of all bullets and limits their range. */ + public void limitRange(){ + limitRange(1f); + } + + /** Makes copies of all bullets and limits their range. */ + public void limitRange(float margin){ + for(var entry : ammoTypes.copy().entries()){ + var copy = entry.value.copy(); + copy.lifetime = (range + margin) / copy.speed; + ammoTypes.put(entry.key, copy); + } } @Override @@ -36,7 +49,7 @@ public class ItemTurret extends Turret{ super.setStats(); stats.remove(Stat.itemCapacity); - stats.add(Stat.ammo, new AmmoListValue<>(ammoTypes)); + stats.add(Stat.ammo, StatValues.ammo(ammoTypes)); } @Override @@ -45,7 +58,7 @@ public class ItemTurret extends Turret{ @Override public void build(Building tile, Table table){ MultiReqImage image = new MultiReqImage(); - content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.icon(Cicon.medium)), + content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.uiIcon), () -> tile instanceof ItemTurretBuild it && !it.ammo.isEmpty() && ((ItemEntry)it.ammo.peek()).item == item))); table.add(image).size(8 * 4); diff --git a/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java b/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java index 22220e5644..a708b39bc7 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java @@ -4,10 +4,10 @@ import arc.math.*; import arc.util.*; import mindustry.entities.bullet.*; import mindustry.gen.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -34,7 +34,7 @@ public class LaserTurret extends PowerTurret{ super.setStats(); stats.remove(Stat.booster); - stats.add(Stat.input, new BoosterListValue(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, false, l -> consumes.liquidfilters.get(l.id))); + stats.add(Stat.input, StatValues.boosters(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, false, l -> consumes.liquidfilters.get(l.id))); } public class LaserTurretBuild extends PowerTurretBuild{ @@ -67,8 +67,8 @@ public class LaserTurret extends PowerTurret{ Liquid liquid = liquids.current(); float maxUsed = consumes.get(ConsumeType.liquid).amount; - float used = (cheating() ? maxUsed * Time.delta : Math.min(liquids.get(liquid), maxUsed * Time.delta)) * liquid.heatCapacity * coolantMultiplier; - reload -= used; + float used = (cheating() ? maxUsed * Time.delta : Math.min(liquids.get(liquid), maxUsed * Time.delta)); + reload -= used * liquid.heatCapacity * coolantMultiplier; liquids.remove(liquid, used); if(Mathf.chance(0.06 * used)){ @@ -77,6 +77,13 @@ public class LaserTurret extends PowerTurret{ } } + @Override + public double sense(LAccess sensor){ + //reload reversed for laser turrets + if(sensor == LAccess.progress) return Mathf.clamp(1f - reload / reloadTime); + return super.sense(sensor); + } + @Override protected void updateShooting(){ if(bulletLife > 0 && bullet != null){ diff --git a/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java b/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java index bb53932b4c..46f94bd0ee 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java @@ -3,6 +3,7 @@ package mindustry.world.blocks.defense.turrets; import arc.graphics.g2d.*; import arc.struct.*; import mindustry.annotations.Annotations.*; +import mindustry.content.*; import mindustry.entities.*; import mindustry.entities.bullet.*; import mindustry.gen.*; @@ -11,7 +12,6 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -32,14 +32,14 @@ public class LiquidTurret extends Turret{ /** Initializes accepted ammo map. Format: [liquid1, bullet1, liquid2, bullet2...] */ public void ammo(Object... objects){ - ammoTypes = OrderedMap.of(objects); + ammoTypes = ObjectMap.of(objects); } @Override public void setStats(){ super.setStats(); - stats.add(Stat.ammo, new AmmoListValue<>(ammoTypes)); + stats.add(Stat.ammo, StatValues.ammo(ammoTypes)); } @Override @@ -88,7 +88,9 @@ public class LiquidTurret extends Turret{ @Override public void updateTile(){ - unit.ammo(unit.type().ammoCapacity * liquids.currentAmount() / liquidCapacity); + if(unit != null){ + unit.ammo(unit.type().ammoCapacity * liquids.currentAmount() / liquidCapacity); + } super.updateTile(); } @@ -116,8 +118,11 @@ public class LiquidTurret extends Turret{ protected void effects(){ BulletType type = peekAmmo(); - type.shootEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); - type.smokeEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); + Effect fshootEffect = shootEffect == Fx.none ? type.shootEffect : shootEffect; + Effect fsmokeEffect = smokeEffect == Fx.none ? type.smokeEffect : smokeEffect; + + fshootEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); + fsmokeEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); shootSound.at(tile); if(shootShake > 0){ diff --git a/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java b/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java index fe568dedfc..be1d3c25b8 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java @@ -4,7 +4,6 @@ import arc.struct.*; import mindustry.entities.bullet.*; import mindustry.logic.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; public class PowerTurret extends Turret{ public BulletType shootType; @@ -18,7 +17,7 @@ public class PowerTurret extends Turret{ @Override public void setStats(){ super.setStats(); - stats.add(Stat.ammo, new AmmoListValue<>(OrderedMap.of(this, shootType))); + stats.add(Stat.ammo, StatValues.ammo(ObjectMap.of(this, shootType))); } @Override @@ -31,7 +30,9 @@ public class PowerTurret extends Turret{ @Override public void updateTile(){ - unit.ammo(power.status * unit.type().ammoCapacity); + if(unit != null){ + unit.ammo(power.status * unit.type().ammoCapacity); + } super.updateTile(); } diff --git a/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java b/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java index bc172e9c46..a4bb4b7d8f 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java @@ -5,7 +5,6 @@ import arc.util.*; import mindustry.type.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -21,7 +20,7 @@ public class ReloadTurret extends BaseTurret{ super.setStats(); if(acceptCoolant){ - stats.add(Stat.booster, new BoosterListValue(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, true, l -> consumes.liquidfilters.get(l.id))); + stats.add(Stat.booster, StatValues.boosters(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, true, l -> consumes.liquidfilters.get(l.id))); } } @@ -29,16 +28,17 @@ public class ReloadTurret extends BaseTurret{ public float reload; protected void updateCooling(){ - float maxUsed = consumes.get(ConsumeType.liquid).amount; + if(reload < reloadTime){ + float maxUsed = consumes.get(ConsumeType.liquid).amount; + Liquid liquid = liquids.current(); - Liquid liquid = liquids.current(); + float used = Math.min(liquids.get(liquid), maxUsed * Time.delta) * baseReloadSpeed(); + reload += used * liquid.heatCapacity * coolantMultiplier; + liquids.remove(liquid, used); - float used = Math.min(Math.min(liquids.get(liquid), maxUsed * Time.delta), Math.max(0, ((reloadTime - reload) / coolantMultiplier) / liquid.heatCapacity)) * baseReloadSpeed(); - reload += used * liquid.heatCapacity * coolantMultiplier; - liquids.remove(liquid, used); - - if(Mathf.chance(0.06 * used)){ - coolEffect.at(x + Mathf.range(size * tilesize / 2f), y + Mathf.range(size * tilesize / 2f)); + if(Mathf.chance(0.06 * used)){ + coolEffect.at(x + Mathf.range(size * tilesize / 2f), y + Mathf.range(size * tilesize / 2f)); + } } } diff --git a/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java b/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java index a7a94a4ad1..50d0f39894 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java @@ -47,7 +47,6 @@ public class TractorBeamTurret extends BaseTurret{ //disabled due to version mismatch problems acceptCoolant = false; - expanded = true; } @Override @@ -64,6 +63,13 @@ public class TractorBeamTurret extends BaseTurret{ stats.add(Stat.damage, damage * 60f, StatUnit.perSecond); } + @Override + public void init(){ + super.init(); + + clipSize = Math.max(clipSize, (range + tilesize) * 2); + } + public class TractorBeamBuild extends BaseTurretBuild{ public @Nullable Unit target; public float lastX, lastY, strength; diff --git a/core/src/mindustry/world/blocks/defense/turrets/Turret.java b/core/src/mindustry/world/blocks/defense/turrets/Turret.java index cf793ad64d..5f551748d2 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/Turret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/Turret.java @@ -106,7 +106,7 @@ public class Turret extends ReloadTurret{ super.setStats(); stats.add(Stat.inaccuracy, (int)inaccuracy, StatUnit.degrees); - stats.add(Stat.reload, 60f / (reloadTime + 1) * (alternate ? 1 : shots), StatUnit.none); + stats.add(Stat.reload, 60f / (reloadTime) * (alternate ? 1 : shots), StatUnit.none); stats.add(Stat.targetsAir, targetAir); stats.add(Stat.targetsGround, targetGround); if(ammoPerShot != 1) stats.add(Stat.ammoUse, ammoPerShot, StatUnit.perShot); @@ -144,7 +144,7 @@ public class Turret extends ReloadTurret{ public boolean logicShooting = false; public @Nullable Posc target; public Vec2 targetPos = new Vec2(); - public BlockUnitc unit = Nulls.blockUnit; + public @Nullable BlockUnitc unit; public boolean wasShooting, charging; @Override @@ -155,7 +155,7 @@ public class Turret extends ReloadTurret{ @Override public void control(LAccess type, double p1, double p2, double p3, double p4){ - if(type == LAccess.shoot && !unit.isPlayer()){ + if(type == LAccess.shoot && (unit == null || !unit.isPlayer())){ targetPos.set(World.unconv((float)p1), World.unconv((float)p2)); logicControlTime = logicControlCooldown; logicShooting = !Mathf.zero(p3); @@ -166,7 +166,7 @@ public class Turret extends ReloadTurret{ @Override public void control(LAccess type, Object p1, double p2, double p3, double p4){ - if(type == LAccess.shootp && !unit.isPlayer()){ + if(type == LAccess.shootp && (unit == null || !unit.isPlayer())){ logicControlTime = logicControlCooldown; logicShooting = !Mathf.zero(p2); @@ -187,16 +187,21 @@ public class Turret extends ReloadTurret{ case shootX -> World.conv(targetPos.x); case shootY -> World.conv(targetPos.y); case shooting -> isShooting() ? 1 : 0; + case progress -> Mathf.clamp(reload / reloadTime); default -> super.sense(sensor); }; } public boolean isShooting(){ - return (isControlled() ? unit.isShooting() : logicControlled() ? logicShooting : target != null); + return (isControlled() ? (unit != null && unit.isShooting()) : logicControlled() ? logicShooting : target != null); } @Override public Unit unit(){ + if(unit == null){ + unit = (BlockUnitc)UnitTypes.block.create(team); + unit.tile(this); + } return (Unit)unit; } @@ -205,7 +210,7 @@ public class Turret extends ReloadTurret{ } public boolean isActive(){ - return target != null || wasShooting; + return (target != null || wasShooting) && enabled; } public void targetPosition(Posc pos){ @@ -247,10 +252,12 @@ public class Turret extends ReloadTurret{ recoil = Mathf.lerpDelta(recoil, 0f, restitution); heat = Mathf.lerpDelta(heat, 0f, cooldown); - unit.health(health); - unit.rotation(rotation); - unit.team(team); - unit.set(x, y); + if(unit != null){ + unit.health(health); + unit.rotation(rotation); + unit.team(team); + unit.set(x, y); + } if(logicControlTime > 0){ logicControlTime -= Time.delta; @@ -353,14 +360,14 @@ public class Turret extends ReloadTurret{ } protected void updateShooting(){ + reload += delta() * peekAmmo().reloadMultiplier * baseReloadSpeed(); + if(reload >= reloadTime && !charging){ BulletType type = peekAmmo(); shoot(type); - reload = 0f; - }else{ - reload += delta() * peekAmmo().reloadMultiplier * baseReloadSpeed(); + reload %= reloadTime; } } diff --git a/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java b/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java index a0eddc20c1..a87fb0348a 100644 --- a/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java +++ b/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java @@ -38,6 +38,11 @@ public class BufferedItemBridge extends ExtendingItemBridge{ } } + @Override + public void doDump(){ + dump(); + } + @Override public void write(Writes write){ super.write(write); diff --git a/core/src/mindustry/world/blocks/distribution/Conveyor.java b/core/src/mindustry/world/blocks/distribution/Conveyor.java index c1f6f53ef6..a5608bd1f6 100644 --- a/core/src/mindustry/world/blocks/distribution/Conveyor.java +++ b/core/src/mindustry/world/blocks/distribution/Conveyor.java @@ -106,8 +106,7 @@ public class Conveyor extends Block implements Autotiler{ public class ConveyorBuild extends Building implements ChainedBuilding{ //parallel array data public Item[] ids = new Item[capacity]; - public float[] xs = new float[capacity]; - public float[] ys = new float[capacity]; + public float[] xs = new float[capacity], ys = new float[capacity]; //amount of items, always < capacity public int len = 0; //next entity @@ -150,7 +149,7 @@ public class Conveyor extends Block implements Autotiler{ tr1.trns(rotation * 90, tilesize, 0); tr2.trns(rotation * 90, -tilesize / 2f, xs[i] * tilesize / 2f); - Draw.rect(item.icon(Cicon.medium), + Draw.rect(item.fullIcon, (tile.x * tilesize + tr1.x * ys[i] + tr2.x), (tile.y * tilesize + tr1.y * ys[i] + tr2.y), itemSize, itemSize); diff --git a/core/src/mindustry/world/blocks/distribution/Duct.java b/core/src/mindustry/world/blocks/distribution/Duct.java new file mode 100644 index 0000000000..6e29f879b5 --- /dev/null +++ b/core/src/mindustry/world/blocks/distribution/Duct.java @@ -0,0 +1,187 @@ +package mindustry.world.blocks.distribution; + +import arc.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.input.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +public class Duct extends Block implements Autotiler{ + public float speed = 5f; + + public @Load(value = "@-top-#", length = 5) TextureRegion[] topRegions; + public @Load(value = "@-bottom-#", length = 5, fallback = "duct-bottom-#") TextureRegion[] botRegions; + + public Duct(String name){ + super(name); + + group = BlockGroup.transportation; + update = true; + solid = false; + hasItems = true; + conveyorPlacement = true; + unloadable = false; + itemCapacity = 1; + noUpdateDisabled = true; + rotate = true; + envEnabled = Env.space | Env.terrestrial | Env.underwater; + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.itemsMoved, 60f / speed, StatUnit.itemsSecond); + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + int[] bits = getTiling(req, list); + + if(bits == null) return; + + Draw.scl(bits[1], bits[2]); + Draw.alpha(0.5f); + Draw.rect(botRegions[bits[0]], req.drawx(), req.drawy(), req.rotation * 90); + Draw.color(); + Draw.rect(topRegions[bits[0]], req.drawx(), req.drawy(), req.rotation * 90); + Draw.scl(); + } + + @Override + public boolean blends(Tile tile, int rotation, int otherx, int othery, int otherrot, Block otherblock){ + return otherblock.outputsItems() && blendsArmored(tile, rotation, otherx, othery, otherrot, otherblock); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{Core.atlas.find("duct-bottom"), topRegions[0]}; + } + + @Override + public void handlePlacementLine(Seq plans){ + Placement.calculateDuctBridges(plans, (DuctBridge)Blocks.ductBridge); + } + + public class DuctBuild extends Building{ + public float progress; + public @Nullable Item current; + public int recDir = 0; + public int blendbits, xscl, yscl, blending; + public @Nullable Building next; + public @Nullable DuctBuild nextc; + + @Override + public void draw(){ + float rotation = rotdeg(); + int r = this.rotation; + + //draw extra ducts facing this one for tiling purposes + for(int i = 0; i < 4; i++){ + if((blending & (1 << i)) != 0 && !(i == 0 && nextc != null)){ + int dir = r - i; + float rot = i == 0 ? rotation : (dir)*90; + drawAt(x + Geometry.d4x(dir) * tilesize*0.75f, y + Geometry.d4y(dir) * tilesize*0.75f, 0, rot, i != 0 ? SliceMode.bottom : SliceMode.top); + } + } + + //draw item + if(current != null){ + Draw.z(Layer.blockUnder + 0.1f); + Tmp.v1.set(Geometry.d4x(recDir) * tilesize / 2f, Geometry.d4y(recDir) * tilesize / 2f) + .lerp(Geometry.d4x(r) * tilesize / 2f, Geometry.d4y(r) * tilesize / 2f, + Mathf.clamp((progress + 1f) / 2f)); + + Draw.rect(current.fullIcon, x + Tmp.v1.x, y + Tmp.v1.y, itemSize, itemSize); + } + + Draw.scl(xscl, yscl); + drawAt(x, y, blendbits, rotation, SliceMode.none); + Draw.reset(); + } + + protected void drawAt(float x, float y, int bits, float rotation, SliceMode slice){ + Draw.z(Layer.blockUnder); + Draw.rect(sliced(botRegions[bits], slice), x, y, rotation); + + Draw.z(Layer.blockUnder + 0.2f); + Draw.color(0.4f, 0.4f, 0.4f, 0.4f); + Draw.rect(sliced(botRegions[bits], slice), x, y, rotation); + Draw.color(); + Draw.rect(sliced(topRegions[bits], slice), x, y, rotation); + } + + @Override + public void updateTile(){ + progress += edelta() / speed * 2f; + + if(current != null && next != null){ + if(progress >= (1f - 1f/speed) && moveForward(current)){ + items.remove(current, 1); + current = null; + progress %= (1f - 1f/speed); + } + }else{ + progress = 0; + } + + if(current == null && items.total() > 0){ + current = items.first(); + } + } + + @Override + public boolean acceptItem(Building source, Item item){ + return current == null && items.total() == 0 && + (source.block instanceof Duct || Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + } + + @Override + public int removeStack(Item item, int amount){ + int removed = super.removeStack(item, amount); + if(item == current) current = null; + return removed; + } + + @Override + public void handleStack(Item item, int amount, Teamc source){ + super.handleStack(item, amount, source); + current = item; + } + + @Override + public void handleItem(Building source, Item item){ + current = item; + progress = -1f; + recDir = relativeToEdge(source.tile); + items.add(item, 1); + noSleep(); + } + + @Override + public void onProximityUpdate(){ + super.onProximityUpdate(); + + int[] bits = buildBlending(tile, rotation, null, true); + blendbits = bits[0]; + xscl = bits[1]; + yscl = bits[2]; + blending = bits[4]; + next = front(); + nextc = next instanceof DuctBuild d ? d : null; + } + } +} diff --git a/core/src/mindustry/world/blocks/distribution/DuctBridge.java b/core/src/mindustry/world/blocks/distribution/DuctBridge.java new file mode 100644 index 0000000000..e040f83879 --- /dev/null +++ b/core/src/mindustry/world/blocks/distribution/DuctBridge.java @@ -0,0 +1,149 @@ +package mindustry.world.blocks.distribution; + +import arc.graphics.g2d.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.core.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.input.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +//TODO display range +public class DuctBridge extends Block{ + public @Load("@-bridge") TextureRegion bridgeRegion; + public @Load("@-bridge-bottom") TextureRegion bridgeBotRegion; + //public @Load("@-bridge-top") TextureRegion bridgeTopRegion; + public @Load("@-arrow") TextureRegion arrowRegion; + public @Load("@-dir") TextureRegion dirRegion; + + public int range = 4; + public float moveDelay = 5f; + + public DuctBridge(String name){ + super(name); + update = true; + solid = true; + rotate = true; + itemCapacity = 4; + hasItems = true; + group = BlockGroup.transportation; + noUpdateDisabled = true; + envEnabled = Env.space | Env.terrestrial | Env.underwater; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(dirRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, dirRegion}; + } + + @Override + public void changePlacementPath(Seq points, int rotation){ + Placement.calculateNodes(points, this, rotation, (point, other) -> Math.max(Math.abs(point.x - other.x), Math.abs(point.y - other.y)) <= range); + } + + public boolean positionsValid(int x1, int y1, int x2, int y2){ + if(x1 == x2){ + return Math.abs(y1 - y2) <= range; + }else if(y1 == y2){ + return Math.abs(x1 - x2) <= range; + }else{ + return false; + } + } + + public class DuctBridgeBuild extends Building{ + public Building[] occupied = new Building[4]; + public float progress = 0f; + + @Override + public void draw(){ + Draw.rect(block.region, x, y); + Draw.rect(dirRegion, x, y, rotdeg()); + var link = findLink(); + if(link != null){ + Draw.z(Layer.power); + Draw.alpha(Renderer.bridgeOpacity); + float + angle = angleTo(link), + cx = (x + link.x)/2f, + cy = (y + link.y)/2f, + len = Math.max(Math.abs(x - link.x), Math.abs(y - link.y)) - size * tilesize; + + Draw.rect(bridgeRegion, cx, cy, len, tilesize, angle); + Draw.color(0.4f, 0.4f, 0.4f, 0.4f * Renderer.bridgeOpacity); + Draw.rect(bridgeBotRegion, cx, cy, len, tilesize, angle); + Draw.reset(); + Draw.alpha(Renderer.bridgeOpacity); + //Draw.rect(bridgeTopRegion, cx, cy, len, tilesize, angle); + + for(float i = 6f; i <= len + size * tilesize - 5f; i += 5f){ + Draw.rect(arrowRegion, x + Geometry.d4x(rotation) * i, y + Geometry.d4y(rotation) * i, angle); + } + + Draw.reset(); + } + } + + @Nullable + public DuctBridgeBuild findLink(){ + for(int i = 1; i <= range; i++){ + Tile other = tile.nearby(Geometry.d4x(rotation) * i, Geometry.d4y(rotation) * i); + if(other.build instanceof DuctBridgeBuild build && build.team == team){ + return build; + } + } + return null; + } + + @Override + public void updateTile(){ + var link = findLink(); + if(link != null){ + link.occupied[rotation % 4] = this; + if(items.any() && link.items.total() < link.block.itemCapacity){ + progress += edelta(); + while(progress > moveDelay){ + Item next = items.take(); + if(next != null && link.items.total() < link.block.itemCapacity){ + link.handleItem(this, next); + } + progress -= moveDelay; + } + } + } + + if(link == null && items.any()){ + Item next = items.first(); + if(moveForward(next)){ + items.remove(next, 1); + } + } + + for(int i = 0; i < 4; i++){ + if(occupied[i] == null || occupied[i].rotation != i || !occupied[i].isValid()){ + occupied[i] = null; + } + } + } + + @Override + public boolean acceptItem(Building source, Item item){ + int rel = this.relativeTo(source); + return items.total() < itemCapacity && rel != rotation && occupied[(rel + 2) % 4] == null; + } + } +} diff --git a/core/src/mindustry/world/blocks/distribution/DuctRouter.java b/core/src/mindustry/world/blocks/distribution/DuctRouter.java new file mode 100644 index 0000000000..b4c78d7ab8 --- /dev/null +++ b/core/src/mindustry/world/blocks/distribution/DuctRouter.java @@ -0,0 +1,125 @@ +package mindustry.world.blocks.distribution; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.meta.*; + +public class DuctRouter extends Block{ + public float speed = 5f; + + public @Load(value = "@-top") TextureRegion topRegion; + + public DuctRouter(String name){ + super(name); + + group = BlockGroup.transportation; + update = true; + solid = false; + hasItems = true; + conveyorPlacement = true; + unloadable = false; + itemCapacity = 1; + noUpdateDisabled = true; + rotate = true; + envEnabled = Env.space | Env.terrestrial | Env.underwater; + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.itemsMoved, 60f / speed, StatUnit.itemsSecond); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, topRegion}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(topRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + public class DuctBuild extends Building{ + public float progress; + public @Nullable Item current; + + @Override + public void draw(){ + Draw.rect(region, x, y); + Draw.rect(topRegion, x, y, rotdeg()); + } + + @Override + public void updateTile(){ + progress += edelta() / speed * 2f; + + if(current != null){ + if(progress >= (1f - 1f/speed)){ + var target = target(); + if(target != null){ + target.handleItem(this, current); + cdump = (byte)((cdump + 1) % 3); + items.remove(current, 1); + current = null; + progress %= (1f - 1f/speed); + } + } + }else{ + progress = 0; + } + + if(current == null && items.total() > 0){ + current = items.first(); + } + } + + @Nullable + public Building target(){ + if(current == null) return null; + + for(int i = -1; i <= 1; i++){ + Building other = nearby(Mathf.mod(rotation + i + cdump, 4)); + if(other != null && other.team == team && other.acceptItem(this, current)){ + return other; + } + } + return null; + } + + @Override + public boolean acceptItem(Building source, Item item){ + return current == null && items.total() == 0 && + (Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + } + + @Override + public int removeStack(Item item, int amount){ + int removed = super.removeStack(item, amount); + if(item == current) current = null; + return removed; + } + + @Override + public void handleStack(Item item, int amount, Teamc source){ + super.handleStack(item, amount, source); + current = item; + } + + @Override + public void handleItem(Building source, Item item){ + current = item; + progress = -1f; + items.add(item, 1); + noSleep(); + } + } +} diff --git a/core/src/mindustry/world/blocks/distribution/ItemBridge.java b/core/src/mindustry/world/blocks/distribution/ItemBridge.java index e711d76c1c..53f69b5aef 100644 --- a/core/src/mindustry/world/blocks/distribution/ItemBridge.java +++ b/core/src/mindustry/world/blocks/distribution/ItemBridge.java @@ -23,7 +23,6 @@ import static mindustry.Vars.*; public class ItemBridge extends Block{ private static BuildPlan otherReq; - public final int timerTransport = timers++; public int range; public float transportTime = 2f; public @Load("@-end") TextureRegion endRegion; @@ -38,7 +37,6 @@ public class ItemBridge extends Block{ update = true; solid = true; hasPower = true; - expanded = true; itemCapacity = 10; configurable = true; hasItems = true; @@ -95,14 +93,12 @@ public class ItemBridge extends Block{ Tile link = findLink(x, y); - Lines.stroke(2f, Pal.placing); for(int i = 0; i < 4; i++){ - Lines.dashLine( + Drawf.dashLine(Pal.placing, x * tilesize + Geometry.d4[i].x * (tilesize / 2f + 2), y * tilesize + Geometry.d4[i].y * (tilesize / 2f + 2), - x * tilesize + Geometry.d4[i].x * (range + 0.5f) * tilesize, - y * tilesize + Geometry.d4[i].y * (range + 0.5f) * tilesize, - range); + x * tilesize + Geometry.d4[i].x * (range) * tilesize, + y * tilesize + Geometry.d4[i].y * (range) * tilesize); } Draw.reset(); @@ -149,6 +145,12 @@ public class ItemBridge extends Block{ return null; } + @Override + public void init(){ + super.init(); + clipSize = Math.max(clipSize, (range + 0.5f) * tilesize * 2); + } + @Override public void handlePlacementLine(Seq plans){ for(int i = 0; i < plans.size - 1; i++){ @@ -167,18 +169,20 @@ public class ItemBridge extends Block{ public class ItemBridgeBuild extends Building{ public int link = -1; + //TODO awful public IntSet incoming = new IntSet(); public float uptime; public float time; public float time2; public float cycleSpeed = 1f; + public float transportCounter; @Override public void playerPlaced(Object config){ super.playerPlaced(config); Tile link = findLink(tile.x, tile.y); - if(linkValid(tile, link) && !proximity.contains(link.build)){ + if(linkValid(tile, link) && this.link != link.pos() && !proximity.contains(link.build)){ link.build.configure(tile.pos()); } @@ -286,7 +290,7 @@ public class ItemBridge extends Block{ Tile other = world.tile(link); if(!linkValid(tile, other)){ - dump(); + doDump(); uptime = 0f; }else{ ((ItemBridgeBuild)other.build).incoming.add(tile.pos()); @@ -301,20 +305,27 @@ public class ItemBridge extends Block{ } } + public void doDump(){ + //allow dumping multiple times per frame + dumpAccumulate(); + } + public void updateTransport(Building other){ - if(uptime >= 0.5f && timer(timerTransport, transportTime)){ + boolean any = false; + transportCounter += edelta(); + while(transportCounter >= transportTime){ Item item = items.take(); if(item != null && other.acceptItem(this, item)){ other.handleItem(this, item); - cycleSpeed = Mathf.lerpDelta(cycleSpeed, 4f, 0.05f); //TODO this is kinda broken, because lerping only happens on a timer - }else{ - cycleSpeed = Mathf.lerpDelta(cycleSpeed, 1f, 0.01f); - if(item != null){ - items.add(item, 1); - items.undoFlow(item); - } + any = true; + }else if(item != null){ + items.add(item, 1); + items.undoFlow(item); } + transportCounter -= transportTime; } + + cycleSpeed = Mathf.lerpDelta(cycleSpeed, any ? 4f : 1f, any ? 0.05f : 0.01f); } @Override diff --git a/core/src/mindustry/world/blocks/distribution/MassDriver.java b/core/src/mindustry/world/blocks/distribution/MassDriver.java index cf3a1d9cd2..b8659c3d89 100644 --- a/core/src/mindustry/world/blocks/distribution/MassDriver.java +++ b/core/src/mindustry/world/blocks/distribution/MassDriver.java @@ -14,6 +14,7 @@ import mindustry.content.*; import mindustry.entities.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.meta.*; @@ -22,7 +23,7 @@ import static mindustry.Vars.*; public class MassDriver extends Block{ public float range; - public float rotateSpeed = 0.04f; + public float rotateSpeed = 5f; public float translation = 7f; public int minDistribute = 10; public float knockback = 4f; @@ -73,7 +74,7 @@ public class MassDriver extends Block{ //check if a mass driver is selected while placing this driver if(!control.input.frag.config.isShown()) return; Building selected = control.input.frag.config.getSelectedTile(); - if(selected == null || !(selected.block instanceof MassDriver) || !(selected.within(x * tilesize, y * tilesize, range))) return; + if(selected == null || selected.block != this || !selected.within(x * tilesize, y * tilesize, range)) return; //if so, draw a dotted line towards it while it is in range float sin = Mathf.absin(Time.time, 6f, 1f); @@ -89,7 +90,7 @@ public class MassDriver extends Block{ Draw.reset(); } - public class DriverBulletData implements Poolable{ + public static class DriverBulletData implements Poolable{ public MassDriverBuild from, to; public int[] items = new int[content.items().size]; @@ -105,9 +106,10 @@ public class MassDriver extends Block{ public float rotation = 90; public float reload = 0f; public DriverState state = DriverState.idle; - public OrderedSet waitingShooters = new OrderedSet<>(); + //TODO use queue? this array usually holds about 3 shooters max anyway + public OrderedSet waitingShooters = new OrderedSet<>(); - public Tile currentShooter(){ + public Building currentShooter(){ return waitingShooters.isEmpty() ? null : waitingShooters.first(); } @@ -125,9 +127,11 @@ public class MassDriver extends Block{ reload = Mathf.clamp(reload - edelta() / reloadTime); } + var current = currentShooter(); + //cleanup waiting shooters that are not valid - if(!shooterValid(currentShooter())){ - waitingShooters.remove(currentShooter()); + if(current != null && !shooterValid(current)){ + waitingShooters.remove(current); } //switch states @@ -142,7 +146,7 @@ public class MassDriver extends Block{ //dump when idle or accepting if(state == DriverState.idle || state == DriverState.accepting){ - dump(); + dumpAccumulate(); } //skip when there's no power @@ -158,7 +162,7 @@ public class MassDriver extends Block{ } //align to shooter rotation - rotation = Mathf.slerpDelta(rotation, tile.angleTo(currentShooter()), rotateSpeed * efficiency()); + rotation = Angles.moveToward(rotation, tile.angleTo(currentShooter()), rotateSpeed * efficiency()); }else if(state == DriverState.shooting){ //if there's nothing to shoot at OR someone wants to shoot at this thing, bail if(!hasLink || (!waitingShooters.isEmpty() && (itemCapacity - items.total() >= minDistribute))){ @@ -173,15 +177,15 @@ public class MassDriver extends Block{ link.block.itemCapacity - link.items.total() >= minDistribute //must have minimum amount of space ){ MassDriverBuild other = (MassDriverBuild)link; - other.waitingShooters.add(tile); + other.waitingShooters.add(this); if(reload <= 0.0001f){ //align to target location - rotation = Mathf.slerpDelta(rotation, targetRotation, rotateSpeed * efficiency()); + rotation = Angles.moveToward(rotation, targetRotation, rotateSpeed * efficiency()); //fire when it's the first in the queue and angles are ready. - if(other.currentShooter() == tile && + if(other.currentShooter() == this && other.state == DriverState.accepting && Angles.near(rotation, targetRotation, 2f) && Angles.near(other.rotation, targetRotation + 180f, 2f)){ //actually fire @@ -189,7 +193,7 @@ public class MassDriver extends Block{ float timeToArrive = Math.min(bulletLifetime, dst(other) / bulletSpeed); Time.run(timeToArrive, () -> { //remove waiting shooters, it's done firing - other.waitingShooters.remove(tile); + other.waitingShooters.remove(this); other.state = DriverState.idle; }); //driver is immediately idle @@ -200,6 +204,12 @@ public class MassDriver extends Block{ } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(1f - reload / reloadTime); + return super.sense(sensor); + } + @Override public void draw(){ Draw.rect(baseRegion, x, y); @@ -222,9 +232,9 @@ public class MassDriver extends Block{ Lines.stroke(1f); Drawf.circles(x, y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.accent); - for(Tile shooter : waitingShooters){ - Drawf.circles(shooter.drawx(), shooter.drawy(), (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); - Drawf.arrow(shooter.drawx(), shooter.drawy(), x, y, size * tilesize + sin, 4f + sin, Pal.place); + for(var shooter : waitingShooters){ + Drawf.circles(shooter.x, shooter.y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.arrow(shooter.x, shooter.y, x, y, size * tilesize + sin, 4f + sin, Pal.place); } if(linkValid()){ @@ -246,7 +256,7 @@ public class MassDriver extends Block{ if(link == other.pos()){ configure(-1); return false; - }else if(other.block instanceof MassDriver && other.dst(tile) <= range && other.team == team){ + }else if(other.block == block && other.dst(tile) <= range && other.team == team){ configure(other.pos()); return false; } @@ -281,11 +291,8 @@ public class MassDriver extends Block{ x + Angles.trnsx(angle, translation), y + Angles.trnsy(angle, translation), angle, -1f, bulletSpeed, bulletLifetime, data); - shootEffect.at(x + Angles.trnsx(angle, translation), - y + Angles.trnsy(angle, translation), angle); - - smokeEffect.at(x + Angles.trnsx(angle, translation), - y + Angles.trnsy(angle, translation), angle); + shootEffect.at(x + Angles.trnsx(angle, translation), y + Angles.trnsy(angle, translation), angle); + smokeEffect.at(x + Angles.trnsx(angle, translation), y + Angles.trnsy(angle, translation), angle); Effect.shake(shake, shake, this); @@ -314,16 +321,13 @@ public class MassDriver extends Block{ bullet.remove(); } - protected boolean shooterValid(Tile other){ - if(other == null) return true; - if(!(other.build instanceof MassDriverBuild entity)) return false; - return entity.link == tile.pos() && tile.dst(other) <= range; + protected boolean shooterValid(Building other){ + return other instanceof MassDriverBuild entity && other.consValid() && entity.block == block && entity.link == pos() && within(other, range); } protected boolean linkValid(){ if(link == -1) return false; - Building link = world.build(this.link); - return link instanceof MassDriverBuild && link.team == team && within(link, range); + return world.build(this.link) instanceof MassDriverBuild other && other.block == block && other.team == team && within(other, range); } @Override @@ -352,8 +356,7 @@ public class MassDriver extends Block{ public enum DriverState{ idle, //nothing is shooting at this mass driver and it does not have any target accepting, //currently getting shot at, unload items - shooting, - unloading; + shooting; public static final DriverState[] all = values(); } diff --git a/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java b/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java index df51bef7ae..7e1697f401 100644 --- a/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java +++ b/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java @@ -11,7 +11,6 @@ import mindustry.gen.*; import mindustry.graphics.*; import mindustry.world.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; import mindustry.world.meta.*; import static mindustry.Vars.*; @@ -21,7 +20,7 @@ public class PayloadConveyor extends Block{ public @Load("@-top") TextureRegion topRegion; public @Load("@-edge") TextureRegion edgeRegion; public Interp interp = Interp.pow5; - public float payloadLimit = 2.5f; + public float payloadLimit = 2.9f; public PayloadConveyor(String name){ super(name); @@ -51,6 +50,13 @@ public class PayloadConveyor extends Block{ } } + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.payloadCapacity, (payloadLimit), StatUnit.blocksSquared); + } + public class PayloadConveyorBuild extends Building{ public @Nullable Payload item; public float progress, itemRotation, animation; @@ -59,6 +65,16 @@ public class PayloadConveyor extends Block{ public boolean blocked; public int step = -1, stepAccepted = -1; + @Override + public boolean canControlSelect(Player player){ + return this.item == null && !player.unit().spawnedByCore && player.unit().hitSize / tilesize <= payloadLimit && player.tileOn().build == this; + } + + @Override + public void onControlSelect(Player player){ + acceptPlayerPayload(player, p -> item = p); + } + @Override public Payload takePayload(){ Payload t = item; @@ -108,6 +124,9 @@ public class PayloadConveyor extends Block{ progress = time() % moveTime; updatePayload(); + if(item != null && next == null){ + PayloadBlock.pushOutput(item, progress / moveTime); + } //TODO nondeterministic input priority int curStep = curStep(); @@ -283,7 +302,7 @@ public class PayloadConveyor extends Block{ if(direction == rotation){ return !blocked || next != null; } - return PayloadAcceptor.blends(this, direction); + return PayloadBlock.blends(this, direction); } protected TextureRegion clipRegion(Rect bounds, Rect sprite, TextureRegion region){ diff --git a/core/src/mindustry/world/blocks/distribution/PayloadRouter.java b/core/src/mindustry/world/blocks/distribution/PayloadRouter.java index 3e1d2949dc..6a73e75bcc 100644 --- a/core/src/mindustry/world/blocks/distribution/PayloadRouter.java +++ b/core/src/mindustry/world/blocks/distribution/PayloadRouter.java @@ -7,6 +7,7 @@ import mindustry.annotations.Annotations.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.world.blocks.payloads.*; public class PayloadRouter extends PayloadConveyor{ @@ -28,6 +29,7 @@ public class PayloadRouter extends PayloadConveyor{ public class PayloadRouterBuild extends PayloadConveyorBuild{ public float smoothRot; + public float controlTime = -1f; @Override public void add(){ @@ -36,7 +38,7 @@ public class PayloadRouter extends PayloadConveyor{ } public void pickNext(){ - if(item != null){ + if(item != null && controlTime <= 0f){ int rotations = 0; do{ rotation = (rotation + 1) % 4; @@ -47,6 +49,16 @@ public class PayloadRouter extends PayloadConveyor{ } } + @Override + public void control(LAccess type, double p1, double p2, double p3, double p4){ + super.control(type, p1, p2, p3, p4); + if(type == LAccess.config){ + rotation = (int)p1; + //when manually controlled, routers do not turn automatically for a while, same as turrets + controlTime = Building.timeToUncontrol; + } + } + @Override public void handlePayload(Building source, Payload payload){ super.handlePayload(source, payload); @@ -62,6 +74,7 @@ public class PayloadRouter extends PayloadConveyor{ public void updateTile(){ super.updateTile(); + controlTime -= Time.delta; smoothRot = Mathf.slerpDelta(smoothRot, rotdeg(), 0.2f); } diff --git a/core/src/mindustry/world/blocks/distribution/StackConveyor.java b/core/src/mindustry/world/blocks/distribution/StackConveyor.java index 6427aa26c1..7c7007cb54 100644 --- a/core/src/mindustry/world/blocks/distribution/StackConveyor.java +++ b/core/src/mindustry/world/blocks/distribution/StackConveyor.java @@ -145,7 +145,7 @@ public class StackConveyor extends Block implements Autotiler{ //item float size = itemSize * Mathf.lerp(Math.min((float)items.total() / itemCapacity, 1), 1f, 0.4f); Drawf.shadow(Tmp.v1.x, Tmp.v1.y, size * 1.2f); - Draw.rect(lastItem.icon(Cicon.medium), Tmp.v1.x, Tmp.v1.y, size, size, 0); + Draw.rect(lastItem.fullIcon, Tmp.v1.x, Tmp.v1.y, size, size, 0); } @Override diff --git a/core/src/mindustry/world/blocks/environment/Bush.java b/core/src/mindustry/world/blocks/environment/Bush.java new file mode 100644 index 0000000000..ec1f61d9c1 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/Bush.java @@ -0,0 +1,46 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.world.*; + +public class Bush extends Prop{ + public @Load(value = "@-bot", fallback = "@") TextureRegion botRegion; + public @Load(value = "@-center") TextureRegion centerRegion; + + public int lobesMin = 7, lobesMax = 7; + public float botAngle = 60f, origin = 0.1f; + public float sclMin = 30f, sclMax = 50f, magMin = 5f, magMax = 15f, timeRange = 40f, spread = 0f; + + static Rand rand = new Rand(); + + public Bush(String name){ + super(name); + variants = 0; + } + + @Override + public void drawBase(Tile tile){ + rand.setSeed(tile.pos()); + float offset = rand.random(180f); + int lobes = rand.random(lobesMin, lobesMax); + for(int i = 0; i < lobes; i++){ + float ba = i / (float)lobes * 360f + offset + rand.range(spread), angle = ba + Mathf.sin(Time.time + rand.random(0, timeRange), rand.random(sclMin, sclMax), rand.random(magMin, magMax)); + float w = region.width * Draw.scl, h = region.height * Draw.scl; + var region = Angles.angleDist(ba, 225f) <= botAngle ? botRegion : this.region; + + Draw.rect(region, + tile.worldx() - Angles.trnsx(angle, origin) + w*0.5f, tile.worldy() - Angles.trnsy(angle, origin), + w, h, + origin*4f, h/2f, + angle + ); + } + + if(centerRegion.found()){ + Draw.rect(centerRegion, tile.worldx(), tile.worldy()); + } + } +} diff --git a/core/src/mindustry/world/blocks/environment/Floor.java b/core/src/mindustry/world/blocks/environment/Floor.java index 02aed4b003..7cee47c4d0 100644 --- a/core/src/mindustry/world/blocks/environment/Floor.java +++ b/core/src/mindustry/world/blocks/environment/Floor.java @@ -4,7 +4,6 @@ import arc.*; import arc.audio.*; import arc.graphics.*; import arc.graphics.g2d.*; -import arc.graphics.g2d.TextureAtlas.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; @@ -15,7 +14,6 @@ import mindustry.gen.*; import mindustry.graphics.*; import mindustry.graphics.MultiPacker.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.*; @@ -50,8 +48,6 @@ public class Floor extends Block{ public @Nullable Liquid liquidDrop = null; /** Multiplier for pumped liquids, used for deep water. */ public float liquidMultiplier = 1f; - /** item that drops from this block, used for drills */ - public @Nullable Item itemDrop = null; /** whether this block can be drowned in */ public boolean isLiquid; /** if true, this block cannot be mined by players. useful for annoying things like sand. */ @@ -119,7 +115,7 @@ public class Floor extends Block{ if(wall == null) wall = Blocks.air; if(decoration == Blocks.air){ - decoration = content.blocks().min(b -> b instanceof Boulder && b.minfo.mod == null && b.breakable ? mapColor.diff(b.mapColor) : Float.POSITIVE_INFINITY); + decoration = content.blocks().min(b -> b instanceof Prop && b.minfo.mod == null && b.breakable ? mapColor.diff(b.mapColor) : Float.POSITIVE_INFINITY); } if(isLiquid && walkEffect == Fx.none){ @@ -134,7 +130,7 @@ public class Floor extends Block{ @Override public void createIcons(MultiPacker packer){ super.createIcons(packer); - packer.add(PageType.editor, "editor-" + name, Core.atlas.getPixmap((AtlasRegion)icon(Cicon.full)).crop()); + packer.add(PageType.editor, "editor-" + name, Core.atlas.getPixmap(fullIcon).crop()); if(blendGroup != this){ return; @@ -147,16 +143,13 @@ public class Floor extends Block{ } } - Color color = new Color(); - Color color2 = new Color(); - PixmapRegion image = Core.atlas.getPixmap((AtlasRegion)icons()[0]); + PixmapRegion image = Core.atlas.getPixmap(icons()[0]); PixmapRegion edge = Core.atlas.getPixmap("edge-stencil"); Pixmap result = new Pixmap(edge.width, edge.height); for(int x = 0; x < edge.width; x++){ for(int y = 0; y < edge.height; y++){ - edge.getPixel(x, y, color); - result.draw(x, y, color.mul(color2.set(image.getPixel(x % image.width, y % image.height)))); + result.set(x, y, Color.muli(edge.get(x, y), image.get(x % image.width, y % image.height))); } } diff --git a/core/src/mindustry/world/blocks/environment/OreBlock.java b/core/src/mindustry/world/blocks/environment/OreBlock.java index c08a3eccce..1ff509465f 100644 --- a/core/src/mindustry/world/blocks/environment/OreBlock.java +++ b/core/src/mindustry/world/blocks/environment/OreBlock.java @@ -14,8 +14,8 @@ import static mindustry.Vars.*; /**An overlay ore for a specific item type.*/ public class OreBlock extends OverlayFloor{ - public OreBlock(Item ore){ - super("ore-" + ore.name); + public OreBlock(String name, Item ore){ + super(name); this.localizedName = ore.localizedName; this.itemDrop = ore; this.variants = 3; @@ -23,6 +23,10 @@ public class OreBlock extends OverlayFloor{ this.useColor = true; } + public OreBlock(Item ore){ + this("ore-" + ore.name, ore); + } + /** For mod use only!*/ public OreBlock(String name){ super(name); @@ -39,25 +43,20 @@ public class OreBlock extends OverlayFloor{ @OverrideCallSuper public void createIcons(MultiPacker packer){ for(int i = 0; i < variants; i++){ - Pixmap image = new Pixmap(32, 32); PixmapRegion shadow = Core.atlas.getPixmap(itemDrop.name + (i + 1)); + Pixmap image = shadow.crop(); - int offset = image.getWidth() / tilesize - 1; - Color color = new Color(); + int offset = image.width / tilesize - 1; + int shadowColor = Color.rgba8888(0, 0, 0, 0.3f); - for(int x = 0; x < image.getWidth(); x++){ - for(int y = offset; y < image.getHeight(); y++){ - shadow.getPixel(x, y - offset, color); - - if(color.a > 0.001f){ - color.set(0, 0, 0, 0.3f); - image.draw(x, y, color); + for(int x = 0; x < image.width; x++){ + for(int y = offset; y < image.height; y++){ + if(shadow.getA(x, y) == 0 && shadow.getA(x, y - offset) != 0){ + image.setRaw(x, y, shadowColor); } } } - image.draw(shadow); - packer.add(PageType.environment, name + (i + 1), image); packer.add(PageType.editor, "editor-" + name + (i + 1), image); diff --git a/core/src/mindustry/world/blocks/environment/Boulder.java b/core/src/mindustry/world/blocks/environment/Prop.java similarity index 71% rename from core/src/mindustry/world/blocks/environment/Boulder.java rename to core/src/mindustry/world/blocks/environment/Prop.java index 5e453a7c28..1056748bce 100644 --- a/core/src/mindustry/world/blocks/environment/Boulder.java +++ b/core/src/mindustry/world/blocks/environment/Prop.java @@ -3,26 +3,25 @@ package mindustry.world.blocks.environment; import arc.*; import arc.graphics.g2d.*; import arc.math.*; +import mindustry.content.*; import mindustry.world.*; -public class Boulder extends Block{ +public class Prop extends Block{ public int variants; - public Boulder(String name){ + public Prop(String name){ super(name); breakable = true; alwaysReplace = true; + instantDeconstruct = true; deconstructThreshold = 0.35f; + breakEffect = Fx.breakProp; } @Override public void drawBase(Tile tile){ - if(variants > 0){ - Draw.rect(variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))], tile.worldx(), tile.worldy()); - }else{ - Draw.rect(region, tile.worldx(), tile.worldy()); - } + Draw.rect(variants > 0 ? variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))] : region, tile.worldx(), tile.worldy()); } @Override diff --git a/core/src/mindustry/world/blocks/environment/StaticClusterWall.java b/core/src/mindustry/world/blocks/environment/StaticClusterWall.java new file mode 100644 index 0000000000..90f9417863 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/StaticClusterWall.java @@ -0,0 +1,25 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import mindustry.annotations.Annotations.*; +import mindustry.world.*; + +public class StaticClusterWall extends StaticWall{ + public @Load(value = "@-cluster#", length = 1) TextureRegion[] clusters; + + public StaticClusterWall(String name){ + super(name); + variants = 1; + } + + @Override + public void drawBase(Tile tile){ + super.drawBase(tile); + + if(Mathf.randomSeed(tile.pos(), 10) < 2){ + Draw.rect(clusters[0], tile.worldx(), tile.worldy(), Mathf.randomSeedRange(tile.pos() + 1, 180f)); + } + } + +} diff --git a/core/src/mindustry/world/blocks/environment/StaticWall.java b/core/src/mindustry/world/blocks/environment/StaticWall.java index a364db4ab2..ff648f6dae 100644 --- a/core/src/mindustry/world/blocks/environment/StaticWall.java +++ b/core/src/mindustry/world/blocks/environment/StaticWall.java @@ -10,7 +10,7 @@ import mindustry.world.*; import static mindustry.Vars.*; -public class StaticWall extends Boulder{ +public class StaticWall extends Prop{ public @Load("@-large") TextureRegion large; public TextureRegion[][] split; @@ -34,6 +34,11 @@ public class StaticWall extends Boulder{ }else{ Draw.rect(region, tile.worldx(), tile.worldy()); } + + //draw ore on top + if(tile.overlay() instanceof WallOreBlock ore){ + ore.drawBase(tile); + } } @Override diff --git a/core/src/mindustry/world/blocks/environment/TreeBlock.java b/core/src/mindustry/world/blocks/environment/TreeBlock.java index d0354c2811..d33c1afc30 100644 --- a/core/src/mindustry/world/blocks/environment/TreeBlock.java +++ b/core/src/mindustry/world/blocks/environment/TreeBlock.java @@ -2,7 +2,6 @@ package mindustry.world.blocks.environment; import arc.graphics.g2d.*; import arc.math.*; -import arc.math.geom.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.graphics.*; @@ -15,16 +14,17 @@ public class TreeBlock extends Block{ public TreeBlock(String name){ super(name); solid = true; - expanded = true; + clipSize = 90; } @Override public void drawBase(Tile tile){ - float x = tile.worldx(), y = tile.worldy(); - float rot = Mathf.randomSeed(tile.pos(), 0, 4) * 90 + Mathf.sin(Time.time + x, 50f, 0.5f) + Mathf.sin(Time.time - y, 65f, 0.9f) + Mathf.sin(Time.time + y - x, 85f, 0.9f); - float w = region.width * Draw.scl, h = region.height * Draw.scl; - float scl = 30f, mag = 0.2f; + float + x = tile.worldx(), y = tile.worldy(), + rot = Mathf.randomSeed(tile.pos(), 0, 4) * 90 + Mathf.sin(Time.time + x, 50f, 0.5f) + Mathf.sin(Time.time - y, 65f, 0.9f) + Mathf.sin(Time.time + y - x, 85f, 0.9f), + w = region.width * Draw.scl, h = region.height * Draw.scl, + scl = 30f, mag = 0.2f; if(shadow.found()){ Draw.z(Layer.power - 1); @@ -32,15 +32,9 @@ public class TreeBlock extends Block{ } Draw.z(Layer.power + 1); - Draw.rectv(region, x, y, w, h, rot, vec -> { - vec.add( - Mathf.sin(vec.y*3 + Time.time, scl, mag) + Mathf.sin(vec.x*3 - Time.time, 70, 0.8f), - Mathf.cos(vec.x*3 + Time.time + 8, scl + 6f, mag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50, 0.2f) - ); - }); - } - - void tweak(Vec2 vec){ - + Draw.rectv(region, x, y, w, h, rot, vec -> vec.add( + Mathf.sin(vec.y*3 + Time.time, scl, mag) + Mathf.sin(vec.x*3 - Time.time, 70, 0.8f), + Mathf.cos(vec.x*3 + Time.time + 8, scl + 6f, mag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50, 0.2f) + )); } } diff --git a/core/src/mindustry/world/blocks/environment/WallOreBlock.java b/core/src/mindustry/world/blocks/environment/WallOreBlock.java new file mode 100644 index 0000000000..cdfffff89e --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/WallOreBlock.java @@ -0,0 +1,16 @@ +package mindustry.world.blocks.environment; + +import mindustry.type.*; + +/**An overlay ore that draws on top of walls. */ +public class WallOreBlock extends OreBlock{ + + public WallOreBlock(Item ore){ + super("wall-ore-" + ore.name, ore); + } + + //mods only + public WallOreBlock(String name){ + super(name); + } +} diff --git a/core/src/mindustry/world/blocks/environment/WavingProp.java b/core/src/mindustry/world/blocks/environment/WavingProp.java new file mode 100644 index 0000000000..e999137af5 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/WavingProp.java @@ -0,0 +1,30 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.world.*; + +public class WavingProp extends Prop{ + + public WavingProp(String name){ + super(name); + } + + @Override + public void drawBase(Tile tile){ + var region = variants > 0 ? variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))] : this.region; + + float + x = tile.worldx(), y = tile.worldy(), + rotmag = 3f, rotscl = 0.5f, + rot = Mathf.randomSeedRange(tile.pos(), 20f) - 45 + Mathf.sin(Time.time + x, 50f * rotscl, 0.5f * rotmag) + Mathf.sin(Time.time - y, 65f * rotscl, 0.9f* rotmag) + Mathf.sin(Time.time + y - x, 85f * rotscl, 0.9f* rotmag), + w = region.width * Draw.scl, h = region.height * Draw.scl, + scl = 30f, mag = 0.3f; + + Draw.rectv(region, x, y, w, h, rot, vec -> vec.add( + Mathf.sin(vec.y*3 + Time.time, scl, mag) + Mathf.sin(vec.x*3 - Time.time, 70, 0.8f), + Mathf.cos(vec.x*3 + Time.time + 8, scl + 6f, mag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50, 0.2f) + )); + } +} diff --git a/core/src/mindustry/world/blocks/environment/WobbleProp.java b/core/src/mindustry/world/blocks/environment/WobbleProp.java new file mode 100644 index 0000000000..8a8f005574 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/WobbleProp.java @@ -0,0 +1,24 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.world.*; + +public class WobbleProp extends Prop{ + public float wscl = 25f, wmag = 0.4f, wtscl = 1f, wmag2 = 1f; + + public WobbleProp(String name){ + super(name); + } + + @Override + public void drawBase(Tile tile){ + var region = variants > 0 ? variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))] : this.region; + + Draw.rectv(region, tile.worldx(), tile.worldy(), region.width * Draw.scl, region.height * Draw.scl, 0, vec -> vec.add( + Mathf.sin(vec.y*3 + Time.time, wscl, wmag) + Mathf.sin(vec.x*3 - Time.time, 70 * wtscl, 0.8f * wmag2), + Mathf.cos(vec.x*3 + Time.time + 8, wscl + 6f, wmag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50 * wtscl, 0.2f * wmag2) + )); + } +} diff --git a/core/src/mindustry/world/blocks/experimental/BlockForge.java b/core/src/mindustry/world/blocks/experimental/BlockForge.java deleted file mode 100644 index fa871d347c..0000000000 --- a/core/src/mindustry/world/blocks/experimental/BlockForge.java +++ /dev/null @@ -1,169 +0,0 @@ -package mindustry.world.blocks.experimental; - -import arc.graphics.*; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import arc.util.*; -import arc.util.io.*; -import mindustry.*; -import mindustry.entities.units.*; -import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.*; -import mindustry.world.blocks.*; -import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; -import mindustry.world.consumers.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class BlockForge extends PayloadAcceptor{ - public float buildSpeed = 0.4f; - public int minBlockSize = 1, maxBlockSize = 2; - - public BlockForge(String name){ - super(name); - - size = 3; - update = true; - outputsPayload = true; - hasItems = true; - configurable = true; - hasPower = true; - rotate = true; - - config(Block.class, (BlockForgeBuild tile, Block block) -> { - if(tile.recipe != block) tile.progress = 0f; - tile.recipe = block; - }); - - consumes.add(new ConsumeItemDynamic((BlockForgeBuild e) -> e.recipe != null ? e.recipe.requirements : ItemStack.empty)); - } - - @Override - public TextureRegion[] icons(){ - return new TextureRegion[]{region, outRegion}; - } - - @Override - public void setBars(){ - super.setBars(); - - bars.add("progress", (BlockForgeBuild entity) -> new Bar("bar.progress", Pal.ammo, () -> entity.recipe == null ? 0f : (entity.progress / entity.recipe.buildCost))); - } - - @Override - public void setStats(){ - super.setStats(); - - stats.add(Stat.output, "@x@ ~ @x@", minBlockSize, minBlockSize, maxBlockSize, maxBlockSize); - } - - - @Override - public void drawRequestRegion(BuildPlan req, Eachable list){ - Draw.rect(region, req.drawx(), req.drawy()); - Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); - } - - public class BlockForgeBuild extends PayloadAcceptorBuild{ - public @Nullable Block recipe; - public float progress, time, heat; - - @Override - public boolean acceptItem(Building source, Item item){ - return items.get(item) < getMaximumAccepted(item); - } - - @Override - public int getMaximumAccepted(Item item){ - if(recipe == null) return 0; - for(ItemStack stack : recipe.requirements){ - if(stack.item == item) return stack.amount * 2; - } - return 0; - } - - @Override - public boolean acceptPayload(Building source, Payload payload){ - return false; - } - - @Override - public void updateTile(){ - boolean produce = recipe != null && consValid() && payload == null; - - if(produce){ - progress += buildSpeed * edelta(); - - if(progress >= recipe.buildCost){ - consume(); - payload = new BuildPayload(recipe, team); - payVector.setZero(); - progress %= 1f; - } - } - - heat = Mathf.lerpDelta(heat, Mathf.num(produce), 0.3f); - time += heat * delta(); - - moveOutPayload(); - } - - @Override - public void buildConfiguration(Table table){ - Seq blocks = Vars.content.blocks().select(b -> b.isVisible() && b.size >= minBlockSize && b.size <= maxBlockSize); - - ItemSelection.buildTable(table, blocks, () -> recipe, this::configure); - } - - @Override - public Object config(){ - return recipe; - } - - @Override - public void draw(){ - Draw.rect(region, x, y); - Draw.rect(outRegion, x, y, rotdeg()); - - if(recipe != null){ - Draw.draw(Layer.blockOver, () -> Drawf.construct(this, recipe, 0, progress / recipe.buildCost, heat, time)); - } - - drawPayload(); - } - - @Override - public void drawSelect(){ - if(recipe != null){ - float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f; - TextureRegion icon = recipe.icon(Cicon.medium); - Draw.mixcol(Color.darkGray, 1f); - //Fixes size because modded content icons are not scaled - Draw.rect(icon, dx - 0.7f, dy - 1f, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); - Draw.reset(); - Draw.rect(icon, dx, dy, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); - } - } - - @Override - public void write(Writes write){ - super.write(write); - write.s(recipe == null ? -1 : recipe.id); - write.f(progress); - } - - @Override - public void read(Reads read, byte revision){ - super.read(read, revision); - recipe = Vars.content.block(read.s()); - progress = read.f(); - } - } -} diff --git a/core/src/mindustry/world/blocks/experimental/BlockLoader.java b/core/src/mindustry/world/blocks/experimental/BlockLoader.java index 172ea8e5b7..41f2aa13f8 100644 --- a/core/src/mindustry/world/blocks/experimental/BlockLoader.java +++ b/core/src/mindustry/world/blocks/experimental/BlockLoader.java @@ -9,11 +9,10 @@ import mindustry.graphics.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; import static mindustry.Vars.*; -public class BlockLoader extends PayloadAcceptor{ +public class BlockLoader extends PayloadBlock{ public final int timerLoad = timers++; public float loadTime = 2f; @@ -58,7 +57,7 @@ public class BlockLoader extends PayloadAcceptor{ Draw.rect(topRegion, req.drawx(), req.drawy()); } - public class BlockLoaderBuild extends PayloadAcceptorBuild{ + public class BlockLoaderBuild extends PayloadBlockBuild{ @Override public boolean acceptPayload(Building source, Payload payload){ @@ -90,7 +89,6 @@ public class BlockLoader extends PayloadAcceptor{ Draw.rect(outRegion, x, y, rotdeg()); Draw.z(Layer.blockOver); - payRotation = rotdeg(); drawPayload(); Draw.z(Layer.blockOver + 0.1f); diff --git a/core/src/mindustry/world/blocks/logic/LogicBlock.java b/core/src/mindustry/world/blocks/logic/LogicBlock.java index 62f23884d5..f38644e5f5 100644 --- a/core/src/mindustry/world/blocks/logic/LogicBlock.java +++ b/core/src/mindustry/world/blocks/logic/LogicBlock.java @@ -197,14 +197,15 @@ public class LogicBlock extends Block{ public Seq links = new Seq<>(); public boolean checkedDuplicates = false; - public void readCompressed(byte[] data, boolean relative){ - DataInputStream stream = new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data))); + /** Block of code to run after load. */ + public @Nullable Runnable loadBlock; - try{ + public void readCompressed(byte[] data, boolean relative){ + try(DataInputStream stream = new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)))){ int version = stream.read(); int bytelen = stream.readInt(); - if(bytelen > maxByteLen) throw new RuntimeException("Malformed logic data! Length: " + bytelen); + if(bytelen > maxByteLen) throw new IOException("Malformed logic data! Length: " + bytelen); byte[] bytes = new byte[bytelen]; stream.readFully(bytes); @@ -243,6 +244,7 @@ public class LogicBlock extends Block{ updateCode(new String(bytes, charset)); }catch(Exception ignored){ + //invalid logic doesn't matter here } } @@ -350,11 +352,8 @@ public class LogicBlock extends Block{ executor.load(asm); }catch(Exception e){ - Log.err("Failed to compile logic program @", code); - Log.err(e); - //handle malformed code and replace it with nothing - executor.load(""); + executor.load(code = ""); } } } @@ -372,6 +371,12 @@ public class LogicBlock extends Block{ @Override public void updateTile(){ + //load up code from read() + if(loadBlock != null){ + loadBlock.run(); + loadBlock = null; + } + executor.team = team; if(!checkedDuplicates){ @@ -447,9 +452,9 @@ public class LogicBlock extends Block{ } public Seq relativeConnections(){ - Seq copy = new Seq<>(links.size); - for(LogicLink l : links){ - LogicLink c = l.copy(); + var copy = new Seq(links.size); + for(var l : links){ + var c = l.copy(); c.x -= tileX(); c.y -= tileY(); copy.add(c); @@ -582,8 +587,7 @@ public class LogicBlock extends Block{ //skip memory, it isn't used anymore read.skip(memory * 8); - updateCode(code, false, asm -> { - + loadBlock = () -> updateCode(code, false, asm -> { //load up the variables that were stored for(int i = 0; i < varcount; i++){ BVar dest = asm.getVar(names[i]); @@ -592,6 +596,7 @@ public class LogicBlock extends Block{ } } }); + } } } diff --git a/core/src/mindustry/world/blocks/payloads/BallisticSilo.java b/core/src/mindustry/world/blocks/payloads/BallisticSilo.java new file mode 100644 index 0000000000..a173a5ec94 --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/BallisticSilo.java @@ -0,0 +1,29 @@ +package mindustry.world.blocks.payloads; + +import mindustry.gen.*; +import mindustry.world.blocks.payloads.NuclearWarhead.*; + +public class BallisticSilo extends PayloadBlock{ + + public BallisticSilo(String name){ + super(name); + } + + public class BallisticSiloBuild extends PayloadBlockBuild{ + @Override + public boolean acceptPayload(Building source, Payload payload){ + return this.payload == null && payload instanceof BuildPayload b && b.build instanceof NuclearWarheadBuild; + } + + @Override + public void updateTile(){ + moveInPayload(); + } + + @Override + public void draw(){ + super.draw(); + drawPayload(); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/BlockForge.java b/core/src/mindustry/world/blocks/payloads/BlockForge.java new file mode 100644 index 0000000000..52827780af --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/BlockForge.java @@ -0,0 +1,89 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.scene.ui.layout.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.*; +import mindustry.ui.*; +import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +/** Configurable BlockProducer variant. */ +public class BlockForge extends BlockProducer{ + public float buildSpeed = 0.4f; + public int minBlockSize = 1, maxBlockSize = 2; + + public BlockForge(String name){ + super(name); + + size = 3; + configurable = true; + + config(Block.class, (BlockForgeBuild tile, Block block) -> { + if(tile.recipe != block) tile.progress = 0f; + if(canProduce(block)){ + tile.recipe = block; + } + }); + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.output, "@x@ ~ @x@", minBlockSize, minBlockSize, maxBlockSize, maxBlockSize); + } + + public boolean canProduce(Block b){ + return b.isVisible() && b.size >= minBlockSize && b.size <= maxBlockSize; + } + + public class BlockForgeBuild extends BlockProducerBuild{ + public @Nullable Block recipe; + + @Override + public @Nullable Block recipe(){ + return recipe; + } + + @Override + public void buildConfiguration(Table table){ + ItemSelection.buildTable(table, content.blocks().select(BlockForge.this::canProduce), () -> recipe, this::configure); + } + + @Override + public Object config(){ + return recipe; + } + + @Override + public void drawSelect(){ + if(recipe != null){ + float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f; + TextureRegion icon = recipe.uiIcon; + Draw.mixcol(Color.darkGray, 1f); + //Fixes size because modded content icons are not scaled + Draw.rect(icon, dx - 0.7f, dy - 1f, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); + Draw.reset(); + Draw.rect(icon, dx, dy, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); + } + } + + @Override + public void write(Writes write){ + super.write(write); + write.s(recipe == null ? -1 : recipe.id); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + recipe = Vars.content.block(read.s()); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/BlockProducer.java b/core/src/mindustry/world/blocks/payloads/BlockProducer.java new file mode 100644 index 0000000000..ec8642cc90 --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/BlockProducer.java @@ -0,0 +1,143 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.type.*; +import mindustry.ui.*; +import mindustry.world.*; +import mindustry.world.consumers.*; + +import static mindustry.Vars.*; + +/** Generic building that produces other buildings. */ +public abstract class BlockProducer extends PayloadBlock{ + public float buildSpeed = 0.4f; + + public BlockProducer(String name){ + super(name); + + size = 3; + update = true; + outputsPayload = true; + hasItems = true; + hasPower = true; + rotate = true; + + consumes.add(new ConsumeItemDynamic((BlockProducerBuild e) -> e.recipe() != null ? e.recipe().requirements : ItemStack.empty)); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, outRegion}; + } + + @Override + public void setBars(){ + super.setBars(); + + bars.add("progress", (BlockProducerBuild entity) -> new Bar("bar.progress", Pal.ammo, () -> entity.recipe() == null ? 0f : (entity.progress / entity.recipe().buildCost))); + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + public abstract class BlockProducerBuild extends PayloadBlockBuild{ + public float progress, time, heat; + + public abstract @Nullable Block recipe(); + + @Override + public boolean acceptItem(Building source, Item item){ + return items.get(item) < getMaximumAccepted(item); + } + + @Override + public int getMaximumAccepted(Item item){ + if(recipe() == null) return 0; + for(ItemStack stack : recipe().requirements){ + if(stack.item == item) return stack.amount * 2; + } + return 0; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return false; + } + + @Override + public void updateTile(){ + var recipe = recipe(); + boolean produce = recipe != null && consValid() && payload == null; + + if(produce){ + progress += buildSpeed * edelta(); + + if(progress >= recipe.buildCost){ + consume(); + payload = new BuildPayload(recipe, team); + payVector.setZero(); + progress %= 1f; + } + } + + heat = Mathf.lerpDelta(heat, Mathf.num(produce), 0.15f); + time += heat * delta(); + + moveOutPayload(); + } + + @Override + public void draw(){ + Draw.rect(region, x, y); + Draw.rect(outRegion, x, y, rotdeg()); + + var recipe = recipe(); + if(recipe != null){ + Drawf.shadow(x, y, recipe.size * tilesize * 2f, progress / recipe.buildCost); + Draw.draw(Layer.blockBuilding, () -> { + Draw.color(Pal.accent); + + for(TextureRegion region : recipe.getGeneratedIcons()){ + Shaders.blockbuild.region = region; + Shaders.blockbuild.progress = progress / recipe.buildCost; + + Draw.rect(region, x, y, recipe.rotate ? rotdeg() : 0); + Draw.flush(); + } + + Draw.color(); + }); + Draw.z(Layer.blockBuilding + 1); + Draw.color(Pal.accent, heat); + + Lines.lineAngleCenter(x + Mathf.sin(time, 10f, Vars.tilesize / 2f * recipe.size + 1f), y, 90, recipe.size * Vars.tilesize + 1f); + + Draw.reset(); + } + + drawPayload(); + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(progress); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + progress = read.f(); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/BuildPayload.java b/core/src/mindustry/world/blocks/payloads/BuildPayload.java index 446954883b..d9e6c08fa0 100644 --- a/core/src/mindustry/world/blocks/payloads/BuildPayload.java +++ b/core/src/mindustry/world/blocks/payloads/BuildPayload.java @@ -5,7 +5,6 @@ import arc.util.io.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; @@ -34,6 +33,16 @@ public class BuildPayload implements Payload{ build.dropped(); } + @Override + public float x(){ + return build.x; + } + + @Override + public float y(){ + return build.y; + } + @Override public float size(){ return build.block.size * tilesize; @@ -55,11 +64,11 @@ public class BuildPayload implements Payload{ @Override public void draw(){ Drawf.shadow(build.x, build.y, build.block.size * tilesize * 2f); - Draw.rect(build.block.icon(Cicon.full), build.x, build.y); + Draw.rect(build.block.fullIcon, build.x, build.y); } @Override - public TextureRegion icon(Cicon icon){ - return block().icon(icon); + public TextureRegion icon(){ + return block().fullIcon; } } diff --git a/core/src/mindustry/world/blocks/payloads/NuclearWarhead.java b/core/src/mindustry/world/blocks/payloads/NuclearWarhead.java new file mode 100644 index 0000000000..4f4805822c --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/NuclearWarhead.java @@ -0,0 +1,18 @@ +package mindustry.world.blocks.payloads; + +import mindustry.gen.*; +import mindustry.world.*; + +public class NuclearWarhead extends Block{ + public float radius = 100f; + + public NuclearWarhead(String name){ + super(name); + solid = true; + update = true; + } + + public class NuclearWarheadBuild extends Building{ + + } +} diff --git a/core/src/mindustry/world/blocks/payloads/Payload.java b/core/src/mindustry/world/blocks/payloads/Payload.java index afce4b0da4..d182f08a37 100644 --- a/core/src/mindustry/world/blocks/payloads/Payload.java +++ b/core/src/mindustry/world/blocks/payloads/Payload.java @@ -1,16 +1,16 @@ package mindustry.world.blocks.payloads; import arc.graphics.g2d.*; +import arc.math.geom.*; import arc.util.*; import arc.util.io.*; import mindustry.game.*; import mindustry.gen.*; -import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; -public interface Payload{ +public interface Payload extends Position{ int payloadUnit = 0, payloadBlock = 1; /** sets this payload's position on the map. */ @@ -22,6 +22,9 @@ public interface Payload{ /** @return hitbox size of the payload. */ float size(); + float x(); + float y(); + /** @return whether this payload was dumped. */ default boolean dump(){ return false; @@ -41,7 +44,17 @@ public interface Payload{ void write(Writes write); /** @return icon describing the contents. */ - TextureRegion icon(Cicon icon); + TextureRegion icon(); + + @Override + default float getX(){ + return x(); + } + + @Override + default float getY(){ + return y(); + } static void write(@Nullable Payload payload, Writes write){ if(payload == null){ diff --git a/core/src/mindustry/world/blocks/payloads/PayloadBlock.java b/core/src/mindustry/world/blocks/payloads/PayloadBlock.java new file mode 100644 index 0000000000..20db57c296 --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadBlock.java @@ -0,0 +1,233 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +public class PayloadBlock extends Block{ + public float payloadSpeed = 0.5f, payloadRotateSpeed = 5f; + + public @Load(value = "@-top", fallback = "factory-top-@size") TextureRegion topRegion; + public @Load(value = "@-out", fallback = "factory-out-@size") TextureRegion outRegion; + public @Load(value = "@-in", fallback = "factory-in-@size") TextureRegion inRegion; + + public PayloadBlock(String name){ + super(name); + + update = true; + sync = true; + } + + public static boolean blends(Building build, int direction){ + int size = build.block.size; + int trns = build.block.size/2 + 1; + Building accept = build.nearby(Geometry.d4(direction).x * trns, Geometry.d4(direction).y * trns); + return accept != null && + accept.block.outputsPayload && + + //if size is the same, block must either be facing this one, or not be rotating + ((accept.block.size == size + && Math.abs(accept.tileX() - build.tileX()) % size == 0 //check alignment + && Math.abs(accept.tileY() - build.tileY()) % size == 0 + && ((accept.block.rotate && accept.tileX() + Geometry.d4(accept.rotation).x * size == build.tileX() && accept.tileY() + Geometry.d4(accept.rotation).y * size == build.tileY()) + || !accept.block.rotate + || !accept.block.outputFacing)) || + + //if the other block is smaller, check alignment + (accept.block.size != size && + (accept.rotation % 2 == 0 ? //check orientation; make sure it's aligned properly with this block. + Math.abs(accept.y - build.y) <= Math.abs(size * tilesize - accept.block.size * tilesize)/2f : //check Y alignment + Math.abs(accept.x - build.x) <= Math.abs(size * tilesize - accept.block.size * tilesize)/2f //check X alignment + )) && (!accept.block.rotate || accept.front() == build || !accept.block.outputFacing) //make sure it's facing this block + ); + } + + public static void pushOutput(Payload payload, float progress){ + float thresh = 0.55f; + if(progress >= thresh){ + boolean legStep = payload instanceof UnitPayload u && u.unit.type.allowLegStep; + float size = payload.size(), radius = size/2f, x = payload.x(), y = payload.y(), scl = Mathf.clamp(((progress - thresh) / (1f - thresh)) * 1.1f); + + Groups.unit.intersect(x - size/2f, y - size/2f, size, size, u -> { + float dst = u.dst(payload); + float rs = radius + u.hitSize/2f; + if(u.isGrounded() && u.type.allowLegStep == legStep && dst < rs){ + u.vel.add(Tmp.v1.set(u.x - x, u.y - y).setLength(Math.min(rs - dst, 1f)).scl(scl)); + } + }); + } + } + + public class PayloadBlockBuild extends Building{ + public @Nullable T payload; + //TODO redundant; already stored in payload? + public Vec2 payVector = new Vec2(); + public float payRotation; + public boolean carried; + + public boolean acceptUnitPayload(Unit unit){ + return false; + } + + @Override + public boolean canControlSelect(Player player){ + return !player.unit().spawnedByCore && this.payload == null && acceptUnitPayload(player.unit()) && player.tileOn().build == this; + } + + @Override + public void onControlSelect(Player player){ + float x = player.x, y = player.y; + acceptPlayerPayload(player, p -> payload = (T)p); + this.payVector.set(x, y).sub(this).clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); + this.payRotation = player.unit().rotation; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return this.payload == null; + } + + @Override + public void handlePayload(Building source, Payload payload){ + this.payload = (T)payload; + this.payVector.set(source).sub(this).clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); + this.payRotation = payload.rotation(); + + updatePayload(); + } + + @Override + public Payload getPayload(){ + return payload; + } + + @Override + public void pickedUp(){ + carried = true; + } + + @Override + public void drawTeamTop(){ + carried = false; + } + + @Override + public Payload takePayload(){ + T t = payload; + payload = null; + return t; + } + + @Override + public void onRemoved(){ + super.onRemoved(); + if(payload != null && !carried) payload.dump(); + } + + public boolean blends(int direction){ + return PayloadBlock.blends(this, direction); + } + + public void updatePayload(){ + if(payload != null){ + payload.set(x + payVector.x, y + payVector.y, payRotation); + } + } + + /** @return true if the payload is in position. */ + public boolean moveInPayload(){ + return moveInPayload(true); + } + + /** @return true if the payload is in position. */ + public boolean moveInPayload(boolean rotate){ + if(payload == null) return false; + + updatePayload(); + + if(rotate){ + payRotation = Angles.moveToward(payRotation, rotate ? rotdeg() : 90f, payloadRotateSpeed * edelta()); + } + payVector.approach(Vec2.ZERO, payloadSpeed * delta()); + + return hasArrived(); + } + + public void moveOutPayload(){ + if(payload == null) return; + + updatePayload(); + + Vec2 dest = Tmp.v1.trns(rotdeg(), size * tilesize/2f); + + payRotation = Angles.moveToward(payRotation, rotdeg(), payloadRotateSpeed * edelta()); + payVector.approach(dest, payloadSpeed * delta()); + + Building front = front(); + boolean canDump = front == null || !front.tile().solid(); + boolean canMove = front != null && (front.block.outputsPayload || front.block.acceptsPayload); + + if(canDump && !canMove){ + pushOutput(payload, 1f - (payVector.dst(dest) / (size * tilesize / 2f))); + } + + if(payVector.within(dest, 0.001f)){ + payVector.clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); + + if(canMove){ + if(movePayload(payload)){ + payload = null; + } + }else if(canDump){ + dumpPayload(); + } + } + } + + public void dumpPayload(){ + if(payload.dump()){ + payload = null; + } + } + + public boolean hasArrived(){ + return payVector.isZero(0.01f); + } + + public void drawPayload(){ + if(payload != null){ + updatePayload(); + + Draw.z(Layer.blockOver); + payload.draw(); + } + } + + @Override + public void write(Writes write){ + super.write(write); + + write.f(payVector.x); + write.f(payVector.y); + write.f(payRotation); + Payload.write(payload, write); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + + payVector.set(read.f(), read.f()); + payRotation = read.f(); + payload = Payload.read(read); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java b/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java new file mode 100644 index 0000000000..50aacfe31f --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java @@ -0,0 +1,481 @@ +package mindustry.world.blocks.payloads; + +import arc.audio.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.logic.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; +import static mindustry.world.blocks.payloads.PayloadMassDriver.PayloadDriverState.*; + +public class PayloadMassDriver extends PayloadBlock{ + public float range = 100f; + public float rotateSpeed = 2f; + public float length = 89 / 8f; + public float knockback = 5f; + public float reloadTime = 30f; + public float chargeTime = 100f; + public float maxPayloadSize = 3; + public float grabWidth = 8f, grabHeight = 11/4f; + public Effect shootEffect = Fx.shootBig2; + public Effect smokeEffect = Fx.shootPayloadDriver; + public Effect receiveEffect = Fx.payloadReceive; + public Sound shootSound = Sounds.shootBig; + public float shake = 3f; + + public Effect transferEffect = new Effect(11f, 300f, e -> { + if(!(e.data instanceof PayloadMassDriverData data)) return; + Tmp.v1.set(data.x, data.y).lerp(data.ox, data.oy, Interp.sineIn.apply(e.fin())); + data.payload.set(Tmp.v1.x, Tmp.v1.y, e.rotation); + data.payload.draw(); + }).layer(Layer.flyingUnitLow - 1); + + public @Load("@-base") TextureRegion baseRegion; + public @Load("@-cap") TextureRegion capRegion; + public @Load("@-left") TextureRegion leftRegion; + public @Load("@-right") TextureRegion rightRegion; + public @Load("@-cap-outline") TextureRegion capOutlineRegion; + public @Load("@-left-outline") TextureRegion leftOutlineRegion; + public @Load("@-right-outline") TextureRegion rightOutlineRegion; + public @Load("bridge-arrow") TextureRegion arrow; + + public PayloadMassDriver(String name){ + super(name); + update = true; + solid = true; + configurable = true; + hasPower = true; + outlineIcon = true; + sync = true; + rotate = true; + outputsPayload = true; + + //point2 is relative + config(Point2.class, (PayloadDriverBuild tile, Point2 point) -> tile.link = Point2.pack(point.x + tile.tileX(), point.y + tile.tileY())); + config(Integer.class, (PayloadDriverBuild tile, Integer point) -> tile.link = point); + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.payloadCapacity, maxPayloadSize, StatUnit.blocksSquared); + stats.add(Stat.reload, 60f / (chargeTime + reloadTime), StatUnit.seconds); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{baseRegion, outRegion, region}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(baseRegion, req.drawx(), req.drawy()); + Draw.rect(topRegion, req.drawx(), req.drawy()); + Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); + Draw.rect(region, req.drawx(), req.drawy()); + } + + @Override + public void drawPlace(int x, int y, int rotation, boolean valid){ + super.drawPlace(x, y, rotation, valid); + + Drawf.dashCircle(x * tilesize, y * tilesize, range, Pal.accent); + + //check if a mass driver is selected while placing this driver + if(!control.input.frag.config.isShown()) return; + Building selected = control.input.frag.config.getSelectedTile(); + if(selected == null || selected.block != this || !selected.within(x * tilesize, y * tilesize, range)) return; + + //if so, draw a dotted line towards it while it is in range + float sin = Mathf.absin(Time.time, 6f, 1f); + Tmp.v1.set(x * tilesize + offset, y * tilesize + offset).sub(selected.x, selected.y).limit((size / 2f + 1) * tilesize + sin + 0.5f); + float x2 = x * tilesize - Tmp.v1.x, y2 = y * tilesize - Tmp.v1.y, + x1 = selected.x + Tmp.v1.x, y1 = selected.y + Tmp.v1.y; + int segs = (int)(selected.dst(x * tilesize, y * tilesize) / tilesize); + + Lines.stroke(4f, Pal.gray); + Lines.dashLine(x1, y1, x2, y2, segs); + Lines.stroke(2f, Pal.placing); + Lines.dashLine(x1, y1, x2, y2, segs); + Draw.reset(); + } + + @Override + public TextureRegion[] makeIconRegions(){ + return new TextureRegion[]{leftRegion, rightRegion, capRegion}; + } + + public class PayloadDriverBuild extends PayloadBlockBuild{ + public int link = -1; + public float turretRotation = 90; + public float reload = 0f, charge = 0f; + public float targetSize = grabWidth*2f, curSize = targetSize; + public float payLength = 0f; + public boolean loaded; + public boolean charging; + public PayloadDriverState state = idle; + public Queue waitingShooters = new Queue<>(); + public Payload recPayload; + + public Building currentShooter(){ + return waitingShooters.isEmpty() ? null : waitingShooters.first(); + } + + @Override + public void updateTile(){ + Building link = world.build(this.link); + boolean hasLink = linkValid(); + + //discharge when charging isn't happening + if(!charging){ + charge -= Time.delta * 10f; + if(charge < 0) charge = 0f; + } + + curSize = Mathf.lerpDelta(curSize, targetSize, 0.05f); + targetSize = grabWidth*2f; + + if(payload != null){ + targetSize = payload.size(); + } + + charging = false; + + if(hasLink){ + this.link = link.pos(); + } + + //reload regardless of state + reload -= edelta() / reloadTime; + if(reload < 0) reload = 0f; + + var current = currentShooter(); + + //cleanup waiting shooters that are not valid + if(current != null && + !( + current instanceof PayloadDriverBuild entity && + entity.consValid() && entity.block == block && + entity.link == pos() && within(current, range) + )){ + waitingShooters.removeFirst(); + } + + //switch states + if(state == idle){ + //start accepting when idle and there's space + if(!waitingShooters.isEmpty() && payload == null){ + state = accepting; + }else if(hasLink){ //switch to shooting if there's a valid link. + state = shooting; + } + } + + //dump when idle or accepting + if((state == idle || state == accepting) && payload != null){ + if(loaded){ + payLength -= payloadSpeed * delta(); + if(payLength <= 0f){ + loaded = false; + payVector.setZero(); + payRotation = Angles.moveToward(payRotation, turretRotation + 180f, payloadRotateSpeed * delta()); + } + }else{ + moveOutPayload(); + } + } + + //skip when there's no power + if(!consValid()){ + return; + } + + if(state == accepting){ + //if there's nothing shooting at this or items are full, bail out + if(currentShooter() == null || payload != null){ + state = idle; + return; + } + + if(currentShooter().getPayload() != null){ + targetSize = recPayload == null ? currentShooter().getPayload().size() : recPayload.size(); + } + + //align to shooter rotation + turretRotation = Angles.moveToward(turretRotation, tile.angleTo(currentShooter()), rotateSpeed * efficiency()); + }else if(state == shooting){ + //if there's nothing to shoot at OR someone wants to shoot at this thing, bail + if(!hasLink || (!waitingShooters.isEmpty() && payload == null)){ + state = idle; + return; + } + + float targetRotation = tile.angleTo(link); + boolean movedOut = false; + + payRotation = Angles.moveToward(payRotation, turretRotation, payloadRotateSpeed * delta()); + if(loaded){ + float loadLength = length - reload*knockback; + payLength += payloadSpeed * delta(); + if(payLength >= loadLength){ + payLength = loadLength; + movedOut = true; + } + }else if(moveInPayload()){ + payLength = 0f; + loaded = true; + } + + //make sure payload firing can happen + if(movedOut && payload != null && link.getPayload() == null){ + var other = (PayloadDriverBuild)link; + + if(!other.waitingShooters.contains(this)){ + other.waitingShooters.addLast(this); + } + + if(reload <= 0){ + //align to target location + turretRotation = Angles.moveToward(turretRotation, targetRotation, rotateSpeed * efficiency()); + + //fire when it's the first in the queue and angles are ready. + if(other.currentShooter() == this && + other.state == accepting && + other.reload <= 0f && + Angles.within(turretRotation, targetRotation, 1f) && Angles.within(other.turretRotation, targetRotation + 180f, 1f)){ + charge += edelta(); + charging = true; + + if(charge >= chargeTime){ + float cx = Angles.trnsx(turretRotation, length), cy = Angles.trnsy(turretRotation, length); + + //effects + shootEffect.at(x + cx, y + cy, turretRotation); + smokeEffect.at(x, y, turretRotation); + + Effect.shake(shake, shake, this); + shootSound.at(this, Mathf.random(0.9f, 1.1f)); + transferEffect.at(x + cx, y + cy, turretRotation, new PayloadMassDriverData(x + cx, y + cy, other.x - cx, other.y - cy, payload)); + Payload pay = payload; + other.recPayload = payload; + + Time.run(transferEffect.lifetime, () -> { + receiveEffect.at(other.x - cx/2f, other.y - cy/2f, other.turretRotation); + Effect.shake(shake, shake, this); + + //transfer payload + other.reload = 1f; + other.handlePayload(this, pay); + other.payVector.set(-cx, -cy); + other.payRotation = turretRotation; + other.payLength = length; + other.loaded = true; + other.updatePayload(); + other.recPayload = null; + + if(other.waitingShooters.size != 0 && other.waitingShooters.first() == this){ + other.waitingShooters.removeFirst(); + } + other.state = idle; + }); + + //reset state after shooting immediately + payload = null; + payLength = 0f; + loaded = false; + state = idle; + reload = 1f; + } + } + } + } + } + } + + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(1f - reload / reloadTime); + return super.sense(sensor); + } + + @Override + public void updatePayload(){ + if(payload != null){ + if(loaded){ + payload.set(x + Angles.trnsx(turretRotation, payLength), y + Angles.trnsy(turretRotation, payLength), payRotation); + }else{ + payload.set(x + payVector.x, y + payVector.y, payRotation); + } + } + } + + @Override + public void draw(){ + float + tx = x + Angles.trnsx(turretRotation + 180f, reload * knockback), + ty = y + Angles.trnsy(turretRotation + 180f, reload * knockback), r = turretRotation - 90; + + Draw.rect(baseRegion, x, y); + + //draw input + for(int i = 0; i < 4; i++){ + if(blends(i) && i != rotation){ + Draw.rect(inRegion, x, y, (i * 90) - 180); + } + } + + Draw.rect(outRegion, x, y, rotdeg()); + + if(payload != null){ + updatePayload(); + + Draw.z(loaded ? Layer.blockOver + 0.2f : Layer.blockOver); + payload.draw(); + } + + Draw.z(Layer.blockOver + 0.1f); + Draw.rect(topRegion, x, y); + + Draw.z(Layer.turret); + //TODO + Drawf.shadow(region, tx - (size / 2f), ty - (size / 2f), r); + + Tmp.v1.trns(turretRotation, 0, -(curSize/2f - grabWidth)); + Tmp.v2.trns(rotation, -Math.max(curSize/2f - grabHeight - length, 0f), 0f); + float rx = tx + Tmp.v1.x + Tmp.v2.x, ry = ty + Tmp.v1.y + Tmp.v2.y; + float lx = tx - Tmp.v1.x + Tmp.v2.x, ly = ty - Tmp.v1.y + Tmp.v2.y; + + Draw.rect(capOutlineRegion, tx, ty, r); + Draw.rect(leftOutlineRegion, lx, ly, r); + Draw.rect(rightOutlineRegion, rx, ry, r); + + Draw.rect(leftRegion, lx, ly, r); + Draw.rect(rightRegion, rx, ry, r); + Draw.rect(capRegion, tx, ty, r); + + Draw.z(Layer.effect); + + if(charge > 0 && linkValid()){ + Building link = world.build(this.link); + + float fin = Interp.pow2Out.apply(charge / chargeTime), fout = 1f-fin, len = length*1.8f, w = curSize/2f + 7f*fout; + Vec2 right = Tmp.v1.trns(turretRotation, len, w); + Vec2 left = Tmp.v2.trns(turretRotation, len, -w); + + Lines.stroke(fin * 1.2f, Pal.accent); + Lines.line(x + left.x, y + left.y, link.x - right.x, link.y - right.y); + Lines.line(x + right.x, y + right.y, link.x - left.x, link.y - left.y); + + for(int i = 0; i < 4; i++){ + Tmp.v3.set(x, y).lerp(link.x, link.y, 0.5f + (i - 2) * 0.1f); + Draw.scl(fin * 1.1f); + Draw.rect(arrow, Tmp.v3.x, Tmp.v3.y, turretRotation); + Draw.scl(); + } + + Draw.reset(); + } + } + + @Override + public void drawConfigure(){ + float sin = Mathf.absin(Time.time, 6f, 1f); + + Draw.color(Pal.accent); + Lines.stroke(1f); + Drawf.circles(x, y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.accent); + + for(var shooter : waitingShooters){ + Drawf.circles(shooter.x, shooter.y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.arrow(shooter.x, shooter.y, x, y, size * tilesize + sin, 4f + sin, Pal.place); + } + + if(linkValid()){ + Building target = world.build(link); + Drawf.circles(target.x, target.y, (target.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.arrow(x, y, target.x, target.y, size * tilesize + sin, 4f + sin); + } + + Drawf.dashCircle(x, y, range, Pal.accent); + } + + @Override + public boolean onConfigureTileTapped(Building other){ + if(this == other){ + configure(-1); + return false; + } + + if(link == other.pos()){ + configure(-1); + return false; + }else if(other.block instanceof PayloadMassDriver && other.dst(tile) <= range && other.team == team){ + configure(other.pos()); + return false; + } + + return true; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return super.acceptPayload(source, payload) && payload.size() <= maxPayloadSize * tilesize; + } + + protected boolean linkValid(){ + return link != -1 && world.build(this.link) instanceof PayloadDriverBuild other && other.block == block && other.team == team && within(other, range); + } + + @Override + public Point2 config(){ + return Point2.unpack(link).sub(tile.x, tile.y); + } + + @Override + public void write(Writes write){ + super.write(write); + write.i(link); + write.f(turretRotation); + write.b((byte)state.ordinal()); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + link = read.i(); + turretRotation = read.f(); + state = PayloadDriverState.all[read.b()]; + } + } + + public static class PayloadMassDriverData{ + public float x, y, ox, oy; + public Payload payload; + + public PayloadMassDriverData(float x, float y, float ox, float oy, Payload payload){ + this.x = x; + this.y = y; + this.ox = ox; + this.oy = oy; + this.payload = payload; + } + } + + public enum PayloadDriverState{ + idle, accepting, shooting; + + public static final PayloadDriverState[] all = values(); + } +} diff --git a/core/src/mindustry/world/blocks/payloads/PayloadSource.java b/core/src/mindustry/world/blocks/payloads/PayloadSource.java new file mode 100644 index 0000000000..aa6a5c9a6c --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadSource.java @@ -0,0 +1,135 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.ui.layout.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.*; +import mindustry.ctype.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.*; + +import static mindustry.Vars.*; + +/** Generic building that produces other buildings. */ +public class PayloadSource extends PayloadBlock{ + + public PayloadSource(String name){ + super(name); + + size = 3; + update = true; + outputsPayload = true; + hasPower = true; + rotate = true; + configurable = true; + + config(Block.class, (PayloadSourceBuild build, Block block) -> { + if(canProduce(block) && build.block != block){ + build.block = block; + build.unit = null; + build.payload = null; + build.scl = 0f; + } + }); + + config(UnitType.class, (PayloadSourceBuild build, UnitType unit) -> { + if(canProduce(unit) && build.unit != unit){ + build.unit = unit; + build.block = null; + build.payload = null; + build.scl = 0f; + } + }); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, outRegion, topRegion}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); + Draw.rect(topRegion, req.drawx(), req.drawy()); + } + + public boolean canProduce(Block b){ + return b.isVisible() && b.size < size; + } + + public boolean canProduce(UnitType t){ + return !t.isHidden(); + } + + public class PayloadSourceBuild extends PayloadBlockBuild{ + public UnitType unit; + public Block block; + public float scl; + + @Override + public void buildConfiguration(Table table){ + ItemSelection.buildTable(table, + content.blocks().select(PayloadSource.this::canProduce).as() + .and(content.units().select(PayloadSource.this::canProduce).as()), + () -> (UnlockableContent)config(), this::configure); + } + + @Override + public Object config(){ + return unit == null ? block : unit; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return false; + } + + @Override + public void updateTile(){ + if(payload == null){ + scl = 0f; + if(unit != null){ + payload = new UnitPayload(unit.create(team)); + }else if(block != null){ + payload = new BuildPayload(block, team); + } + payVector.setZero(); + payRotation = rotdeg(); + } + scl = Mathf.lerpDelta(scl, 1f, 0.1f); + + moveOutPayload(); + } + + @Override + public void draw(){ + Draw.rect(region, x, y); + Draw.rect(outRegion, x, y, rotdeg()); + Draw.rect(topRegion, x, y); + + Draw.scl(scl); + drawPayload(); + Draw.reset(); + } + + @Override + public void write(Writes write){ + super.write(write); + write.s(unit == null ? -1 : unit.id); + write.s(block == null ? -1 : block.id); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + unit = Vars.content.unit(read.s()); + block = Vars.content.block(read.s()); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/PayloadVoid.java b/core/src/mindustry/world/blocks/payloads/PayloadVoid.java new file mode 100644 index 0000000000..434753f0ee --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadVoid.java @@ -0,0 +1,57 @@ +package mindustry.world.blocks.payloads; + +import arc.audio.*; +import arc.graphics.g2d.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; +import mindustry.graphics.*; + +public class PayloadVoid extends PayloadBlock{ + public Effect incinerateEffect = Fx.blastExplosion; + public Sound incinerateSound = Sounds.bang; + + public PayloadVoid(String name){ + super(name); + + outputsPayload = false; + acceptsPayload = true; + update = true; + rotate = false; + size = 3; + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, topRegion}; + } + + public class BlockLoaderBuild extends PayloadBlockBuild{ + + @Override + public void draw(){ + Draw.rect(region, x, y); + + //draw input + for(int i = 0; i < 4; i++){ + if(blends(i)){ + Draw.rect(inRegion, x, y, (i * 90) - 180); + } + } + + Draw.rect(topRegion, x, y); + + Draw.z(Layer.blockOver); + drawPayload(); + } + + @Override + public void updateTile(){ + if(moveInPayload(false) && cons.valid()){ + payload = null; + incinerateEffect.at(this); + incinerateSound.at(this); + } + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/UnitPayload.java b/core/src/mindustry/world/blocks/payloads/UnitPayload.java index ee090b0a68..2300dba10b 100644 --- a/core/src/mindustry/world/blocks/payloads/UnitPayload.java +++ b/core/src/mindustry/world/blocks/payloads/UnitPayload.java @@ -12,8 +12,6 @@ import mindustry.entities.EntityCollisions.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.ui.*; public class UnitPayload implements Payload{ public static final float deactiveDuration = 40f; @@ -38,6 +36,16 @@ public class UnitPayload implements Payload{ unit.rotation = rotation; } + @Override + public float x(){ + return unit.x; + } + + @Override + public float y(){ + return unit.y; + } + @Override public float rotation(){ return unit.rotation; @@ -71,6 +79,11 @@ public class UnitPayload implements Payload{ if(!nearEmpty) return false; } + //cannnot dump when there's a lot of overlap going on + if(!unit.type.flying && Units.count(unit.x, unit.y, unit.physicSize(), o -> o.isGrounded() && (o.type.allowLegStep == unit.type.allowLegStep)) > 0){ + return false; + } + //no client dumping if(Vars.net.client()) return true; @@ -87,8 +100,8 @@ public class UnitPayload implements Payload{ //TODO should not happen if(unit.type == null) return; - Drawf.shadow(unit.x, unit.y, 20); - Draw.rect(unit.type.icon(Cicon.full), unit.x, unit.y, unit.rotation - 90); + unit.type.drawSoftShadow(unit); + Draw.rect(unit.type.fullIcon, unit.x, unit.y, unit.rotation - 90); unit.type.drawCell(unit); //draw warning @@ -106,7 +119,7 @@ public class UnitPayload implements Payload{ } @Override - public TextureRegion icon(Cicon icon){ - return unit.type.icon(icon); + public TextureRegion icon(){ + return unit.type.fullIcon; } } diff --git a/core/src/mindustry/world/blocks/power/LightBlock.java b/core/src/mindustry/world/blocks/power/LightBlock.java index 8f035689b9..8a88a8e017 100644 --- a/core/src/mindustry/world/blocks/power/LightBlock.java +++ b/core/src/mindustry/world/blocks/power/LightBlock.java @@ -29,6 +29,13 @@ public class LightBlock extends Block{ config(Integer.class, (LightBuild tile, Integer value) -> tile.color = value); } + @Override + public void init(){ + lightRadius = radius; + emitLight = true; + super.init(); + } + public class LightBuild extends Building{ public int color = Pal.accent.rgba(); public float smoothTime = 1f; @@ -67,7 +74,7 @@ public class LightBlock extends Block{ @Override public void drawLight(){ - Drawf.light(team, x, y, radius * Math.min(smoothTime, 2f), Tmp.c1.set(color), brightness * efficiency()); + Drawf.light(team, x, y, lightRadius * Math.min(smoothTime, 2f), Tmp.c1.set(color), brightness * efficiency()); } @Override diff --git a/core/src/mindustry/world/blocks/power/NuclearReactor.java b/core/src/mindustry/world/blocks/power/NuclearReactor.java index 2a53e1b4c0..6ed9f11189 100644 --- a/core/src/mindustry/world/blocks/power/NuclearReactor.java +++ b/core/src/mindustry/world/blocks/power/NuclearReactor.java @@ -30,6 +30,7 @@ public class NuclearReactor extends PowerGenerator{ public Color lightColor = Color.valueOf("7f19ea"); public Color coolColor = new Color(1, 1, 1, 0f); public Color hotColor = Color.valueOf("ff9575a3"); + public Effect explodeEffect = Fx.reactorExplosion; /** ticks to consume 1 fuel */ public float itemDuration = 120; /** heating per frame * fullness */ @@ -135,26 +136,9 @@ public class NuclearReactor extends PowerGenerator{ if((fuel < 5 && heat < 0.5f) || !state.rules.reactorExplosions) return; Effect.shake(6f, 16f, x, y); - Fx.nuclearShockwave.at(x, y); - for(int i = 0; i < 6; i++){ - Time.run(Mathf.random(40), () -> Fx.nuclearcloud.at(x, y)); - } - Damage.damage(x, y, explosionRadius * tilesize, explosionDamage * 4); - for(int i = 0; i < 20; i++){ - Time.run(Mathf.random(50), () -> { - tr.rnd(Mathf.random(40f)); - Fx.explosion.at(tr.x + x, tr.y + y); - }); - } - - for(int i = 0; i < 70; i++){ - Time.run(Mathf.random(80), () -> { - tr.rnd(Mathf.random(120f)); - Fx.nuclearsmoke.at(tr.x + x, tr.y + y); - }); - } + explodeEffect.at(x, y); } @Override diff --git a/core/src/mindustry/world/blocks/power/PowerDiode.java b/core/src/mindustry/world/blocks/power/PowerDiode.java index 6e2052250e..422c978aef 100644 --- a/core/src/mindustry/world/blocks/power/PowerDiode.java +++ b/core/src/mindustry/world/blocks/power/PowerDiode.java @@ -35,7 +35,7 @@ public class PowerDiode extends Block{ @Override public void drawRequestRegion(BuildPlan req, Eachable list){ - Draw.rect(icon(Cicon.full), req.drawx(), req.drawy()); + Draw.rect(fullIcon, req.drawx(), req.drawy()); Draw.rect(arrow, req.drawx(), req.drawy(), !rotate ? 0 : req.rotation * 90); } diff --git a/core/src/mindustry/world/blocks/production/AttributeCrafter.java b/core/src/mindustry/world/blocks/production/AttributeCrafter.java new file mode 100644 index 0000000000..5312c3d4b3 --- /dev/null +++ b/core/src/mindustry/world/blocks/production/AttributeCrafter.java @@ -0,0 +1,58 @@ +package mindustry.world.blocks.production; + +import arc.*; +import mindustry.graphics.*; +import mindustry.ui.*; +import mindustry.world.meta.*; + +/** A crafter that gains efficiency from attribute tiles. */ +public class AttributeCrafter extends GenericCrafter{ + public Attribute attribute = Attribute.heat; + public float baseEfficiency = 1f; + public float boostScale = 1f; + public float maxBoost = 1f; + + public AttributeCrafter(String name){ + super(name); + } + + @Override + public void drawPlace(int x, int y, int rotation, boolean valid){ + drawPlaceText(Core.bundle.format("bar.efficiency", + (int)((baseEfficiency + Math.min(maxBoost, boostScale * sumAttribute(attribute, x, y))) * 100f)), x, y, valid); + } + + @Override + public void setBars(){ + super.setBars(); + + bars.add("efficiency", entity -> + new Bar(() -> + Core.bundle.format("bar.efficiency", (int)(entity.efficiency() * 100)), + () -> Pal.lightOrange, + entity::efficiency)); + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.affinities, attribute, boostScale); + } + + public class AttributeCrafterBuild extends GenericCrafterBuild{ + public float attrsum; + + @Override + public float efficiency(){ + return (baseEfficiency + Math.min(maxBoost, boostScale * attrsum)) * super.efficiency(); + } + + @Override + public void onProximityUpdate(){ + super.onProximityUpdate(); + + attrsum = sumAttribute(attribute, tile.x, tile.y); + } + } +} diff --git a/core/src/mindustry/world/blocks/production/AttributeSmelter.java b/core/src/mindustry/world/blocks/production/AttributeSmelter.java index c2650ab030..edece33611 100644 --- a/core/src/mindustry/world/blocks/production/AttributeSmelter.java +++ b/core/src/mindustry/world/blocks/production/AttributeSmelter.java @@ -1,58 +1,25 @@ package mindustry.world.blocks.production; -import arc.*; -import mindustry.graphics.*; -import mindustry.ui.*; -import mindustry.world.meta.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import mindustry.annotations.Annotations.*; -/** A smelter that gains efficiency from attribute tiles. */ -public class AttributeSmelter extends GenericSmelter{ - public Attribute attribute = Attribute.heat; - public float baseEfficiency = 1f; - public float boostScale = 1f; +/** @deprecated use AttributeCrafter instead, this is only a transition class. No flame effects are drawn, to encourage transition! */ +@Deprecated +public class AttributeSmelter extends AttributeCrafter{ + //parameters are kept for compatibility but deliberately unused + public Color flameColor = Color.valueOf("ffc999"); + public @Load("@-top") TextureRegion topRegion; + //compat public float maxHeatBoost = 1f; public AttributeSmelter(String name){ super(name); } - @Override - public void drawPlace(int x, int y, int rotation, boolean valid){ - drawPlaceText(Core.bundle.format("bar.efficiency", - (int)((baseEfficiency + Math.min(maxHeatBoost, boostScale * sumAttribute(attribute, x, y))) * 100f)), x, y, valid); - } + //unused, kept for compatibility + @Deprecated + public class AttributeSmelterBuild extends AttributeCrafterBuild{ - @Override - public void setBars(){ - super.setBars(); - - bars.add("efficiency", entity -> - new Bar(() -> - Core.bundle.format("bar.efficiency", (int)(entity.efficiency() * 100)), - () -> Pal.lightOrange, - entity::efficiency)); - } - - @Override - public void setStats(){ - super.setStats(); - - stats.add(Stat.affinities, attribute, boostScale); - } - - public class AttributeSmelterBuild extends SmelterBuild{ - public float attrsum; - - @Override - public float efficiency(){ - return (baseEfficiency + Math.min(maxHeatBoost, boostScale * attrsum)) * super.efficiency(); - } - - @Override - public void onProximityUpdate(){ - super.onProximityUpdate(); - - attrsum = sumAttribute(attribute, tile.x, tile.y); - } } } diff --git a/core/src/mindustry/world/blocks/production/BeamDrill.java b/core/src/mindustry/world/blocks/production/BeamDrill.java new file mode 100644 index 0000000000..32ffdc90d2 --- /dev/null +++ b/core/src/mindustry/world/blocks/production/BeamDrill.java @@ -0,0 +1,254 @@ +package mindustry.world.blocks.production; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.type.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +public class BeamDrill extends Block{ + public @Load("minelaser") TextureRegion laser; + public @Load("minelaser-end") TextureRegion laserEnd; + public @Load("@-top") TextureRegion topRegion; + + public float drillTime = 200f; + public int range = 5; + public int tier = 1; + public float laserWidth = 0.7f; + /** Effect randomly played while drilling. */ + public Effect updateEffect = Fx.mineSmall; + + public BeamDrill(String name){ + super(name); + + hasItems = true; + rotate = true; + update = true; + solid = true; + drawArrow = false; + } + + @Override + public void init(){ + clipSize = Math.max(clipSize, size * tilesize + (range + 1) * tilesize); + super.init(); + } + + @Override + public boolean outputsItems(){ + return true; + } + + @Override + public boolean rotatedOutput(int x, int y){ + return false; + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, topRegion}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(topRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + @Override + public void drawPlace(int x, int y, int rotation, boolean valid){ + Item item = null; + boolean multiple = false; + int count = 0; + + for(int i = 0; i < size; i++){ + getLaserPos(x, y, rotation, i, Tmp.p1); + + int j = 0; + Item found = null; + for(; j < range; j++){ + int rx = Tmp.p1.x + Geometry.d4x(rotation)*j, ry = Tmp.p1.y + Geometry.d4y(rotation)*j; + Tile other = world.tile(rx, ry); + if(other != null){ + if(other.solid()){ + Item drop = other.wallDrop(); + if(drop != null && drop.hardness <= tier){ + found = drop; + count ++; + } + break; + } + } + } + + if(found != null){ + //check if multiple items will be drilled + if(item != found && item != null){ + multiple = true; + } + item = found; + } + + int len = Math.min(j, range - 1); + Drawf.dashLine(found == null ? Pal.remove : Pal.placing, + Tmp.p1.x * tilesize, + Tmp.p1.y *tilesize, + (Tmp.p1.x + Geometry.d4x(rotation)*len) * tilesize, + (Tmp.p1.y + Geometry.d4y(rotation)*len) * tilesize + ); + } + + if(item != null){ + float width = drawPlaceText(Core.bundle.formatFloat("bar.drillspeed", 60f / drillTime * count, 2), x, y, valid); + if(!multiple){ + float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5, s = iconSmall / 4f; + Draw.mixcol(Color.darkGray, 1f); + Draw.rect(item.fullIcon, dx, dy - 1, s, s); + Draw.reset(); + Draw.rect(item.fullIcon, dx, dy, s, s); + } + } + + } + + void getLaserPos(int tx, int ty, int rotation, int i, Point2 out){ + int cornerX = tx - (size-1)/2, cornerY = ty - (size-1)/2, s = size; + switch(rotation){ + case 0 -> out.set(cornerX + s, cornerY + i); + case 1 -> out.set(cornerX + i, cornerY + s); + case 2 -> out.set(cornerX - 1, cornerY + i); + case 3 -> out.set(cornerX + i, cornerY - 1); + } + } + + public class BeamDrillBuild extends Building{ + public Tile[] facing = new Tile[size]; + public Point2[] lasers = new Point2[size]; + public @Nullable Item lastItem; + + public float time; + public float warmup; + + @Override + public void drawSelect(){ + + if(lastItem != null){ + float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f, s = iconSmall / 4f; + Draw.mixcol(Color.darkGray, 1f); + Draw.rect(lastItem.fullIcon, dx, dy - 1, s, s); + Draw.reset(); + Draw.rect(lastItem.fullIcon, dx, dy, s, s); + } + } + + @Override + public void updateTile(){ + super.updateTile(); + + if(lasers[0] == null) updateLasers(); + boolean cons = shouldConsume(); + + warmup = Mathf.lerpDelta(warmup, Mathf.num(consValid()), 0.1f); + lastItem = null; + boolean multiple = false; + + //update facing tiles + for(int p = 0; p < size; p++){ + Point2 l = lasers[p]; + Tile dest = null; + for(int i = 0; i < range; i++){ + int rx = l.x + Geometry.d4x(rotation)*i, ry = l.y + Geometry.d4y(rotation)*i; + Tile other = world.tile(rx, ry); + if(other != null){ + if(other.solid()){ + Item drop = other.wallDrop(); + if(drop != null && drop.hardness <= tier){ + if(lastItem != drop && lastItem != null){ + multiple = true; + } + lastItem = drop; + dest = other; + } + break; + } + } + } + + facing[p] = dest; + if(cons && dest != null && Mathf.chanceDelta(0.05 * warmup)){ + updateEffect.at(dest.worldx() + Mathf.range(4f), dest.worldy() + Mathf.range(4f), dest.wallDrop().color); + } + } + + //when multiple items are present, count that as no item + if(multiple){ + lastItem = null; + } + + time += edelta(); + + if(time >= drillTime){ + for(Tile tile : facing){ + Item drop = tile == null ? null : tile.wallDrop(); + if(items.total() < itemCapacity && drop != null){ + items.add(drop, 1); + } + } + time %= drillTime; + } + + if(timer(timerDump, dumpTime)){ + dump(); + } + } + + @Override + public boolean shouldConsume(){ + return items.total() < itemCapacity; + } + + @Override + public void draw(){ + Draw.rect(block.region, x, y); + Draw.rect(topRegion, x, y, rotdeg()); + + Draw.z(Layer.power - 1); + var dir = Geometry.d4(rotation); + + for(int i = 0; i < size; i++){ + Tile face = facing[i]; + if(face != null){ + Point2 p = lasers[i]; + Drawf.laser(team, laser, laserEnd, (p.x - dir.x/2f) * tilesize, (p.y - dir.y/2f) * tilesize, + face.worldx() - (dir.x/2f)*(tilesize), face.worldy() - (dir.y/2f)*(tilesize), + (laserWidth + Mathf.absin(Time.time + i*4 + (id%20)*6, 3f, 0.07f)) * warmup); + } + } + Draw.reset(); + } + + @Override + public void onProximityUpdate(){ + //when rotated. + updateLasers(); + } + + void updateLasers(){ + for(int i = 0; i < size; i++){ + if(lasers[i] == null) lasers[i] = new Point2(); + getLaserPos(tileX(), tileY(), rotation, i, lasers[i]); + } + } + } +} diff --git a/core/src/mindustry/world/blocks/production/Cultivator.java b/core/src/mindustry/world/blocks/production/Cultivator.java index 57f79a98a0..4e51ae072b 100644 --- a/core/src/mindustry/world/blocks/production/Cultivator.java +++ b/core/src/mindustry/world/blocks/production/Cultivator.java @@ -1,18 +1,20 @@ package mindustry.world.blocks.production; -import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; -import arc.util.*; import arc.util.io.*; import mindustry.annotations.Annotations.*; -import mindustry.content.*; -import mindustry.graphics.*; -import mindustry.ui.*; import mindustry.world.meta.*; +/** + * @deprecated use GenericCrafter or AttributeCrafter with a DrawCultivator instead. + * WARNING: If you switch from a class that used Cultivator to a GenericCrafter, make sure you set legacyReadWarmup to true! Failing to do so will break saves. + * This class has been gutted of its behavior. + * */ +@Deprecated public class Cultivator extends GenericCrafter{ + //fields are kept for compatibility public Color plantColor = Color.valueOf("5541b1"); public Color plantColorLight = Color.valueOf("7457ce"); public Color bottomColor = Color.valueOf("474747"); @@ -25,84 +27,11 @@ public class Cultivator extends GenericCrafter{ public Cultivator(String name){ super(name); - craftEffect = Fx.none; - } - - @Override - public void setBars(){ - super.setBars(); - bars.add("multiplier", (CultivatorBuild entity) -> new Bar(() -> - Core.bundle.formatFloat("bar.efficiency", - ((entity.boost + 1f + attribute.env()) * entity.warmup) * 100f, 1), - () -> Pal.ammo, - () -> entity.warmup)); - } - - @Override - public void setStats(){ - super.setStats(); - - stats.add(Stat.affinities, attribute); - } - - @Override - public void drawPlace(int x, int y, int rotation, boolean valid){ - super.drawPlace(x, y, rotation, valid); - - drawPlaceText(Core.bundle.formatFloat("bar.efficiency", (1 + sumAttribute(attribute, x, y)) * 100, 1), x, y, valid); - } - - @Override - public TextureRegion[] icons(){ - return new TextureRegion[]{region, topRegion}; } public class CultivatorBuild extends GenericCrafterBuild{ - public float warmup; - public float boost; - - @Override - public void updateTile(){ - super.updateTile(); - - warmup = Mathf.lerpDelta(warmup, consValid() ? 1f : 0f, 0.015f); - } - - @Override - public void draw(){ - Draw.rect(region, x, y); - - Drawf.liquid(middleRegion, x, y, warmup, plantColor); - - Draw.color(bottomColor, plantColorLight, warmup); - - random.setSeed(tile.pos()); - for(int i = 0; i < 12; i++){ - float offset = random.nextFloat() * 999999f; - float x = random.range(4f), y = random.range(4f); - float life = 1f - (((Time.time + offset) / 50f) % recurrence); - - if(life > 0){ - Lines.stroke(warmup * (life + 0.2f)); - Lines.poly(x + x, y + y, 8, (1f - life) * 3f); - } - } - - Draw.color(); - Draw.rect(topRegion, x, y); - } - - @Override - public void onProximityUpdate(){ - super.onProximityAdded(); - - boost = sumAttribute(attribute, tile.x, tile.y); - } - - @Override - public float getProgressIncrease(float baseTime){ - return super.getProgressIncrease(baseTime) * (1f + boost + attribute.env()); - } + //compat + public float warmup, boost; @Override public void write(Writes write){ diff --git a/core/src/mindustry/world/blocks/production/Drill.java b/core/src/mindustry/world/blocks/production/Drill.java index cab281a85f..8c627ea493 100644 --- a/core/src/mindustry/world/blocks/production/Drill.java +++ b/core/src/mindustry/world/blocks/production/Drill.java @@ -6,6 +6,7 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.struct.*; import arc.util.*; +import arc.util.io.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.entities.*; @@ -13,12 +14,12 @@ import mindustry.entities.units.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -122,11 +123,11 @@ public class Drill extends Block{ if(returnItem != null){ float width = drawPlaceText(Core.bundle.formatFloat("bar.drillspeed", 60f / (drillTime + hardnessDrillMultiplier * returnItem.hardness) * returnCount, 2), x, y, valid); - float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5; + float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5, s = iconSmall / 4f; Draw.mixcol(Color.darkGray, 1f); - Draw.rect(returnItem.icon(Cicon.small), dx, dy - 1); + Draw.rect(returnItem.fullIcon, dx, dy - 1, s, s); Draw.reset(); - Draw.rect(returnItem.icon(Cicon.small), dx, dy); + Draw.rect(returnItem.fullIcon, dx, dy, s, s); if(drawMineItem){ Draw.color(returnItem.color); @@ -146,7 +147,7 @@ public class Drill extends Block{ public void setStats(){ super.setStats(); - stats.add(Stat.drillTier, new BlockFilterValue(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= tier)); + stats.add(Stat.drillTier, StatValues.blocks(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= tier)); stats.add(Stat.drillSpeed, 60f / drillTime * size * size, StatUnit.itemsSecond); if(liquidBoostIntensity != 1){ @@ -225,16 +226,18 @@ public class Drill extends Block{ @Override public void drawSelect(){ if(dominantItem != null){ - float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f; + float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f, s = iconSmall / 4f; Draw.mixcol(Color.darkGray, 1f); - Draw.rect(dominantItem.icon(Cicon.small), dx, dy - 1); + Draw.rect(dominantItem.fullIcon, dx, dy - 1, s, s); Draw.reset(); - Draw.rect(dominantItem.icon(Cicon.small), dx, dy); + Draw.rect(dominantItem.fullIcon, dx, dy, s, s); } } @Override public void onProximityUpdate(){ + super.onProximityUpdate(); + countOre(tile); dominantItem = returnItem; dominantItems = returnCount; @@ -247,7 +250,7 @@ public class Drill extends Block{ } if(timer(timerDump, dumpTime)){ - dump(dominantItem); + dump(items.has(dominantItem) ? dominantItem : null); } timeDrilled += warmup * delta(); @@ -285,6 +288,12 @@ public class Drill extends Block{ } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress && dominantItem != null) return Mathf.clamp(progress / (drillTime + hardnessDrillMultiplier * dominantItem.hardness)); + return super.sense(sensor); + } + @Override public void drawCracks(){} @@ -315,6 +324,27 @@ public class Drill extends Block{ Draw.color(); } } + + @Override + public byte version(){ + return 1; + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(progress); + write.f(warmup); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + if(revision >= 1){ + progress = read.f(); + warmup = read.f(); + } + } } } diff --git a/core/src/mindustry/world/blocks/production/Fracker.java b/core/src/mindustry/world/blocks/production/Fracker.java index 0ce66cd54c..a3dc07b312 100644 --- a/core/src/mindustry/world/blocks/production/Fracker.java +++ b/core/src/mindustry/world/blocks/production/Fracker.java @@ -18,10 +18,12 @@ public class Fracker extends SolidPump{ hasItems = true; ambientSound = Sounds.drill; ambientSoundVolume = 0.03f; + envRequired |= Env.groundOil; } @Override public void setStats(){ + stats.timePeriod = itemUseTime; super.setStats(); stats.add(Stat.productionTime, itemUseTime / 60f, StatUnit.seconds); diff --git a/core/src/mindustry/world/blocks/production/GenericCrafter.java b/core/src/mindustry/world/blocks/production/GenericCrafter.java index 11b6c37bc0..bd684fcf8a 100644 --- a/core/src/mindustry/world/blocks/production/GenericCrafter.java +++ b/core/src/mindustry/world/blocks/production/GenericCrafter.java @@ -8,6 +8,7 @@ import arc.util.io.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.gen.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.draw.*; @@ -21,6 +22,9 @@ public class GenericCrafter extends Block{ public Effect craftEffect = Fx.none; public Effect updateEffect = Fx.none; public float updateEffectChance = 0.04f; + public float warmupSpeed = 0.019f; + /** Only used for legacy cultivator blocks. */ + public boolean legacyReadWarmup = false; public DrawBlock drawer = new DrawBlock(); @@ -37,11 +41,12 @@ public class GenericCrafter extends Block{ @Override public void setStats(){ + stats.timePeriod = craftTime; super.setStats(); stats.add(Stat.productionTime, craftTime / 60f, StatUnit.seconds); if(outputItem != null){ - stats.add(Stat.output, outputItem); + stats.add(Stat.output, StatValues.items(craftTime, outputItem)); } if(outputLiquid != null){ @@ -82,9 +87,15 @@ public class GenericCrafter extends Block{ drawer.draw(this); } + @Override + public void drawLight(){ + super.drawLight(); + drawer.drawLight(this); + } + @Override public boolean shouldConsume(){ - if(outputItem != null && items.get(outputItem.item) >= itemCapacity){ + if(outputItem != null && items.get(outputItem.item) + outputItem.amount > itemCapacity){ return false; } return (outputLiquid == null || !(liquids.get(outputLiquid.liquid) >= liquidCapacity - 0.001f)) && enabled; @@ -96,13 +107,13 @@ public class GenericCrafter extends Block{ progress += getProgressIncrease(craftTime); totalProgress += delta(); - warmup = Mathf.lerpDelta(warmup, 1f, 0.02f); + warmup = Mathf.approachDelta(warmup, 1f, warmupSpeed); if(Mathf.chanceDelta(updateEffectChance)){ updateEffect.at(getX() + Mathf.range(size * 4f), getY() + Mathf.range(size * 4)); } }else{ - warmup = Mathf.lerp(warmup, 0f, 0.02f); + warmup = Mathf.approachDelta(warmup, 0f, warmupSpeed); } if(progress >= 1f){ @@ -122,7 +133,7 @@ public class GenericCrafter extends Block{ progress %= 1f; } - if(outputItem != null && timer(timerDump, dumpTime)){ + if(outputItem != null && timer(timerDump, dumpTime / timeScale)){ dump(outputItem.item); } @@ -131,6 +142,12 @@ public class GenericCrafter extends Block{ } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(progress); + return super.sense(sensor); + } + @Override public int getMaximumAccepted(Item item){ return itemCapacity; @@ -146,6 +163,7 @@ public class GenericCrafter extends Block{ super.write(write); write.f(progress); write.f(warmup); + if(legacyReadWarmup) write.f(0f); } @Override @@ -153,6 +171,7 @@ public class GenericCrafter extends Block{ super.read(read, revision); progress = read.f(); warmup = read.f(); + if(legacyReadWarmup) read.f(); } } } diff --git a/core/src/mindustry/world/blocks/production/GenericSmelter.java b/core/src/mindustry/world/blocks/production/GenericSmelter.java index 1c7e0383ff..9d68130cb0 100644 --- a/core/src/mindustry/world/blocks/production/GenericSmelter.java +++ b/core/src/mindustry/world/blocks/production/GenericSmelter.java @@ -8,10 +8,12 @@ import mindustry.annotations.Annotations.*; import mindustry.gen.*; import mindustry.graphics.*; -/** A GenericCrafter with a new glowing region drawn on top. */ +/** @deprecated this class has no new functionality over GenericCrafter, use GenericCrafter with a DrawSmelter drawer instead. See vanilla smelter blocks. */ +@Deprecated public class GenericSmelter extends GenericCrafter{ public Color flameColor = Color.valueOf("ffc999"); public @Load("@-top") TextureRegion topRegion; + public float flameRadius = 3f, flameRadiusIn = 1.9f, flameRadiusScl = 5f, flameRadiusMag = 2f, flameRadiusInMag = 1f; public GenericSmelter(String name){ super(name); @@ -35,10 +37,10 @@ public class GenericSmelter extends GenericCrafter{ Draw.alpha(((1f - g) + Mathf.absin(Time.time, 8f, g) + Mathf.random(r) - r) * warmup); Draw.tint(flameColor); - Fill.circle(x, y, 3f + Mathf.absin(Time.time, 5f, 2f) + cr); + Fill.circle(x, y, flameRadius + Mathf.absin(Time.time, flameRadiusScl, flameRadiusMag) + cr); Draw.color(1f, 1f, 1f, warmup); Draw.rect(topRegion, x, y); - Fill.circle(x, y, 1.9f + Mathf.absin(Time.time, 5f, 1f) + cr); + Fill.circle(x, y, flameRadiusIn + Mathf.absin(Time.time, flameRadiusScl, flameRadiusInMag) + cr); Draw.color(); } diff --git a/core/src/mindustry/world/blocks/production/LiquidConverter.java b/core/src/mindustry/world/blocks/production/LiquidConverter.java index 3ea25117e0..0b0ad4a0f9 100644 --- a/core/src/mindustry/world/blocks/production/LiquidConverter.java +++ b/core/src/mindustry/world/blocks/production/LiquidConverter.java @@ -1,5 +1,6 @@ package mindustry.world.blocks.production; +import arc.math.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; @@ -47,16 +48,25 @@ public class LiquidConverter extends GenericCrafter{ ConsumeLiquid cl = consumes.get(ConsumeType.liquid); if(cons.valid()){ + if(Mathf.chanceDelta(updateEffectChance)){ + updateEffect.at(getX() + Mathf.range(size * 4f), getY() + Mathf.range(size * 4)); + } + + warmup = Mathf.lerpDelta(warmup, 1f, 0.02f); float use = Math.min(cl.amount * edelta(), liquidCapacity - liquids.get(outputLiquid.liquid)); + float ratio = outputLiquid.amount / cl.amount; liquids.remove(cl.liquid, Math.min(use, liquids.get(cl.liquid))); progress += use / cl.amount; - liquids.add(outputLiquid.liquid, use); + liquids.add(outputLiquid.liquid, use * ratio); if(progress >= craftTime){ consume(); progress %= craftTime; } + }else{ + //warmup is still 1 even if not consuming + warmup = Mathf.lerp(warmup, cons.canConsume() ? 1f : 0f, 0.02f); } dumpLiquid(outputLiquid.liquid); diff --git a/core/src/mindustry/world/blocks/production/PayloadAcceptor.java b/core/src/mindustry/world/blocks/production/PayloadAcceptor.java index df21f8ad6f..be5d296008 100644 --- a/core/src/mindustry/world/blocks/production/PayloadAcceptor.java +++ b/core/src/mindustry/world/blocks/production/PayloadAcceptor.java @@ -1,186 +1,18 @@ package mindustry.world.blocks.production; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.math.geom.*; -import arc.util.*; -import arc.util.io.*; -import mindustry.annotations.Annotations.*; -import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.world.*; import mindustry.world.blocks.payloads.*; -import static mindustry.Vars.*; - -public class PayloadAcceptor extends Block{ - public float payloadSpeed = 0.5f, payloadRotateSpeed = 5f; - - public @Load(value = "@-top", fallback = "factory-top-@size") TextureRegion topRegion; - public @Load(value = "@-out", fallback = "factory-out-@size") TextureRegion outRegion; - public @Load(value = "@-in", fallback = "factory-in-@size") TextureRegion inRegion; +/** @deprecated used PayloadBlock instead. */ +@Deprecated +public abstract class PayloadAcceptor extends PayloadBlock{ public PayloadAcceptor(String name){ super(name); - - update = true; - sync = true; } - public static boolean blends(Building build, int direction){ - int size = build.block.size; - int trns = build.block.size/2 + 1; - Building accept = build.nearby(Geometry.d4(direction).x * trns, Geometry.d4(direction).y * trns); - return accept != null && - accept.block.outputsPayload && + /** @deprecated used PayloadBlockBuild instead. */ + @Deprecated + public class PayloadAcceptorBuild extends PayloadBlockBuild{ - //if size is the same, block must either be facing this one, or not be rotating - ((accept.block.size == size - && Math.abs(accept.tileX() - build.tileX()) % size == 0 //check alignment - && Math.abs(accept.tileY() - build.tileY()) % size == 0 - && ((accept.block.rotate && accept.tileX() + Geometry.d4(accept.rotation).x * size == build.tileX() && accept.tileY() + Geometry.d4(accept.rotation).y * size == build.tileY()) - || !accept.block.rotate - || !accept.block.outputFacing)) || - - //if the other block is smaller, check alignment - (accept.block.size < size && - (accept.rotation % 2 == 0 ? //check orientation; make sure it's aligned properly with this block. - Math.abs(accept.y - build.y) <= (size * tilesize - accept.block.size * tilesize)/2f : //check Y alignment - Math.abs(accept.x - build.x) <= (size * tilesize - accept.block.size * tilesize)/2f //check X alignment - )) && (!accept.block.rotate || accept.front() == build || !accept.block.outputFacing) //make sure it's facing this block - ); - } - - public class PayloadAcceptorBuild extends Building{ - public @Nullable T payload; - public Vec2 payVector = new Vec2(); - public float payRotation; - public boolean carried; - - @Override - public boolean acceptPayload(Building source, Payload payload){ - return this.payload == null; - } - - @Override - public void handlePayload(Building source, Payload payload){ - this.payload = (T)payload; - this.payVector.set(source).sub(this).clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); - this.payRotation = payload.rotation(); - - updatePayload(); - } - - @Override - public Payload getPayload(){ - return payload; - } - - @Override - public void pickedUp(){ - carried = true; - } - - @Override - public void drawTeamTop(){ - carried = false; - } - - @Override - public Payload takePayload(){ - T t = payload; - payload = null; - return t; - } - - @Override - public void onRemoved(){ - super.onRemoved(); - if(payload != null && !carried) payload.dump(); - } - - public boolean blends(int direction){ - return PayloadAcceptor.blends(this, direction); - } - - public void updatePayload(){ - if(payload != null){ - payload.set(x + payVector.x, y + payVector.y, payRotation); - } - } - - /** @return true if the payload is in position. */ - public boolean moveInPayload(){ - if(payload == null) return false; - - updatePayload(); - - payRotation = Angles.moveToward(payRotation, rotate ? rotdeg() : 90f, payloadRotateSpeed * edelta()); - payVector.approach(Vec2.ZERO, payloadSpeed * delta()); - - return hasArrived(); - } - - public void moveOutPayload(){ - if(payload == null) return; - - updatePayload(); - - Vec2 dest = Tmp.v1.trns(rotdeg(), size* tilesize/2f); - - payRotation = Angles.moveToward(payRotation, rotdeg(), payloadRotateSpeed * edelta()); - payVector.approach(dest, payloadSpeed * delta()); - - if(payVector.within(dest, 0.001f)){ - payVector.clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); - - Building front = front(); - if(front != null && front.block.outputsPayload){ - if(movePayload(payload)){ - payload = null; - } - }else if(front == null || !front.tile().solid()){ - dumpPayload(); - } - } - } - - public void dumpPayload(){ - if(payload.dump()){ - payload = null; - } - } - - public boolean hasArrived(){ - return payVector.isZero(0.01f); - } - - public void drawPayload(){ - if(payload != null){ - updatePayload(); - - Draw.z(Layer.blockOver); - payload.draw(); - } - } - - @Override - public void write(Writes write){ - super.write(write); - - write.f(payVector.x); - write.f(payVector.y); - write.f(payRotation); - Payload.write(payload, write); - } - - @Override - public void read(Reads read, byte revision){ - super.read(read, revision); - - payVector.set(read.f(), read.f()); - payRotation = read.f(); - payload = Payload.read(read); - } } } diff --git a/core/src/mindustry/world/blocks/production/Pump.java b/core/src/mindustry/world/blocks/production/Pump.java index 692b637428..fd150ba38a 100644 --- a/core/src/mindustry/world/blocks/production/Pump.java +++ b/core/src/mindustry/world/blocks/production/Pump.java @@ -6,7 +6,6 @@ import arc.graphics.g2d.*; import mindustry.game.*; import mindustry.graphics.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.liquid.*; import mindustry.world.meta.*; @@ -48,11 +47,11 @@ public class Pump extends LiquidBlock{ if(liquidDrop != null){ float width = drawPlaceText(Core.bundle.formatFloat("bar.pumpspeed", amount * pumpAmount * 60f, 0), x, y, valid); - float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5; + float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5, s = iconSmall / 4f; Draw.mixcol(Color.darkGray, 1f); - Draw.rect(liquidDrop.icon(Cicon.small), dx, dy - 1); + Draw.rect(liquidDrop.fullIcon, dx, dy - 1, s, s); Draw.reset(); - Draw.rect(liquidDrop.icon(Cicon.small), dx, dy); + Draw.rect(liquidDrop.fullIcon, dx, dy, s, s); } } diff --git a/core/src/mindustry/world/blocks/production/Separator.java b/core/src/mindustry/world/blocks/production/Separator.java index 494a2977d8..eb6acdd878 100644 --- a/core/src/mindustry/world/blocks/production/Separator.java +++ b/core/src/mindustry/world/blocks/production/Separator.java @@ -3,6 +3,7 @@ package mindustry.world.blocks.production; import arc.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.util.*; import arc.util.io.*; import mindustry.annotations.Annotations.*; import mindustry.gen.*; @@ -11,7 +12,6 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; /** * Extracts a random list of items from an input item and an input liquid. @@ -34,15 +34,10 @@ public class Separator extends Block{ @Override public void setStats(){ + stats.timePeriod = craftTime; super.setStats(); - stats.add(Stat.output, new ItemFilterValue(item -> { - for(ItemStack i : results){ - if(item == i.item) return true; - } - return false; - })); - + stats.add(Stat.output, StatValues.items(item -> Structs.contains(results, i -> i.item == item))); stats.add(Stat.productionTime, craftTime / 60f, StatUnit.seconds); } diff --git a/core/src/mindustry/world/blocks/production/SingleBlockProducer.java b/core/src/mindustry/world/blocks/production/SingleBlockProducer.java new file mode 100644 index 0000000000..5dab7d775d --- /dev/null +++ b/core/src/mindustry/world/blocks/production/SingleBlockProducer.java @@ -0,0 +1,23 @@ +package mindustry.world.blocks.production; + +import arc.util.*; +import mindustry.content.*; +import mindustry.world.*; +import mindustry.world.blocks.payloads.*; + +public class SingleBlockProducer extends BlockProducer{ + public Block result = Blocks.router; + + public SingleBlockProducer(String name){ + super(name); + } + + public class SingleBlockProducerBuild extends BlockProducerBuild{ + + @Nullable + @Override + public Block recipe(){ + return result; + } + } +} diff --git a/core/src/mindustry/world/blocks/sandbox/ItemSource.java b/core/src/mindustry/world/blocks/sandbox/ItemSource.java index aee34a9446..c534c0a601 100644 --- a/core/src/mindustry/world/blocks/sandbox/ItemSource.java +++ b/core/src/mindustry/world/blocks/sandbox/ItemSource.java @@ -24,6 +24,7 @@ public class ItemSource extends Block{ configurable = true; saveConfig = true; noUpdateDisabled = true; + envEnabled = Env.any; config(Item.class, (ItemSourceBuild tile, Item item) -> tile.outputItem = item); configClear((ItemSourceBuild tile) -> tile.outputItem = null); diff --git a/core/src/mindustry/world/blocks/sandbox/ItemVoid.java b/core/src/mindustry/world/blocks/sandbox/ItemVoid.java index 030433d43d..fcda607542 100644 --- a/core/src/mindustry/world/blocks/sandbox/ItemVoid.java +++ b/core/src/mindustry/world/blocks/sandbox/ItemVoid.java @@ -11,6 +11,7 @@ public class ItemVoid extends Block{ super(name); group = BlockGroup.transportation; update = solid = acceptsItems = true; + envEnabled = Env.any; } public class ItemVoidBuild extends Building{ diff --git a/core/src/mindustry/world/blocks/sandbox/PowerSource.java b/core/src/mindustry/world/blocks/sandbox/PowerSource.java index b392ce8813..8953a595b6 100644 --- a/core/src/mindustry/world/blocks/sandbox/PowerSource.java +++ b/core/src/mindustry/world/blocks/sandbox/PowerSource.java @@ -1,6 +1,7 @@ package mindustry.world.blocks.sandbox; import mindustry.world.blocks.power.*; +import mindustry.world.meta.*; public class PowerSource extends PowerNode{ @@ -11,6 +12,8 @@ public class PowerSource extends PowerNode{ maxNodes = 100; outputsPower = true; consumesPower = false; + //TODO maybe don't? + envEnabled = Env.any; } public class PowerSourceBuild extends PowerNodeBuild{ diff --git a/core/src/mindustry/world/blocks/sandbox/PowerVoid.java b/core/src/mindustry/world/blocks/sandbox/PowerVoid.java index e97622c609..93427b7e02 100644 --- a/core/src/mindustry/world/blocks/sandbox/PowerVoid.java +++ b/core/src/mindustry/world/blocks/sandbox/PowerVoid.java @@ -8,6 +8,7 @@ public class PowerVoid extends PowerBlock{ public PowerVoid(String name){ super(name); consumes.power(Float.MAX_VALUE); + envEnabled = Env.any; } @Override diff --git a/core/src/mindustry/world/blocks/storage/CoreBlock.java b/core/src/mindustry/world/blocks/storage/CoreBlock.java index 45b2ce32d1..a5d61237e3 100644 --- a/core/src/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/mindustry/world/blocks/storage/CoreBlock.java @@ -6,6 +6,7 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; +import arc.util.*; import mindustry.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -19,7 +20,6 @@ import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.*; -import mindustry.world.blocks.*; import mindustry.world.blocks.units.*; import mindustry.world.meta.*; import mindustry.world.modules.*; @@ -37,6 +37,7 @@ public class CoreBlock extends StorageBlock{ public int ammoAmount = 5; public float resupplyRate = 10f; public float resupplyRange = 60f; + public float captureInvicibility = 60f * 15f; public Item resupplyItem = Items.copper; public CoreBlock(String name){ @@ -46,12 +47,16 @@ public class CoreBlock extends StorageBlock{ update = true; hasItems = true; priority = TargetPriority.core; - flags = EnumSet.of(BlockFlag.core, BlockFlag.unitModifier); + flags = EnumSet.of(BlockFlag.core); unitCapModifier = 10; loopSound = Sounds.respawning; loopSoundVolume = 1f; drawDisabled = false; canOverdrive = false; + + //support everything + envEnabled = Env.any; + drawDisabled = false; replaceable = false; } @@ -97,6 +102,15 @@ public class CoreBlock extends StorageBlock{ )); } + @Override + public void init(){ + //assign to update clipSize internally + lightRadius = 30f + 20f * size; + emitLight = true; + + super.init(); + } + @Override public boolean canBreak(Tile tile){ return false; @@ -156,19 +170,29 @@ public class CoreBlock extends StorageBlock{ if(!canPlaceOn(world.tile(x, y), player.team())){ - drawPlaceText(Core.bundle.get((player.team().core() != null && player.team().core().items.has(requirements, state.rules.buildCostMultiplier)) || state.rules.infiniteResources ? + drawPlaceText(Core.bundle.get( + (player.team().core() != null && player.team().core().items.has(requirements, state.rules.buildCostMultiplier)) || state.rules.infiniteResources ? "bar.corereq" : "bar.noresources" ), x, y, valid); - } } - public class CoreBuild extends Building implements ControlBlock{ + public class CoreBuild extends Building{ public int storageCapacity; - //note that this unit is never actually used for control; the possession handler makes the player respawn when this unit is controlled - public BlockUnitc unit = Nulls.blockUnit; public boolean noEffect = false; + public Team lastDamage = Team.derelict; + public float iframes = -1f; + + @Override + public void damage(@Nullable Team source, float damage){ + if(iframes > 0) return; + + if(source != null && source != team){ + lastDamage = source; + } + super.damage(source, damage); + } @Override public double sense(LAccess sensor){ @@ -177,23 +201,34 @@ public class CoreBlock extends StorageBlock{ } @Override - public void created(){ - unit = (BlockUnitc)UnitTypes.block.create(team); - unit.tile(this); + public boolean canControlSelect(Player player){ + return true; } @Override - public Unit unit(){ - return (Unit)unit; + public void onControlSelect(Player player){ + Fx.spawn.at(player); + if(net.client()){ + control.input.controlledType = null; + } + + player.clearUnit(); + player.deathTimer = Player.deathDelay + 1f; + requestSpawn(player); } public void requestSpawn(Player player){ + //do not try to respawn in unsupported environments at all + if(!unitType.supportsEnv(state.rules.environment)) return; + Call.playerSpawn(tile, player); } @Override public void updateTile(){ + iframes -= Time.delta; + //resupply nearby units if(items.has(resupplyItem) && timer(timerResupply, resupplyRate) && ResupplyPoint.resupply(this, resupplyRange, ammoAmount, resupplyItem.color)){ items.remove(resupplyItem, 1); @@ -208,7 +243,13 @@ public class CoreBlock extends StorageBlock{ @Override public void onDestroyed(){ - super.onDestroyed(); + if(state.rules.coreCapture){ + //just create an explosion, no fire. this prevents immediate recapture + Damage.dynamicExplosion(x, y, 0, 0, 0, tilesize * block.size / 2f, state.rules.damageExplosions); + Fx.commandSend.at(x, y, 140f); + }else{ + super.onDestroyed(); + } //add a spawn to the map for future reference - waves should be disabled, so it shouldn't matter if(state.isCampaign() && team == state.rules.waveTeam && team.cores().size <= 1){ @@ -221,9 +262,18 @@ public class CoreBlock extends StorageBlock{ } } + @Override + public void afterDestroyed(){ + if(state.rules.coreCapture){ + tile.setBlock(block, lastDamage); + //core is invincible for several seconds to prevent recapture + ((CoreBuild)tile.build).iframes = captureInvicibility; + } + } + @Override public void drawLight(){ - Drawf.light(team, x, y, 30f + 20f * size, Pal.accent, 0.65f + Mathf.absin(20f, 0.1f)); + Drawf.light(team, x, y, lightRadius, Pal.accent, 0.65f + Mathf.absin(20f, 0.1f)); } @Override @@ -238,6 +288,8 @@ public class CoreBlock extends StorageBlock{ @Override public void onProximityUpdate(){ + super.onProximityUpdate(); + for(Building other : state.teams.cores(team)){ if(other.tile() != tile){ this.items = other.items; diff --git a/core/src/mindustry/world/blocks/storage/Unloader.java b/core/src/mindustry/world/blocks/storage/Unloader.java index a2fa940da9..d43015b7cf 100644 --- a/core/src/mindustry/world/blocks/storage/Unloader.java +++ b/core/src/mindustry/world/blocks/storage/Unloader.java @@ -10,12 +10,12 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.*; +import mindustry.world.meta.*; import static mindustry.Vars.*; public class Unloader extends Block{ public float speed = 1f; - public final int timerUnload = timers++; public Unloader(String name){ super(name); @@ -33,6 +33,12 @@ public class Unloader extends Block{ configClear((UnloaderBuild tile) -> tile.sortItem = null); } + @Override + public void setStats(){ + super.setStats(); + stats.add(Stat.speed, 60f / speed, StatUnit.itemsSecond); + } + @Override public void drawRequestConfig(BuildPlan req, Eachable list){ drawRequestConfigCenter(req, req.config, "unloader-center"); @@ -45,6 +51,7 @@ public class Unloader extends Block{ } public class UnloaderBuild extends Building{ + public float unloadTimer = 0f; public Item sortItem = null; public Building dumpingTo; public int offset = 0; @@ -52,7 +59,8 @@ public class Unloader extends Block{ @Override public void updateTile(){ - if(timer(timerUnload, speed / timeScale)){ + if((unloadTimer += delta()) >= speed){ + boolean any = false; if(rotations == null || rotations.length != proximity.size){ rotations = new int[proximity.size]; } @@ -72,6 +80,7 @@ public class Unloader extends Block{ //remove item if it's dumped correctly if(put(item)){ other.items.remove(item, 1); + any = true; if(sortItem == null){ rotations[pos] = item.id + 1; @@ -84,6 +93,12 @@ public class Unloader extends Block{ } } + if(any){ + unloadTimer %= speed; + }else{ + unloadTimer = Math.min(unloadTimer, speed); + } + if(proximity.size > 0){ offset ++; offset %= proximity.size; diff --git a/core/src/mindustry/world/blocks/units/Reconstructor.java b/core/src/mindustry/world/blocks/units/Reconstructor.java index 77647f2909..c2e5dd6c4a 100644 --- a/core/src/mindustry/world/blocks/units/Reconstructor.java +++ b/core/src/mindustry/world/blocks/units/Reconstructor.java @@ -13,6 +13,7 @@ import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.blocks.payloads.*; @@ -71,12 +72,12 @@ public class Reconstructor extends UnitBlock{ for(var upgrade : upgrades){ float size = 8 * 3; if(upgrade[0].unlockedNow() && upgrade[1].unlockedNow()){ - table.image(upgrade[0].icon(Cicon.small)).size(size).padRight(4).padLeft(10).scaling(Scaling.fit).right(); + table.image(upgrade[0].uiIcon).size(size).padRight(4).padLeft(10).scaling(Scaling.fit).right(); table.add(upgrade[0].localizedName).left(); table.add("[lightgray] -> "); - table.image(upgrade[1].icon(Cicon.small)).size(size).padRight(4).scaling(Scaling.fit); + table.image(upgrade[1].uiIcon).size(size).padRight(4).scaling(Scaling.fit); table.add(upgrade[1].localizedName).left(); table.row(); } @@ -107,6 +108,11 @@ public class Reconstructor extends UnitBlock{ return progress / constructTime; } + @Override + public boolean acceptUnitPayload(Unit unit){ + return hasUpgrade(unit.type); + } + @Override public boolean acceptPayload(Building source, Payload payload){ return this.payload == null @@ -147,7 +153,7 @@ public class Reconstructor extends UnitBlock{ if(constructing() && hasArrived()){ Draw.draw(Layer.blockOver, () -> { Draw.alpha(1f - progress/ constructTime); - Draw.rect(payload.unit.type.icon(Cicon.full), x, y, payload.rotation() - 90); + Draw.rect(payload.unit.type.fullIcon, x, y, payload.rotation() - 90); Draw.reset(); Drawf.construct(this, upgrade(payload.unit.type), payload.rotation() - 90f, progress / constructTime, speedScl, time); }); @@ -193,6 +199,12 @@ public class Reconstructor extends UnitBlock{ time += edelta() * speedScl * state.rules.unitBuildSpeedMultiplier; } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(fraction()); + return super.sense(sensor); + } + @Override public boolean shouldConsume(){ return constructing(); diff --git a/core/src/mindustry/world/blocks/units/RepairPoint.java b/core/src/mindustry/world/blocks/units/RepairPoint.java index b84ed1d119..b79413e3c4 100644 --- a/core/src/mindustry/world/blocks/units/RepairPoint.java +++ b/core/src/mindustry/world/blocks/units/RepairPoint.java @@ -8,29 +8,47 @@ import arc.struct.*; import arc.util.*; import arc.util.io.*; import mindustry.annotations.Annotations.*; +import mindustry.content.*; import mindustry.entities.*; +import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.logic.*; import mindustry.world.*; +import mindustry.world.consumers.*; import mindustry.world.meta.*; import static mindustry.Vars.*; public class RepairPoint extends Block{ static final Rect rect = new Rect(); + static final Rand rand = new Rand(); public int timerTarget = timers++; + public int timerEffect = timers++; public float repairRadius = 50f; public float repairSpeed = 0.3f; public float powerUse; + public float length = 5f; + public float beamWidth = 1f; + public float pulseRadius = 6f; + public float pulseStroke = 2f; + public boolean acceptCoolant = false; - public @Load("@-base") TextureRegion baseRegion; - public @Load("laser") TextureRegion laser; - public @Load("laser-end") TextureRegion laserEnd; + public float coolantUse = 0.5f; + /** Effect displayed when coolant is used. */ + public Effect coolEffect = Fx.fuelburn; + /** How much healing is increased by with heat capacity. */ + public float coolantMultiplier = 1f; - public Color laserColor = Color.valueOf("e8ffd7"); + public @Load(value = "@-base", fallback = "block-@size") TextureRegion baseRegion; + public @Load("laser-white") TextureRegion laser; + public @Load("laser-white-end") TextureRegion laserEnd; + public @Load("laser-top") TextureRegion laserTop; + public @Load("laser-top-end") TextureRegion laserTopEnd; + + public Color laserColor = Color.valueOf("98ffa9"), laserTopColor = Color.white.cpy(); public RepairPoint(String name){ super(name); @@ -39,18 +57,30 @@ public class RepairPoint extends Block{ flags = EnumSet.of(BlockFlag.repair); hasPower = true; outlineIcon = true; - expanded = true; + //yeah, this isn't the same thing, but it's close enough + group = BlockGroup.projectors; } @Override public void setStats(){ super.setStats(); stats.add(Stat.range, repairRadius / tilesize, StatUnit.blocks); + stats.add(Stat.repairSpeed, repairSpeed * 60f, StatUnit.perSecond); + + if(acceptCoolant){ + stats.add(Stat.booster, StatValues.strengthBoosters(coolantMultiplier, l -> consumes.liquidfilters.get(l.id))); + } } @Override public void init(){ - consumes.powerCond(powerUse, entity -> ((RepairPointBuild)entity).target != null); + if(acceptCoolant){ + hasLiquids = true; + consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, coolantUse)).optional(true, true); + } + + consumes.powerCond(powerUse, (RepairPointBuild entity) -> entity.target != null); + clipSize = Math.max(clipSize, (repairRadius + tilesize) * 2); super.init(); } @@ -66,8 +96,56 @@ public class RepairPoint extends Block{ return new TextureRegion[]{baseRegion, region}; } + public static void drawBeam(float x, float y, float rotation, float length, int id, Sized target, Team team, + float strength, float pulseStroke, float pulseRadius, float beamWidth, + Vec2 lastEnd, Vec2 offset, + Color laserColor, Color laserTopColor, + TextureRegion laser, TextureRegion laserEnd, TextureRegion laserTop, TextureRegion laserTopEnd){ + if(target != null){ + float + originX = x + Angles.trnsx(rotation, length), + originY = y + Angles.trnsy(rotation, length); + + rand.setSeed(id + (target instanceof Entityc e ? e.id() : 0)); + + lastEnd.set(target).sub(originX, originY); + lastEnd.setLength(Math.max(2f, lastEnd.len())); + + lastEnd.add(offset.trns( + rand.random(360f) + Time.time/2f, + Mathf.sin(Time.time + rand.random(200f), 55f, rand.random(target.hitSize() * 0.2f, target.hitSize() * 0.45f)) + ).rotate(target instanceof Rotc rot ? rot.rotation() : 0f)); + + lastEnd.add(originX, originY); + } + + if(strength > 0.01f){ + float + originX = x + Angles.trnsx(rotation, length), + originY = y + Angles.trnsy(rotation, length); + + Draw.z(Layer.flyingUnit + 1); //above all units + + Draw.color(laserColor); + + float f = (Time.time / 85f + rand.random(1f)) % 1f; + + Draw.alpha(1f - Interp.pow5In.apply(f)); + Lines.stroke(strength * pulseStroke); + Lines.circle(lastEnd.x, lastEnd.y, 1f + f * pulseRadius); + + Draw.color(laserColor); + Drawf.laser(team, laser, laserEnd, originX, originY, lastEnd.x, lastEnd.y, strength * beamWidth); + Draw.z(Layer.flyingUnit + 1.1f); + Draw.color(laserTopColor); + Drawf.laser(team, laserTop, laserTopEnd, originX, originY, lastEnd.x, lastEnd.y, strength * beamWidth); + Draw.color(); + } + } + public class RepairPointBuild extends Building implements Ranged{ public Unit target; + public Vec2 offset = new Vec2(), lastEnd = new Vec2(); public float strength, rotation = 90; @Override @@ -78,17 +156,9 @@ public class RepairPoint extends Block{ Drawf.shadow(region, x - (size / 2f), y - (size / 2f), rotation - 90); Draw.rect(region, x, y, rotation - 90); - if(target != null && Angles.angleDist(angleTo(target), rotation) < 30f){ - Draw.z(Layer.flyingUnit + 1); //above all units - float ang = angleTo(target); - float len = 5f; - - Draw.color(laserColor); - Drawf.laser(team, laser, laserEnd, - x + Angles.trnsx(ang, len), y + Angles.trnsy(ang, len), - target.x(), target.y(), strength); - Draw.color(); - } + drawBeam(x, y, rotation, length, id, target, team, strength, + pulseStroke, pulseRadius, beamWidth, lastEnd, offset, laserColor, laserTopColor, + laser, laserEnd, laserTop, laserTopEnd); } @Override @@ -98,21 +168,33 @@ public class RepairPoint extends Block{ @Override public void updateTile(){ - boolean targetIsBeingRepaired = false; - if(target != null && (target.dead() || target.dst(tile) - target.hitSize/2f > repairRadius || target.health() >= target.maxHealth())){ - target = null; - }else if(target != null && consValid()){ - target.heal(repairSpeed * strength * edelta()); - rotation = Mathf.slerpDelta(rotation, angleTo(target), 0.5f * efficiency() * timeScale); - targetIsBeingRepaired = true; + float multiplier = 1f; + if(acceptCoolant){ + var liq = consumes.get(ConsumeType.liquid); + multiplier = liq.valid(this) ? 1f + liquids.current().heatCapacity * coolantMultiplier : 1f; } - if(target != null && targetIsBeingRepaired){ - strength = Mathf.lerpDelta(strength, 1f, 0.08f * Time.delta); - }else{ - strength = Mathf.lerpDelta(strength, 0f, 0.07f * Time.delta); + if(target != null && (target.dead() || target.dst(tile) - target.hitSize/2f > repairRadius || target.health() >= target.maxHealth())){ + target = null; } + if(target == null){ + offset.setZero(); + } + + boolean healed = false; + + if(target != null && consValid()){ + float angle = Angles.angle(x, y, target.x + offset.x, target.y + offset.y); + if(Angles.angleDist(angle, rotation) < 30f){ + healed = true; + target.heal(repairSpeed * strength * edelta() * multiplier); + } + rotation = Mathf.slerpDelta(rotation, angle, 0.5f * efficiency() * timeScale); + } + + strength = Mathf.lerpDelta(strength, healed ? 1f : 0f, 0.08f * Time.delta); + if(timer(timerTarget, 20)){ rect.setSize(repairRadius * 2).setCenter(x, y); target = Units.closest(team, x, y, repairRadius, Unit::damaged); diff --git a/core/src/mindustry/world/blocks/units/UnitBlock.java b/core/src/mindustry/world/blocks/units/UnitBlock.java index 872745c334..4c78b650ad 100644 --- a/core/src/mindustry/world/blocks/units/UnitBlock.java +++ b/core/src/mindustry/world/blocks/units/UnitBlock.java @@ -1,20 +1,12 @@ package mindustry.world.blocks.units; -import arc.*; -import arc.math.*; -import arc.util.*; import mindustry.annotations.Annotations.*; -import mindustry.content.*; -import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.world.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; import mindustry.world.meta.*; -import static mindustry.Vars.*; - -public class UnitBlock extends PayloadAcceptor{ +public class UnitBlock extends PayloadBlock{ public UnitBlock(String name){ super(name); @@ -31,7 +23,7 @@ public class UnitBlock extends PayloadAcceptor{ build.spawned(); } - public class UnitBuild extends PayloadAcceptorBuild{ + public class UnitBuild extends PayloadBlockBuild{ public float progress, time, speedScl; public void spawned(){ diff --git a/core/src/mindustry/world/blocks/units/UnitFactory.java b/core/src/mindustry/world/blocks/units/UnitFactory.java index 2cd64fd33e..8c337e9919 100644 --- a/core/src/mindustry/world/blocks/units/UnitFactory.java +++ b/core/src/mindustry/world/blocks/units/UnitFactory.java @@ -98,7 +98,7 @@ public class UnitFactory extends UnitBlock{ table.row(); for(var plan : p){ if(plan.unit.unlockedNow()){ - table.image(plan.unit.icon(Cicon.small)).size(8 * 3).padRight(2).right(); + table.image(plan.unit.uiIcon).size(8 * 3).padRight(2).right(); table.add(plan.unit.localizedName).left(); table.add(Strings.autoFixed(plan.time / 60f, 1) + " " + Core.bundle.get("unit.seconds")).color(Color.lightGray).padLeft(12).left(); table.row(); @@ -143,6 +143,7 @@ public class UnitFactory extends UnitBlock{ @Override public Object senseObject(LAccess sensor){ if(sensor == LAccess.config) return currentPlan == -1 ? null : plans.get(currentPlan).unit; + if(sensor == LAccess.progress) return Mathf.clamp(fraction()); return super.senseObject(sensor); } @@ -172,7 +173,7 @@ public class UnitFactory extends UnitBlock{ table.table(t -> { t.left(); t.image().update(i -> { - i.setDrawable(currentPlan == -1 ? Icon.cancel : reg.set(plans.get(currentPlan).unit.icon(Cicon.medium))); + i.setDrawable(currentPlan == -1 ? Icon.cancel : reg.set(plans.get(currentPlan).unit.uiIcon)); i.setScaling(Scaling.fit); i.setColor(currentPlan == -1 ? Color.lightGray : Color.white); }).size(32).padBottom(-4).padRight(2); diff --git a/core/src/mindustry/world/consumers/Consume.java b/core/src/mindustry/world/consumers/Consume.java index 3066dbb9bb..e1e9cac916 100644 --- a/core/src/mindustry/world/consumers/Consume.java +++ b/core/src/mindustry/world/consumers/Consume.java @@ -8,10 +8,10 @@ import mindustry.world.meta.*; /** An abstract class that defines a type of resource that a block can consume. */ public abstract class Consume{ /** If true, this consumer will not influence consumer validity. */ - protected boolean optional; + public boolean optional; /** If true, this consumer will be displayed as a boost input. */ - protected boolean booster; - protected boolean update = true; + public boolean booster; + public boolean update = true; /** * Apply a filter to items accepted. diff --git a/core/src/mindustry/world/consumers/ConsumeItemDynamic.java b/core/src/mindustry/world/consumers/ConsumeItemDynamic.java index 580ba7ed1d..009824d1d8 100644 --- a/core/src/mindustry/world/consumers/ConsumeItemDynamic.java +++ b/core/src/mindustry/world/consumers/ConsumeItemDynamic.java @@ -46,7 +46,7 @@ public class ConsumeItemDynamic extends Consume{ int i = 0; for(ItemStack stack : items.get(tile)){ - table.add(new ReqImage(new ItemImage(stack.item.icon(Cicon.medium), stack.amount), + table.add(new ReqImage(new ItemImage(stack.item.uiIcon, stack.amount), () -> tile.items != null && tile.items.has(stack.item, stack.amount))).padRight(8).left(); if(++i % 4 == 0) table.row(); } diff --git a/core/src/mindustry/world/consumers/ConsumeItemFilter.java b/core/src/mindustry/world/consumers/ConsumeItemFilter.java index 93d70a46f3..b4346d0c6e 100644 --- a/core/src/mindustry/world/consumers/ConsumeItemFilter.java +++ b/core/src/mindustry/world/consumers/ConsumeItemFilter.java @@ -7,7 +7,6 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -31,7 +30,7 @@ public class ConsumeItemFilter extends Consume{ @Override public void build(Building tile, Table table){ MultiReqImage image = new MultiReqImage(); - content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.icon(Cicon.medium), 1), + content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.uiIcon, 1), () -> tile.items != null && tile.items.has(item)))); table.add(image).size(8 * 4); @@ -71,6 +70,6 @@ public class ConsumeItemFilter extends Consume{ @Override public void display(Stats stats){ - stats.add(booster ? Stat.booster : Stat.input, new ItemFilterValue(filter)); + stats.add(booster ? Stat.booster : Stat.input, stats.timePeriod < 0 ? StatValues.items(filter) : StatValues.items(stats.timePeriod, filter)); } } diff --git a/core/src/mindustry/world/consumers/ConsumeItems.java b/core/src/mindustry/world/consumers/ConsumeItems.java index c4f5b03cfd..be41ed773c 100644 --- a/core/src/mindustry/world/consumers/ConsumeItems.java +++ b/core/src/mindustry/world/consumers/ConsumeItems.java @@ -6,7 +6,6 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; public class ConsumeItems extends Consume{ public final ItemStack[] items; @@ -22,7 +21,7 @@ public class ConsumeItems extends Consume{ @Override public void applyItemFilter(Bits filter){ - for(ItemStack stack : items){ + for(var stack : items){ filter.set(stack.item.id); } } @@ -36,10 +35,10 @@ public class ConsumeItems extends Consume{ public void build(Building tile, Table table){ table.table(c -> { int i = 0; - for(ItemStack stack : items){ - c.add(new ReqImage(new ItemImage(stack.item.icon(Cicon.medium), stack.amount), + for(var stack : items){ + c.add(new ReqImage(new ItemImage(stack.item.uiIcon, stack.amount), () -> tile.items != null && tile.items.has(stack.item, stack.amount))).padRight(8); - if(++i % 4 == 0) table.row(); + if(++i % 4 == 0) c.row(); } }).left(); } @@ -56,7 +55,7 @@ public class ConsumeItems extends Consume{ @Override public void trigger(Building entity){ - for(ItemStack stack : items){ + for(var stack : items){ entity.items.remove(stack); } } @@ -68,6 +67,6 @@ public class ConsumeItems extends Consume{ @Override public void display(Stats stats){ - stats.add(booster ? Stat.booster : Stat.input, new ItemListValue(items)); + stats.add(booster ? Stat.booster : Stat.input, stats.timePeriod < 0 ? StatValues.items(items) : StatValues.items(stats.timePeriod, items)); } } diff --git a/core/src/mindustry/world/consumers/ConsumeLiquid.java b/core/src/mindustry/world/consumers/ConsumeLiquid.java index 6a4f765edb..fc345f88cc 100644 --- a/core/src/mindustry/world/consumers/ConsumeLiquid.java +++ b/core/src/mindustry/world/consumers/ConsumeLiquid.java @@ -7,6 +7,8 @@ import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + public class ConsumeLiquid extends ConsumeLiquidBase{ public final Liquid liquid; @@ -26,7 +28,7 @@ public class ConsumeLiquid extends ConsumeLiquidBase{ @Override public void build(Building tile, Table table){ - table.add(new ReqImage(liquid.icon(Cicon.medium), () -> valid(tile))).size(8 * 4); + table.add(new ReqImage(liquid.uiIcon, () -> valid(tile))).size(iconMed).top().left(); } @Override diff --git a/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java b/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java index 5b1881458b..8a7fc026d1 100644 --- a/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java +++ b/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java @@ -7,7 +7,6 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -28,7 +27,7 @@ public class ConsumeLiquidFilter extends ConsumeLiquidBase{ public void build(Building build, Table table){ Seq list = content.liquids().select(l -> !l.isHidden() && filter.get(l)); MultiReqImage image = new MultiReqImage(); - list.each(liquid -> image.add(new ReqImage(liquid.icon(Cicon.medium), () -> + list.each(liquid -> image.add(new ReqImage(liquid.uiIcon, () -> build.liquids != null && build.liquids.current() == liquid && build.liquids.get(liquid) >= Math.max(use(build), amount * build.delta())))); table.add(image).size(8 * 4); @@ -51,6 +50,6 @@ public class ConsumeLiquidFilter extends ConsumeLiquidBase{ @Override public void display(Stats stats){ - stats.add(booster ? Stat.booster : Stat.input, new LiquidFilterValue(filter, amount * 60f, true)); + stats.add(booster ? Stat.booster : Stat.input, StatValues.liquids(filter, amount * 60f, true)); } } diff --git a/core/src/mindustry/world/draw/DrawAnimation.java b/core/src/mindustry/world/draw/DrawAnimation.java index 6c4de1ef36..f0699634a4 100644 --- a/core/src/mindustry/world/draw/DrawAnimation.java +++ b/core/src/mindustry/world/draw/DrawAnimation.java @@ -15,17 +15,21 @@ public class DrawAnimation extends DrawBlock{ public TextureRegion liquid, top; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); Draw.rect( sine ? - frames[(int)Mathf.absin(entity.totalProgress, frameSpeed, frameCount - 0.001f)] : - frames[(int)((entity.totalProgress / frameSpeed) % frameCount)], - entity.x, entity.y); - Draw.color(Color.clear, entity.liquids.current().color, entity.liquids.total() / entity.block.liquidCapacity); - Draw.rect(liquid, entity.x, entity.y); - Draw.color(); - Draw.rect(top, entity.x, entity.y); + frames[(int)Mathf.absin(build.totalProgress, frameSpeed, frameCount - 0.001f)] : + frames[(int)((build.totalProgress / frameSpeed) % frameCount)], + build.x, build.y); + if(build.liquids != null){ + Draw.color(Color.clear, build.liquids.current().color, build.liquids.total() / build.block.liquidCapacity); + Draw.rect(liquid, build.x, build.y); + Draw.color(); + } + if(top.found()){ + Draw.rect(top, build.x, build.y); + } } @Override diff --git a/core/src/mindustry/world/draw/DrawArcSmelter.java b/core/src/mindustry/world/draw/DrawArcSmelter.java new file mode 100644 index 0000000000..efab1368ed --- /dev/null +++ b/core/src/mindustry/world/draw/DrawArcSmelter.java @@ -0,0 +1,69 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +//TODO +public class DrawArcSmelter extends DrawBlock{ + public TextureRegion top, bottom; + public Color flameColor = Color.valueOf("f58349"), midColor = Color.valueOf("f2d585"); + public float flameRad = 1f, circleSpace = 2f, flameRadiusScl = 3f, flameRadiusMag = 0.3f, circleStroke = 1.5f; + + public float alpha = 0.68f; + public int particles = 25; + public float particleLife = 40f, particleRad = 7f, particleStroke = 1.1f, particleLen = 3f; + + @Override + public void draw(GenericCrafterBuild build){ + Draw.rect(bottom, build.x, build.y); + + if(build.warmup > 0f && flameColor.a > 0.001f){ + + + Lines.stroke(circleStroke * build.warmup); + + float si = Mathf.absin(flameRadiusScl, flameRadiusMag); + float a = alpha * build.warmup; + Draw.blend(Blending.additive); + + Draw.color(midColor, a); + Fill.circle(build.x, build.y, flameRad + si); + + Draw.color(flameColor, a); + Lines.circle(build.x, build.y, (flameRad + circleSpace + si) * build.warmup); + + Lines.stroke(particleStroke * build.warmup); + + float base = (Time.time / particleLife); + rand.setSeed(build.id); + for(int i = 0; i < particles; i++){ + float fin = (rand.random(1f) + base) % 1f, fout = 1f - fin; + float angle = rand.random(360f); + float len = particleRad * Interp.pow2Out.apply(fin); + Lines.lineAngle(build.x + Angles.trnsx(angle, len), build.y + Angles.trnsy(angle, len), angle, particleLen * fout * build.warmup); + } + + Draw.blend(); + Draw.reset(); + } + + Draw.rect(top, build.x, build.y); + Draw.rect(build.block.region, build.x, build.y); + } + + @Override + public void load(Block block){ + top = Core.atlas.find(block.name + "-top"); + bottom = Core.atlas.find(block.name + "-bottom"); + } + + @Override + public TextureRegion[] icons(Block block){ + return new TextureRegion[]{bottom, block.region, top}; + } +} diff --git a/core/src/mindustry/world/draw/DrawBlock.java b/core/src/mindustry/world/draw/DrawBlock.java index d40d8fe583..4c186f137b 100644 --- a/core/src/mindustry/world/draw/DrawBlock.java +++ b/core/src/mindustry/world/draw/DrawBlock.java @@ -1,16 +1,23 @@ package mindustry.world.draw; import arc.graphics.g2d.*; +import arc.math.*; import mindustry.world.*; import mindustry.world.blocks.production.GenericCrafter.*; /** An implementation of custom rendering behavior for a block. * This is used mostly for mods. */ public class DrawBlock{ + protected static final Rand rand = new Rand(); /** Draws the block. */ - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y, entity.block.rotate ? entity.rotdeg() : 0); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y, build.block.rotate ? build.rotdeg() : 0); + } + + /** Draws any extra light for the block. */ + public void drawLight(GenericCrafterBuild build){ + } /** Load any relevant texture regions. */ diff --git a/core/src/mindustry/world/draw/DrawCells.java b/core/src/mindustry/world/draw/DrawCells.java new file mode 100644 index 0000000000..9730e29f2d --- /dev/null +++ b/core/src/mindustry/world/draw/DrawCells.java @@ -0,0 +1,57 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.graphics.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +public class DrawCells extends DrawBlock{ + public TextureRegion bottom, middle; + public Color color = Color.white.cpy(), particleColorFrom = Color.black.cpy(), particleColorTo = Color.black.cpy(); + public int particles = 12; + public float range = 4f, recurrence = 6f, radius = 3f, lifetime = 60f; + + @Override + public void draw(GenericCrafterBuild build){ + + Draw.rect(bottom, build.x, build.y); + + Drawf.liquid(middle, build.x, build.y, build.warmup, color); + + if(build.warmup > 0.001f){ + rand.setSeed(build.id); + for(int i = 0; i < particles; i++){ + float offset = rand.nextFloat() * 999999f; + float x = rand.range(range), y = rand.range(range); + float fin = 1f - (((Time.time + offset) / lifetime) % recurrence); + float ca = rand.random(0.1f, 1f); + float fslope = Mathf.slope(fin); + + if(fin > 0){ + Draw.color(particleColorFrom, particleColorTo, ca); + Draw.alpha(build.warmup); + + Fill.circle(build.x + x, build.y + y, fslope * radius); + } + } + } + + Draw.color(); + Draw.rect(build.block.region, build.x, build.y); + } + + @Override + public void load(Block block){ + bottom = Core.atlas.find(block.name + "-bottom"); + middle = Core.atlas.find(block.name + "-middle"); + } + + @Override + public TextureRegion[] icons(Block block){ + return new TextureRegion[]{bottom, block.region}; + } +} diff --git a/core/src/mindustry/world/draw/DrawCultivator.java b/core/src/mindustry/world/draw/DrawCultivator.java new file mode 100644 index 0000000000..dcfe8c6c95 --- /dev/null +++ b/core/src/mindustry/world/draw/DrawCultivator.java @@ -0,0 +1,56 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.util.*; +import mindustry.graphics.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +public class DrawCultivator extends DrawBlock{ + public Color plantColor = Color.valueOf("5541b1"); + public Color plantColorLight = Color.valueOf("7457ce"); + public Color bottomColor = Color.valueOf("474747"); + + public int bubbles = 12, sides = 8; + public float strokeMin = 0.2f, spread = 3f, timeScl = 70f; + public float recurrence = 6f, radius = 3f; + + public TextureRegion middle; + public TextureRegion top; + + @Override + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); + + Drawf.liquid(middle, build.x, build.y, build.warmup, plantColor); + + Draw.color(bottomColor, plantColorLight, build.warmup); + + rand.setSeed(build.pos()); + for(int i = 0; i < bubbles; i++){ + float x = rand.range(spread), y = rand.range(spread); + float life = 1f - ((Time.time / timeScl + rand.random(recurrence)) % recurrence); + + if(life > 0){ + Lines.stroke(build.warmup * (life + strokeMin)); + Lines.poly(build.x + x, build.y + y, sides, (1f - life) * radius); + } + } + + Draw.color(); + Draw.rect(top, build.x, build.y); + } + + @Override + public void load(Block block){ + middle = Core.atlas.find(block.name + "-middle"); + top = Core.atlas.find(block.name + "-top"); + } + + @Override + public TextureRegion[] icons(Block block){ + return new TextureRegion[]{block.region, top}; + } +} diff --git a/core/src/mindustry/world/draw/DrawGlow.java b/core/src/mindustry/world/draw/DrawGlow.java index d15d477b61..47748cad06 100644 --- a/core/src/mindustry/world/draw/DrawGlow.java +++ b/core/src/mindustry/world/draw/DrawGlow.java @@ -11,10 +11,10 @@ public class DrawGlow extends DrawBlock{ public TextureRegion top; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y); - Draw.alpha(Mathf.absin(entity.totalProgress, glowScale, glowAmount) * entity.warmup); - Draw.rect(top, entity.x, entity.y); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); + Draw.alpha(Mathf.absin(build.totalProgress, glowScale, glowAmount) * build.warmup); + Draw.rect(top, build.x, build.y); Draw.reset(); } diff --git a/core/src/mindustry/world/draw/DrawMixer.java b/core/src/mindustry/world/draw/DrawMixer.java index bb785e01f0..850ac5c6dd 100644 --- a/core/src/mindustry/world/draw/DrawMixer.java +++ b/core/src/mindustry/world/draw/DrawMixer.java @@ -10,19 +10,19 @@ public class DrawMixer extends DrawBlock{ public TextureRegion liquid, top, bottom; @Override - public void draw(GenericCrafterBuild entity){ - float rotation = entity.block.rotate ? entity.rotdeg() : 0; + public void draw(GenericCrafterBuild build){ + float rotation = build.block.rotate ? build.rotdeg() : 0; - Draw.rect(bottom, entity.x, entity.y, rotation); + Draw.rect(bottom, build.x, build.y, rotation); - if(entity.liquids.total() > 0.001f){ - Draw.color(((GenericCrafter)entity.block).outputLiquid.liquid.color); - Draw.alpha(entity.liquids.get(((GenericCrafter)entity.block).outputLiquid.liquid) / entity.block.liquidCapacity); - Draw.rect(liquid, entity.x, entity.y, rotation); + if(build.liquids.total() > 0.001f){ + Draw.color(((GenericCrafter)build.block).outputLiquid.liquid.color); + Draw.alpha(build.liquids.get(((GenericCrafter)build.block).outputLiquid.liquid) / build.block.liquidCapacity); + Draw.rect(liquid, build.x, build.y, rotation); Draw.color(); } - Draw.rect(top, entity.x, entity.y, rotation); + Draw.rect(top, build.x, build.y, rotation); } @Override diff --git a/core/src/mindustry/world/draw/DrawRotator.java b/core/src/mindustry/world/draw/DrawRotator.java index ca92aaf351..166245fa6c 100644 --- a/core/src/mindustry/world/draw/DrawRotator.java +++ b/core/src/mindustry/world/draw/DrawRotator.java @@ -9,9 +9,9 @@ public class DrawRotator extends DrawBlock{ public TextureRegion rotator; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y); - Draw.rect(rotator, entity.x, entity.y, entity.totalProgress * 2f); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); + Draw.rect(rotator, build.x, build.y, build.totalProgress * 2f); } @Override diff --git a/core/src/mindustry/world/draw/DrawSmelter.java b/core/src/mindustry/world/draw/DrawSmelter.java new file mode 100644 index 0000000000..5b4880eba9 --- /dev/null +++ b/core/src/mindustry/world/draw/DrawSmelter.java @@ -0,0 +1,57 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.graphics.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +public class DrawSmelter extends DrawBlock{ + public Color flameColor = Color.valueOf("ffc999"); + public TextureRegion top; + public float lightRadius = 60f, lightAlpha = 0.65f, lightSinScl = 10f, lightSinMag = 5; + public float flameRadius = 3f, flameRadiusIn = 1.9f, flameRadiusScl = 5f, flameRadiusMag = 2f, flameRadiusInMag = 1f; + + public DrawSmelter(){ + } + + public DrawSmelter(Color flameColor){ + this.flameColor = flameColor; + } + + @Override + public void load(Block block){ + top = Core.atlas.find(block.name + "-top"); + } + + @Override + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y, build.block.rotate ? build.rotdeg() : 0); + + if(build.warmup > 0f && flameColor.a > 0.001f){ + float g = 0.3f; + float r = 0.06f; + float cr = Mathf.random(0.1f); + + Draw.z(Layer.block + 0.01f); + + Draw.alpha(((1f - g) + Mathf.absin(Time.time, 8f, g) + Mathf.random(r) - r) * build.warmup); + + Draw.tint(flameColor); + Fill.circle(build.x, build.y, flameRadius + Mathf.absin(Time.time, flameRadiusScl, flameRadiusMag) + cr); + Draw.color(1f, 1f, 1f, build.warmup); + Draw.rect(top, build.x, build.y); + Fill.circle(build.x, build.y, flameRadiusIn + Mathf.absin(Time.time, flameRadiusScl, flameRadiusInMag) + cr); + + Draw.color(); + } + } + + @Override + public void drawLight(GenericCrafterBuild build){ + Drawf.light(build.team, build.x, build.y, (lightRadius + Mathf.absin(lightSinScl, lightSinMag)) * build.warmup * build.block.size, flameColor, lightAlpha); + } +} diff --git a/core/src/mindustry/world/draw/DrawWeave.java b/core/src/mindustry/world/draw/DrawWeave.java index 5ccd476b5f..0d6c94dcb9 100644 --- a/core/src/mindustry/world/draw/DrawWeave.java +++ b/core/src/mindustry/world/draw/DrawWeave.java @@ -12,22 +12,22 @@ public class DrawWeave extends DrawBlock{ public TextureRegion weave, bottom; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(bottom, entity.x, entity.y); - Draw.rect(weave, entity.x, entity.y, entity.totalProgress); + public void draw(GenericCrafterBuild build){ + Draw.rect(bottom, build.x, build.y); + Draw.rect(weave, build.x, build.y, build.totalProgress); Draw.color(Pal.accent); - Draw.alpha(entity.warmup); + Draw.alpha(build.warmup); Lines.lineAngleCenter( - entity.x + Mathf.sin(entity.totalProgress, 6f, Vars.tilesize / 3f * entity.block.size), - entity.y, + build.x + Mathf.sin(build.totalProgress, 6f, Vars.tilesize / 3f * build.block.size), + build.y, 90, - entity.block.size * Vars.tilesize / 2f); + build.block.size * Vars.tilesize / 2f); Draw.reset(); - Draw.rect(entity.block.region, entity.x, entity.y); + Draw.rect(build.block.region, build.x, build.y); } @Override diff --git a/core/src/mindustry/world/meta/Attribute.java b/core/src/mindustry/world/meta/Attribute.java index 07699ba6b3..5114d3c10a 100644 --- a/core/src/mindustry/world/meta/Attribute.java +++ b/core/src/mindustry/world/meta/Attribute.java @@ -2,23 +2,47 @@ package mindustry.world.meta; import mindustry.*; -public enum Attribute{ - /** Heat of this block. Used for calculating output of thermal generators. */ - heat, - /** Spore content of this block. Used for increasing cultivator yield. */ - spores, - /** Water content of this block. Used for increasing water extractor yield. */ - water, - /** Oil content of this block. Used for increasing oil extractor yield. */ - oil, +public class Attribute{ + public static Attribute[] all = {}; + + public static final Attribute + /** Heat content. Used for thermal generator yield. */ + heat = add("heat"), + /** Spore content. Used for cultivator yield. */ + spores = add("spores"), + /** Water content. Used for water extractor yield. */ + water = add("water"), + /** Oil content. Used for oil extractor yield. */ + oil = add("oil"), /** Light coverage. Negative values decrease solar panel efficiency. */ - light; + light = add("light"); - public static final Attribute[] all = values(); + public final int id; + public final String name; - /** @return the envrionmental value for this attribute. */ + /** @return the environmental value for this attribute. */ public float env(){ if(Vars.state == null) return 0; return Vars.state.envAttrs.get(this); } + + Attribute(int id, String name){ + this.id = id; + this.name = name; + } + + @Override + public String toString(){ + return name; + } + + /** Automatically registers this attribute for use. Do not call after mod init. */ + public static Attribute add(String name){ + Attribute a = new Attribute(all.length, name); + Attribute[] prev = all; + all = new Attribute[all.length + 1]; + System.arraycopy(prev, 0, all, 0, a.id); + all[a.id] = a; + return a; + } } diff --git a/core/src/mindustry/world/meta/BlockFlag.java b/core/src/mindustry/world/meta/BlockFlag.java index 2fad22868e..c68e396278 100644 --- a/core/src/mindustry/world/meta/BlockFlag.java +++ b/core/src/mindustry/world/meta/BlockFlag.java @@ -22,10 +22,13 @@ public enum BlockFlag{ resupply, /** Any reactor block. */ reactor, - /** Any block that boosts unit capacity. */ + /** This flag is unused, and will be removed. */ + @Deprecated unitModifier, /** Blocks that extinguishes fires. */ - extinguisher; + extinguisher, + /** Just a launch pad. */ + launchPad; public final static BlockFlag[] all = values(); diff --git a/core/src/mindustry/world/meta/Env.java b/core/src/mindustry/world/meta/Env.java new file mode 100644 index 0000000000..6ebe3a6227 --- /dev/null +++ b/core/src/mindustry/world/meta/Env.java @@ -0,0 +1,14 @@ +package mindustry.world.meta; + +/** Environmental flags for different types of locations. */ +public class Env{ + public static final int + terrestrial = 1, + space = 1 << 1, + underwater = 1 << 2, + spores = 1 << 3, + scorching = 1 << 4, + groundOil = 1 << 5, + groundWater = 1 << 6, + any = 0xffffffff; +} diff --git a/core/src/mindustry/world/meta/Stat.java b/core/src/mindustry/world/meta/Stat.java index f5f92afdfc..c5810c6018 100644 --- a/core/src/mindustry/world/meta/Stat.java +++ b/core/src/mindustry/world/meta/Stat.java @@ -70,6 +70,7 @@ public enum Stat{ speedIncrease(StatCat.function), repairTime(StatCat.function), + repairSpeed(StatCat.function), range(StatCat.function), shootRange(StatCat.function), inaccuracy(StatCat.function), diff --git a/core/src/mindustry/world/meta/StatValues.java b/core/src/mindustry/world/meta/StatValues.java new file mode 100644 index 0000000000..5499cc1ca1 --- /dev/null +++ b/core/src/mindustry/world/meta/StatValues.java @@ -0,0 +1,310 @@ +package mindustry.world.meta; + +import arc.*; +import arc.func.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.ui.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import mindustry.content.*; +import mindustry.ctype.*; +import mindustry.entities.bullet.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.ui.*; +import mindustry.world.*; +import mindustry.world.blocks.defense.turrets.*; +import mindustry.world.blocks.environment.*; + +import static mindustry.Vars.*; + +/** Utilities for displaying certain stats in a table. */ +public class StatValues{ + + public static StatValue string(String value, Object... args){ + String result = Strings.format(value, args); + return table -> table.add(result); + } + + public static StatValue bool(boolean value){ + return table -> table.add(!value ? "@no" : "@yes"); + } + + public static StatValue number(float value, StatUnit unit){ + return table -> { + int precision = Math.abs((int)value - value) <= 0.001f ? 0 : Math.abs((int)(value * 10) - value * 10) <= 0.001f ? 1 : 2; + + table.add(Strings.fixed(value, precision)); + table.add((unit.space ? " " : "") + unit.localized()); + }; + } + + public static StatValue liquid(Liquid liquid, float amount, boolean perSecond){ + return table -> table.add(new LiquidDisplay(liquid, amount, perSecond)); + } + + public static StatValue liquids(Boolf filter, float amount, boolean perSecond){ + return table -> { + Seq list = new Seq<>(); + + for(Liquid item : content.liquids()){ + if(!item.isHidden() && filter.get(item)) list.add(item); + } + + for(int i = 0; i < list.size; i++){ + table.add(new LiquidDisplay(list.get(i), amount, perSecond)).padRight(5); + + if(i != list.size - 1){ + table.add("/"); + } + } + }; + } + + public static StatValue items(ItemStack... stacks){ + return items(true, stacks); + } + + public static StatValue items(boolean displayName, ItemStack... stacks){ + return table -> { + for(ItemStack stack : stacks){ + table.add(new ItemDisplay(stack.item, stack.amount, displayName)).padRight(5); + } + }; + } + + public static StatValue items(float timePeriod, ItemStack... stacks){ + return table -> { + for(ItemStack stack : stacks){ + table.add(new ItemDisplay(stack.item, stack.amount, timePeriod, true)).padRight(5); + } + }; + } + + public static StatValue items(Boolf filter){ + return items(-1, filter); + } + + public static StatValue items(float timePeriod, Boolf filter){ + return table -> { + Seq list = content.items().select(filter); + + for(int i = 0; i < list.size; i++){ + Item item = list.get(i); + + table.add(timePeriod <= 0 ? new ItemDisplay(item) : new ItemDisplay(item, 0, timePeriod, true)).padRight(5); + + if(i != list.size - 1){ + table.add("/"); + } + } + }; + } + + public static StatValue content(UnlockableContent content){ + return table -> { + table.add(new Image(content.uiIcon)).size(iconSmall).padRight(3); + table.add(content.localizedName).padRight(3); + }; + } + + public static StatValue floorEfficiency(Floor floor, float multiplier, boolean startZero){ + return table -> table.stack( + new Image(floor.uiIcon).setScaling(Scaling.fit), + new Table(t -> t.top().right().add((multiplier < 0 ? "[scarlet]" : startZero ? "[accent]" : "[accent]+") + (int)((multiplier) * 100) + "%").style(Styles.outlineLabel)) + ); + } + + public static StatValue blocks(Boolf pred){ + return blocks(content.blocks().select(pred)); + } + + public static StatValue blocks(Seq list){ + return table -> table.table(l -> { + l.left(); + + for(int i = 0; i < list.size; i++){ + Block item = list.get(i); + + l.image(item.uiIcon).size(iconSmall).padRight(2).padLeft(2).padTop(3).padBottom(3); + l.add(item.localizedName).left().padLeft(1).padRight(4); + if(i % 5 == 4){ + l.row(); + } + } + }); + } + + public static StatValue boosters(float reload, float maxUsed, float multiplier, boolean baseReload, Boolf filter){ + return table -> { + table.row(); + table.table(c -> { + for(Liquid liquid : content.liquids()){ + if(!filter.get(liquid)) continue; + + c.image(liquid.uiIcon).size(3 * 8).padRight(4).right().top(); + c.add(liquid.localizedName).padRight(10).left().top(); + c.table(Tex.underline, bt -> { + bt.left().defaults().padRight(3).left(); + + float reloadRate = (baseReload ? 1f : 0f) + maxUsed * multiplier * liquid.heatCapacity; + float standardReload = baseReload ? reload : reload / (maxUsed * multiplier * 0.4f); + float result = standardReload / (reload / reloadRate); + bt.add(Core.bundle.format("bullet.reload", Strings.autoFixed(result, 2))); + }).left().padTop(-9); + c.row(); + } + }).colspan(table.getColumns()); + table.row(); + }; + } + + public static StatValue strengthBoosters(float multiplier, Boolf filter){ + return table -> { + table.row(); + table.table(c -> { + for(Liquid liquid : content.liquids()){ + if(!filter.get(liquid)) continue; + + c.image(liquid.uiIcon).size(3 * 8).padRight(4).right().top(); + c.add(liquid.localizedName).padRight(10).left().top(); + c.table(Tex.underline, bt -> { + bt.left().defaults().padRight(3).left(); + + float newRate = (1f + multiplier * liquid.heatCapacity); + bt.add(Core.bundle.format("bar.strength", Strings.autoFixed(newRate, 2))); + }).left().padTop(-9); + c.row(); + } + }).colspan(table.getColumns()); + table.row(); + }; + } + + public static StatValue weapons(UnitType unit, Seq weapons){ + return table -> { + table.row(); + for(int i = 0; i < weapons.size;i ++){ + Weapon weapon = weapons.get(i); + + if(weapon.flipSprite){ + //flipped weapons are not given stats + continue; + } + + TextureRegion region = !weapon.name.equals("") && weapon.outlineRegion.found() ? weapon.outlineRegion : unit.fullIcon; + + table.image(region).size(60).scaling(Scaling.bounded).right().top(); + + table.table(Tex.underline, w -> { + w.left().defaults().padRight(3).left(); + + if(weapon.inaccuracy > 0){ + sep(w, "[lightgray]" + Stat.inaccuracy.localized() + ": [white]" + (int)weapon.inaccuracy + " " + StatUnit.degrees.localized()); + } + sep(w, "[lightgray]" + Stat.reload.localized() + ": " + (weapon.mirror ? "2x " : "") + "[white]" + Strings.autoFixed(60f / weapon.reload * weapon.shots, 2)); + + ammo(ObjectMap.of(unit, weapon.bullet)).display(w); + }).padTop(-9).left(); + table.row(); + } + }; + } + + public static StatValue ammo(ObjectMap map){ + return table -> { + + table.row(); + + var orderedKeys = map.keys().toSeq(); + orderedKeys.sort(); + + for(T t : orderedKeys){ + boolean unit = t instanceof UnitType; + + BulletType type = map.get(t); + + //no point in displaying unit icon twice + if(!unit & !(t instanceof PowerTurret)){ + table.image(icon(t)).size(3 * 8).padRight(4).right().top(); + table.add(t.localizedName).padRight(10).left().top(); + } + + table.table(bt -> { + bt.left().defaults().padRight(3).left(); + + if(type.damage > 0 && (type.collides || type.splashDamage <= 0)){ + if(type.continuousDamage() > 0){ + bt.add(Core.bundle.format("bullet.damage", type.continuousDamage()) + StatUnit.perSecond.localized()); + }else{ + bt.add(Core.bundle.format("bullet.damage", type.damage)); + } + } + + if(type.buildingDamageMultiplier != 1){ + sep(bt, Core.bundle.format("bullet.buildingdamage", (int)(type.buildingDamageMultiplier * 100))); + } + + if(type.splashDamage > 0){ + sep(bt, Core.bundle.format("bullet.splashdamage", (int)type.splashDamage, Strings.fixed(type.splashDamageRadius / tilesize, 1))); + } + + if(!unit && !Mathf.equal(type.ammoMultiplier, 1f)){ + sep(bt, Core.bundle.format("bullet.multiplier", (int)type.ammoMultiplier)); + } + + if(!Mathf.equal(type.reloadMultiplier, 1f)){ + sep(bt, Core.bundle.format("bullet.reload", Strings.autoFixed(type.reloadMultiplier, 2))); + } + + if(type.knockback > 0){ + sep(bt, Core.bundle.format("bullet.knockback", Strings.autoFixed(type.knockback, 2))); + } + + if(type.healPercent > 0f){ + sep(bt, Core.bundle.format("bullet.healpercent", (int)type.healPercent)); + } + + if(type.pierce || type.pierceCap != -1){ + sep(bt, type.pierceCap == -1 ? "@bullet.infinitepierce" : Core.bundle.format("bullet.pierce", type.pierceCap)); + } + + if(type.incendAmount > 0){ + sep(bt, "@bullet.incendiary"); + } + + if(type.status != StatusEffects.none){ + sep(bt, (type.minfo.mod == null ? type.status.emoji() : "") + "[stat]" + type.status.localizedName); + } + + if(type.homingPower > 0.01f){ + sep(bt, "@bullet.homing"); + } + + if(type.lightning > 0){ + sep(bt, Core.bundle.format("bullet.lightning", type.lightning, type.lightningDamage < 0 ? type.damage : type.lightningDamage)); + } + + if(type.fragBullet != null){ + sep(bt, "@bullet.frag"); + } + }).padTop(unit ? 0 : -9).left().get().background(unit ? null : Tex.underline); + + table.row(); + } + }; + } + + //for AmmoListValue + + private static void sep(Table table, String text){ + table.row(); + table.add(text); + } + + private static TextureRegion icon(UnlockableContent t){ + return t.uiIcon; + } +} diff --git a/core/src/mindustry/world/meta/Stats.java b/core/src/mindustry/world/meta/Stats.java index 39d887f188..d5d91f2c20 100644 --- a/core/src/mindustry/world/meta/Stats.java +++ b/core/src/mindustry/world/meta/Stats.java @@ -6,7 +6,6 @@ import arc.util.*; import mindustry.*; import mindustry.type.*; import mindustry.world.blocks.environment.*; -import mindustry.world.meta.values.*; /** Hold and organizes a list of block stats. */ public class Stats{ @@ -14,6 +13,8 @@ public class Stats{ public boolean useCategories = false; /** Whether these stats are initialized yet. */ public boolean intialized = false; + /** Production time period in ticks. Used for crafters. **/ + public float timePeriod = -1; @Nullable private OrderedMap>> map; @@ -21,7 +22,7 @@ public class Stats{ /** Adds a single float value with this stat, formatted to 2 decimal places. */ public void add(Stat stat, float value, StatUnit unit){ - add(stat, new NumberValue(value, unit)); + add(stat, StatValues.number(value, unit)); } /** Adds a single float value with this stat and no unit. */ @@ -31,27 +32,27 @@ public class Stats{ /** Adds an integer percent stat value. Value is assumed to be in the 0-1 range. */ public void addPercent(Stat stat, float value){ - add(stat, new NumberValue((int)(value * 100), StatUnit.percent)); + add(stat, StatValues.number((int)(value * 100), StatUnit.percent)); } /** Adds a single y/n boolean value. */ public void add(Stat stat, boolean value){ - add(stat, new BooleanValue(value)); + add(stat, StatValues.bool(value)); } /** Adds an item value. */ public void add(Stat stat, Item item){ - add(stat, new ItemListValue(new ItemStack(item, 1))); + add(stat, StatValues.items(new ItemStack(item, 1))); } /** Adds an item value. */ public void add(Stat stat, ItemStack item){ - add(stat, new ItemListValue(item)); + add(stat, StatValues.items(item)); } /** Adds an item value. */ public void add(Stat stat, Liquid liquid, float amount, boolean perSecond){ - add(stat, new LiquidValue(liquid, amount, perSecond)); + add(stat, StatValues.liquid(liquid, amount, perSecond)); } public void add(Stat stat, Attribute attr){ @@ -70,13 +71,13 @@ public class Stats{ for(var block : Vars.content.blocks() .select(block -> block instanceof Floor f && f.attributes.get(attr) != 0 && !(f.isLiquid && !floating)) .as().with(s -> s.sort(f -> f.attributes.get(attr)))){ - add(stat, new FloorEfficiencyValue(block, block.attributes.get(attr) * scale, startZero)); + add(stat, StatValues.floorEfficiency(block, block.attributes.get(attr) * scale, startZero)); } } /** Adds a single string value with this stat. */ public void add(Stat stat, String format, Object... args){ - add(stat, new StringValue(format, args)); + add(stat, StatValues.string(format, args)); } /** Adds a stat value. */ diff --git a/core/src/mindustry/world/meta/values/AmmoListValue.java b/core/src/mindustry/world/meta/values/AmmoListValue.java deleted file mode 100644 index 9a72a85a24..0000000000 --- a/core/src/mindustry/world/meta/values/AmmoListValue.java +++ /dev/null @@ -1,115 +0,0 @@ -package mindustry.world.meta.values; - -import arc.*; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import arc.util.*; -import mindustry.content.*; -import mindustry.ctype.*; -import mindustry.entities.bullet.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.blocks.defense.turrets.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class AmmoListValue implements StatValue{ - private final ObjectMap map; - - public AmmoListValue(ObjectMap map){ - this.map = map; - } - - @Override - public void display(Table table){ - - table.row(); - - for(T t : map.keys()){ - boolean unit = t instanceof UnitType; - - BulletType type = map.get(t); - - //no point in displaying unit icon twice - if(!unit & !(t instanceof PowerTurret)){ - table.image(icon(t)).size(3 * 8).padRight(4).right().top(); - table.add(t.localizedName).padRight(10).left().top(); - } - - table.table(bt -> { - bt.left().defaults().padRight(3).left(); - - if(type.damage > 0 && (type.collides || type.splashDamage <= 0)){ - if(type.continuousDamage() > 0){ - bt.add(Core.bundle.format("bullet.damage", type.continuousDamage()) + StatUnit.perSecond.localized()); - }else{ - bt.add(Core.bundle.format("bullet.damage", type.damage)); - } - } - - if(type.buildingDamageMultiplier != 1){ - sep(bt, Core.bundle.format("bullet.buildingdamage", (int)(type.buildingDamageMultiplier * 100))); - } - - if(type.splashDamage > 0){ - sep(bt, Core.bundle.format("bullet.splashdamage", (int)type.splashDamage, Strings.fixed(type.splashDamageRadius / tilesize, 1))); - } - - if(!unit && !Mathf.equal(type.ammoMultiplier, 1f) && !(type instanceof LiquidBulletType)){ - sep(bt, Core.bundle.format("bullet.multiplier", (int)type.ammoMultiplier)); - } - - if(!Mathf.equal(type.reloadMultiplier, 1f)){ - sep(bt, Core.bundle.format("bullet.reload", Strings.autoFixed(type.reloadMultiplier, 2))); - } - - if(type.knockback > 0){ - sep(bt, Core.bundle.format("bullet.knockback", Strings.autoFixed(type.knockback, 2))); - } - - if(type.healPercent > 0f){ - sep(bt, Core.bundle.format("bullet.healpercent", (int)type.healPercent)); - } - - if(type.pierce || type.pierceCap != -1){ - sep(bt, type.pierceCap == -1 ? "@bullet.infinitepierce" : Core.bundle.format("bullet.pierce", type.pierceCap)); - } - - if(type.incendAmount > 0){ - sep(bt, "@bullet.incendiary"); - } - - if(type.status != StatusEffects.none){ - sep(bt, (type.minfo.mod == null ? type.status.emoji() : "") + "[stat]" + type.status.localizedName); - } - - if(type.homingPower > 0.01f){ - sep(bt, "@bullet.homing"); - } - - if(type.lightning > 0){ - sep(bt, Core.bundle.format("bullet.lightning", type.lightning, type.lightningDamage < 0 ? type.damage : type.lightningDamage)); - } - - if(type.fragBullet != null){ - sep(bt, "@bullet.frag"); - } - }).padTop(unit ? 0 : -9).left().get().background(unit ? null : Tex.underline); - - table.row(); - } - } - - void sep(Table table, String text){ - table.row(); - table.add(text); - } - - TextureRegion icon(T t){ - return t.icon(Cicon.medium); - } -} diff --git a/core/src/mindustry/world/meta/values/BlockFilterValue.java b/core/src/mindustry/world/meta/values/BlockFilterValue.java deleted file mode 100644 index 3c4d0fe56e..0000000000 --- a/core/src/mindustry/world/meta/values/BlockFilterValue.java +++ /dev/null @@ -1,37 +0,0 @@ -package mindustry.world.meta.values; - -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.ui.*; -import mindustry.world.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class BlockFilterValue implements StatValue{ - public final Boolf pred; - - public BlockFilterValue(Boolf pred){ - this.pred = pred; - } - - @Override - public void display(Table table){ - Seq list = content.blocks().select(pred); - - table.table(l -> { - l.left(); - - for(int i = 0; i < list.size; i++){ - Block item = list.get(i); - - l.image(item.icon(Cicon.small)).size(8 * 3).padRight(2).padLeft(2).padTop(3).padBottom(3); - l.add(item.localizedName).left().padLeft(1).padRight(4); - if(i % 5 == 4){ - l.row(); - } - } - }); - } -} diff --git a/core/src/mindustry/world/meta/values/BlockListValue.java b/core/src/mindustry/world/meta/values/BlockListValue.java deleted file mode 100644 index a1cd5adffe..0000000000 --- a/core/src/mindustry/world/meta/values/BlockListValue.java +++ /dev/null @@ -1,33 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.ui.*; -import mindustry.world.*; -import mindustry.world.meta.*; - -public class BlockListValue implements StatValue{ - public final Seq list; - - public BlockListValue(Seq list){ - this.list = list; - } - - @Override - public void display(Table table){ - - table.table(l -> { - l.left(); - - for(int i = 0; i < list.size; i++){ - Block item = list.get(i); - - l.image(item.icon(Cicon.small)).size(8 * 3).padRight(2).padLeft(2).padTop(3).padBottom(3); - l.add(item.localizedName).left().padLeft(1).padRight(4); - if(i % 5 == 4){ - l.row(); - } - } - }); - } -} diff --git a/core/src/mindustry/world/meta/values/BooleanValue.java b/core/src/mindustry/world/meta/values/BooleanValue.java deleted file mode 100644 index c1da598008..0000000000 --- a/core/src/mindustry/world/meta/values/BooleanValue.java +++ /dev/null @@ -1,17 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import mindustry.world.meta.*; - -public class BooleanValue implements StatValue{ - private final boolean value; - - public BooleanValue(boolean value){ - this.value = value; - } - - @Override - public void display(Table table){ - table.add(!value ? "@no" : "@yes"); - } -} diff --git a/core/src/mindustry/world/meta/values/BoosterListValue.java b/core/src/mindustry/world/meta/values/BoosterListValue.java deleted file mode 100644 index 6f08fc9eac..0000000000 --- a/core/src/mindustry/world/meta/values/BoosterListValue.java +++ /dev/null @@ -1,56 +0,0 @@ -package mindustry.world.meta.values; - -import arc.*; -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class BoosterListValue implements StatValue{ - protected float reload, maxUsed, multiplier; - protected boolean baseReload; - protected Boolf filter; - - public BoosterListValue(float reload, float maxUsed, float multiplier, boolean baseReload, Boolf filter){ - this.reload = reload; - this.maxUsed = maxUsed; - this.baseReload = baseReload; - this.multiplier = multiplier; - this.filter = filter; - } - - @Override - public void display(Table table){ - - table.row(); - table.table(c -> { - for(Liquid liquid : content.liquids()){ - if(!filter.get(liquid)) continue; - - c.image(liquid.icon(Cicon.medium)).size(3 * 8).padRight(4).right().top(); - c.add(liquid.localizedName).padRight(10).left().top(); - c.table(Tex.underline, bt -> { - bt.left().defaults().padRight(3).left(); - - float reloadRate = (baseReload ? 1f : 0f) + maxUsed * multiplier * liquid.heatCapacity; - float standardReload = baseReload ? reload : reload / (maxUsed * multiplier * 0.4f); - float result = standardReload / (reload / reloadRate); - bt.add(Core.bundle.format("bullet.reload", Strings.autoFixed(result, 2))); - }).left().padTop(-9); - c.row(); - } - }).colspan(table.getColumns()); - table.row(); - - } - - void sep(Table table, String text){ - table.row(); - table.add(text); - } -} diff --git a/core/src/mindustry/world/meta/values/FloorEfficiencyValue.java b/core/src/mindustry/world/meta/values/FloorEfficiencyValue.java deleted file mode 100644 index ad32ecf453..0000000000 --- a/core/src/mindustry/world/meta/values/FloorEfficiencyValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.*; -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.ui.*; -import mindustry.world.blocks.environment.*; -import mindustry.world.meta.*; - -public class FloorEfficiencyValue implements StatValue{ - private final Floor floor; - private final float multiplier; - private final boolean startZero; - - public FloorEfficiencyValue(Floor floor, float multiplier, boolean startZero){ - this.floor = floor; - this.multiplier = multiplier; - this.startZero = startZero; - } - - @Override - public void display(Table table){ - table.stack(new Image(floor.icon(Cicon.medium)).setScaling(Scaling.fit), new Table(t -> { - t.top().right().add((multiplier < 0 ? "[scarlet]" : startZero ? "[accent]" : "[accent]+") + (int)((multiplier) * 100) + "%").style(Styles.outlineLabel); - })); - } -} diff --git a/core/src/mindustry/world/meta/values/FloorValue.java b/core/src/mindustry/world/meta/values/FloorValue.java deleted file mode 100644 index 45ff92bc7d..0000000000 --- a/core/src/mindustry/world/meta/values/FloorValue.java +++ /dev/null @@ -1,21 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.*; -import arc.scene.ui.layout.*; -import mindustry.ui.*; -import mindustry.world.blocks.environment.*; -import mindustry.world.meta.*; - -public class FloorValue implements StatValue{ - private final Floor floor; - - public FloorValue(Floor floor){ - this.floor = floor; - } - - @Override - public void display(Table table){ - table.add(new Image(floor.icon(Cicon.small))).padRight(3); - table.add(floor.localizedName).padRight(3); - } -} diff --git a/core/src/mindustry/world/meta/values/ItemFilterValue.java b/core/src/mindustry/world/meta/values/ItemFilterValue.java deleted file mode 100644 index 78db854658..0000000000 --- a/core/src/mindustry/world/meta/values/ItemFilterValue.java +++ /dev/null @@ -1,33 +0,0 @@ -package mindustry.world.meta.values; - -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class ItemFilterValue implements StatValue{ - private final Boolf filter; - - public ItemFilterValue(Boolf filter){ - this.filter = filter; - } - - @Override - public void display(Table table){ - Seq list = content.items().select(filter); - - for(int i = 0; i < list.size; i++){ - Item item = list.get(i); - - table.add(new ItemDisplay(item)).padRight(5); - - if(i != list.size - 1){ - table.add("/"); - } - } - } -} diff --git a/core/src/mindustry/world/meta/values/ItemListValue.java b/core/src/mindustry/world/meta/values/ItemListValue.java deleted file mode 100644 index 164b6a2a3c..0000000000 --- a/core/src/mindustry/world/meta/values/ItemListValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -public class ItemListValue implements StatValue{ - private final ItemStack[] stacks; - private final boolean displayName; - - public ItemListValue(ItemStack... stacks){ - this(true, stacks); - } - - public ItemListValue(boolean displayName, ItemStack... stacks){ - this.stacks = stacks; - this.displayName = displayName; - } - - @Override - public void display(Table table){ - for(ItemStack stack : stacks){ - table.add(new ItemDisplay(stack.item, stack.amount, displayName)).padRight(5); - } - } -} diff --git a/core/src/mindustry/world/meta/values/LiquidFilterValue.java b/core/src/mindustry/world/meta/values/LiquidFilterValue.java deleted file mode 100644 index d5aef23376..0000000000 --- a/core/src/mindustry/world/meta/values/LiquidFilterValue.java +++ /dev/null @@ -1,39 +0,0 @@ -package mindustry.world.meta.values; - -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class LiquidFilterValue implements StatValue{ - private final Boolf filter; - private final float amount; - private final boolean perSecond; - - public LiquidFilterValue(Boolf filter, float amount, boolean perSecond){ - this.filter = filter; - this.amount = amount; - this.perSecond = perSecond; - } - - @Override - public void display(Table table){ - Seq list = new Seq<>(); - - for(Liquid item : content.liquids()){ - if(!item.isHidden() && filter.get(item)) list.add(item); - } - - for(int i = 0; i < list.size; i++){ - table.add(new LiquidDisplay(list.get(i), amount, perSecond)).padRight(5); - - if(i != list.size - 1){ - table.add("/"); - } - } - } -} diff --git a/core/src/mindustry/world/meta/values/LiquidValue.java b/core/src/mindustry/world/meta/values/LiquidValue.java deleted file mode 100644 index 6139aed5ce..0000000000 --- a/core/src/mindustry/world/meta/values/LiquidValue.java +++ /dev/null @@ -1,23 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -public class LiquidValue implements StatValue{ - private final Liquid liquid; - private final float amount; - private final boolean perSecond; - - public LiquidValue(Liquid liquid, float amount, boolean perSecond){ - this.liquid = liquid; - this.amount = amount; - this.perSecond = perSecond; - } - - @Override - public void display(Table table){ - table.add(new LiquidDisplay(liquid, amount, perSecond)); - } -} diff --git a/core/src/mindustry/world/meta/values/NumberValue.java b/core/src/mindustry/world/meta/values/NumberValue.java deleted file mode 100644 index fa815f4d49..0000000000 --- a/core/src/mindustry/world/meta/values/NumberValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.world.meta.*; - -/** - * A stat that is a number with a unit attacked. - * The number is rounded to 2 decimal places by default. - */ -public class NumberValue implements StatValue{ - private final StatUnit unit; - private final float value; - - public NumberValue(float value, StatUnit unit){ - this.unit = unit; - this.value = value; - } - - @Override - public void display(Table table){ - int precision = Math.abs((int)value - value) <= 0.001f ? 0 : Math.abs((int)(value * 10) - value * 10) <= 0.001f ? 1 : 2; - - table.add(Strings.fixed(value, precision)); - table.add((unit.space ? " " : "") + unit.localized()); - } -} diff --git a/core/src/mindustry/world/meta/values/StringValue.java b/core/src/mindustry/world/meta/values/StringValue.java deleted file mode 100644 index b36a9d47e4..0000000000 --- a/core/src/mindustry/world/meta/values/StringValue.java +++ /dev/null @@ -1,18 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.world.meta.*; - -public class StringValue implements StatValue{ - private final String value; - - public StringValue(String value, Object... args){ - this.value = Strings.format(value, args); - } - - @Override - public void display(Table table){ - table.add(value); - } -} diff --git a/core/src/mindustry/world/meta/values/WeaponListValue.java b/core/src/mindustry/world/meta/values/WeaponListValue.java deleted file mode 100644 index 6833837f5c..0000000000 --- a/core/src/mindustry/world/meta/values/WeaponListValue.java +++ /dev/null @@ -1,55 +0,0 @@ -package mindustry.world.meta.values; - -import arc.graphics.g2d.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import arc.util.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -public class WeaponListValue implements StatValue{ - private final Seq weapons; - private final UnitType unit; - - public WeaponListValue(UnitType unit, Seq weapons){ - this.weapons = weapons; - this.unit = unit; - } - - @Override - public void display(Table table){ - table.row(); - for(int i = 0; i < weapons.size;i ++){ - Weapon weapon = weapons.get(i); - - if(weapon.flipSprite){ - //flipped weapons are not given stats - continue; - } - - TextureRegion region = !weapon.name.equals("") && weapon.outlineRegion.found() ? weapon.outlineRegion : unit.icon(Cicon.full); - - table.image(region).size(60).scaling(Scaling.bounded).right().top(); - - table.table(Tex.underline, w -> { - w.left().defaults().padRight(3).left(); - - if(weapon.inaccuracy > 0){ - sep(w, "[lightgray]" + Stat.inaccuracy.localized() + ": [white]" + (int)weapon.inaccuracy + " " + StatUnit.degrees.localized()); - } - sep(w, "[lightgray]" + Stat.reload.localized() + ": " + (weapon.mirror ? "2x " : "") + "[white]" + Strings.autoFixed(60f / weapon.reload * weapon.shots, 2)); - - var bullet = new AmmoListValue(OrderedMap.of(unit, weapon.bullet)); - bullet.display(w); - }).padTop(-9).left(); - table.row(); - } - } - - void sep(Table table, String text){ - table.row(); - table.add(text); - } -} diff --git a/core/src/mindustry/world/modules/ConsumeModule.java b/core/src/mindustry/world/modules/ConsumeModule.java index 431fa3fd1f..4714903c5d 100644 --- a/core/src/mindustry/world/modules/ConsumeModule.java +++ b/core/src/mindustry/world/modules/ConsumeModule.java @@ -66,6 +66,10 @@ public class ConsumeModule extends BlockModule{ return valid && entity.shouldConsume() && entity.enabled; } + public boolean canConsume(){ + return valid && entity.enabled; + } + public boolean optionalValid(){ return valid() && optionalValid && entity.enabled; } diff --git a/desktop/build.gradle b/desktop/build.gradle index 81459ed156..948b5413f9 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -18,7 +18,7 @@ task run(dependsOn: classes, type: JavaExec){ ignoreExitValue = true if(System.getProperty("os.name").toLowerCase().contains("mac")){ - jvmArgs("-XstartOnFirstThread", "-Djava.awt.headless=true") + jvmArgs("-XstartOnFirstThread") } jvmArgs += "-XX:+ShowCodeDetailsInExceptionMessages" @@ -42,6 +42,7 @@ task dist(type: Jar, dependsOn: configurations.runtimeClasspath){ from files(sourceSets.main.output.resourcesDir) from {configurations.runtimeClasspath.collect{ it.isDirectory() ? it : zipTree(it) }} from files(project.assetsDir) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveFileName = "${appName}.jar" @@ -56,6 +57,7 @@ if(!project.ext.hasSprites()){ dist.dependsOn ":tools:pack" } +//this is only for local testing task steamtest(dependsOn: dist){ doLast{ copy{ @@ -70,6 +72,7 @@ task steamtest(dependsOn: dist){ } } +//TODO replace with jpackage templates PackrConfig.Platform.values().each{ platform -> task "packr${platform.toString()}"{ dependsOn dist diff --git a/desktop/src/mindustry/desktop/DesktopLauncher.java b/desktop/src/mindustry/desktop/DesktopLauncher.java index 29fe405905..a1438cbbcd 100644 --- a/desktop/src/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/mindustry/desktop/DesktopLauncher.java @@ -4,13 +4,14 @@ import arc.*; import arc.Files.*; import arc.backend.sdl.*; import arc.backend.sdl.jni.*; +import arc.discord.*; +import arc.discord.DiscordRPC.*; import arc.files.*; import arc.func.*; import arc.math.*; import arc.struct.*; import arc.util.*; import arc.util.serialization.*; -import club.minnced.discord.rpc.*; import com.codedisaster.steamworks.*; import mindustry.*; import mindustry.core.*; @@ -19,6 +20,7 @@ import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.net.*; import mindustry.net.Net.*; +import mindustry.service.*; import mindustry.type.*; import java.io.*; @@ -26,8 +28,8 @@ import java.io.*; import static mindustry.Vars.*; public class DesktopLauncher extends ClientLauncher{ - public final static String discordID = "610508934456934412"; - boolean useDiscord = OS.is64Bit && !OS.isARM && !OS.hasProp("nodiscord"), loadError = false; + public final static long discordID = 610508934456934412L; + boolean useDiscord = !OS.hasProp("nodiscord"), loadError = false; Throwable steamError; public static void main(String[] arg){ @@ -38,6 +40,10 @@ public class DesktopLauncher extends ClientLauncher{ maximized = true; width = 900; height = 700; + //enable gl3 with command-line argument + if(Structs.contains(arg, "-gl3")){ + gl30 = true; + } setWindowIcon(FileType.internal, "icons/icon_64.png"); }}); }catch(Throwable e){ @@ -52,13 +58,15 @@ public class DesktopLauncher extends ClientLauncher{ if(useDiscord){ try{ - DiscordRPC.INSTANCE.Discord_Initialize(discordID, null, true, "1127400"); + DiscordRPC.connect(discordID); Log.info("Initialized Discord rich presence."); - Runtime.getRuntime().addShutdownHook(new Thread(DiscordRPC.INSTANCE::Discord_Shutdown)); + Runtime.getRuntime().addShutdownHook(new Thread(DiscordRPC::close)); + }catch(NoDiscordClientException none){ + //don't log if no client is found + useDiscord = false; }catch(Throwable t){ useDiscord = false; - Log.err("Failed to initialize discord. Enable debug logging for details."); - Log.debug("Discord init error: \n@\n", Strings.getStackTrace(t)); + Log.warn("Failed to initialize Discord RPC - you are likely using a JVM <16."); } } @@ -122,6 +130,40 @@ public class DesktopLauncher extends ClientLauncher{ SVars.user = new SUser(); boolean[] isShutdown = {false}; + service = new GameService(){ + + @Override + public boolean enabled(){ + return true; + } + + @Override + public void completeAchievement(String name){ + SVars.stats.stats.setAchievement(name); + SVars.stats.stats.storeStats(); + } + + @Override + public boolean isAchieved(String name){ + return SVars.stats.stats.isAchieved(name, false); + } + + @Override + public int getStat(String name, int def){ + return SVars.stats.stats.getStatI(name, def); + } + + @Override + public void setStat(String name, int amount){ + SVars.stats.stats.setStatI(name, amount); + } + + @Override + public void storeStats(){ + SVars.stats.onUpdate(); + } + }; + Events.on(ClientLoadEvent.class, event -> { Core.settings.defaults("name", SVars.net.friends.getPersonaName()); if(player.name.isEmpty()){ @@ -246,7 +288,6 @@ public class DesktopLauncher extends ClientLauncher{ String uiState = ""; if(inGame){ - //TODO implement nice name for sector gameMapWithWave = Strings.capitalize(Strings.stripColors(state.map.name())); if(state.rules.waves){ @@ -267,7 +308,7 @@ public class DesktopLauncher extends ClientLauncher{ } if(useDiscord){ - DiscordRichPresence presence = new DiscordRichPresence(); + RichPresence presence = new RichPresence(); if(inGame){ presence.state = gameMode + gamePlayersSuffix; @@ -281,7 +322,7 @@ public class DesktopLauncher extends ClientLauncher{ presence.largeImageKey = "logo"; - DiscordRPC.INSTANCE.Discord_UpdatePresence(presence); + DiscordRPC.send(presence); } if(steam){ diff --git a/desktop/src/mindustry/desktop/steam/SNet.java b/desktop/src/mindustry/desktop/steam/SNet.java index d016cac1a4..750c40cd42 100644 --- a/desktop/src/mindustry/desktop/steam/SNet.java +++ b/desktop/src/mindustry/desktop/steam/SNet.java @@ -4,9 +4,7 @@ import arc.*; import arc.func.*; import arc.struct.*; import arc.util.*; -import arc.util.pooling.*; import com.codedisaster.steamworks.*; -import com.codedisaster.steamworks.SteamFriends.*; import com.codedisaster.steamworks.SteamMatchmaking.*; import com.codedisaster.steamworks.SteamNetworking.*; import mindustry.core.*; @@ -58,6 +56,11 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, int fromID = from.getAccountID(); Object output = serializer.read(readBuffer); + //it may be theoretically possible for this to be a framework message, if the packet is malicious or corrupted + if(!(output instanceof Packet)) return; + + Packet pack = (Packet)output; + if(net.server()){ SteamConnection con = steamConnections.get(fromID); try{ @@ -74,13 +77,13 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, net.handleServerReceived(con, c); } - net.handleServerReceived(con, output); + net.handleServerReceived(con, pack); }catch(Throwable e){ Log.err(e); } }else if(currentServer != null && fromID == currentServer.getAccountID()){ try{ - net.handleClientReceived(output); + net.handleClientReceived(pack); }catch(Throwable t){ net.handleException(t); } @@ -120,7 +123,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } @Override - public void sendClient(Object object, SendMode mode){ + public void sendClient(Object object, boolean reliable){ if(isSteamClient()){ if(currentServer == null){ Log.info("Not connected, quitting."); @@ -134,17 +137,15 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, int length = writeBuffer.position(); writeBuffer.flip(); - snet.sendP2PPacket(currentServer, writeBuffer, mode == SendMode.tcp || length >= 1200 ? P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); + snet.sendP2PPacket(currentServer, writeBuffer, reliable || length >= 1200 ? P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); }catch(Exception e){ net.showError(e); } - Pools.free(object); }else{ - provider.sendClient(object, mode); + provider.sendClient(object, reliable); } } - @Override public void disconnectClient(){ if(isSteamClient()){ @@ -163,6 +164,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, @Override public void discoverServers(Cons callback, Runnable done){ smat.addRequestLobbyListResultCountFilter(32); + smat.addRequestLobbyListDistanceFilter(LobbyDistanceFilter.Worldwide); smat.requestLobbyList(); lobbyCallback = callback; @@ -226,11 +228,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onFavoritesListChanged(int i, int i1, int i2, int i3, int i4, boolean b, int i5){ - - } - @Override public void onLobbyInvite(SteamID steamIDUser, SteamID steamIDLobby, long gameID){ Log.info("onLobbyInvite @ @ @", steamIDLobby.getAccountID(), steamIDUser.getAccountID(), gameID); @@ -279,11 +276,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, Core.app.post(() -> Core.app.post(() -> Core.app.post(() -> Log.info("Server: @\nClient: @\nActive: @", net.server(), net.client(), net.active())))); } - @Override - public void onLobbyDataUpdate(SteamID steamID, SteamID steamID1, boolean b){ - - } - @Override public void onLobbyChatUpdate(SteamID lobby, SteamID who, SteamID changer, ChatMemberStateChange change){ Log.info("lobby @: @ caused @'s change: @", lobby.getAccountID(), who.getAccountID(), changer.getAccountID(), change); @@ -301,16 +293,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onLobbyChatMessage(SteamID steamID, SteamID steamID1, ChatEntryType chatEntryType, int i){ - - } - - @Override - public void onLobbyGameCreated(SteamID steamID, SteamID steamID1, int i, short i1){ - - } - @Override public void onLobbyMatchList(int matches){ Log.info("found @ matches @", matches, lobbyDoneCallback); @@ -331,7 +313,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, smat.getNumLobbyMembers(lobby), Strings.parseInt(smat.getLobbyData(lobby, "version"), -1), smat.getLobbyData(lobby, "versionType"), - mode == null || mode.isEmpty() ? Gamemode.survival : Gamemode.valueOf(mode), + Gamemode.valueOf(mode), smat.getLobbyMemberLimit(lobby), "", null @@ -349,11 +331,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onLobbyKicked(SteamID steamID, SteamID steamID1, boolean b){ - Log.info("Kicked: @ @ @", steamID, steamID1, b); - } - @Override public void onLobbyCreated(SteamResult result, SteamID steamID){ if(!net.server()){ @@ -381,11 +358,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onFavoritesListAccountsUpdated(SteamResult steamResult){ - - } - @Override public void onP2PSessionConnectFail(SteamID steamIDRemote, P2PSessionError sessionError){ if(net.server()){ @@ -406,50 +378,14 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onSetPersonaNameResponse(boolean b, boolean b1, SteamResult steamResult){ - - } - - @Override - public void onPersonaStateChange(SteamID steamID, PersonaChange personaChange){ - - } - - @Override - public void onGameOverlayActivated(boolean b){ - - } - @Override public void onGameLobbyJoinRequested(SteamID lobby, SteamID steamIDFriend){ Log.info("onGameLobbyJoinRequested @ @", lobby, steamIDFriend); smat.joinLobby(lobby); } - @Override - public void onAvatarImageLoaded(SteamID steamID, int i, int i1, int i2){ - - } - - @Override - public void onFriendRichPresenceUpdate(SteamID steamID, int i){ - - } - - @Override - public void onGameRichPresenceJoinRequested(SteamID steamID, String connect){ - Log.info("onGameRichPresenceJoinRequested @ @", steamID, connect); - } - - @Override - public void onGameServerChangeRequested(String server, String password){ - - } - public class SteamConnection extends NetConnection{ final SteamID sid; - final P2PSessionState state = new P2PSessionState(); public SteamConnection(SteamID sid){ super(sid.getAccountID() + ""); @@ -458,7 +394,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } @Override - public void send(Object object, SendMode mode){ + public void send(Object object, boolean reliable){ try{ writeBuffer.limit(writeBuffer.capacity()); writeBuffer.position(0); @@ -466,7 +402,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, int length = writeBuffer.position(); writeBuffer.flip(); - snet.sendP2PPacket(sid, writeBuffer, mode == SendMode.tcp || length >= 1200 ? object instanceof StreamChunk ? P2PSend.ReliableWithBuffering : P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); + snet.sendP2PPacket(sid, writeBuffer, reliable || length >= 1200 ? object instanceof StreamChunk ? P2PSend.ReliableWithBuffering : P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); }catch(Exception e){ Log.err(e); Log.info("Error sending packet. Disconnecting invalid client!"); @@ -479,7 +415,8 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, @Override public boolean isConnected(){ - snet.getP2PSessionState(sid, state); + //TODO ??? + //snet.getP2PSessionState(sid, state); return true;//state.isConnectionActive(); } diff --git a/desktop/src/mindustry/desktop/steam/SStats.java b/desktop/src/mindustry/desktop/steam/SStats.java index 204c8a98b2..1a5ed54b77 100644 --- a/desktop/src/mindustry/desktop/steam/SStats.java +++ b/desktop/src/mindustry/desktop/steam/SStats.java @@ -1,63 +1,27 @@ package mindustry.desktop.steam; import arc.*; -import arc.struct.*; import arc.util.*; import com.codedisaster.steamworks.*; -import mindustry.*; -import mindustry.content.*; -import mindustry.entities.units.*; import mindustry.game.EventType.*; -import mindustry.game.SectorInfo.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.world.*; -import mindustry.world.blocks.distribution.*; import static mindustry.Vars.*; -import static mindustry.desktop.steam.SAchievement.*; -@SuppressWarnings("unchecked") public class SStats implements SteamUserStatsCallback{ public final SteamUserStats stats = new SteamUserStats(this); private boolean updated = false; private int statSavePeriod = 4; //in minutes - private ObjectSet blocksBuilt = new ObjectSet<>(), unitsBuilt = new ObjectSet<>(); - private ObjectSet t5s = new ObjectSet<>(); - private IntSet checked = new IntSet(); - public SStats(){ stats.requestCurrentStats(); Events.on(ClientLoadEvent.class, e -> { - unitsBuilt = Core.settings.getJson("units-built" , ObjectSet.class, String.class, ObjectSet::new); - blocksBuilt = Core.settings.getJson("blocks-built" , ObjectSet.class, String.class, ObjectSet::new); - t5s = ObjectSet.with(UnitTypes.omura, UnitTypes.reign, UnitTypes.toxopid, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.corvus); - - Core.app.addListener(new ApplicationListener(){ - Interval i = new Interval(); - - @Override - public void update(){ - if(i.get(60f)){ - checkUpdate(); - } - } - }); - Timer.schedule(() -> { if(updated){ stats.storeStats(); } }, statSavePeriod * 60, statSavePeriod * 60); - - if(Items.thorium.unlocked()) obtainThorium.complete(); - if(Items.titanium.unlocked()) obtainTitanium.complete(); - if(!content.sectors().contains(s -> s.locked())){ - unlockAllZones.complete(); - } }); } @@ -65,287 +29,9 @@ public class SStats implements SteamUserStatsCallback{ this.updated = true; } - void checkUpdate(){ - if(campaign()){ - SStat.maxUnitActive.max(Groups.unit.count(t -> t.team == player.team())); - - if(Groups.unit.count(u -> u.type == UnitTypes.poly && u.team == player.team()) >= 10){ - active10Polys.complete(); - } - - for(Building entity : player.team().cores()){ - if(!content.items().contains(i -> entity.items.get(i) < entity.block.itemCapacity)){ - fillCoreAllCampaign.complete(); - break; - } - } - } - } - - private void registerEvents(){ - - Events.on(UnitDestroyEvent.class, e -> { - if(campaign()){ - if(e.unit.team != Vars.player.team()){ - SStat.unitsDestroyed.add(); - - if(e.unit.isBoss()){ - SStat.bossesDefeated.add(); - } - } - } - }); - - Events.on(TurnEvent.class, e -> { - float total = 0; - for(Planet planet : content.planets()){ - for(Sector sec : planet.sectors){ - if(sec.hasBase()){ - for(ExportStat v : sec.info.production.values()){ - if(v.mean > 0) total += v.mean * 60; - } - } - } - } - - SStat.maxProduction.max(Math.round(total)); - }); - - Events.run(Trigger.newGame, () -> Core.app.post(() -> { - if(campaign() && player.core() != null && player.core().items.total() >= 10 * 1000){ - drop10kitems.complete(); - } - })); - - Events.on(CommandIssueEvent.class, e -> { - if(campaign() && e.command == UnitCommand.attack){ - issueAttackCommand.complete(); - } - }); - - Events.on(BlockBuildEndEvent.class, e -> { - if(campaign() && e.unit != null && e.unit.isLocal() && !e.breaking){ - SStat.blocksBuilt.add(); - - if(e.tile.block() == Blocks.router && e.tile.build.proximity().contains(t -> t.block == Blocks.router)){ - chainRouters.complete(); - } - - if(e.tile.block() == Blocks.groundFactory){ - buildGroundFactory.complete(); - } - - if(blocksBuilt.add(e.tile.block().name)){ - if(blocksBuilt.contains("meltdown") && blocksBuilt.contains("spectre") && blocksBuilt.contains("foreshadow")){ - buildMeltdownSpectre.complete(); - } - - save(); - } - - if(e.tile.block() instanceof Conveyor){ - checked.clear(); - check: { - Tile current = e.tile; - for(int i = 0; i < 4; i++){ - checked.add(current.pos()); - if(current.build == null) break check; - Tile next = current.nearby(current.build.rotation); - if(next != null && next.block() instanceof Conveyor){ - current = next; - }else{ - break check; - } - } - - if(current == e.tile && checked.size == 4){ - circleConveyor.complete(); - } - } - } - } - }); - - Events.on(UnitCreateEvent.class, e -> { - if(campaign()){ - if(unitsBuilt.add(e.unit.type.name)){ - SStat.unitTypesBuilt.set(content.units().count(u -> unitsBuilt.contains(u.name) && !u.isHidden())); - } - - if(t5s.contains(e.unit.type)){ - buildT5.complete(); - } - } - }); - - Events.on(UnitControlEvent.class, e -> { - if(e.unit instanceof BlockUnitc && ((BlockUnitc)e.unit).tile().block == Blocks.router){ - becomeRouter.complete(); - } - }); - - Events.on(SchematicCreateEvent.class, e -> { - SStat.schematicsCreated.add(); - }); - - Events.on(BlockDestroyEvent.class, e -> { - if(campaign() && e.tile.team() != player.team()){ - SStat.blocksDestroyed.add(); - } - }); - - Events.on(MapMakeEvent.class, e -> SStat.mapsMade.add()); - - Events.on(MapPublishEvent.class, e -> SStat.mapsPublished.add()); - - Events.on(UnlockEvent.class, e -> { - if(e.content == Items.thorium) obtainThorium.complete(); - if(e.content == Items.titanium) obtainTitanium.complete(); - if(e.content instanceof SectorPreset && !content.sectors().contains(s -> s.locked())){ - unlockAllZones.complete(); - } - }); - - Events.run(Trigger.openWiki, openWiki::complete); - - Events.run(Trigger.exclusionDeath, dieExclusion::complete); - - Events.on(UnitDrownEvent.class, e -> { - if(campaign() && e.unit.isPlayer()){ - drown.complete(); - } - }); - - trigger(Trigger.acceleratorUse, useAccelerator); - - trigger(Trigger.impactPower, powerupImpactReactor); - - trigger(Trigger.flameAmmo, useFlameAmmo); - - trigger(Trigger.turretCool, coolTurret); - - trigger(Trigger.suicideBomb, suicideBomb); - - Events.run(Trigger.enablePixelation, enablePixelation::complete); - - Events.run(Trigger.thoriumReactorOverheat, () -> { - if(campaign()){ - SStat.reactorsOverheated.add(); - } - }); - - trigger(Trigger.shock, shockWetEnemy); - - trigger(Trigger.phaseDeflectHit, killEnemyPhaseWall); - - Events.on(LaunchItemEvent.class, e -> { - if(campaign()){ - launchItemPad.complete(); - } - }); - - Events.on(PickupEvent.class, e -> { - if(e.carrier.isPlayer() && campaign() && e.unit != null && t5s.contains(e.unit.type)){ - pickupT5.complete(); - } - }); - - Events.on(UnitCreateEvent.class, e -> { - if(campaign() && e.unit.team() == player.team()){ - SStat.unitsBuilt.add(); - } - }); - - Events.on(SectorLaunchEvent.class, e -> { - SStat.timesLaunched.add(); - }); - - Events.on(LaunchItemEvent.class, e -> { - SStat.itemsLaunched.add(e.stack.amount); - }); - - Events.on(WaveEvent.class, e -> { - if(campaign()){ - SStat.maxWavesSurvived.max(Vars.state.wave); - - if(state.stats.buildingsBuilt == 0 && state.wave >= 10){ - survive10WavesNoBlocks.complete(); - } - } - }); - - Events.on(PlayerJoin.class, e -> { - if(Vars.net.server()){ - SStat.maxPlayersServer.max(Groups.player.size()); - } - }); - - Runnable checkUnlocks = () -> { - if(Blocks.router.unlocked()) researchRouter.complete(); - - if(!TechTree.all.contains(t -> t.content.locked())){ - researchAll.complete(); - } - }; - - //check unlocked stuff on load as well - Events.on(ResearchEvent.class, e -> checkUnlocks.run()); - Events.on(UnlockEvent.class, e -> checkUnlocks.run()); - Events.on(ClientLoadEvent.class, e -> checkUnlocks.run()); - - Events.on(WinEvent.class, e -> { - if(state.rules.pvp){ - SStat.pvpsWon.add(); - } - }); - - Events.on(SectorCaptureEvent.class, e -> { - if(e.sector.isBeingPlayed() || net.client()){ - if(Vars.state.wave <= 5 && state.rules.attackMode){ - defeatAttack5Waves.complete(); - } - - if(state.stats.buildingsDestroyed == 0){ - captureNoBlocksBroken.complete(); - } - } - - if(Vars.state.rules.attackMode){ - SStat.attacksWon.add(); - } - - if(!e.sector.isBeingPlayed() && !net.client()){ - captureBackground.complete(); - } - - if(!e.sector.planet.sectors.contains(s -> !s.hasBase())){ - captureAllSectors.complete(); - } - - SStat.sectorsControlled.set(e.sector.planet.sectors.count(Sector::hasBase)); - }); - } - - private void save(){ - Core.settings.putJson("units-built" , String.class, unitsBuilt); - Core.settings.putJson("blocks-built" , String.class, blocksBuilt); - } - - private void trigger(Trigger trigger, SAchievement ach){ - Events.run(trigger, () -> { - if(campaign()){ - ach.complete(); - } - }); - } - - private boolean campaign(){ - return Vars.state.isCampaign(); - } - @Override public void onUserStatsReceived(long gameID, SteamID steamID, SteamResult result){ - registerEvents(); + service.init(); if(result != SteamResult.OK){ Log.err("Failed to receive steam stats: @", result); @@ -362,34 +48,4 @@ public class SStats implements SteamUserStatsCallback{ updated = true; } } - - @Override - public void onUserStatsUnloaded(SteamID steamID){ - - } - - @Override - public void onUserAchievementStored(long l, boolean b, String s, int i, int i1){ - - } - - @Override - public void onLeaderboardFindResult(SteamLeaderboardHandle steamLeaderboardHandle, boolean b){ - - } - - @Override - public void onLeaderboardScoresDownloaded(SteamLeaderboardHandle steamLeaderboardHandle, SteamLeaderboardEntriesHandle steamLeaderboardEntriesHandle, int i){ - - } - - @Override - public void onLeaderboardScoreUploaded(boolean b, SteamLeaderboardHandle steamLeaderboardHandle, int i, boolean b1, int i1, int i2){ - - } - - @Override - public void onGlobalStatsReceived(long l, SteamResult steamResult){ - - } } diff --git a/desktop/src/mindustry/desktop/steam/SUser.java b/desktop/src/mindustry/desktop/steam/SUser.java index 730b12763e..1b368e7982 100644 --- a/desktop/src/mindustry/desktop/steam/SUser.java +++ b/desktop/src/mindustry/desktop/steam/SUser.java @@ -1,23 +1,7 @@ package mindustry.desktop.steam; import com.codedisaster.steamworks.*; -import com.codedisaster.steamworks.SteamAuth.*; public class SUser implements SteamUserCallback{ public final SteamUser user = new SteamUser(this); - - @Override - public void onValidateAuthTicket(SteamID steamID, AuthSessionResponse authSessionResponse, SteamID ownerSteamID){ - - } - - @Override - public void onMicroTxnAuthorization(int appID, long orderID, boolean authorized){ - - } - - @Override - public void onEncryptedAppTicket(SteamResult result){ - - } } diff --git a/desktop/src/mindustry/desktop/steam/SWorkshop.java b/desktop/src/mindustry/desktop/steam/SWorkshop.java index ac6e5f5060..23a22360d4 100644 --- a/desktop/src/mindustry/desktop/steam/SWorkshop.java +++ b/desktop/src/mindustry/desktop/steam/SWorkshop.java @@ -1,18 +1,19 @@ package mindustry.desktop.steam; import arc.*; -import com.codedisaster.steamworks.*; -import com.codedisaster.steamworks.SteamRemoteStorage.*; -import com.codedisaster.steamworks.SteamUGC.*; -import arc.struct.*; import arc.files.*; import arc.func.*; import arc.scene.ui.*; +import arc.struct.*; import arc.util.*; +import com.codedisaster.steamworks.*; +import com.codedisaster.steamworks.SteamRemoteStorage.*; +import com.codedisaster.steamworks.SteamUGC.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.maps.*; import mindustry.mod.Mods.*; +import mindustry.service.*; import mindustry.type.*; import mindustry.ui.dialogs.*; @@ -42,7 +43,7 @@ public class SWorkshop implements SteamUGCCallback{ workshopFiles.put(LoadedMod.class, folders.select(f -> f.child("mod.json").exists() || f.child("mod.hjson").exists())); if(!workshopFiles.get(Map.class).isEmpty()){ - SAchievement.downloadMapWorkshop.complete(); + Achievement.downloadMapWorkshop.complete(); } workshopFiles.each((type, list) -> { @@ -171,7 +172,7 @@ public class SWorkshop implements SteamUGCCallback{ ugc.submitItemUpdate(h, changelog == null ? "" : changelog); if(p instanceof Map){ - SAchievement.publishMap.complete(); + Achievement.publishMap.complete(); } }, () -> p.addSteamID(sid)); } @@ -228,11 +229,6 @@ public class SWorkshop implements SteamUGCCallback{ } } - @Override - public void onRequestUGCDetails(SteamUGCDetails details, SteamResult result){ - - } - @Override public void onUGCQueryCompleted(SteamUGCQuery query, int numResultsReturned, int totalMatchingResults, boolean isCachedData, SteamResult result){ Log.info("GET QUERY " + query); @@ -263,7 +259,7 @@ public class SWorkshop implements SteamUGCCallback{ ItemInstallInfo info = new ItemInstallInfo(); ugc.getItemInstallInfo(publishedFileID, info); Log.info("Item subscribed from @", info.getFolder()); - SAchievement.downloadMapWorkshop.complete(); + Achievement.downloadMapWorkshop.complete(); } @Override @@ -311,42 +307,12 @@ public class SWorkshop implements SteamUGCCallback{ @Override public void onDownloadItemResult(int appID, SteamPublishedFileID publishedFileID, SteamResult result){ - SAchievement.downloadMapWorkshop.complete(); + Achievement.downloadMapWorkshop.complete(); ItemInstallInfo info = new ItemInstallInfo(); ugc.getItemInstallInfo(publishedFileID, info); Log.info("Item downloaded to @", info.getFolder()); } - @Override - public void onUserFavoriteItemsListChanged(SteamPublishedFileID publishedFileID, boolean wasAddRequest, SteamResult result){ - - } - - @Override - public void onSetUserItemVote(SteamPublishedFileID publishedFileID, boolean voteUp, SteamResult result){ - - } - - @Override - public void onGetUserItemVote(SteamPublishedFileID publishedFileID, boolean votedUp, boolean votedDown, boolean voteSkipped, SteamResult result){ - - } - - @Override - public void onStartPlaytimeTracking(SteamResult result){ - - } - - @Override - public void onStopPlaytimeTracking(SteamResult result){ - - } - - @Override - public void onStopPlaytimeTrackingForAllItems(SteamResult result){ - - } - @Override public void onDeleteItem(SteamPublishedFileID publishedFileID, SteamResult result){ ItemInstallInfo info = new ItemInstallInfo(); diff --git a/fastlane/metadata/android/en-US/changelogs/29791.txt b/fastlane/metadata/android/en-US/changelogs/29791.txt new file mode 100644 index 0000000000..cd220923ab --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/29791.txt @@ -0,0 +1,3 @@ +[This is a truncated changelog, see Github for full notes] +- Fixed flying unit AI +- Made conduits/conveyors upgrades follow paths (Contributed by @Slava0135) diff --git a/gradle.properties b/gradle.properties index 2fa78cb0d0..0152a046ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.daemon=true -org.gradle.jvmargs=-Xms256m -Xmx1024m +org.gradle.jvmargs=-Xms256m -Xmx1024m --illegal-access=permit # Don't recompute annotations if sources haven't been changed kapt.incremental.apt = true # Multithreaded @@ -8,4 +8,4 @@ kapt.use.worker.api=true kapt.include.compile.classpath=false # I don't need to use the kotlin stdlib yet, so remove it to prevent extra bloat & method count issues kotlin.stdlib.default.dependency=false -archash=0e99b0291f81d74d335dca8b0cf3bf26931f1197 +archash=5364d0187882a5e2f09850fe2505608dd7cf93f3 diff --git a/ios/robovm.xml b/ios/robovm.xml index 98de82cd7b..c94ef0fe26 100644 --- a/ios/robovm.xml +++ b/ios/robovm.xml @@ -19,12 +19,7 @@ net.jpountz.lz4.** arc.scene.** - mindustry.gen.Call - mindustry.net.** - mindustry.world.blocks.** - mindustry.logic.** - mindustry.world.blocks.** - mindustry.entities.effect.** + mindustry.** com.android.okhttp.HttpHandler com.android.okhttp.HttpsHandler com.android.org.conscrypt.** diff --git a/ios/src/mindustry/ios/IOSLauncher.java b/ios/src/mindustry/ios/IOSLauncher.java index eda03585bc..4171862d2b 100644 --- a/ios/src/mindustry/ios/IOSLauncher.java +++ b/ios/src/mindustry/ios/IOSLauncher.java @@ -75,7 +75,6 @@ public class IOSLauncher extends IOSApplication.Delegate{ class ChooserDelegate extends NSObject implements UIDocumentBrowserViewControllerDelegate{ @Override public void didPickDocumentURLs(UIDocumentBrowserViewController controller, NSArray documentURLs){ - } @Override diff --git a/settings.gradle b/settings.gradle index 52b0980311..ef9b57e54a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,6 +39,9 @@ if(!hasProperty("release")){ ':Arc:extensions:packer', ':Arc:extensions:g3d', ':Arc:extensions:fx', + ':Arc:extensions:flabel', + ':Arc:extensions:discord', + ':Arc:extensions:profiling', ':Arc:natives', ':Arc:natives:natives-desktop', ':Arc:natives:natives-android', diff --git a/tools/build.gradle b/tools/build.gradle index 173cae9721..3a1141fb1f 100644 --- a/tools/build.gradle +++ b/tools/build.gradle @@ -1,14 +1,17 @@ sourceSets.main.java.srcDirs = ["src/"] -import arc.struct.* -import arc.graphics.* -import arc.packer.* -import arc.util.* -import javax.imageio.* -import java.awt.Graphics2D -import java.awt.image.* -import java.util.concurrent.* + +import arc.files.Fi +import arc.graphics.Color +import arc.graphics.Pixmap +import arc.packer.TexturePacker +import arc.struct.IntIntMap +import arc.struct.IntMap + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit def genFolder = "../core/assets-raw/sprites_out/generated/" def doAntialias = !project.hasProperty("disableAntialias") @@ -25,16 +28,17 @@ def transformColors = { List> list -> } newColors.each{ color -> - colorMap.put(color.argb8888(), newColors) - colorIndexMap.put(color.argb8888(), newColors.indexOf(color)) + colorMap.put(color.rgba(), newColors) + colorIndexMap.put(color.rgba(), newColors.indexOf(color)) } } } - +//TODO implementing this in gradle is a bad idea //d4816b transformColors([["a387ea", "8a73c6", "5c5e9f"], ["6e7080", "989aa4", "b0bac0"], ["bc5452", "ea8878", "feb380"], - ["de9458", "f8c266", "ffe18f"], ["feb380", "ea8878", "bc5452"], ["d4816b", "eab678", "ffd37f"], ["ffffff", "dcc6c6", "9d7f7f"]]) + ["de9458", "f8c266", "ffe18f"], ["feb380", "ea8878", "bc5452"], ["d4816b", "eab678", "ffd37f"], + ["ffffff", "dcc6c6", "9d7f7f"], ["df7646", "b23a4d", "752249"], ["3c3837", "515151", "646567"]]) def antialias = { File file -> if(!doAntialias) return @@ -44,11 +48,10 @@ def antialias = { File file -> return } - def image = ImageIO.read(file) - def out = ImageIO.read(file) + def image = new Pixmap(new Fi(file)) + def out = image.copy() def getRGB = { int ix, int iy -> - //if(ix <= 0 || iy <= 0 || ix >= image.width || iy >= image.height) return 0 - return image.getRGB(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) + return image.getRaw(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) } def color = new Color() @@ -56,8 +59,8 @@ def antialias = { File file -> def suma = new Color() int[] p = new int[9] - for(int x = 0; x < image.getWidth(); x++){ - for(int y = 0; y < image.getHeight(); y++){ + for(int x = 0; x < image.width; x++){ + for(int y = 0; y < image.height; y++){ int A = getRGB(x - 1, y + 1), B = getRGB(x, y + 1), C = getRGB(x + 1, y + 1), @@ -68,7 +71,7 @@ def antialias = { File file -> H = getRGB(x, y - 1), I = getRGB(x + 1, y - 1) - Arrays.fill(p, E); + Arrays.fill(p, E) if(D == B && D != H && B != F) p[0] = D if((D == B && D != H && B != F && E != C) || (B == F && B != D && F != H && E != A)) p[1] = B @@ -82,21 +85,21 @@ def antialias = { File file -> suma.set(0) for(int val : p){ - color.argb8888(val) + color.rgba8888(val) suma.r += color.r * color.a suma.g += color.g * color.a suma.b += color.b * color.a suma.a += color.a } - float fm = suma.a <= 0.001f ? 0f : (float) (1f / suma.a) + float fm = suma.a <= 0.001f ? 0f : (float)(1f / suma.a) suma.mul(fm, fm, fm, fm) float total = 0 sum.set(0) for(int val : p){ - color.argb8888(val) + color.rgba8888(val) float a = color.a color.lerp(suma, (float) (1f - a)) sum.r += color.r @@ -108,92 +111,19 @@ def antialias = { File file -> fm = (float)(1f / total) sum.mul(fm, fm, fm, fm) - out.setRGB(x, y, sum.argb8888()) + out.setRaw(x, y, sum.rgba8888()) sum.set(0) } } - ImageIO.write(out, "png", file) -} + image.dispose() + out.dispose() -def medianBlur = { File file -> - def image = ImageIO.read(file) - def result = new BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_ARGB) - def radius = 4 - IntSeq array = new IntSeq() - - def getRGB = { int ix, int iy -> - return image.getRGB(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) - } - - for(int x = 0; x < image.width; x++){ - for(int y = 0; y < image.height; y++){ - array.clear() - - for(int dx = -radius; dx <= radius; dx ++){ - for(int dy = -radius; dy <= radius; dy ++){ - if(dx*dx + dy*dy <= radius*radius){ - array.add(getRGB(x + dx, y + dy)) - } - } - } - - array.sort() - - result.setRGB(x, y, array.get((int)(array.size / 2))) - } - } - - ImageIO.write(result, "png", file) -} - -def scaleImage = { File file -> - def image = ImageIO.read(file) - for(int iteration in 0..1){ - def scaled = new BufferedImage(image.width * 2, image.height * 2, BufferedImage.TYPE_INT_ARGB) - - def getRGB = { int ix, int iy -> - return image.getRGB(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) - } - - for(int x = 0; x < image.width; x++){ - for(int y = 0; y < image.height; y++){ - int p = image.getRGB(x, y) - int p1 = p, p2 = p, p3 = p, p4 = p - - int A = getRGB(x - 1, y + 1), - B = getRGB(x, y + 1), - C = getRGB(x + 1, y + 1), - D = getRGB(x - 1, y), - E = getRGB(x, y), - F = getRGB(x + 1, y), - G = getRGB(x - 1, y - 1), - H = getRGB(x, y - 1), - I = getRGB(x + 1, y - 1), - J = getRGB(x, y + 2), - K = getRGB(x - 2, y), - L = getRGB(x + 2, y), - M = getRGB(x, y - 2) - - if(B == D && B != F && D != H && (E != A || E == C || E == G || A == J || A == K)) p1 = B - if(B == F && B != D && F != H && (E != C || E == A || E == I || C == J || C == L)) p2 = F - if(D == H && B != D && F != H && (E != G || E == A || E == I || G == K || G == M)) p3 = D - if(F == H && B != F && D != H && (E != I || E == C || E == G || I == L || I == M)) p4 = H - - scaled.setRGB(x * 2, y * 2 + 1, p1) - scaled.setRGB(x * 2 + 1, y * 2 + 1, p2) - scaled.setRGB(x * 2, y * 2, p3) - scaled.setRGB(x * 2 + 1, y * 2, p4) - } - } - image = scaled - } - - ImageIO.write(image, "png", file) + new Fi(file).writePng(out) } def tileImage = { File file -> - def image = ImageIO.read(file) + def image = new Pixmap(new Fi(file)) for(x in 0..image.width-1){ for(y in 0..image.height-1){ @@ -201,104 +131,36 @@ def tileImage = { File file -> def rx = image.height - 1 - y def ry = x - image.setRGB(x, y, image.getRGB(rx, image.height - 1 - ry)) + image.setRaw(x, y, image.getRaw(rx, image.height - 1 - ry)) } } } - def result = new BufferedImage(image.width * 2, image.height * 2, image.getType()) - Graphics2D graphics = result.createGraphics() - graphics.drawImage(image, image.width, 0, -image.width, image.height, null) + def result = new Pixmap(image.width * 2, image.height * 2) - graphics.drawImage(image, image.width, 0, image.width, image.height, null) + result.draw(image.flipX(), 0, 0) + result.draw(image, image.width, 0) + result.draw(image.flipX().flipY(), 0, image.height) + result.draw(image.flipY(), image.width, image.height) - graphics.drawImage(image, image.width, image.height*2, -image.width, -image.height, null) - - graphics.drawImage(image, image.width, image.height*2, image.width, -image.height, null) - - for(int x = 0; x < result.width; x++){ - for(int y = 0; y < result.height; y++){ - int p = result.getRGB(x, y) + for(x in 0..result.width-1){ + for(y in 0..result.height-1){ + int p = result.getRaw(x, y) if(x <= y){ List list = colorMap.get(p) int index = colorIndexMap.get(p, -1) if(index != -1){ int resultIndex = (x == y ? 1 : index == 2 ? 0 : index == 0 ? 2 : 1); - result.setRGB(x, y, list[resultIndex].argb8888()) + result.setRaw(x, y, list[resultIndex].rgba()) } } } } - ImageIO.write(result, "png", file) -} - -task swapColors(){ - doLast{ - if(project.hasProperty("colors")){ - def carr = new File(getProperty("colors")).text.split("\n") - def map = [:] - def swaps = 0 - carr.each{ str -> map[Color.valueOf(str.split("=")[0]).argb8888()] = Color.valueOf(str.split("=")[1]).argb8888() } - def tmpc = new Color() - - fileTree(dir: '../core/assets-raw/sprites', include: "**/*.png").visit{ file -> - if(file.isDirectory()) return - - boolean found = false - - def img = ImageIO.read(file.file) - for(x in (0..img.getWidth() - 1)){ - for(y in (0..img.getHeight() - 1)){ - def c = img.getRGB(x, y) - tmpc.argb8888(c) - if(tmpc.a < 0.1f) continue - if(map.containsKey(c)){ - img.setRGB(x, y, (int)map.get(c)) - found = true - } - } - } - if(found){ - swaps++ - ImageIO.write(img, "png", file.file) - } - } - println "Swapped $swaps images." - }else{ - throw new InvalidUserDataException("No replacement colors specified. Use -Pcolors=\"\"") - } - } -} - -task genPalette(){ - doLast{ - def total = 0 - def size = 32 - def outImage = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - def colorsUsed = new IntSet() - - fileTree(dir: '../core/assets-raw/sprites/blocks', include: "**/*.png").visit{ file -> - if(file.isDirectory()) return - - def img = ImageIO.read(file.file) - for(x in (0..img.getWidth() - 1)){ - for(y in (0..img.getHeight() - 1)){ - def c = img.getRGB(x, y) - - if(Tmp.c1.argb8888(c).a > 0.999f && colorsUsed.add(c)){ - outImage.setRGB((int)(total / size), total % size, c) - total ++ - } - } - } - } - - ImageIO.write(outImage, "png", new File("palette.png")) - println "Found $total colors." - - } + new Fi(file).writePng(result) + result.dispose() + image.dispose() } task antialiasImages(){ @@ -310,15 +172,6 @@ task antialiasImages(){ } } -task scaleImages(){ - doLast{ - for(def img : project.getProperty("images").split(",")){ - println(project.getProperty("startdir") + "/" + img) - scaleImage(new File(project.getProperty("startdir") + "/" + img)) - } - } -} - task tileImages(){ doLast{ for(def img : project.getProperty("images").split(",")){ @@ -383,7 +236,7 @@ task pack(dependsOn: [classes, configurations.runtimeClasspath]){ println("\n\nPacking normal 4096 sprites...\n\n") //pack normal sprites - TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/").absolutePath, "sprites.atlas") + TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/").absolutePath, "sprites.aatls") println("\n\nPacking fallback 2048 sprites...\n\n") @@ -393,7 +246,7 @@ task pack(dependsOn: [classes, configurations.runtimeClasspath]){ } //pack fallback 2048x2048 sprites - TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/fallback/").absolutePath, "sprites.atlas") + TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/fallback/").absolutePath, "sprites.aatls") } } @@ -402,7 +255,6 @@ task genSprites(dependsOn: classes, type: JavaExec){ main = "mindustry.tools.ImagePacker" classpath = sourceSets.main.runtimeClasspath - jvmArgs("-Djava.awt.headless=true") standardInput = System.in workingDir = genFolder } @@ -435,11 +287,4 @@ task updateScripts(dependsOn: classes, type: JavaExec){ classpath = sourceSets.main.runtimeClasspath standardInput = System.in workingDir = "../" -} - -task genBindings(dependsOn: classes, type: JavaExec){ - main = "mindustry.tools.BindingsGenerator" - classpath = sourceSets.main.runtimeClasspath - standardInput = System.in - workingDir = "../" } \ No newline at end of file diff --git a/tools/src/mindustry/tools/Edgifier.java b/tools/src/mindustry/tools/Edgifier.java deleted file mode 100644 index f83694d13a..0000000000 --- a/tools/src/mindustry/tools/Edgifier.java +++ /dev/null @@ -1,88 +0,0 @@ -package mindustry.tools; - -import arc.files.*; -import arc.graphics.*; -import arc.struct.*; -import arc.util.*; - -public class Edgifier{ - - public static void main(String[] args){ - ArcNativesLoader.load(); - - Pixmap pixmap = new Pixmap(Fi.get("/home/anuke/Projects/Mindustry/core/assets-raw/sprites/units/reaper.png")); - - Fi.get("/home/anuke/out.png").writePNG(edgify(pixmap, 5)); - } - - private static Pixmap edgify(Pixmap in, int chunk){ - Pixmap out = new Pixmap(in.getWidth(), in.getHeight()); - IntSeq side1 = new IntSeq(), side2 = new IntSeq(); - - for(int x = 0; x < in.getWidth(); x += chunk){ - for(int y = 0; y < in.getHeight(); y += chunk){ - int bestErrors = Integer.MAX_VALUE; - int bestRotation = 0; - int bestSide1 = 0, bestSide2 = 0; - - for(int rotation = 0; rotation < 8; rotation++){ - side1.clear(); - side2.clear(); - - //assign pixels present on each side - for(int cx = 0; cx < chunk; cx++){ - for(int cy = 0; cy < chunk; cy++){ - boolean side = classify(rotation, cx, cy, chunk); - - int pixel = in.getPixel(x + cx, y + cy); - if(Pixmaps.empty(pixel)) pixel = 0; //all alpha=0 pixels are treated as 0 - - (side ? side1 : side2).add(pixel); - } - } - - //find most popular element here - int mode1 = side1.mode(), mode2 = side2.mode(); - //total errors; 'incorrect' pixels - int errors = (side1.size - side1.count(mode1)) + (side2.size - side2.count(mode2)); - - //Log.info("errors for rotation={0}: {1}", rotation, errors); - - //update if better - if(errors < bestErrors){ - bestRotation = rotation; - bestSide1 = mode1; - bestSide2 = mode2; - bestErrors = errors; - } - } - - //Log.info("Best result for {0},{1}: rotation={2} 1={3} 2={4} errors={5}", x, y, bestRotation, bestSide1, bestSide2, bestErrors); - - //draw with the best result - for(int cx = 0; cx < chunk; cx++){ - for(int cy = 0; cy < chunk; cy++){ - boolean side = classify(bestRotation, cx, cy, chunk); - out.draw(x + cx, y + cy, side ? bestSide1 : bestSide2); - } - } - } - } - - return out; - } - - private static boolean classify(int rotation, int x, int y, int chunk){ - switch(rotation){ - case 0: //return y >= chunk / 2; - case 1: return y < x; - case 2: //return x <= chunk / 2; - case 3: return (chunk - 1 - y) < x; - case 4: //return (y > chunk / 2); - case 5: return (y <= x); - case 6: //return (x < chunk / 2); - case 7: return ((chunk - 1 - y) <= x); - default: throw new IllegalArgumentException("Invalid rotation: " + rotation); - } - } -} diff --git a/tools/src/mindustry/tools/Generators.java b/tools/src/mindustry/tools/Generators.java index 5535b11c49..5c6efbd2ec 100644 --- a/tools/src/mindustry/tools/Generators.java +++ b/tools/src/mindustry/tools/Generators.java @@ -14,48 +14,25 @@ import mindustry.ctype.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.tools.ImagePacker.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.*; import mindustry.world.blocks.environment.*; import mindustry.world.blocks.legacy.*; import mindustry.world.meta.*; +import java.util.concurrent.*; + import static mindustry.Vars.*; +import static mindustry.tools.ImagePacker.*; public class Generators{ - //used for changing colors in the UI - testing only - static final IntIntMap paletteMap = IntIntMap.with( - //empty for now - 0x454545ff, 0x00000000,//0x32394bff, - 0x00000099, 0x00000000//0x000000ff - ); - static final Cicon logicIcon = Cicon.medium; + static final int logicIconSize = (int)iconMed, maxUiIcon = 128; - public static void generate(){ - ObjectMap gens = new ObjectMap<>(); + public static void run(){ + ObjectMap gens = new ObjectMap<>(); - if(!paletteMap.isEmpty()){ - ImagePacker.generate("uipalette", () -> { - Fi.get("../ui").walk(fi -> { - if(!fi.extEquals("png")) return; - - Pixmap pix = new Pixmap(fi); - pix.setBlending(Pixmap.Blending.sourceOver); - pix.each((x, y) -> { - int value = pix.getPixel(x, y); - pix.draw(x, y, paletteMap.get(value, value)); - }); - - fi.writePNG(pix); - }); - }); - } - - ImagePacker.generate("splashes", () -> { - ArcNativesLoader.load(); + generate("splashes", () -> { int frames = 12; int size = 32; @@ -70,90 +47,129 @@ public class Generators{ pixmap.each((x, y) -> { float dst = Mathf.dst(x, y, size/2f, size/2f); if(Math.abs(dst - radius) <= stroke){ - pixmap.draw(x, y, Color.white); + pixmap.set(x, y, Color.white); } }); - Fi.get("splash-" + i + ".png").writePNG(pixmap); + Fi.get("splash-" + i + ".png").writePng(pixmap); pixmap.dispose(); } }); - ImagePacker.generate("cliffs", () -> { - int size = 64; - Color dark = new Color(0.5f, 0.5f, 0.6f, 1f).mul(0.98f); - Color mid = Color.lightGray; + generate("bubbles", () -> { - Image[] images = new Image[8]; + int frames = 16; + int size = 40; + for(int i = 0; i < frames; i++){ + float fin = (float)i / (frames); + float fout = 1f - fin; + float stroke = 3.5f * fout; + float radius = (size/2f) * fin; + float shinelen = radius / 2.5f, shinerad = stroke*1.5f + 0.3f; + float shinex = size/2f + shinelen / Mathf.sqrt2, shiney = size/2f - shinelen / Mathf.sqrt2; + + Pixmap pixmap = new Pixmap(size, size); + + pixmap.each((x, y) -> { + float dst = Mathf.dst(x, y, size/2f, size/2f); + if(Math.abs(dst - radius) <= stroke || Mathf.within(x, y, shinex, shiney, shinerad)){ + pixmap.set(x, y, Color.white); + } + }); + + Fi.get("bubble-" + i + ".png").writePng(pixmap); + + pixmap.dispose(); + } + }); + + generate("cliffs", () -> { + ExecutorService exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + int size = 64; + int dark = new Color(0.5f, 0.5f, 0.6f, 1f).mul(0.98f).rgba(); + int mid = Color.lightGray.rgba(); + + Pixmap[] images = new Pixmap[8]; for(int i = 0; i < 8; i++){ - images[i] = ImagePacker.get("cliff" + i); + images[i] = new Pixmap(((GenRegion)Core.atlas.find("cliff" + i)).path); } for(int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; i++){ - Image result = new Image(size, size); - byte[][] mask = new byte[size][size]; + int bi = i; + exec.execute(() -> { + Color color = new Color(); + Pixmap result = new Pixmap(size, size); + byte[][] mask = new byte[size][size]; - byte val = (byte)i; - //check each bit/direction - for(int j = 0; j < 8; j++){ - if((val & (1 << j)) != 0){ - if(j % 2 == 1 && (((val & (1 << (j + 1))) != 0) != ((val & (1 << (j - 1))) != 0))){ - continue; - } - - Image image = images[j]; - image.each((x, y) -> { - Color color = image.getColor(x, y); - if(color.a > 0.1){ - //white -> bit 1 -> top - //black -> bit 2 -> bottom - mask[x][y] |= (color.r > 0.5f ? 1 : 2); + byte val = (byte)bi; + //check each bit/direction + for(int j = 0; j < 8; j++){ + if((val & (1 << j)) != 0){ + if(j % 2 == 1 && (((val & (1 << (j + 1))) != 0) != ((val & (1 << (j - 1))) != 0))){ + continue; } - }); - } - } - result.each((x, y) -> { - byte m = mask[x][y]; - if(m != 0){ - //mid - if(m == 3){ - //find nearest non-mid color - byte best = 0; - float bestDst = 0; - boolean found = false; - //expand search range until found - for(int rad = 9; rad < 64; rad += 7){ - for(int cx = Math.max(x - rad, 0); cx <= Math.min(x + rad, size - 1); cx++){ - for(int cy = Math.max(y - rad, 0); cy <= Math.min(y + rad, size - 1); cy++){ - byte nval = mask[cx][cy]; - if(nval == 1 || nval == 2){ - float dst2 = Mathf.dst2(cx, cy, x, y); - if(dst2 <= rad * rad && (!found || dst2 < bestDst)){ - best = nval; - bestDst = dst2; - found = true; + Pixmap image = images[j]; + image.each((x, y) -> { + color.set(image.getRaw(x, y)); + if(color.a > 0.1){ + //white -> bit 1 -> top + //black -> bit 2 -> bottom + mask[x][y] |= (color.r > 0.5f ? 1 : 2); + } + }); + } + } + + result.each((x, y) -> { + byte m = mask[x][y]; + if(m != 0){ + //mid + if(m == 3){ + //find nearest non-mid color + byte best = 0; + float bestDst = 0; + boolean found = false; + //expand search range until found + for(int rad = 9; rad < 64; rad += 7){ + for(int cx = Math.max(x - rad, 0); cx <= Math.min(x + rad, size - 1); cx++){ + for(int cy = Math.max(y - rad, 0); cy <= Math.min(y + rad, size - 1); cy++){ + byte nval = mask[cx][cy]; + if(nval == 1 || nval == 2){ + float dst2 = Mathf.dst2(cx, cy, x, y); + if(dst2 <= rad * rad && (!found || dst2 < bestDst)){ + best = nval; + bestDst = dst2; + found = true; + } } } } } + + if(found){ + m = best; + } } - if(found){ - m = best; - } + result.setRaw(x, y, m == 1 ? Color.whiteRgba : m == 2 ? dark : mid); } + }); - result.draw(x, y, m == 1 ? Color.white : m == 2 ? dark : mid); - } + Fi.get("../blocks/environment/cliffmask" + (val & 0xff) + ".png").writePng(result); }); + } - result.save("../blocks/environment/cliffmask" + (val & 0xff)); + try{ + exec.shutdown(); + exec.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + }catch(Exception e){ + throw new RuntimeException("go away", e); } }); - ImagePacker.generate("cracks", () -> { + generate("cracks", () -> { RidgedPerlin r = new RidgedPerlin(1, 3); for(int size = 1; size <= BlockRenderer.maxCrackSize; size++){ int dim = size * 32; @@ -161,17 +177,17 @@ public class Generators{ for(int i = 0; i < steps; i++){ float fract = i / (float)steps; - Image image = new Image(dim, dim); + Pixmap image = new Pixmap(dim, dim); for(int x = 0; x < dim; x++){ for(int y = 0; y < dim; y++){ float dst = Mathf.dst((float)x/dim, (float)y/dim, 0.5f, 0.5f) * 2f; if(dst < 1.2f && r.getValue(x, y, 1f / 40f) - dst*(1f-fract) > 0.16f){ - image.draw(x, y, Color.white); + image.setRaw(x, y, Color.whiteRgba); } } } - Image output = new Image(image.width, image.height); + Pixmap output = new Pixmap(image.width, image.height); int rad = 3; //median filter @@ -181,30 +197,31 @@ public class Generators{ for(int cx = -rad; cx < rad; cx++){ for(int cy = -rad; cy < rad; cy++){ int wx = Mathf.clamp(cx + x, 0, output.width - 1), wy = Mathf.clamp(cy + y, 0, output.height - 1); - Color color = image.getColor(wx, wy); - if(color.a > 0.5f){ + int color = image.getRaw(wx, wy); + if((color & 0xff) > 127){ whites ++; }else{ clears ++; } } } - output.draw(x, y, whites >= clears ? Color.white : Color.clear); + output.setRaw(x, y, whites >= clears ? Color.whiteRgba : Color.clearRgba); } } - output.save("cracks-" + size + "-" + i); + Fi.get("cracks-" + size + "-" + i + ".png").writePng(output); } } }); - ImagePacker.generate("block-icons", () -> { - Image colors = new Image(content.blocks().size, 1); + generate("block-icons", () -> { + Pixmap colors = new Pixmap(content.blocks().size, 1); for(Block block : content.blocks()){ if(block.isAir() || block instanceof ConstructBlock || block instanceof OreBlock || block instanceof LegacyBlock) continue; block.load(); + block.loadIcon(); TextureRegion[] regions = block.getGeneratedIcons(); @@ -216,20 +233,25 @@ public class Generators{ } } - Image shardTeamTop = null; + for(TextureRegion region : block.makeIconRegions()){ + GenRegion gen = (GenRegion)region; + save(get(region).outline(block.outlineColor, block.outlineRadius), gen.name + "-outline"); + } + + Pixmap shardTeamTop = null; if(block.teamRegion.found()){ - Image teamr = ImagePacker.get(block.teamRegion); + Pixmap teamr = get(block.teamRegion); for(Team team : Team.all){ if(team.hasPalette){ - Image out = new Image(teamr.width, teamr.height); + Pixmap out = new Pixmap(teamr.width, teamr.height); teamr.each((x, y) -> { - int color = teamr.getColor(x, y).rgba8888(); + int color = teamr.getRaw(x, y); int index = color == 0xffffffff ? 0 : color == 0xdcc6c6ff ? 1 : color == 0x9d7f7fff ? 2 : -1; - out.draw(x, y, index == -1 ? teamr.getColor(x, y) : team.palette[index]); + out.setRaw(x, y, index == -1 ? teamr.getRaw(x, y) : team.palette[index].rgba()); }); - out.save(block.name + "-team-" + team.name); + save(out, block.name + "-team-" + team.name); if(team == Team.sharded){ shardTeamTop = out; @@ -243,87 +265,63 @@ public class Generators{ } try{ - Image last = null; + Pixmap last = null; if(block.outlineIcon){ - int radius = 4; GenRegion region = (GenRegion)regions[block.outlinedIcon >= 0 ? block.outlinedIcon : regions.length -1]; - Image base = ImagePacker.get(region); - Image out = last = new Image(region.width, region.height); - for(int x = 0; x < out.width; x++){ - for(int y = 0; y < out.height; y++){ - - Color color = base.getColor(x, y); - out.draw(x, y, color); - if(color.a < 1f){ - boolean found = false; - outer: - for(int rx = -radius; rx <= radius; rx++){ - for(int ry = -radius; ry <= radius; ry++){ - if(Mathf.dst(rx, ry) <= radius && base.getColor(rx + x, ry + y).a > 0.01f){ - found = true; - break outer; - } - } - } - if(found){ - out.draw(x, y, block.outlineColor); - } - } - } - } + Pixmap base = get(region); + Pixmap out = last = base.outline(block.outlineColor, block.outlineRadius); //do not run for legacy ones if(block.outlinedIcon >= 0){ //prevents the regions above from being ignored/invisible/etc for(int i = block.outlinedIcon + 1; i < regions.length; i++){ - out.draw(ImagePacker.get(regions[i])); + out.draw(get(regions[i]), true); } } region.path.delete(); - out.save(block.name); + save(out, block.name); } - Image image = ImagePacker.get(regions[0]); + if(!regions[0].found()){ + continue; + } + + Pixmap image = get(regions[0]); int i = 0; for(TextureRegion region : regions){ i++; if(i != regions.length || last == null){ - image.draw(region); + image.draw(get(region), true); }else{ - image.draw(last); + image.draw(last, true); } //draw shard (default team top) on top of first sprite if(region == block.teamRegions[Team.sharded.id] && shardTeamTop != null){ - image.draw(shardTeamTop); + image.draw(shardTeamTop, true); } } if(!(regions.length == 1 && regions[0] == Core.atlas.find(block.name) && shardTeamTop == null)){ - image.save("block-" + block.name + "-full"); + save(image, "block-" + block.name + "-full"); } - image.save("../editor/" + block.name + "-icon-editor"); + save(image, "../editor/" + block.name + "-icon-editor"); - for(Cicon icon : Cicon.scaled){ - Image scaled = new Image(icon.size, icon.size); - scaled.drawScaled(image); - scaled.save("../ui/block-" + block.name + "-" + icon.name()); - - if(icon == logicIcon && block.synthetic() && block.buildVisibility != BuildVisibility.hidden){ - image.save(block.name + "-icon-logic"); - } + if(block.buildVisibility != BuildVisibility.hidden){ + saveScaled(image, block.name + "-icon-logic", logicIconSize); } + saveScaled(image, "../ui/block-" + block.name + "-ui", Math.min(image.width, maxUiIcon)); boolean hasEmpty = false; - Color average = new Color(); + Color average = new Color(), c = new Color(); float asum = 0f; for(int x = 0; x < image.width; x++){ for(int y = 0; y < image.height; y++){ - Color color = image.getColor(x, y); + Color color = c.set(image.get(x, y)); average.r += color.r*color.a; average.g += color.g*color.a; average.b += color.b*color.a; @@ -343,94 +341,81 @@ public class Generators{ } //encode square sprite in alpha channel average.a = hasEmpty ? 0.1f : 1f; - colors.draw(block.id, 0, average); + colors.setRaw(block.id, 0, average.rgba()); }catch(NullPointerException e){ Log.err("Block &ly'@'&lr has an null region!", block); } } - colors.save("../../../assets/sprites/block_colors"); + save(colors, "../../../assets/sprites/block_colors"); }); - ImagePacker.generate("shallows", () -> { + generate("shallows", () -> { content.blocks().each(b -> b instanceof ShallowLiquid, floor -> { - Image overlay = ImagePacker.get(floor.liquidBase.region); + Pixmap overlay = get(floor.liquidBase.region); int index = 0; for(TextureRegion region : floor.floorBase.variantRegions()){ - Image res = new Image(32, 32); - res.draw(ImagePacker.get(region)); + Pixmap res = get(region).copy(); for(int x = 0; x < res.width; x++){ for(int y = 0; y < res.height; y++){ - Color color = overlay.getColor(x, y).a(floor.liquidOpacity); - res.draw(x, y, color); + res.set(x, y, Pixmap.blend((overlay.getRaw(x, y) & 0xffffff00) | (int)(floor.liquidOpacity * 255), res.getRaw(x, y))); } } String name = floor.name + "" + (++index); - res.save("../blocks/environment/" + name); - res.save("../editor/editor-" + name); + save(res, "../blocks/environment/" + name); + save(res, "../editor/editor-" + name); gens.put(floor, res); } }); }); - ImagePacker.generate("item-icons", () -> { + generate("item-icons", () -> { for(UnlockableContent item : Seq.withArrays(content.items(), content.liquids(), content.statusEffects())){ - if(item instanceof StatusEffect && !ImagePacker.has(item.getContentType().name() + "-" + item.name)){ + if(item instanceof StatusEffect && !has(item.getContentType().name() + "-" + item.name)){ continue; } - Image base = ImagePacker.get(item.getContentType().name() + "-" + item.name); + Pixmap base = get(item.getContentType().name() + "-" + item.name); //tint status effect icon color if(item instanceof StatusEffect){ StatusEffect stat = (StatusEffect)item; - Image tint = base; - base.each((x, y) -> tint.draw(x, y, tint.getColor(x, y).mul(stat.color))); + Pixmap tint = base; + base.each((x, y) -> tint.setRaw(x, y, Color.muli(tint.getRaw(x, y), stat.color.rgba()))); //outline the image - Image container = new Image(38, 38); - container.draw(base, 3, 3); - base = container.outline(3, Pal.gray); + Pixmap container = new Pixmap(38, 38); + container.draw(base, 3, 3, true); + base = container.outline(Pal.gray, 3); } - for(Cicon icon : Cicon.scaled){ - //if(icon.size == base.width) continue; - Image image = new Image(icon.size, icon.size); - image.drawScaled(base); - image.save((item instanceof StatusEffect ? "../ui/" : "") + item.getContentType().name() + "-" + item.name + "-" + icon.name(), !(item instanceof StatusEffect)); - - if(icon == Cicon.medium){ - image.save("../ui/" + item.getContentType() + "-" + item.name + "-icon"); - } - - if(icon == logicIcon){ - image.save(item.name + "-icon-logic"); - } - } + saveScaled(base, item.name + "-icon-logic", logicIconSize); + save(base, "../ui/" + item.getContentType().name() + "-" + item.name + "-ui"); } }); - ImagePacker.generate("unit-icons", () -> content.units().each(type -> { + //TODO broken, freezes + generate("unit-icons", () -> content.units().each(type -> { if(type.isHidden()) return; //hidden units don't generate ObjectSet outlined = new ObjectSet<>(); try{ type.load(); + type.loadIcon(); type.init(); - Color outc = Pal.darkerMetal; - Func outline = i -> i.outline(3, outc); + Func outline = i -> i.outline(Pal.darkerMetal, 3); Cons outliner = t -> { if(t != null && t.found()){ - ImagePacker.replace(t, outline.get(ImagePacker.get(t))); + replace(t, outline.get(get(t))); } }; for(Weapon weapon : type.weapons){ - if(outlined.add(weapon.name) && ImagePacker.has(weapon.name)){ - outline.get(ImagePacker.get(weapon.name)).save(weapon.name + "-outline"); + if(outlined.add(weapon.name) && has(weapon.name)){ + save(outline.get(get(weapon.name)), weapon.name + "-outline"); } } @@ -440,47 +425,54 @@ public class Generators{ outliner.get(type.baseJointRegion); if(type.constructor.get() instanceof Legsc) outliner.get(type.legRegion); - Image image = outline.get(ImagePacker.get(type.region)); + Pixmap image = outline.get(get(type.region)); - image.save(type.name + "-outline"); + save(image, type.name + "-outline"); //draw mech parts if(type.constructor.get() instanceof Mechc){ - image.drawCenter(type.baseRegion); - image.drawCenter(type.legRegion); - image.drawCenter(type.legRegion, true, false); - image.draw(type.region); + drawCenter(image, get(type.baseRegion)); + drawCenter(image, get(type.legRegion)); + drawCenter(image, get(type.legRegion).flipX()); + image.draw(get(type.region), true); } //draw outlines for(Weapon weapon : type.weapons){ weapon.load(); - image.draw(outline.get(ImagePacker.get(weapon.region)), + image.draw(weapon.flipSprite ? outline.get(get(weapon.region)).flipX() : outline.get(get(weapon.region)), (int)(weapon.x / Draw.scl + image.width / 2f - weapon.region.width / 2f), (int)(-weapon.y / Draw.scl + image.height / 2f - weapon.region.height / 2f), - weapon.flipSprite, false); + true + ); } //draw base region on top to mask weapons - image.draw(type.region); + image.draw(get(type.region), true); + int baseColor = Color.valueOf("ffa665").rgba(); - Image baseCell = ImagePacker.get(type.cellRegion); - Image cell = new Image(type.cellRegion.width, type.cellRegion.height); - cell.each((x, y) -> cell.draw(x, y, baseCell.getColor(x, y).mul(Color.valueOf("ffa665")))); + Pixmap baseCell = get(type.cellRegion); + Pixmap cell = new Pixmap(type.cellRegion.width, type.cellRegion.height); + cell.each((x, y) -> cell.set(x, y, Color.muli(baseCell.getRaw(x, y), baseColor))); - image.draw(cell, image.width / 2 - cell.width / 2, image.height / 2 - cell.height / 2); + image.draw(cell, image.width / 2 - cell.width / 2, image.height / 2 - cell.height / 2, true); for(Weapon weapon : type.weapons){ weapon.load(); - image.draw(weapon.top ? outline.get(ImagePacker.get(weapon.region)) : ImagePacker.get(weapon.region), + Pixmap wepReg = weapon.top ? outline.get(get(weapon.region)) : get(weapon.region); + if(weapon.flipSprite){ + wepReg = wepReg.flipX(); + } + + image.draw(wepReg, (int)(weapon.x / Draw.scl + image.width / 2f - weapon.region.width / 2f), (int)(-weapon.y / Draw.scl + image.height / 2f - weapon.region.height / 2f), - weapon.flipSprite, false); + true); } - image.save("unit-" + type.name + "-full"); + save(image, "unit-" + type.name + "-full"); Rand rand = new Rand(); rand.setSeed(type.name.hashCode()); @@ -492,9 +484,9 @@ public class Generators{ float offsetRange = Math.max(image.width, image.height) * 0.15f; Vec2 offset = new Vec2(1, 1).rotate(rand.random(360f)).setLength(rand.random(0, offsetRange)).add(image.width/2f, image.height/2f); - Image[] wrecks = new Image[splits]; + Pixmap[] wrecks = new Pixmap[splits]; for(int i = 0; i < wrecks.length; i++){ - wrecks[i] = new Image(image.width, image.height); + wrecks[i] = new Pixmap(image.width, image.height); } RidgedPerlin r = new RidgedPerlin(1, 3); @@ -510,92 +502,82 @@ public class Generators{ //distort edges with random noise float noise = (float)Noise.rawNoise(dst / (9f + image.width/70f)) * (60 + image.width/30f); int section = (int)Mathf.clamp(Mathf.mod(offset.angleTo(x, y) + noise + degrees, 360f) / 360f * splits, 0, splits - 1); - if(!vval) wrecks[section].draw(x, y, image.getColor(x, y).mul(rValue ? 0.7f : 1f)); + if(!vval) wrecks[section].setRaw(x, y, Color.muli(image.getRaw(x, y), rValue ? 0.7f : 1f)); }); for(int i = 0; i < wrecks.length; i++){ - wrecks[i].save(type.name + "-wreck" + i); + save(wrecks[i], "../rubble/" + type.name + "-wreck" + i); } - for(Cicon icon : Cicon.scaled){ - Vec2 size = Scaling.fit.apply(image.width, image.height, icon.size, icon.size); - Image scaled = new Image((int)size.x, (int)size.y); + int maxd = Math.min(Math.max(image.width, image.height), maxUiIcon); + Pixmap fit = new Pixmap(maxd, maxd); + drawScaledFit(fit, image); - scaled.drawScaled(image); - scaled.save("../ui/unit-" + type.name + "-" + icon.name()); - - if(icon == logicIcon){ - scaled.save(type.name + "-icon-logic"); - } - } + saveScaled(fit, type.name + "-icon-logic", logicIconSize); + save(fit, "../ui/unit-" + type.name + "-ui"); }catch(IllegalArgumentException e){ Log.err("WARNING: Skipping unit @: @", type.name, e.getMessage()); } })); - ImagePacker.generate("ore-icons", () -> { + generate("ore-icons", () -> { content.blocks().each(b -> b instanceof OreBlock, ore -> { + String prefix = ore instanceof WallOreBlock ? "wall-ore-" : "ore-"; Item item = ore.itemDrop; + int shadowColor = Color.rgba8888(0, 0, 0, 0.3f); - for(int i = 0; i < 3; i++){ + for(int i = 0; i < ore.variants; i++){ //get base image to draw on - Image image = new Image(32, 32); - Image shadow = ImagePacker.get(item.name + (i + 1)); + Pixmap base = get((ore instanceof WallOreBlock ? "wall-" : "") + item.name + (i + 1)); + Pixmap image = base.copy(); int offset = image.width / tilesize - 1; for(int x = 0; x < image.width; x++){ for(int y = offset; y < image.height; y++){ - Color color = shadow.getColor(x, y - offset); - //draw semi transparent background - if(color.a > 0.001f){ - color.set(0, 0, 0, 0.3f); - image.draw(x, y, color); + if(base.getA(x, y - offset) != 0){ + image.setRaw(x, y, Pixmap.blend(shadowColor, base.getRaw(x, y))); } } } - image.draw(ImagePacker.get(item.name + (i + 1))); - image.save("../blocks/environment/ore-" + item.name + (i + 1)); - image.save("../editor/editor-ore-" + item.name + (i + 1)); + image.draw(base, true); + save(image, "../blocks/environment/" + prefix + item.name + (i + 1)); + save(image, "../editor/editor-" + prefix + item.name + (i + 1)); - image.save("block-" + ore.name + "-full"); - for(Cicon icon : Cicon.scaled){ - Image scaled = new Image(icon.size, icon.size); - scaled.drawScaled(image); - scaled.save("../ui/block-" + ore.name + "-" + icon.name()); - } + save(image, "block-" + ore.name + "-full"); + save(image, "../ui/block-" + ore.name + "-ui"); } }); }); - ImagePacker.generate("edges", () -> { + generate("edges", () -> { content.blocks().each(b -> b instanceof Floor && !(b instanceof OverlayFloor), floor -> { - if(ImagePacker.has(floor.name + "-edge") || floor.blendGroup != floor){ + if(has(floor.name + "-edge") || floor.blendGroup != floor){ return; } try{ - Image image = gens.get(floor, ImagePacker.get(floor.getGeneratedIcons()[0])); - Image edge = ImagePacker.get("edge-stencil"); - Image result = new Image(edge.width, edge.height); + Pixmap image = gens.get(floor, get(floor.getGeneratedIcons()[0])); + Pixmap edge = get("edge-stencil"); + Pixmap result = new Pixmap(edge.width, edge.height); for(int x = 0; x < edge.width; x++){ for(int y = 0; y < edge.height; y++){ - result.draw(x, y, edge.getColor(x, y).mul(image.getColor(x % image.width, y % image.height))); + result.set(x, y, Color.muli(edge.getRaw(x, y), image.get(x % image.width, y % image.height))); } } - result.save("../blocks/environment/" + floor.name + "-edge"); + save(result, "../blocks/environment/" + floor.name + "-edge"); }catch(Exception ignored){} }); }); - ImagePacker.generate("scorches", () -> { + generate("scorches", () -> { for(int size = 0; size < 10; size++){ for(int i = 0; i < 3; i++){ ScorchGenerator gen = new ScorchGenerator(); @@ -615,7 +597,7 @@ public class Generators{ Pixmap out = gen.generate(); Pixmap median = Pixmaps.median(out, 2, 0.75); - Fi.get("../rubble/scorch-" + size + "-" + i + ".png").writePNG(median); + Fi.get("../rubble/scorch-" + size + "-" + i + ".png").writePng(median); out.dispose(); median.dispose(); } @@ -623,4 +605,30 @@ public class Generators{ }); } + /** Generates a scorch pixmap based on parameters. Thread safe, unless multiple scorch generators are running in parallel. */ + public static class ScorchGenerator{ + private static final Simplex sim = new Simplex(); + + public int size = 80, seed = 0, color = Color.whiteRgba; + public double scale = 18, pow = 2, octaves = 4, pers = 0.4, add = 2, nscl = 4.5f; + + public Pixmap generate(){ + Pixmap pix = new Pixmap(size, size); + sim.setSeed(seed); + + pix.each((x, y) -> { + double dst = Mathf.dst(x, y, size/2, size/2) / (size / 2f); + double scaled = Math.abs(dst - 0.5f) * 5f + add; + scaled -= noise(Angles.angle(x, y, size/2, size/2))*nscl; + if(scaled < 1.5f) pix.setRaw(x, y, color); + }); + + return pix; + } + + private double noise(float angle){ + return Math.pow(sim.octaveNoise2D(octaves, pers, 1 / scale, Angles.trnsx(angle, size/2f) + size/2f, Angles.trnsy(angle, size/2f) + size/2f), pow); + } + } + } diff --git a/tools/src/mindustry/tools/IconConverter.java b/tools/src/mindustry/tools/IconConverter.java index c672e2a119..50a7f1bb86 100644 --- a/tools/src/mindustry/tools/IconConverter.java +++ b/tools/src/mindustry/tools/IconConverter.java @@ -17,7 +17,6 @@ public class IconConverter{ Fi.get("fontgen/icon_parts").deleteDirectory(); Fi[] list = new Fi("icons").list(); - ArcNativesLoader.load(); Seq files = new Seq<>(); for(Fi img : list){ @@ -42,25 +41,25 @@ public class IconConverter{ } void convert(Pixmap pixmap, Fi output){ - boolean[][] grid = new boolean[pixmap.getWidth()][pixmap.getHeight()]; + boolean[][] grid = new boolean[pixmap.width][pixmap.height]; - for(int x = 0; x < pixmap.getWidth(); x++){ - for(int y = 0; y < pixmap.getHeight(); y++){ - grid[x][pixmap.getHeight() - 1 - y] = !Pixmaps.empty(pixmap.getPixel(x, y)); + for(int x = 0; x < pixmap.width; x++){ + for(int y = 0; y < pixmap.height; y++){ + grid[x][pixmap.height - 1 - y] = !pixmap.empty(x, y); } } float xscl = 1f, yscl = 1f;//resolution / (float)pixmap.getWidth(), yscl = resolution / (float)pixmap.getHeight(); float scl = xscl; - width = pixmap.getWidth(); - height = pixmap.getHeight(); + width = pixmap.width; + height = pixmap.height; - out.append("\n"); + out.append("\n"); - for(int x = -1; x < pixmap.getWidth(); x++){ - for(int y = -1; y < pixmap.getHeight(); y++){ - int index = index(x, y, pixmap.getWidth(), pixmap.getHeight(), grid); + for(int x = -1; x < pixmap.width; x++){ + for(int y = -1; y < pixmap.height; y++){ + int index = index(x, y, pixmap.width, pixmap.height, grid); float leftx = x * xscl, boty = y * yscl, rightx = x * xscl + xscl, topy = y * xscl + yscl, midx = x * xscl + xscl / 2f, midy = y * yscl + yscl / 2f; diff --git a/tools/src/mindustry/tools/Image.java b/tools/src/mindustry/tools/Image.java deleted file mode 100644 index 83594f22be..0000000000 --- a/tools/src/mindustry/tools/Image.java +++ /dev/null @@ -1,229 +0,0 @@ -package mindustry.tools; - -import arc.func.*; -import arc.graphics.Color; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.math.geom.*; -import arc.struct.*; -import arc.util.*; -import mindustry.tools.ImagePacker.*; - -import javax.imageio.*; -import java.awt.*; -import java.awt.image.*; -import java.io.*; - -class Image{ - private static Seq toDispose = new Seq<>(); - - private BufferedImage image; - private Graphics2D graphics; - private Color color = new Color(); - - public final int width, height; - - Image(TextureRegion region){ - this(ImagePacker.buf(region)); - } - - Image(BufferedImage src){ - this.image = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_ARGB); - this.graphics = image.createGraphics(); - this.graphics.drawImage(src, 0, 0, null); - this.width = image.getWidth(); - this.height = image.getHeight(); - - toDispose.add(this); - } - - Image(int width, int height){ - this(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)); - } - - Image copy(){ - Image out =new Image(width, height); - out.draw(this); - return out; - } - - boolean isEmpty(int x, int y){ - if(!Structs.inBounds(x, y, width, height)){ - return true; - } - Color color = getColor(x, y); - return color.a <= 0.001f; - } - - Color getColor(int x, int y){ - if(!Structs.inBounds(x, y, width, height)) return color.set(0, 0, 0, 0); - int i = image.getRGB(x, y); - color.argb8888(i); - return color; - } - - Image outline(int radius, Color outlineColor){ - Image out = copy(); - for(int x = 0; x < out.width; x++){ - for(int y = 0; y < out.height; y++){ - - Color color = getColor(x, y); - out.draw(x, y, color); - if(color.a < 1f){ - boolean found = false; - outer: - for(int rx = -radius; rx <= radius; rx++){ - for(int ry = -radius; ry <= radius; ry++){ - if(Mathf.dst(rx, ry) <= radius && getColor(rx + x, ry + y).a > 0.01f){ - found = true; - break outer; - } - } - } - if(found){ - out.draw(x, y, outlineColor); - } - } - } - } - return out; - } - - Image shadow(float alpha, int rad){ - Image out = silhouette(new Color(0f, 0f, 0f, alpha)).blur(rad); - out.draw(this); - return out; - } - - Image silhouette(Color color){ - Image out = copy(); - - each((x, y) -> out.draw(x, y, getColor(x, y).set(color.r, color.g, color.b, this.color.a * color.a))); - - return out; - } - - Image blur(int radius){ - Image out = copy(); - Color c = new Color(); - int[] sum = {0}; - - for(int x = 0; x < out.width; x++){ - for(int y = 0; y < out.height; y++){ - sum[0] = 0; - - Geometry.circle(x, y, radius, (cx, cy) -> { - int rx = Mathf.clamp(cx, 0, out.width - 1), ry = Mathf.clamp(cy, 0, out.height - 1); - - Color other = getColor(rx, ry); - c.r += other.r; - c.g += other.g; - c.b += other.b; - c.a += other.a; - sum[0] ++; - }); - - c.mula(1f / sum[0]); - - out.draw(x, y, c); - } - } - - return out; - } - - void each(Intc2 cons){ - for(int x = 0; x < width; x++){ - for(int y = 0; y < height; y++){ - cons.get(x, y); - } - } - } - - void draw(int x, int y, Color color){ - graphics.setColor(new java.awt.Color(color.r, color.g, color.b, color.a)); - graphics.fillRect(x, y, 1, 1); - } - - - /** Draws a region at the top left corner. */ - void draw(TextureRegion region){ - draw(region, 0, 0, false, false); - } - - /** Draws a region at the center. */ - void drawCenter(TextureRegion region){ - draw(region, (width - region.width) / 2, (height - region.height) / 2, false, false); - } - - /** Draws a region at the center. */ - void drawCenter(TextureRegion region, boolean flipx, boolean flipy){ - draw(region, (width - region.width) / 2, (height - region.height) / 2, flipx, flipy); - } - - void drawScaled(Image image){ - graphics.drawImage(image.image.getScaledInstance(width, height, java.awt.Image.SCALE_AREA_AVERAGING), 0, 0, width, height, null); - } - - /** Draws an image at the top left corner. */ - void draw(Image image){ - draw(image, 0, 0); - } - - /** Draws an image at the coordinates specified. */ - void draw(Image image, int x, int y){ - graphics.drawImage(image.image, x, y, null); - } - - void draw(TextureRegion region, boolean flipx, boolean flipy){ - draw(region, 0, 0, flipx, flipy); - } - - void draw(TextureRegion region, int x, int y, boolean flipx, boolean flipy){ - GenRegion.validate(region); - - draw(ImagePacker.get(region), x, y, flipx, flipy); - } - - void draw(Image region, int x, int y, boolean flipx, boolean flipy){ - - int ofx = 0, ofy = 0; - - graphics.drawImage(region.image, - x, y, - x + region.width, - y + region.height, - (flipx ? region.width : 0) + ofx, - (flipy ? region.height : 0) + ofy, - (flipx ? 0 : region.width) + ofx, - (flipy ? 0 : region.height) + ofy, - null); - } - - /** @param name Name of texture file name to create, without any extensions. */ - void save(String name){ - try{ - ImageIO.write(image, "png", new File(name + ".png")); - }catch(IOException e){ - throw new RuntimeException(e); - } - } - - void save(String name, boolean antialias){ - save(name); - if(!antialias){ - new File(name + ".png").setLastModified(0); - } - } - - static int total(){ - return toDispose.size; - } - - static void dispose(){ - for(Image image : toDispose){ - image.graphics.dispose(); - } - toDispose.clear(); - } -} diff --git a/tools/src/mindustry/tools/ImagePacker.java b/tools/src/mindustry/tools/ImagePacker.java index 25c5fa6b27..a787540c15 100644 --- a/tools/src/mindustry/tools/ImagePacker.java +++ b/tools/src/mindustry/tools/ImagePacker.java @@ -2,8 +2,10 @@ package mindustry.tools; import arc.*; import arc.files.*; +import arc.graphics.*; import arc.graphics.g2d.*; import arc.graphics.g2d.TextureAtlas.*; +import arc.math.geom.*; import arc.struct.*; import arc.util.*; import arc.util.Log.*; @@ -12,20 +14,16 @@ import mindustry.*; import mindustry.content.*; import mindustry.core.*; import mindustry.ctype.*; -import mindustry.type.*; -import mindustry.world.*; import mindustry.world.blocks.*; -import javax.imageio.*; -import java.awt.image.*; import java.io.*; public class ImagePacker{ - static ObjectMap regionCache = new ObjectMap<>(); - static ObjectMap imageCache = new ObjectMap<>(); + static ObjectMap cache = new ObjectMap<>(); public static void main(String[] args) throws Exception{ Vars.headless = true; + //makes PNG loading slightly faster ArcNativesLoader.load(); Log.logger = new NoopLogHandler(); @@ -36,66 +34,59 @@ public class ImagePacker{ Fi.get("../../../assets-raw/sprites_out").walk(path -> { if(!path.extEquals("png")) return; - String fname = path.nameWithoutExtension(); - - try{ - BufferedImage image = ImageIO.read(path.file()); - - if(image == null) throw new IOException("image " + path.absolutePath() + " is null for terrible reasons"); - GenRegion region = new GenRegion(fname, path){{ - width = image.getWidth(); - height = image.getHeight(); - u2 = v2 = 1f; - u = v = 0f; - }}; - - regionCache.put(fname, region); - imageCache.put(fname, image); - }catch(IOException e){ - throw new RuntimeException(e); - } + cache.put(path.nameWithoutExtension(), new PackIndex(path)); }); Core.atlas = new TextureAtlas(){ @Override public AtlasRegion find(String name){ - if(!regionCache.containsKey(name)){ + if(!cache.containsKey(name)){ GenRegion region = new GenRegion(name, null); region.invalid = true; return region; } - return (AtlasRegion)regionCache.get(name); + + PackIndex index = cache.get(name); + if(index.pixmap == null){ + index.pixmap = new Pixmap(index.file); + index.region = new GenRegion(name, index.file){{ + width = index.pixmap.width; + height = index.pixmap.height; + u2 = v2 = 1f; + u = v = 0f; + }}; + } + return index.region; } @Override public AtlasRegion find(String name, TextureRegion def){ - if(!regionCache.containsKey(name)){ + if(!cache.containsKey(name)){ return (AtlasRegion)def; } - return (AtlasRegion)regionCache.get(name); + return find(name); } @Override public AtlasRegion find(String name, String def){ - if(!regionCache.containsKey(name)){ - return (AtlasRegion)regionCache.get(def); + if(!cache.containsKey(name)){ + return find(def); } - return (AtlasRegion)regionCache.get(name); + return find(name); } @Override public boolean has(String s){ - return regionCache.containsKey(s); + return cache.containsKey(s); } }; Draw.scl = 1f / Core.atlas.find("scale_marker").width; Time.mark(); - Generators.generate(); + Generators.run(); Log.info("&ly[Generator]&lc Total time to generate: &lg@&lcms", Time.elapsed()); - Log.info("&ly[Generator]&lc Total images created: &lg@", Image.total()); - Image.dispose(); + //Log.info("&ly[Generator]&lc Total images created: &lg@", Image.total()); //format: //character-ID=contentname:texture-name @@ -130,9 +121,7 @@ public class ImagePacker{ } static String texname(UnlockableContent c){ - if(c instanceof Block) return "block-" + c.name + "-medium"; - if(c instanceof UnitType) return "unit-" + c.name + "-medium"; - return c.getContentType() + "-" + c.name + "-icon"; + return c.getContentType() + "-" + c.name + "-ui"; } static void generate(String name, Runnable run){ @@ -141,15 +130,7 @@ public class ImagePacker{ Log.info("&ly[Generator]&lc Time to generate &lm@&lc: &lg@&lcms", name, Time.elapsed()); } - static BufferedImage buf(TextureRegion region){ - return imageCache.get(((AtlasRegion)region).name); - } - - static Image create(int width, int height){ - return new Image(width, height); - } - - static Image get(String name){ + static Pixmap get(String name){ return get(Core.atlas.find(name)); } @@ -157,18 +138,40 @@ public class ImagePacker{ return Core.atlas.has(name); } - static Image get(TextureRegion region){ - GenRegion.validate(region); + static Pixmap get(TextureRegion region){ + validate(region); - return new Image(imageCache.get(((AtlasRegion)region).name)); + return cache.get(((AtlasRegion)region).name).pixmap.copy(); } - static void replace(String name, Image image){ - image.save(name); + static void save(Pixmap pix, String path){ + Fi.get(path + ".png").writePng(pix); + } + + static void drawCenter(Pixmap pix, Pixmap other){ + pix.draw(other, pix.width/2 - other.width/2, pix.height/2 - other.height/2, true); + } + + static void saveScaled(Pixmap pix, String name, int size){ + Pixmap scaled = new Pixmap(size, size); + //TODO bad linear scaling + scaled.draw(pix, 0, 0, pix.width, pix.height, 0, 0, size, size, true, true); + save(scaled, name); + } + + static void drawScaledFit(Pixmap base, Pixmap image){ + Vec2 size = Scaling.fit.apply(image.width, image.height, base.width, base.height); + int wx = (int)size.x, wy = (int)size.y; + //TODO bad linear scaling + base.draw(image, 0, 0, image.width, image.height, base.width/2 - wx/2, base.height/2 - wy/2, wx, wy, true, true); + } + + static void replace(String name, Pixmap image){ + Fi.get(name + ".png").writePng(image); ((GenRegion)Core.atlas.find(name)).path.delete(); } - static void replace(TextureRegion region, Image image){ + static void replace(TextureRegion region, Pixmap image){ replace(((GenRegion)region).name, image); } @@ -176,6 +179,12 @@ public class ImagePacker{ throw new IllegalArgumentException(Strings.format(message, args)); } + static void validate(TextureRegion region){ + if(((GenRegion)region).invalid){ + ImagePacker.err("Region does not exist: @", ((GenRegion)region).name); + } + } + static class GenRegion extends AtlasRegion{ boolean invalid; Fi path; @@ -190,11 +199,15 @@ public class ImagePacker{ public boolean found(){ return !invalid; } + } - static void validate(TextureRegion region){ - if(((GenRegion)region).invalid){ - ImagePacker.err("Region does not exist: @", ((GenRegion)region).name); - } + static class PackIndex{ + @Nullable AtlasRegion region; + @Nullable Pixmap pixmap; + Fi file; + + public PackIndex(Fi file){ + this.file = file; } } } diff --git a/tools/src/mindustry/tools/ScriptMainGenerator.java b/tools/src/mindustry/tools/ScriptMainGenerator.java index 0406783661..14ffde2b2b 100644 --- a/tools/src/mindustry/tools/ScriptMainGenerator.java +++ b/tools/src/mindustry/tools/ScriptMainGenerator.java @@ -79,15 +79,18 @@ public class ScriptMainGenerator{ "mindustry.entities.abilities", "mindustry.ai.types", "mindustry.type.weather", + "mindustry.type.weapons", "mindustry.game.Objectives", "mindustry.world.blocks", - "mindustry.world.draw" + "mindustry.world.draw", + "mindustry.type" ); String classTemplate = "package mindustry.mod;\n" + "\n" + "import arc.struct.*;\n" + "/** Generated class. Maps simple class names to concrete classes. For use in JSON mods. */\n" + + "@SuppressWarnings(\"deprecation\")\n" + "public class ClassMap{\n" + " public static final ObjectMap> classes = new ObjectMap<>();\n" + " \n" +