From cfa5f952de2b199446f65f3ffec86265cdf61ce6 Mon Sep 17 00:00:00 2001 From: Retera Date: Tue, 22 Sep 2020 22:30:07 -0400 Subject: [PATCH] Update to include race, basic target types, better combat UI and behaviors --- core/assets/warsmash.ini | 30 +- core/assets/warsmash131.ini | 17 ++ .../etheller/warsmash/WarsmashGdxGame.java | 87 +++--- .../etheller/warsmash/WarsmashGdxMapGame.java | 164 +++++------ .../etheller/warsmash/parsers/fdf/GameUI.java | 24 +- .../parsers/fdf/frames/SpriteFrame.java | 3 +- .../parsers/fdf/frames/TextureFrame.java | 4 +- .../warsmash/parsers/w3x/w3i/Player.java | 44 ++- .../com/etheller/warsmash/util/Quadtree.java | 16 +- .../warsmash/util/QuadtreeIntersector.java | 13 + .../warsmash/viewer5/AudioContext.java | 1 - .../handlers/mdx/AttachmentInstance.java | 2 +- .../mdx/EventObjectEmitterObject.java | 25 +- .../viewer5/handlers/mdx/EventObjectSnd.java | 3 + .../handlers/mdx/MdxComplexInstance.java | 37 ++- .../viewer5/handlers/mdx/SdSequence.java | 6 +- .../handlers/mdx/SequenceLoopMode.java | 9 + .../viewer5/handlers/tga/TgaFile.java | 6 +- .../viewer5/handlers/w3x/AnimationTokens.java | 2 + ...{StandSequence.java => SequenceUtils.java} | 24 +- .../viewer5/handlers/w3x/SplatModel.java | 106 ++++++- .../viewer5/handlers/w3x/UnitAckSound.java | 109 +++++++ .../viewer5/handlers/w3x/UnitSound.java | 120 ++++++++ .../viewer5/handlers/w3x/UnitSoundset.java | 128 +-------- .../viewer5/handlers/w3x/War3MapViewer.java | 203 +++++++++---- .../handlers/w3x/environment/PathingGrid.java | 9 +- .../handlers/w3x/environment/Terrain.java | 7 + .../w3x/rendersim/RenderAttackInstant.java | 40 +++ .../w3x/rendersim/RenderAttackProjectile.java | 51 ++-- .../handlers/w3x/rendersim/RenderEffect.java | 7 + .../handlers/w3x/rendersim/RenderUnit.java | 93 +++++- .../w3x/simulation/CDestructable.java | 17 ++ .../w3x/simulation/CGameplayConstants.java | 64 ++++- .../handlers/w3x/simulation/CItem.java | 16 ++ .../handlers/w3x/simulation/CPlayer.java | 35 --- .../handlers/w3x/simulation/CSimulation.java | 105 +++++-- .../handlers/w3x/simulation/CUnit.java | 266 +++++++++++++++++- .../simulation/CUnitAnimationListener.java | 8 +- .../w3x/simulation/CUnitEnumFunction.java | 5 + .../w3x/simulation/CUnitStateListener.java | 17 ++ .../handlers/w3x/simulation/CUnitType.java | 26 +- .../handlers/w3x/simulation/CWidget.java | 18 +- .../w3x/simulation/CWorldCollision.java | 169 +++++++++-- .../simulation/abilities/CAbilityAttack.java | 15 +- .../w3x/simulation/combat/CAttackType.java | 19 +- .../w3x/simulation/combat/CDefenseType.java | 2 + .../combat/attacks/CUnitAttack.java | 6 +- .../combat/attacks/CUnitAttackInstant.java | 9 + .../combat/attacks/CUnitAttackMissile.java | 13 + .../attacks/CUnitAttackMissileBounce.java | 75 ++++- .../attacks/CUnitAttackMissileSplash.java | 67 +++++ .../combat/attacks/CUnitAttackNormal.java | 8 + .../combat/projectile/CAttackProjectile.java | 43 ++- .../w3x/simulation/data/CUnitData.java | 237 ++++++++-------- .../w3x/simulation/orders/CAttackOrder.java | 69 ++++- .../w3x/simulation/orders/CMoveOrder.java | 123 ++++++-- .../pathing/CPathfindingProcessor.java | 115 ++++++-- .../w3x/simulation/players/CAllianceType.java | 14 + .../w3x/simulation/players/CMapControl.java | 10 + .../w3x/simulation/players/CPlayer.java | 109 +++++++ .../{ => players}/CPlayerController.java | 2 +- .../w3x/simulation/players/CRace.java | 30 ++ .../simulation/players/CRacePreference.java | 11 + .../simulation/util/ProjectileCreator.java | 10 - .../util/SimulationRenderController.java | 19 ++ .../viewer5/handlers/w3x/ui/MeleeUI.java | 105 ++++++- .../warsmash/desktop/DesktopLauncher.java | 10 +- 67 files changed, 2513 insertions(+), 744 deletions(-) create mode 100644 core/assets/warsmash131.ini create mode 100644 core/src/com/etheller/warsmash/util/QuadtreeIntersector.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/mdx/SequenceLoopMode.java rename core/src/com/etheller/warsmash/viewer5/handlers/w3x/{StandSequence.java => SequenceUtils.java} (87%) create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitAckSound.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSound.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackInstant.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderEffect.java delete mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayer.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitEnumFunction.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitStateListener.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CAllianceType.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapControl.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayer.java rename core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/{ => players}/CPlayerController.java (78%) create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRace.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRacePreference.java delete mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/ProjectileCreator.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/SimulationRenderController.java diff --git a/core/assets/warsmash.ini b/core/assets/warsmash.ini index 276c5ba..549c548 100644 --- a/core/assets/warsmash.ini +++ b/core/assets/warsmash.ini @@ -1,15 +1,23 @@ [DataSources] -Count=5 -Type00=Folder -Path00="E:\Backups\Warcraft III 1.30 but dead\War3mod.mpq" -Type01=Folder -Path01="E:\Backups\Warcraft\Data\127" -Type02=Folder -Path02="..\..\resources" -Type03=Folder -Path03="E:\Backups\Warsmash\Data" +Count=7 +Type00=MPQ +Path00="E:\Games\Warcraft III Patch 1.22\war3.mpq" +Type01=MPQ +Path01="E:\Games\Warcraft III Patch 1.22\War3x.mpq" +Type02=MPQ +Path02="E:\Games\Warcraft III Patch 1.22\War3xlocal.mpq" +Type03=MPQ +Path03="E:\Games\Warcraft III Patch 1.22\War3Patch.mpq" Type04=Folder -Path04="." +Path04="..\..\resources" +Type05=Folder +Path05="E:\Backups\Warsmash\Data" +Type06=Folder +Path06="." [Map] -FilePath="PitchRoll.w3x" \ No newline at end of file +//FilePath="CombatUnitTests.w3x" +FilePath="PitchRoll.w3x" +//FilePath="PlayerPeasants.w3m" +//FilePath="FireLord.w3x" +//FilePath="Maps\Campaign\NightElf03.w3m" \ No newline at end of file diff --git a/core/assets/warsmash131.ini b/core/assets/warsmash131.ini new file mode 100644 index 0000000..452f706 --- /dev/null +++ b/core/assets/warsmash131.ini @@ -0,0 +1,17 @@ +[DataSources] +Count=5 +Type00=Folder +Path00="E:\Games\Warcraft III CASC 1.31\war3.w3mod" +Type01=Folder +Path01="E:\Games\Warcraft III CASC 1.31\war3.w3mod\_locales\enus.w3mod" +Type02=Folder +Path02="..\..\resources" +Type03=Folder +Path03="E:\Backups\Warsmash\Data" +Type04=Folder +Path04="." + +[Map] +FilePath="PitchRoll.w3x" +//FilePath="ReforgedGeorgeVacation.w3x" +//FilePath="Maps\Campaign\NightElf03.w3m" \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/WarsmashGdxGame.java b/core/src/com/etheller/warsmash/WarsmashGdxGame.java index 81d8a76..3b3121d 100644 --- a/core/src/com/etheller/warsmash/WarsmashGdxGame.java +++ b/core/src/com/etheller/warsmash/WarsmashGdxGame.java @@ -7,7 +7,6 @@ import java.util.Arrays; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.SpriteBatch; @@ -19,7 +18,6 @@ import com.etheller.warsmash.datasources.DataSource; import com.etheller.warsmash.datasources.DataSourceDescriptor; import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; import com.etheller.warsmash.parsers.mdlx.Sequence; -import com.etheller.warsmash.util.DataSourceFileHandle; import com.etheller.warsmash.viewer5.Camera; import com.etheller.warsmash.viewer5.CanvasProvider; import com.etheller.warsmash.viewer5.ModelViewer; @@ -29,6 +27,7 @@ import com.etheller.warsmash.viewer5.SolvedPath; import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; import com.etheller.warsmash.viewer5.handlers.mdx.MdxHandler; import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvider { private static final boolean SPIN = false; @@ -68,21 +67,21 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide this.viewer.addHandler(new MdxHandler()); this.viewer.enableAudio(); - final Scene scene = this.viewer.addWorldScene(); -// scene.enableAudio(); + final Scene scene = this.viewer.addSimpleScene(); + scene.enableAudio(); this.cameraManager = new CameraManager(); this.cameraManager.setupCamera(scene); // this.mainModel = (MdxModel) this.viewer.load("Doodads\\Cinematic\\ArthasIllidanFight\\ArthasIllidanFight.mdx", - this.mainModel = (MdxModel) this.viewer.load("UI\\Glues\\SinglePlayer\\NightElf_Exp\\NightElf_Exp.mdx", +// this.mainModel = (MdxModel) this.viewer.load("UI\\Glues\\SinglePlayer\\NightElf_Exp\\NightElf_Exp.mdx", // this.mainModel = (MdxModel) this.viewer.load("Abilities\\Spells\\Orc\\FeralSpirit\\feralspirittarget.mdx", - new PathSolver() { - @Override - public SolvedPath solve(final String src, final Object solverParams) { - return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); - } - }, null); +// new PathSolver() { +// @Override +// public SolvedPath solve(final String src, final Object solverParams) { +// return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); +// } +// }, null); // final EventObjectEmitterObject evt = this.mainModel.getEventObjects().get(1); // for (final Sequence seq : this.mainModel.getSequences()) { @@ -91,18 +90,20 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide // System.out.println(Arrays.toString(evt.keyFrames)); // System.out.println(evt.name); - this.mainInstance = (MdxComplexInstance) this.mainModel.addInstance(0); +// this.mainInstance = (MdxComplexInstance) this.mainModel.addInstance(0); - this.mainInstance.setScene(scene); - - final int animIndex = 0; - this.modelCamera = this.mainModel.cameras.get(animIndex); - this.mainInstance.setSequence(animIndex); - - this.mainInstance.setSequenceLoopMode(4); +// this.mainInstance.setScene(scene); +// +// final int animIndex = 0; +// this.modelCamera = this.mainModel.cameras.get(animIndex); +// this.mainInstance.setSequence(animIndex); +// +// this.mainInstance.setSequenceLoopMode(SequenceLoopMode.LOOP_TO_NEXT_ANIMATION); // acolytesHarvestingSceneJoke2(scene); + singleModelScene(scene, "Buildings\\Undead\\Necropolis\\Necropolis.mdx", "birth"); + System.out.println("Loaded"); Gdx.gl30.glClearColor(0.5f, 0.5f, 0.5f, 1); // TODO remove white background @@ -141,7 +142,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide } instance3.setSequence(animIndex); - instance3.setSequenceLoopMode(2); + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); } private void singleModelScene(final Scene scene, final String path, final String animName) { @@ -160,11 +161,12 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide for (final Sequence s : model2.getSequences()) { if (s.getName().toLowerCase().startsWith(animName)) { animIndex = model2.getSequences().indexOf(s); + break; } } instance3.setSequence(animIndex); - instance3.setSequenceLoopMode(2); + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); } private void acolytesHarvestingScene(final Scene scene) { @@ -196,7 +198,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide } acolyteInstance.setSequence(animIndex); - acolyteInstance.setSequenceLoopMode(2); + acolyteInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); final double angle = ((Math.PI * 2) / 5) * i; acolyteInstance.localLocation.x = (float) Math.cos(angle) * 256; @@ -209,7 +211,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide effectInstance.setSequence(1); - effectInstance.setSequenceLoopMode(2); + effectInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); effectInstance.localLocation.x = (float) Math.cos(angle) * 256; effectInstance.localLocation.y = (float) Math.sin(angle) * 256; effectInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle)); @@ -228,7 +230,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide mineInstance.setSequence(2); - mineInstance.setSequenceLoopMode(2); + mineInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); } private void acolytesHarvestingSceneJoke2(final Scene scene) { @@ -260,7 +262,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide } acolyteInstance.setSequence(animIndex); - acolyteInstance.setSequenceLoopMode(2); + acolyteInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); final double angle = ((Math.PI * 2) / 5) * i; acolyteInstance.localLocation.x = (float) Math.cos(angle) * 256; @@ -273,7 +275,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide effectInstance.setSequence(1); - effectInstance.setSequenceLoopMode(2); + effectInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); effectInstance.localLocation.x = (float) Math.cos(angle) * 256; effectInstance.localLocation.y = (float) Math.sin(angle) * 256; effectInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle)); @@ -295,7 +297,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide mineInstance.localScale.y = 2; mineInstance.localScale.z = 2; - mineInstance.setSequenceLoopMode(2); + mineInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); final MdxModel mineModel2 = (MdxModel) this.viewer .load("abilities\\spells\\undead\\unsummon\\unsummontarget.mdx", new PathSolver() { @Override @@ -309,7 +311,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide mineInstance2.setSequence(0); - mineInstance2.setSequenceLoopMode(2); + mineInstance2.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); } private void makeFourHundred(final Scene scene, final MdxModel model2) { @@ -323,7 +325,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide final int animIndex = i % model2.getSequences().size(); instance3.setSequence(animIndex); - instance3.setSequenceLoopMode(2); + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); } } @@ -339,7 +341,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide final int animIndex = i % model2.getSequences().size(); instance3.setSequence(animIndex); - instance3.setSequenceLoopMode(2); + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); } } @@ -353,13 +355,13 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide private com.etheller.warsmash.viewer5.handlers.mdx.Camera modelCamera; private final float[] cameraPositionTemp = new float[3]; private final float[] cameraTargetTemp = new float[3]; - private boolean firstFrame = true; + private final boolean firstFrame = true; @Override public void render() { Gdx.gl30.glBindVertexArray(VAO); if (SPIN) { - this.cameraManager.horizontalAngle += 0.01; + this.cameraManager.horizontalAngle += 0.0001; if (this.cameraManager.horizontalAngle > (2 * Math.PI)) { this.cameraManager.horizontalAngle = 0; } @@ -384,14 +386,14 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide this.mainInstance.setSequence(sequence); this.mainInstance.frame += (int) (Gdx.graphics.getRawDeltaTime() * 1000); } - if (this.firstFrame) { - final Music music = Gdx.audio.newMusic(new DataSourceFileHandle(this.viewer.dataSource, - "Sound\\Ambient\\DoodadEffects\\FinalCinematic.mp3")); - music.setVolume(0.2f); - music.setLooping(true); - music.play(); - this.firstFrame = false; - } +// if (this.firstFrame) { +// final Music music = Gdx.audio.newMusic(new DataSourceFileHandle(this.viewer.dataSource, +// "Sound\\Ambient\\DoodadEffects\\FinalCinematic.mp3")); +// music.setVolume(0.2f); +// music.setLooping(true); +// music.play(); +// this.firstFrame = false; +// } } @Override @@ -445,7 +447,7 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide this.zoomFactor = 0.1f; this.horizontalAngle = (float) (Math.PI / 2); this.verticalAngle = (float) (Math.PI / 4); - this.distance = 1000; + this.distance = 500; this.position = new Vector3(); this.target = new Vector3(0, 0, 50); this.worldUp = new Vector3(0, 0, 1); @@ -503,6 +505,9 @@ public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvide WarsmashGdxGame.this.modelCamera.nearClippingPlane, WarsmashGdxGame.this.modelCamera.farClippingPlane); } + else { + this.camera.perspective(70, this.camera.getAspect(), 100, 5000); + } this.camera.moveToAndFace(this.position, this.target, this.worldUp); } diff --git a/core/src/com/etheller/warsmash/WarsmashGdxMapGame.java b/core/src/com/etheller/warsmash/WarsmashGdxMapGame.java index db4ca3d..e7aa3ea 100644 --- a/core/src/com/etheller/warsmash/WarsmashGdxMapGame.java +++ b/core/src/com/etheller/warsmash/WarsmashGdxMapGame.java @@ -30,8 +30,7 @@ import com.badlogic.gdx.math.Quaternion; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; -import com.badlogic.gdx.utils.viewport.FitViewport; -import com.badlogic.gdx.utils.viewport.Viewport; +import com.badlogic.gdx.utils.viewport.ExtendViewport; import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; import com.etheller.warsmash.datasources.DataSource; import com.etheller.warsmash.datasources.DataSourceDescriptor; @@ -51,12 +50,9 @@ import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; import com.etheller.warsmash.viewer5.handlers.mdx.ReplaceableIds; import com.etheller.warsmash.viewer5.handlers.tga.TgaFile; -import com.etheller.warsmash.viewer5.handlers.w3x.UnitSoundset.UnitAckSound; +import com.etheller.warsmash.viewer5.handlers.w3x.UnitSound; import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; -import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.CommandCardIcon; import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.COrder; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityStop; import com.etheller.warsmash.viewer5.handlers.w3x.ui.MeleeUI; public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProvider, InputProcessor { @@ -74,18 +70,14 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv // libGDX stuff private OrthographicCamera uiCamera; private BitmapFont font; - private BitmapFont font24; private BitmapFont font20; private SpriteBatch batch; - private Viewport uiViewport; + private ExtendViewport uiViewport; private GlyphLayout glyphLayout; private Texture consoleUITexture; - private RenderUnit selectedUnit; private int selectedSoundCount = 0; - private Texture activeButtonTexture; - private Rectangle minimap; private Rectangle minimapFilledArea; @@ -171,20 +163,11 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv final float w = Gdx.graphics.getWidth(); final float h = Gdx.graphics.getHeight(); - this.tempRect.x = 0; - this.tempRect.y = 0; - this.tempRect.width = w; - this.tempRect.height = h; - this.uiScene.camera.viewport(this.tempRect); - this.uiScene.camera.ortho(0, 0.8f, 0, 0.6f, 0, 1); - final FreeTypeFontGenerator fontGenerator = new FreeTypeFontGenerator( new DataSourceFileHandle(this.viewer.dataSource, "fonts\\FRIZQT__.TTF")); final FreeTypeFontParameter fontParam = new FreeTypeFontParameter(); fontParam.size = 32; this.font = fontGenerator.generateFont(fontParam); - fontParam.size = 24; - this.font24 = fontGenerator.generateFont(fontParam); fontParam.size = 20; this.font20 = fontGenerator.generateFont(fontParam); this.glyphLayout = new GlyphLayout(); @@ -193,10 +176,10 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv // height // Height is multiplied by aspect ratio. this.uiCamera = new OrthographicCamera(); - this.uiViewport = new FitViewport(1600, 1200, this.uiCamera); + this.uiViewport = new ExtendViewport(1600, 1200, this.uiCamera); this.uiViewport.update(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); - this.uiCamera.position.set(this.uiCamera.viewportWidth / 2f, this.uiCamera.viewportHeight / 2f, 0); + this.uiCamera.position.set(this.uiViewport.getMinWorldWidth() / 2, this.uiViewport.getMinWorldHeight() / 2, 0); this.uiCamera.update(); this.batch = new SpriteBatch(); @@ -223,9 +206,6 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv } } - this.activeButtonTexture = ImageUtils.getBLPTexture(this.viewer.dataSource, - "UI\\Widgets\\Console\\Human\\CommandButton\\human-activebutton.blp"); - for (int i = 0; i < this.teamColors.length; i++) { this.teamColors[i] = ImageUtils.getBLPTexture(this.viewer.dataSource, "ReplaceableTextures\\" + ReplaceableIds.getPathString(1) + ReplaceableIds.getIdString(i) + ".blp"); @@ -236,8 +216,8 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv Gdx.input.setInputProcessor(this); -// final Music music = Gdx.audio.newMusic( -// new DataSourceFileHandle(this.viewer.dataSource, "Sound\\Music\\mp3Music\\War2IntroMusic.mp3")); +// final Music music = Gdx.audio +// .newMusic(new DataSourceFileHandle(this.viewer.dataSource, "Sound\\Music\\mp3Music\\Undead2.mp3")); // music.setVolume(0.2f); // music.setLooping(true); // music.play(); @@ -277,6 +257,27 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv }); this.meleeUI.main(); fontGenerator.dispose(); + + updateUIScene(); + + this.meleeUI.resize(); + } + + private void updateUIScene() { + this.tempRect.x = this.uiViewport.getScreenX(); + this.tempRect.y = this.uiViewport.getScreenY(); + this.tempRect.width = this.uiViewport.getScreenWidth(); + this.tempRect.height = this.uiViewport.getScreenHeight(); + this.uiScene.camera.viewport(this.tempRect); + final float worldWidth = this.uiViewport.getWorldWidth(); + final float worldHeight = this.uiViewport.getWorldHeight(); + final float xScale = worldWidth / this.uiViewport.getMinWorldWidth(); + final float yScale = worldHeight / this.uiViewport.getMinWorldHeight(); + final float uiSceneWidth = 0.8f * xScale; + final float uiSceneHeight = 0.6f * yScale; + final float uiSceneX = ((0.8f - uiSceneWidth) / 2); + final float uiSceneY = ((0.6f - uiSceneHeight) / 2); + this.uiScene.camera.ortho(uiSceneX, uiSceneWidth + uiSceneX, uiSceneY, uiSceneHeight + uiSceneY, -1f, 1); } @Override @@ -313,32 +314,11 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv this.font.setColor(Color.YELLOW); final String fpsString = "FPS: " + Gdx.graphics.getFramesPerSecond(); this.glyphLayout.setText(this.font, fpsString); - this.font.draw(this.batch, fpsString, (this.uiViewport.getWorldWidth() - this.glyphLayout.width) / 2, 1100); + this.font.draw(this.batch, fpsString, (this.uiViewport.getMinWorldWidth() - this.glyphLayout.width) / 2, 1100); this.batch.draw(this.minimapTexture, this.minimap.x, this.minimap.y, this.minimap.width, this.minimap.height); - if (this.selectedUnit != null) { - int messageIndex = 0; - for (final Message message : this.messages) { - this.font20.draw(this.batch, message.text, 100, 400 + (25 * (messageIndex++))); - } - this.font20.setColor(Color.WHITE); - - final COrder currentOrder = this.selectedUnit.getSimulationUnit().getCurrentOrder(); - for (final CommandCardIcon commandCardIcon : this.selectedUnit.getCommandCardIcons()) { - this.batch.draw(commandCardIcon.getTexture(), 1235 + (86.8f * commandCardIcon.getX()), - 190 - (88 * commandCardIcon.getY()), 78f, 78f); - if (((currentOrder != null) && (currentOrder.getOrderId() == commandCardIcon.getOrderId())) - || ((currentOrder == null) && (commandCardIcon.getOrderId() == CAbilityStop.ORDER_ID))) { - final int blendDstFunc = this.batch.getBlendDstFunc(); - final int blendSrcFunc = this.batch.getBlendSrcFunc(); - this.batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE); - this.batch.draw(this.activeButtonTexture, 1235 + (86.8f * commandCardIcon.getX()), - 190 - (88 * commandCardIcon.getY()), 78f, 78f); - this.batch.setBlendFunction(blendSrcFunc, blendDstFunc); - } - } - } + final Rectangle playableMapArea = this.viewer.terrain.getPlayableMapArea(); for (final RenderUnit unit : this.viewer.units) { if (unit.playerIndex >= WarsmashConstants.MAX_PLAYERS) { System.err.println(unit.row.getName() + " at ( " + unit.location[0] + ", " + unit.location[1] + " )" @@ -347,10 +327,12 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv } final Texture minimapIcon = this.teamColors[unit.playerIndex]; this.batch.draw(minimapIcon, - this.minimapFilledArea.x + (((unit.location[0] - this.viewer.terrain.centerOffset[0]) - / ((this.viewer.terrain.columns - 1) * 128f)) * this.minimapFilledArea.width), - this.minimapFilledArea.y + (((unit.location[1] - this.viewer.terrain.centerOffset[1]) - / ((this.viewer.terrain.rows - 1) * 128f)) * this.minimapFilledArea.height), + this.minimapFilledArea.x + + (((unit.location[0] - playableMapArea.getX()) / (playableMapArea.getWidth())) + * this.minimapFilledArea.width), + this.minimapFilledArea.y + + (((unit.location[1] - playableMapArea.getY()) / (playableMapArea.getHeight())) + * this.minimapFilledArea.height), 4, 4); } this.batch.end(); @@ -411,14 +393,9 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv final float portraitTestHeight = (100 / 480f) * height; this.uiViewport.update(width, height); - this.uiCamera.position.set(this.uiCamera.viewportWidth / 2, this.uiCamera.viewportHeight / 2, 0); + this.uiCamera.position.set(this.uiViewport.getMinWorldWidth() / 2, this.uiViewport.getMinWorldHeight() / 2, 0); - this.tempRect.x = this.uiViewport.getScreenX(); - this.tempRect.y = this.uiViewport.getScreenY(); - this.tempRect.width = this.uiViewport.getScreenWidth(); - this.tempRect.height = this.uiViewport.getScreenHeight(); - this.uiScene.camera.viewport(this.tempRect); - this.uiScene.camera.ortho(0f, 0.8f, 0f, 0.6f, -1f, 1); + updateUIScene(); this.meleeUI.resize(); } @@ -621,20 +598,6 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv clickLocationTemp2.y = screenY; this.uiViewport.unproject(clickLocationTemp2); - if (this.selectedUnit != null) { - for (final CommandCardIcon commandCardIcon : this.selectedUnit.getCommandCardIcons()) { - if (new Rectangle(1235 + (86.8f * commandCardIcon.getX()), 190 - (88 * commandCardIcon.getY()), 78f, - 78f).contains(clickLocationTemp2)) { - if (button == Input.Buttons.RIGHT) { - this.messages.add(new Message(Gdx.input.getCurrentEventTime(), "Right mouse click")); - } - else { - this.messages.add(new Message(Gdx.input.getCurrentEventTime(), "Left mouse click")); - } - return true; - } - } - } if (this.minimapFilledArea.contains(clickLocationTemp2.x, clickLocationTemp2.y)) { final float clickX = (clickLocationTemp2.x - this.minimapFilledArea.x) / this.minimapFilledArea.width; final float clickY = (clickLocationTemp2.y - this.minimapFilledArea.y) / this.minimapFilledArea.height; @@ -646,24 +609,32 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv } if (button == Input.Buttons.RIGHT) { final RenderUnit rayPickUnit = this.viewer.rayPickUnit(screenX, worldScreenY); - if ((rayPickUnit != null) && (this.selectedUnit != null) - && (rayPickUnit.playerIndex != this.selectedUnit.playerIndex)) { - if (this.viewer.orderSmart(rayPickUnit)) { - this.meleeUI.portraitTalk(); - this.selectedSoundCount = 0; + if (this.meleeUI.getSelectedUnit() != null) { + if ((rayPickUnit != null) && (rayPickUnit.playerIndex != this.meleeUI.getSelectedUnit().playerIndex) + && !rayPickUnit.getSimulationUnit().isDead()) { + if (this.viewer.orderSmart(rayPickUnit)) { + if (this.meleeUI.getSelectedUnit().soundset.yesAttack.playUnitResponse( + this.viewer.worldScene.audioContext, this.meleeUI.getSelectedUnit())) { + this.meleeUI.portraitTalk(); + } + this.selectedSoundCount = 0; + } } - } - else { - this.viewer.getClickLocation(clickLocationTemp, screenX, (int) worldScreenY); - System.out.println(clickLocationTemp); - this.viewer.showConfirmation(clickLocationTemp, 0, 1, 0); - final int x = (int) ((clickLocationTemp.x - this.viewer.terrain.centerOffset[0]) / 128); - final int y = (int) ((clickLocationTemp.y - this.viewer.terrain.centerOffset[1]) / 128); - System.out.println(x + "," + y); - this.viewer.terrain.logRomp(x, y); - if (this.viewer.orderSmart(clickLocationTemp.x, clickLocationTemp.y)) { - this.meleeUI.portraitTalk(); - this.selectedSoundCount = 0; + else { + this.viewer.getClickLocation(clickLocationTemp, screenX, (int) worldScreenY); + System.out.println(clickLocationTemp); + this.viewer.showConfirmation(clickLocationTemp, 0, 1, 0); + final int x = (int) ((clickLocationTemp.x - this.viewer.terrain.centerOffset[0]) / 128); + final int y = (int) ((clickLocationTemp.y - this.viewer.terrain.centerOffset[1]) / 128); + System.out.println(x + "," + y); + this.viewer.terrain.logRomp(x, y); + if (this.viewer.orderSmart(clickLocationTemp.x, clickLocationTemp.y)) { + if (this.meleeUI.getSelectedUnit().soundset.yes.playUnitResponse( + this.viewer.worldScene.audioContext, this.meleeUI.getSelectedUnit())) { + this.meleeUI.portraitTalk(); + } + this.selectedSoundCount = 0; + } } } } @@ -671,14 +642,13 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv final List selectedUnits = this.viewer.selectUnit(screenX, worldScreenY, false); if (!selectedUnits.isEmpty()) { final RenderUnit unit = selectedUnits.get(0); - final boolean selectionChanged = this.selectedUnit != unit; + final boolean selectionChanged = this.meleeUI.getSelectedUnit() != unit; boolean playedNewSound = false; if (selectionChanged) { this.selectedSoundCount = 0; } - this.selectedUnit = unit; if (unit.soundset != null) { - UnitAckSound ackSoundToPlay = unit.soundset.what; + UnitSound ackSoundToPlay = unit.soundset.what; final int pissedSoundCount = unit.soundset.pissed.getSoundCount(); int soundIndex; if ((this.selectedSoundCount >= 3) && (pissedSoundCount > 0)) { @@ -688,8 +658,7 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv else { soundIndex = (int) (Math.random() * ackSoundToPlay.getSoundCount()); } - if (ackSoundToPlay.play(this.viewer.worldScene.audioContext, unit.location[0], unit.location[1], - soundIndex)) { + if (ackSoundToPlay.playUnitResponse(this.viewer.worldScene.audioContext, unit, soundIndex)) { this.selectedSoundCount++; if ((this.selectedSoundCount - 3) >= pissedSoundCount) { this.selectedSoundCount = 0; @@ -705,7 +674,6 @@ public class WarsmashGdxMapGame extends ApplicationAdapter implements CanvasProv } } else { - this.selectedUnit = null; this.meleeUI.selectUnit(null); } } diff --git a/core/src/com/etheller/warsmash/parsers/fdf/GameUI.java b/core/src/com/etheller/warsmash/parsers/fdf/GameUI.java index fbf8f2f..994aa5b 100644 --- a/core/src/com/etheller/warsmash/parsers/fdf/GameUI.java +++ b/core/src/com/etheller/warsmash/parsers/fdf/GameUI.java @@ -12,6 +12,7 @@ import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator; import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator.FreeTypeFontParameter; +import com.badlogic.gdx.utils.viewport.ExtendViewport; import com.badlogic.gdx.utils.viewport.FitViewport; import com.badlogic.gdx.utils.viewport.Viewport; import com.etheller.warsmash.datasources.DataSource; @@ -62,7 +63,13 @@ public final class GameUI extends AbstractUIFrame implements UIFrame { this.viewport = viewport; this.uiScene = uiScene; this.modelViewer = modelViewer; - this.renderBounds.set(0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); + if (viewport instanceof ExtendViewport) { + this.renderBounds.set(0, 0, ((ExtendViewport) viewport).getMinWorldWidth(), + ((ExtendViewport) viewport).getMinWorldHeight()); + } + else { + this.renderBounds.set(0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); + } this.templates = new FrameTemplateEnvironment(); this.fontGenerator = fontGenerator; this.fontParam = new FreeTypeFontParameter(); @@ -312,10 +319,16 @@ public final class GameUI extends AbstractUIFrame implements UIFrame { } public static float convertX(final Viewport viewport, final float fdfX) { + if (viewport instanceof ExtendViewport) { + return (fdfX / 0.8f) * ((ExtendViewport) viewport).getMinWorldWidth(); + } return (fdfX / 0.8f) * viewport.getWorldWidth(); } public static float convertY(final Viewport viewport, final float fdfY) { + if (viewport instanceof ExtendViewport) { + return (fdfY / 0.6f) * ((ExtendViewport) viewport).getMinWorldHeight(); + } return (fdfY / 0.6f) * viewport.getWorldHeight(); } @@ -325,8 +338,13 @@ public final class GameUI extends AbstractUIFrame implements UIFrame { } Texture texture = this.pathToTexture.get(path); if (texture == null) { - texture = ImageUtils.getBLPTexture(this.dataSource, path); - this.pathToTexture.put(path, texture); + try { + texture = ImageUtils.getBLPTexture(this.dataSource, path); + this.pathToTexture.put(path, texture); + } + catch (final Exception exc) { + + } } return texture; } diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SpriteFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SpriteFrame.java index 1cfbd53..b783df6 100644 --- a/core/src/com/etheller/warsmash/parsers/fdf/frames/SpriteFrame.java +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SpriteFrame.java @@ -7,6 +7,7 @@ import com.badlogic.gdx.utils.viewport.Viewport; import com.etheller.warsmash.viewer5.Scene; import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; public class SpriteFrame extends AbstractRenderableFrame { @@ -24,7 +25,7 @@ public class SpriteFrame extends AbstractRenderableFrame { } if (model != null) { this.instance = (MdxComplexInstance) model.addInstance(); - this.instance.setSequenceLoopMode(1); + this.instance.setSequenceLoopMode(SequenceLoopMode.MODEL_LOOP); this.instance.setScene(this.scene); this.instance.setLocation(this.renderBounds.x, this.renderBounds.y, 0); } diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/TextureFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/TextureFrame.java index da90653..61ba209 100644 --- a/core/src/com/etheller/warsmash/parsers/fdf/frames/TextureFrame.java +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/TextureFrame.java @@ -36,7 +36,9 @@ public class TextureFrame extends AbstractRenderableFrame { file = gameUI.getSkinField(file); } final Texture texture = gameUI.loadTexture(file); - setTexture(texture); + if (texture != null) { + setTexture(texture); + } } public void setTexture(final Texture texture) { diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/Player.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/Player.java index 889bfac..35653c4 100644 --- a/core/src/com/etheller/warsmash/parsers/w3x/w3i/Player.java +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/Player.java @@ -32,8 +32,8 @@ public class Player { this.allyLowPriorities = ParseUtils.readUInt32(stream); this.allyHighPriorities = ParseUtils.readUInt32(stream); if (version > 30) { - enemyLowPrioritiesFlags = ParseUtils.readUInt32(stream); - enemyHighPrioritiesFlags = ParseUtils.readUInt32(stream); + this.enemyLowPrioritiesFlags = ParseUtils.readUInt32(stream); + this.enemyHighPrioritiesFlags = ParseUtils.readUInt32(stream); } } @@ -51,4 +51,44 @@ public class Player { public int getByteLength() { return 33 + this.name.length(); } + + public War3ID getId() { + return this.id; + } + + public int getType() { + return this.type; + } + + public int getRace() { + return this.race; + } + + public int getIsFixedStartPosition() { + return this.isFixedStartPosition; + } + + public String getName() { + return this.name; + } + + public float[] getStartLocation() { + return this.startLocation; + } + + public long getAllyLowPriorities() { + return this.allyLowPriorities; + } + + public long getAllyHighPriorities() { + return this.allyHighPriorities; + } + + public long getEnemyLowPrioritiesFlags() { + return this.enemyLowPrioritiesFlags; + } + + public long getEnemyHighPrioritiesFlags() { + return this.enemyHighPrioritiesFlags; + } } diff --git a/core/src/com/etheller/warsmash/util/Quadtree.java b/core/src/com/etheller/warsmash/util/Quadtree.java index d3ae6d3..939a03f 100644 --- a/core/src/com/etheller/warsmash/util/Quadtree.java +++ b/core/src/com/etheller/warsmash/util/Quadtree.java @@ -38,34 +38,36 @@ public class Quadtree { add(node, 0); } - public boolean intersectsAnythingOtherThan(final T sourceObjectToIgnore, final Rectangle bounds) { + public boolean intersect(final Rectangle bounds, final QuadtreeIntersector intersector) { if (this.leaf) { for (int i = 0; i < this.nodes.size; i++) { final Node node = this.nodes.get(i); - if ((node.object != sourceObjectToIgnore) && node.bounds.overlaps(bounds)) { - return true; + if (node.bounds.overlaps(bounds)) { + if (intersector.onIntersect(node.object)) { + return true; + } } } return false; } else { if (this.northeast.bounds.overlaps(bounds)) { - if (this.northeast.intersectsAnythingOtherThan(sourceObjectToIgnore, bounds)) { + if (this.northeast.intersect(bounds, intersector)) { return true; } } if (this.northwest.bounds.overlaps(bounds)) { - if (this.northwest.intersectsAnythingOtherThan(sourceObjectToIgnore, bounds)) { + if (this.northwest.intersect(bounds, intersector)) { return true; } } if (this.southwest.bounds.overlaps(bounds)) { - if (this.southwest.intersectsAnythingOtherThan(sourceObjectToIgnore, bounds)) { + if (this.southwest.intersect(bounds, intersector)) { return true; } } if (this.southeast.bounds.overlaps(bounds)) { - if (this.southeast.intersectsAnythingOtherThan(sourceObjectToIgnore, bounds)) { + if (this.southeast.intersect(bounds, intersector)) { return true; } } diff --git a/core/src/com/etheller/warsmash/util/QuadtreeIntersector.java b/core/src/com/etheller/warsmash/util/QuadtreeIntersector.java new file mode 100644 index 0000000..fa7a890 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/QuadtreeIntersector.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.util; + +public interface QuadtreeIntersector { + /** + * Handles what to do when the intersector finds an intersecting object, + * returning true if we should stop the intersection test and process no more + * objects. + * + * @param intersectingObject + * @return + */ + boolean onIntersect(T intersectingObject); +} diff --git a/core/src/com/etheller/warsmash/viewer5/AudioContext.java b/core/src/com/etheller/warsmash/viewer5/AudioContext.java index 6ee37f0..77e5154 100644 --- a/core/src/com/etheller/warsmash/viewer5/AudioContext.java +++ b/core/src/com/etheller/warsmash/viewer5/AudioContext.java @@ -3,7 +3,6 @@ package com.etheller.warsmash.viewer5; public class AudioContext { private boolean running = false; public Listener listener = new Listener(); - public long lastUnitResponseEndTimeMillis; public AudioDestination destination = new AudioDestination() { }; diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AttachmentInstance.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AttachmentInstance.java index cbabcc7..5b31ca5 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AttachmentInstance.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AttachmentInstance.java @@ -13,7 +13,7 @@ public class AttachmentInstance implements UpdatableObject { final MdxModel internalModel = attachment.internalModel; final MdxComplexInstance internalInstance = (MdxComplexInstance) internalModel.addInstance(); - internalInstance.setSequenceLoopMode(2); + internalInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); internalInstance.dontInheritScaling = false; internalInstance.hide(); internalInstance.setParent(instance.nodes[attachment.objectId]); diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitterObject.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitterObject.java index bf1663e..be44cf7 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitterObject.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitterObject.java @@ -279,20 +279,27 @@ public class EventObjectEmitterObject extends GenericObject implements EmitterOb final String[] fileNames = ((String) animSoundsRow.get("FileNames")).split(","); final GenericResource[] resources = new GenericResource[fileNames.length]; for (int i = 0; i < fileNames.length; i++) { - final String pathString = pathSolver.solve( - ((String) animSoundsRow.get("DirectoryBase")) + fileNames[i], - model.solverParams).finalSrc; - final GenericResource genericResource = viewer.loadGeneric(pathString, - FetchDataTypeName.ARRAY_BUFFER, new LoadGenericSoundCallback(pathString)); - if (genericResource == null) { - throw new IllegalStateException("Null sound: " + fileNames[i]); + final String path = ((String) animSoundsRow.get("DirectoryBase")) + fileNames[i]; + try { + final String pathString = pathSolver.solve(path, model.solverParams).finalSrc; + final GenericResource genericResource = viewer.loadGeneric(pathString, + FetchDataTypeName.ARRAY_BUFFER, new LoadGenericSoundCallback(pathString)); + if (genericResource == null) { + System.err.println("Null sound: " + fileNames[i]); + } + resources[i] = genericResource; + } + catch (final Exception exc) { + System.err.println("Failed to load sound: " + path); + exc.printStackTrace(); } - resources[i] = genericResource; } // TODO JS async removed for (final GenericResource resource : resources) { - this.decodedBuffers.add((Sound) resource.data); + if (resource != null) { + this.decodedBuffers.add((Sound) resource.data); + } } this.ok = true; } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSnd.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSnd.java index f7bf38a..575fea3 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSnd.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSnd.java @@ -29,6 +29,9 @@ public class EventObjectSnd extends EmittedObject decodedBuffers = emitterObject.decodedBuffers; + if (decodedBuffers.isEmpty()) { + return; + } final AudioPanner panner = audioContext.createPanner(); final AudioBufferSource source = audioContext.createBufferSource(); final Vector3 location = node.worldLocation; diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxComplexInstance.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxComplexInstance.java index ded730c..16887a9 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxComplexInstance.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxComplexInstance.java @@ -42,10 +42,11 @@ public class MdxComplexInstance extends ModelInstance { public MdxNode[] nodes; public SkeletalNode[] sortedNodes; public int frame = 0; + public float floatingFrame = 0; // Global sequences public int counter = 0; public int sequence = -1; - public int sequenceLoopMode = 0; + public SequenceLoopMode sequenceLoopMode = SequenceLoopMode.NEVER_LOOP; public boolean sequenceEnded = false; public float[] vertexColor = { 1, 1, 1, 1 }; // Particles do not spawn when the sequence is -1, or when the sequence finished @@ -523,34 +524,40 @@ public class MdxComplexInstance extends ModelInstance { if (sequenceId != -1) { final Sequence sequence = model.sequences.get(sequenceId); final long[] interval = sequence.getInterval(); - final int frameTime = (int) (dt * 1000 * this.animationSpeed); + final float frameTime = (dt * 1000 * this.animationSpeed); - this.frame += frameTime; - this.counter += frameTime; + final int lastIntegerFrame = this.frame; + this.floatingFrame += frameTime; + this.frame = (int) this.floatingFrame; + final int integerFrameTime = this.frame - lastIntegerFrame; + this.counter += integerFrameTime; this.allowParticleSpawn = true; - if (this.frame >= interval[1]) { - if ((this.sequenceLoopMode == 2) || ((this.sequenceLoopMode == 1) && (sequence.getFlags() == 0))) { - this.frame = (int) interval[0]; // TODO not cast + if (this.floatingFrame >= interval[1]) { + if ((this.sequenceLoopMode == SequenceLoopMode.ALWAYS_LOOP) + || ((this.sequenceLoopMode == SequenceLoopMode.MODEL_LOOP) && (sequence.getFlags() == 0))) { + this.floatingFrame = this.frame = (int) interval[0]; // TODO not cast this.resetEventEmitters(); } - else if (this.sequenceLoopMode == 4) { // faux queued animation mode - final int framesPast = this.frame - (int) interval[1]; + else if (this.sequenceLoopMode == SequenceLoopMode.LOOP_TO_NEXT_ANIMATION) { // faux queued animation + // mode + final float framesPast = this.floatingFrame - interval[1]; final List sequences = model.sequences; this.sequence = (this.sequence + 1) % sequences.size(); - this.frame = (int) sequences.get(this.sequence).getInterval()[0] + framesPast; // TODO not cast + this.floatingFrame = sequences.get(this.sequence).getInterval()[0] + framesPast; // TODO not cast + this.frame = (int) this.floatingFrame; this.sequenceEnded = false; this.resetEventEmitters(); this.forced = true; } else { - this.frame = (int) interval[1]; // TODO not cast - this.counter -= frameTime; + this.floatingFrame = this.frame = (int) interval[1]; // TODO not cast + this.counter -= integerFrameTime; this.allowParticleSpawn = false; } - if (this.sequenceLoopMode == 3) { + if (this.sequenceLoopMode == SequenceLoopMode.NEVER_LOOP_AND_HIDE_WHEN_DONE) { hide(); } @@ -636,10 +643,12 @@ public class MdxComplexInstance extends ModelInstance { if ((id < 0) || (id > (sequences.size() - 1))) { this.sequence = -1; this.frame = 0; + this.floatingFrame = 0; this.allowParticleSpawn = false; } else { this.frame = (int) sequences.get(id).getInterval()[0]; // TODO not cast + this.floatingFrame = this.frame; this.sequenceEnded = false; } @@ -656,7 +665,7 @@ public class MdxComplexInstance extends ModelInstance { * and 2 to always loop. 3 was added by Retera as "hide after done" for gameplay * spawned effects */ - public MdxComplexInstance setSequenceLoopMode(final int mode) { + public MdxComplexInstance setSequenceLoopMode(final SequenceLoopMode mode) { this.sequenceLoopMode = mode; return this; diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdSequence.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdSequence.java index 1a6801d..beb0523 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdSequence.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdSequence.java @@ -109,11 +109,11 @@ public final class SdSequence { // with the default value. if (framesBuilder.get(framesBuilder.size() - 1) != end) { framesBuilder.add(end); - valuesBuilder.add(valuesBuilder.get(0)); + valuesBuilder.add(valuesBuilder.get(valuesBuilder.size() - 1)); if (interpolationType > 1) { - inTansBuilder.add(inTansBuilder.get(0)); - outTansBuilder.add(outTansBuilder.get(0)); + inTansBuilder.add(inTansBuilder.get(inTansBuilder.size() - 1)); + outTansBuilder.add(outTansBuilder.get(outTansBuilder.size() - 1)); } } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SequenceLoopMode.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SequenceLoopMode.java new file mode 100644 index 0000000..16c249a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SequenceLoopMode.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public enum SequenceLoopMode { + NEVER_LOOP, + MODEL_LOOP, + ALWAYS_LOOP, + NEVER_LOOP_AND_HIDE_WHEN_DONE, // used by spawned effects + LOOP_TO_NEXT_ANIMATION; // used by the Arthas vs Illidan tech demo +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaFile.java b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaFile.java index f7a766b..8ac69dc 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaFile.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaFile.java @@ -24,7 +24,7 @@ public class TgaFile { /** * Read a TGA image from a file - * + * * @param file * @return * @throws FileNotFoundException @@ -37,7 +37,7 @@ public class TgaFile { /** * Read a TGA image from an input stream. - * + * * @param name * @param stream * @return @@ -142,7 +142,7 @@ public class TgaFile { /** * Write a BufferedImage to a TGA file BufferedImages should be TYPE_INT_ARGB or * TYPE_INT_RGB - * + * * @param src * @param file * @throws IOException diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/AnimationTokens.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/AnimationTokens.java index bbed7f5..3d8c34c 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/AnimationTokens.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/AnimationTokens.java @@ -19,6 +19,7 @@ public class AnimationTokens { public static enum SecondaryTag { ALTERNATE, ALTERNATEEX, + BONE, CHAIN, CHANNEL, COMPLETE, @@ -56,6 +57,7 @@ public class AnimationTokens { SMALL, SPIKED, SPIN, + SPELL, SWIM, TALK, THIRD, diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/StandSequence.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SequenceUtils.java similarity index 87% rename from core/src/com/etheller/warsmash/viewer5/handlers/w3x/StandSequence.java rename to core/src/com/etheller/warsmash/viewer5/handlers/w3x/SequenceUtils.java index 61937a4..6dcc083 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/StandSequence.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SequenceUtils.java @@ -10,7 +10,11 @@ import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; -public class StandSequence { +public class SequenceUtils { + public static final EnumSet EMPTY = EnumSet.noneOf(SecondaryTag.class); + public static final EnumSet READY = EnumSet.of(SecondaryTag.READY); + public static final EnumSet FLESH = EnumSet.of(SecondaryTag.FLESH); + public static final EnumSet BONE = EnumSet.of(SecondaryTag.BONE); private static final StandSequenceComparator STAND_SEQUENCE_COMPARATOR = new StandSequenceComparator(); @@ -87,7 +91,8 @@ public class StandSequence { } public static IndexedSequence selectSequence(final AnimationTokens.PrimaryTag type, - final EnumSet tags, final List sequences) { + final EnumSet tags, final List sequences, + final boolean allowRarityVariations) { final List filtered = filterSequences(type, tags, sequences); filtered.sort(STAND_SEQUENCE_COMPARATOR); @@ -101,7 +106,7 @@ public class StandSequence { break; } - if ((Math.random() * 10) > rarity) { + if (((Math.random() * 10) > rarity) && allowRarityVariations) { return filtered.get(i); } } @@ -207,17 +212,22 @@ public class StandSequence { } } - public static void randomSequence(final MdxComplexInstance target, final PrimaryTag animationName, - final EnumSet secondaryAnimationTags) { + public static Sequence randomSequence(final MdxComplexInstance target, final PrimaryTag animationName, + final EnumSet secondaryAnimationTags, final boolean allowRarityVariations) { final MdxModel model = (MdxModel) target.model; final List sequences = model.getSequences(); - final IndexedSequence sequence = selectSequence(animationName, secondaryAnimationTags, sequences); + final IndexedSequence sequence = selectSequence(animationName, secondaryAnimationTags, sequences, + allowRarityVariations); if (sequence != null) { target.setSequence(sequence.index); + return sequence.sequence; } else { - randomStandSequence(target); + if (!secondaryAnimationTags.isEmpty()) { + return randomSequence(target, animationName, EMPTY, allowRarityVariations); + } + return null; } } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java index bec4e5c..df693ab 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java @@ -24,6 +24,8 @@ public class SplatModel { private final Texture texture; private final List batches; public final float[] color; + private final List locations; + private final List splatInstances; public SplatModel(final GL30 gl, final Texture texture, final List locations, final float[] centerOffset, final List> unitMapping) { @@ -31,16 +33,49 @@ public class SplatModel { this.batches = new ArrayList<>(); this.color = new float[] { 1, 1, 1, 1 }; + this.locations = locations; + if ((unitMapping != null) && (unitMapping.size() > 0)) { + this.splatInstances = new ArrayList<>(); + for (int i = 0; i < unitMapping.size(); i++) { + this.splatInstances.add(new SplatMover(this)); + } + } + else { + this.splatInstances = null; + } + loadBatches(gl, centerOffset); + if ((unitMapping != null) && (unitMapping.size() > 0)) { + if (this.splatInstances.size() != unitMapping.size()) { + throw new IllegalStateException(); + } + for (int i = 0; i < this.splatInstances.size(); i++) { + unitMapping.get(i).accept(this.splatInstances.get(i)); + } + } + } + + public void compact(final GL30 gl, final float[] centerOffset) { + // delete all the batches + for (final Batch b : this.batches) { + // Vertices + gl.glDeleteBuffer(b.vertexBuffer); + + // Faces. + gl.glDeleteBuffer(b.faceBuffer); + } + this.batches.clear(); + + loadBatches(gl, centerOffset); + } + + private void loadBatches(final GL30 gl, final float[] centerOffset) { final List vertices = new ArrayList<>(); final List uvs = new ArrayList<>(); final List indices = new ArrayList<>(); final List batchRenderUnits = new ArrayList<>(); - final int instances = locations.size(); + final int instances = this.locations.size(); for (int idx = 0; idx < instances; ++idx) { - final Consumer unit = ((unitMapping != null) && (idx < unitMapping.size())) - ? unitMapping.get(idx) - : null; - final float[] locs = locations.get(idx); + final float[] locs = this.locations.get(idx); final float x0 = locs[0]; final float y0 = locs[1]; final float x1 = locs[2]; @@ -61,7 +96,8 @@ public class SplatModel { * ((int) Math.ceil((x1 - x0) / 128.0) + 1); int start = vertices.size(); - final SplatMover splatMover = unit == null ? null : new SplatMover(start * 3 * 4, indices.size() * 6 * 2); + final SplatMover splatMover = (this.splatInstances == null) ? null + : this.splatInstances.get(idx).reset(start * 3 * 4, indices.size() * 6 * 2, idx); final int numVertsToCrate = splatMover == null ? newVerts : maxPossibleVerts; if (numVertsToCrate > MAX_VERTICES) { @@ -124,8 +160,7 @@ public class SplatModel { } } } - if (unit != null) { - unit.accept(splatMover); + if (this.splatInstances != null) { batchRenderUnits.add(splatMover); while (splatMover.indices.size() < maxPossibleFaces) { @@ -140,7 +175,6 @@ public class SplatModel { if (indices.size() > 0) { this.addBatch(gl, vertices, uvs, indices, batchRenderUnits); } - } private void addBatch(final GL30 gl, final List vertices, final List uvs, @@ -213,17 +247,28 @@ public class SplatModel { public float uvYScale; public float uvXScale; private int vertexBuffer; - private final int startOffset; - private final int start; + private int startOffset; + private int start; private final List vertices = new ArrayList<>(); private final List uvs = new ArrayList<>(); private final List indices = new ArrayList<>(); - private final int indicesStartOffset; + private int indicesStartOffset; + private int index; + private final SplatModel splatModel; - private SplatMover(final int i, final int indicesStartOffset) { + private SplatMover(final SplatModel splatModel) { + this.splatModel = splatModel; + } + + private SplatMover reset(final int i, final int indicesStartOffset, final int index) { this.startOffset = i; this.indicesStartOffset = indicesStartOffset; this.start = i / 12; + this.index = index; + this.vertices.clear(); + this.uvs.clear(); + this.indices.clear(); + return this; } public void move(final float deltaX, final float deltaY, final float[] centerOffset) { @@ -325,5 +370,40 @@ public class SplatModel { gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.uvsOffset + ((this.startOffset / 3) * 2), 4 * 2 * this.uvs.size(), RenderMathUtils.wrap(this.uvs)); } + + public void destroy(final GL30 gl, final float[] centerOffset) { + this.splatModel.locations.remove(this.index); + this.splatModel.splatInstances.remove(this.index); + this.splatModel.compact(gl, centerOffset); + } + + public void hide() { + // does not remove the shadow, just makes it not show, so it would still be + // using GPU resources + final GL30 gl = Gdx.gl30; + for (final float[] vertex : this.vertices) { + for (int i = 0; i < vertex.length; i++) { + vertex[i] = 0.0f; + } + } + for (final int[] indices : this.indices) { + for (int i = 0; i < indices.length; i++) { + indices[i] = 0; + } + } + for (final float[] uv : this.uvs) { + for (int i = 0; i < uv.length; i++) { + uv[i] = 0; + } + } + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.vertexBuffer); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.startOffset, 4 * 3 * this.vertices.size(), + RenderMathUtils.wrap(this.vertices)); + gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, this.faceBuffer); + gl.glBufferSubData(GL30.GL_ELEMENT_ARRAY_BUFFER, this.indicesStartOffset, 6 * 2 * this.indices.size(), + RenderMathUtils.wrapFaces(this.indices)); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.uvsOffset + ((this.startOffset / 3) * 2), + 4 * 2 * this.uvs.size(), RenderMathUtils.wrap(this.uvs)); + } } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitAckSound.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitAckSound.java new file mode 100644 index 0000000..9bd79e8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitAckSound.java @@ -0,0 +1,109 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.utils.TimeUtils; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.util.DataSourceFileHandle; +import com.etheller.warsmash.viewer5.AudioBufferSource; +import com.etheller.warsmash.viewer5.AudioContext; +import com.etheller.warsmash.viewer5.AudioPanner; +import com.etheller.warsmash.viewer5.gl.Extensions; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; + +public final class UnitAckSound { + private static final UnitAckSound SILENT = new UnitAckSound(0, 0, 0, 0, 0, 0); + + private final List sounds = new ArrayList<>(); + private final float volume; + private final float pitch; + private final float pitchVariation; + private final float minDistance; + private final float maxDistance; + private final float distanceCutoff; + + private Sound lastPlayedSound; + + public static UnitAckSound create(final DataSource dataSource, final DataTable unitAckSounds, final String soundName, + final String soundType) { + final Element row = unitAckSounds.get(soundName + soundType); + if (row == null) { + return SILENT; + } + final String fileNames = row.getField("FileNames"); + String directoryBase = row.getField("DirectoryBase"); + if ((directoryBase.length() > 1) && !directoryBase.endsWith("\\")) { + directoryBase += "\\"; + } + final float volume = row.getFieldFloatValue("Volume"); + final float pitch = row.getFieldFloatValue("Pitch"); + final float pitchVariation = row.getFieldFloatValue("PitchVariance"); + final float minDistance = row.getFieldFloatValue("MinDistance"); + final float maxDistance = row.getFieldFloatValue("MaxDistance"); + final float distanceCutoff = row.getFieldFloatValue("DistanceCutoff"); + final UnitAckSound sound = new UnitAckSound(volume, pitch, pitchVariation, minDistance, maxDistance, distanceCutoff); + for (final String fileName : fileNames.split(",")) { + String filePath = directoryBase + fileName; + if (!filePath.toLowerCase().endsWith(".wav")) { + filePath += ".wav"; + } + if (dataSource.has(filePath)) { + sound.sounds.add(Gdx.audio.newSound(new DataSourceFileHandle(dataSource, filePath))); + } + } + return sound; + } + + public UnitAckSound(final float volume, final float pitch, final float pitchVariation, final float minDistance, + final float maxDistance, final float distanceCutoff) { + this.volume = volume; + this.pitch = pitch; + this.pitchVariation = pitchVariation; + this.minDistance = minDistance; + this.maxDistance = maxDistance; + this.distanceCutoff = distanceCutoff; + } + + public boolean play(final AudioContext audioContext, final RenderUnit unit) { + return play(audioContext, unit, (int) (Math.random() * this.sounds.size())); + } + + public boolean play(final AudioContext audioContext, final RenderUnit unit, final int index) { + if (this.sounds.isEmpty()) { + return false; + } + final long millisTime = TimeUtils.millis(); + if (millisTime < unit.lastUnitResponseEndTimeMillis) { + return false; + } + + final AudioPanner panner = audioContext.createPanner(); + final AudioBufferSource source = audioContext.createBufferSource(); + + // Panner settings + panner.setPosition(unit.location[0], unit.location[1], unit.location[2]); + panner.maxDistance = this.distanceCutoff; + panner.refDistance = this.minDistance; + panner.connect(audioContext.destination); + + // Source. + source.buffer = this.sounds.get(index); + source.connect(panner); + + // Make a sound. + source.start(0); + this.lastPlayedSound = source.buffer; + final float duration = Extensions.soundLengthExtension.getDuration(this.lastPlayedSound); + unit.lastUnitResponseEndTimeMillis = millisTime + (long) (1000 * duration); + return true; + } + + public int getSoundCount() { + return this.sounds.size(); + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSound.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSound.java new file mode 100644 index 0000000..68a991e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSound.java @@ -0,0 +1,120 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.utils.TimeUtils; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.util.DataSourceFileHandle; +import com.etheller.warsmash.viewer5.AudioBufferSource; +import com.etheller.warsmash.viewer5.AudioContext; +import com.etheller.warsmash.viewer5.AudioPanner; +import com.etheller.warsmash.viewer5.gl.Extensions; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; + +public final class UnitSound { + private static final UnitSound SILENT = new UnitSound(0, 0, 0, 0, 0, 0); + + private final List sounds = new ArrayList<>(); + private final float volume; + private final float pitch; + private final float pitchVariation; + private final float minDistance; + private final float maxDistance; + private final float distanceCutoff; + + private Sound lastPlayedSound; + + public static UnitSound create(final DataSource dataSource, final DataTable unitAckSounds, final String soundName, + final String soundType) { + final Element row = unitAckSounds.get(soundName + soundType); + if (row == null) { + return SILENT; + } + final String fileNames = row.getField("FileNames"); + String directoryBase = row.getField("DirectoryBase"); + if ((directoryBase.length() > 1) && !directoryBase.endsWith("\\")) { + directoryBase += "\\"; + } + final float volume = row.getFieldFloatValue("Volume"); + final float pitch = row.getFieldFloatValue("Pitch"); + final float pitchVariation = row.getFieldFloatValue("PitchVariance"); + final float minDistance = row.getFieldFloatValue("MinDistance"); + final float maxDistance = row.getFieldFloatValue("MaxDistance"); + final float distanceCutoff = row.getFieldFloatValue("DistanceCutoff"); + final UnitSound sound = new UnitSound(volume, pitch, pitchVariation, minDistance, maxDistance, distanceCutoff); + for (final String fileName : fileNames.split(",")) { + String filePath = directoryBase + fileName; + if (!filePath.toLowerCase().endsWith(".wav")) { + filePath += ".wav"; + } + if (dataSource.has(filePath)) { + sound.sounds.add(Gdx.audio.newSound(new DataSourceFileHandle(dataSource, filePath))); + } + } + return sound; + } + + public UnitSound(final float volume, final float pitch, final float pitchVariation, final float minDistance, + final float maxDistance, final float distanceCutoff) { + this.volume = volume; + this.pitch = pitch; + this.pitchVariation = pitchVariation; + this.minDistance = minDistance; + this.maxDistance = maxDistance; + this.distanceCutoff = distanceCutoff; + } + + public boolean playUnitResponse(final AudioContext audioContext, final RenderUnit unit) { + return playUnitResponse(audioContext, unit, (int) (Math.random() * this.sounds.size())); + } + + public boolean playUnitResponse(final AudioContext audioContext, final RenderUnit unit, final int index) { + final long millisTime = TimeUtils.millis(); + if (millisTime < unit.lastUnitResponseEndTimeMillis) { + return false; + } + if (play(audioContext, unit.location[0], unit.location[1])) { + final float duration = Extensions.soundLengthExtension.getDuration(this.lastPlayedSound); + unit.lastUnitResponseEndTimeMillis = millisTime + (long) (1000 * duration); + return true; + } + return false; + } + + public boolean play(final AudioContext audioContext, final float x, final float y) { + return play(audioContext, x, y, (int) (Math.random() * this.sounds.size())); + } + + public boolean play(final AudioContext audioContext, final float x, final float y, final int index) { + if (this.sounds.isEmpty()) { + return false; + } + + final AudioPanner panner = audioContext.createPanner(); + final AudioBufferSource source = audioContext.createBufferSource(); + + // Panner settings + panner.setPosition(x, y, 0); + panner.maxDistance = this.distanceCutoff; + panner.refDistance = this.minDistance; + panner.connect(audioContext.destination); + + // Source. + source.buffer = this.sounds.get(index); + source.connect(panner); + + // Make a sound. + source.start(0); + this.lastPlayedSound = source.buffer; + return true; + } + + public int getSoundCount() { + return this.sounds.size(); + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSoundset.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSoundset.java index 40d7253..70553ef 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSoundset.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSoundset.java @@ -1,127 +1,23 @@ package com.etheller.warsmash.viewer5.handlers.w3x; -import java.util.ArrayList; -import java.util.List; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Sound; -import com.badlogic.gdx.utils.TimeUtils; import com.etheller.warsmash.datasources.DataSource; import com.etheller.warsmash.units.DataTable; -import com.etheller.warsmash.units.Element; -import com.etheller.warsmash.util.DataSourceFileHandle; -import com.etheller.warsmash.viewer5.AudioBufferSource; -import com.etheller.warsmash.viewer5.AudioContext; -import com.etheller.warsmash.viewer5.AudioPanner; -import com.etheller.warsmash.viewer5.gl.Extensions; public class UnitSoundset { - public final UnitAckSound what; - public final UnitAckSound pissed; - public final UnitAckSound yesAttack; - public final UnitAckSound yes; - public final UnitAckSound ready; - public final UnitAckSound warcry; + public final UnitSound what; + public final UnitSound pissed; + public final UnitSound yesAttack; + public final UnitSound yes; + public final UnitSound ready; + public final UnitSound warcry; public UnitSoundset(final DataSource dataSource, final DataTable unitAckSounds, final String soundName) { - this.what = UnitAckSound.create(dataSource, unitAckSounds, soundName, "What"); - this.pissed = UnitAckSound.create(dataSource, unitAckSounds, soundName, "Pissed"); - this.yesAttack = UnitAckSound.create(dataSource, unitAckSounds, soundName, "YesAttack"); - this.yes = UnitAckSound.create(dataSource, unitAckSounds, soundName, "Yes"); - this.ready = UnitAckSound.create(dataSource, unitAckSounds, soundName, "Ready"); - this.warcry = UnitAckSound.create(dataSource, unitAckSounds, soundName, "Warcry"); + this.what = UnitSound.create(dataSource, unitAckSounds, soundName, "What"); + this.pissed = UnitSound.create(dataSource, unitAckSounds, soundName, "Pissed"); + this.yesAttack = UnitSound.create(dataSource, unitAckSounds, soundName, "YesAttack"); + this.yes = UnitSound.create(dataSource, unitAckSounds, soundName, "Yes"); + this.ready = UnitSound.create(dataSource, unitAckSounds, soundName, "Ready"); + this.warcry = UnitSound.create(dataSource, unitAckSounds, soundName, "Warcry"); } - public static final class UnitAckSound { - private static final UnitAckSound SILENT = new UnitAckSound(0, 0, 0, 0, 0, 0); - - private final List sounds = new ArrayList<>(); - private final float volume; - private final float pitch; - private final float pitchVariation; - private final float minDistance; - private final float maxDistance; - private final float distanceCutoff; - - private Sound lastPlayedSound; - - public static UnitAckSound create(final DataSource dataSource, final DataTable unitAckSounds, - final String soundName, final String soundType) { - final Element row = unitAckSounds.get(soundName + soundType); - if (row == null) { - return SILENT; - } - final String fileNames = row.getField("FileNames"); - String directoryBase = row.getField("DirectoryBase"); - if ((directoryBase.length() > 1) && !directoryBase.endsWith("\\")) { - directoryBase += "\\"; - } - final float volume = row.getFieldFloatValue("Volume"); - final float pitch = row.getFieldFloatValue("Pitch"); - final float pitchVariation = row.getFieldFloatValue("PitchVariance"); - final float minDistance = row.getFieldFloatValue("MinDistance"); - final float maxDistance = row.getFieldFloatValue("MaxDistance"); - final float distanceCutoff = row.getFieldFloatValue("DistanceCutoff"); - final UnitAckSound sound = new UnitAckSound(volume, pitch, pitchVariation, minDistance, maxDistance, - distanceCutoff); - for (final String fileName : fileNames.split(",")) { - String filePath = directoryBase + fileName; - if (!filePath.toLowerCase().endsWith(".wav")) { - filePath += ".wav"; - } - if (dataSource.has(filePath)) { - sound.sounds.add(Gdx.audio.newSound(new DataSourceFileHandle(dataSource, filePath))); - } - } - return sound; - } - - public UnitAckSound(final float volume, final float pitch, final float pitchVariation, final float minDistance, - final float maxDistance, final float distanceCutoff) { - this.volume = volume; - this.pitch = pitch; - this.pitchVariation = pitchVariation; - this.minDistance = minDistance; - this.maxDistance = maxDistance; - this.distanceCutoff = distanceCutoff; - } - - public boolean play(final AudioContext audioContext, final float x, final float y) { - return play(audioContext, x, y, (int) (Math.random() * this.sounds.size())); - } - - public boolean play(final AudioContext audioContext, final float x, final float y, final int index) { - if (this.sounds.isEmpty()) { - return false; - } - final long millisTime = TimeUtils.millis(); - if (millisTime < audioContext.lastUnitResponseEndTimeMillis) { - return false; - } - - final AudioPanner panner = audioContext.createPanner(); - final AudioBufferSource source = audioContext.createBufferSource(); - - // Panner settings - panner.setPosition(x, y, 0); - panner.maxDistance = this.distanceCutoff; - panner.refDistance = this.minDistance; - panner.connect(audioContext.destination); - - // Source. - source.buffer = this.sounds.get(index); - source.connect(panner); - - // Make a sound. - source.start(0); - this.lastPlayedSound = source.buffer; - final float duration = Extensions.soundLengthExtension.getDuration(this.lastPlayedSound); - audioContext.lastUnitResponseEndTimeMillis = millisTime + (long) (1000 * duration); - return true; - } - - public int getSoundCount() { - return this.sounds.size(); - } - } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/War3MapViewer.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/War3MapViewer.java index 7b9a16b..30f82e5 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/War3MapViewer.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/War3MapViewer.java @@ -17,6 +17,8 @@ import java.util.Map; import java.util.Random; import java.util.function.Consumer; +import javax.imageio.ImageIO; + import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; @@ -60,11 +62,14 @@ import com.etheller.warsmash.viewer5.gl.WebGL; import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; import com.etheller.warsmash.viewer5.handlers.mdx.MdxHandler; import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; import com.etheller.warsmash.viewer5.handlers.tga.TgaFile; import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; import com.etheller.warsmash.viewer5.handlers.w3x.environment.Terrain; import com.etheller.warsmash.viewer5.handlers.w3x.environment.Terrain.Splat; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderAttackInstant; import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderAttackProjectile; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderEffect; import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderItem; import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; @@ -74,11 +79,13 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityAttack; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.BooleanAbilityActivationReceiver; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.CWidgetAbilityTargetCheckReceiver; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.PointAbilityTargetCheckReceiver; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ProjectileCreator; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.SimulationRenderController; import mpq.MPQArchive; import mpq.MPQException; @@ -129,7 +136,7 @@ public class War3MapViewer extends ModelViewer { public MappedData unitMetaData = new MappedData(); public List units = new ArrayList<>(); public List items = new ArrayList<>(); - public List projectiles = new ArrayList<>(); + public List projectiles = new ArrayList<>(); public boolean unitsReady; public War3Map mapMpq; public PathSolver mapPathSolver = PathSolver.DEFAULT; @@ -143,6 +150,7 @@ public class War3MapViewer extends ModelViewer { public List selModels = new ArrayList<>(); public List selected = new ArrayList<>(); private DataTable unitAckSoundsTable; + private DataTable unitCombatSoundsTable; private DataTable miscData; private DataTable unitGlobalStrings; private MdxComplexInstance confirmationInstance; @@ -159,6 +167,8 @@ public class War3MapViewer extends ModelViewer { private final List selectionCircleSizes = new ArrayList<>(); + private final Map unitToRenderPeer = new HashMap<>(); + public War3MapViewer(final DataSource dataSource, final CanvasProvider canvas) { super(dataSource, canvas); this.gameDataSource = dataSource; @@ -229,6 +239,11 @@ public class War3MapViewer extends ModelViewer { try (InputStream terrainSlkStream = this.dataSource.getResourceAsStream("UI\\SoundInfo\\UnitAckSounds.slk")) { this.unitAckSoundsTable.readSLK(terrainSlkStream); } + this.unitCombatSoundsTable = new DataTable(worldEditStrings); + try (InputStream terrainSlkStream = this.dataSource + .getResourceAsStream("UI\\SoundInfo\\UnitCombatSounds.slk")) { + this.unitCombatSoundsTable.readSLK(terrainSlkStream); + } this.miscData = new DataTable(worldEditStrings); try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("UI\\MiscData.txt")) { this.miscData.readTXT(miscDataTxtStream, true); @@ -236,6 +251,9 @@ public class War3MapViewer extends ModelViewer { try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("Units\\MiscData.txt")) { this.miscData.readTXT(miscDataTxtStream, true); } + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("Units\\MiscGame.txt")) { + this.miscData.readTXT(miscDataTxtStream, true); + } this.unitGlobalStrings = new DataTable(worldEditStrings); try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("Units\\UnitGlobalStrings.txt")) { this.unitGlobalStrings.readTXT(miscDataTxtStream, true); @@ -294,7 +312,8 @@ public class War3MapViewer extends ModelViewer { try (InputStream mapStream = compoundDataSource.getResourceAsStream(tileset + ".mpq")) { if (mapStream == null) { tilesetSource = new CompoundDataSource(Arrays.asList(compoundDataSource, - new SubdirDataSource(compoundDataSource, tileset + ".mpq/"))); + new SubdirDataSource(compoundDataSource, tileset + ".mpq/"), + new SubdirDataSource(compoundDataSource, "_tilesets/" + tileset + ".w3mod/"))); } else { final byte[] mapData = IOUtils.toByteArray(mapStream); @@ -306,7 +325,8 @@ public class War3MapViewer extends ModelViewer { } catch (final IOException exc) { tilesetSource = new CompoundDataSource( - Arrays.asList(compoundDataSource, new SubdirDataSource(compoundDataSource, tileset + ".mpq/"))); + Arrays.asList(compoundDataSource, new SubdirDataSource(compoundDataSource, tileset + ".mpq/"), + new SubdirDataSource(compoundDataSource, "_tilesets/" + tileset + ".w3mod/"))); } } catch (final MPQException e) { @@ -339,7 +359,7 @@ public class War3MapViewer extends ModelViewer { final MdxModel confirmation = (MdxModel) load("UI\\Feedback\\Confirmation\\Confirmation.mdx", PathSolver.DEFAULT, null); this.confirmationInstance = (MdxComplexInstance) confirmation.addInstance(); - this.confirmationInstance.setSequenceLoopMode(3); + this.confirmationInstance.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP_AND_HIDE_WHEN_DONE); this.confirmationInstance.setSequence(0); this.confirmationInstance.setScene(this.worldScene); @@ -352,57 +372,124 @@ public class War3MapViewer extends ModelViewer { final Warcraft3MapObjectData modifications = this.mapMpq.readModifications(); this.simulation = new CSimulation(this.miscData, modifications.getUnits(), modifications.getAbilities(), - new ProjectileCreator() { + new SimulationRenderController() { + private final Map keyToCombatSound = new HashMap<>(); + @Override - public CAttackProjectile create(final CSimulation simulation, final CUnit source, - final int attackIndex, final CWidget target) { + public CAttackProjectile createAttackProjectile(final CSimulation simulation, final float launchX, + final float launchY, final float launchFacing, final CUnit source, + final CUnitAttackMissile unitAttack, final CWidget target, final float damage, + final int bounceIndex) { final War3ID typeId = source.getTypeId(); - final int a1ProjectileSpeed = simulation.getUnitData().getA1ProjectileSpeed(typeId); - final float a1ProjectileArc = simulation.getUnitData().getA1ProjectileArc(typeId); - String a1MissileArt = simulation.getUnitData().getA1MissileArt(typeId); - final int a1MinDamage = simulation.getUnitData().getA1MinDamage(typeId); - final int a1MaxDamage = simulation.getUnitData().getA1MaxDamage(typeId); + final int projectileSpeed = unitAttack.getProjectileSpeed(); + final float projectileArc = unitAttack.getProjectileArc(); + String missileArt = unitAttack.getProjectileArt(); final float projectileLaunchX = simulation.getUnitData().getProjectileLaunchX(typeId); final float projectileLaunchY = simulation.getUnitData().getProjectileLaunchY(typeId); final float projectileLaunchZ = simulation.getUnitData().getProjectileLaunchZ(typeId); - final int damage = War3MapViewer.this.seededRandom.nextInt(a1MaxDamage - a1MinDamage) - + a1MinDamage; - if (a1MissileArt.toLowerCase().endsWith(".mdl")) { - a1MissileArt = a1MissileArt.substring(0, a1MissileArt.length() - 4); + if (missileArt.toLowerCase().endsWith(".mdl")) { + missileArt = missileArt.substring(0, missileArt.length() - 4); } - if (!a1MissileArt.toLowerCase().endsWith(".mdx")) { - a1MissileArt += ".mdx"; + if (!missileArt.toLowerCase().endsWith(".mdx")) { + missileArt += ".mdx"; } - final float facing = (float) Math.toRadians(source.getFacing()); + final float facing = launchFacing; final float sinFacing = (float) Math.sin(facing); final float cosFacing = (float) Math.cos(facing); - final float x = (source.getX() + (projectileLaunchY * cosFacing)) - - (projectileLaunchX * sinFacing); - final float y = source.getY() + (projectileLaunchY * sinFacing) - + (projectileLaunchX * cosFacing); + final float x = (launchX + (projectileLaunchY * cosFacing)) + (projectileLaunchX * sinFacing); + final float y = (launchY + (projectileLaunchY * sinFacing)) - (projectileLaunchX * cosFacing); final float height = War3MapViewer.this.terrain.getGroundHeight(x, y) + source.getFlyHeight() + projectileLaunchZ; final CAttackProjectile simulationAttackProjectile = new CAttackProjectile(x, y, - a1ProjectileSpeed, target, source, damage); + projectileSpeed, target, source, damage, unitAttack, bounceIndex); - final MdxModel model = (MdxModel) load(a1MissileArt, War3MapViewer.this.mapPathSolver, + final MdxModel model = (MdxModel) load(missileArt, War3MapViewer.this.mapPathSolver, War3MapViewer.this.solverParams); final MdxComplexInstance modelInstance = (MdxComplexInstance) model.addInstance(); modelInstance.setTeamColor(source.getPlayerIndex()); modelInstance.setScene(War3MapViewer.this.worldScene); - StandSequence.randomBirthSequence(modelInstance); + if (bounceIndex == 0) { + SequenceUtils.randomBirthSequence(modelInstance); + } + else { + SequenceUtils.randomStandSequence(modelInstance); + } modelInstance.setLocation(x, y, height); final RenderAttackProjectile renderAttackProjectile = new RenderAttackProjectile( - simulationAttackProjectile, modelInstance, height, a1ProjectileArc, War3MapViewer.this); + simulationAttackProjectile, modelInstance, height, projectileArc, War3MapViewer.this); War3MapViewer.this.projectiles.add(renderAttackProjectile); return simulationAttackProjectile; } + + @Override + public void createInstantAttackEffect(final CSimulation cSimulation, final CUnit source, + final CUnitAttackInstant unitAttack, final CWidget target) { + final War3ID typeId = source.getTypeId(); + + String missileArt = unitAttack.getProjectileArt(); + final float projectileLaunchX = War3MapViewer.this.simulation.getUnitData() + .getProjectileLaunchX(typeId); + final float projectileLaunchY = War3MapViewer.this.simulation.getUnitData() + .getProjectileLaunchY(typeId); + if (missileArt.toLowerCase().endsWith(".mdl")) { + missileArt = missileArt.substring(0, missileArt.length() - 4); + } + if (!missileArt.toLowerCase().endsWith(".mdx")) { + missileArt += ".mdx"; + } + final float facing = (float) Math.toRadians(source.getFacing()); + final float sinFacing = (float) Math.sin(facing); + final float cosFacing = (float) Math.cos(facing); + final float x = (source.getX() + (projectileLaunchY * cosFacing)) + + (projectileLaunchX * sinFacing); + final float y = (source.getY() + (projectileLaunchY * sinFacing)) + - (projectileLaunchX * cosFacing); + + final float targetX = target.getX(); + final float targetY = target.getY(); + final float angleToTarget = (float) Math.atan2(targetY - y, targetX - x); + + final float height = War3MapViewer.this.terrain.getGroundHeight(targetX, targetY) + + target.getFlyHeight() + target.getImpactZ(); + + final MdxModel model = (MdxModel) load(missileArt, War3MapViewer.this.mapPathSolver, + War3MapViewer.this.solverParams); + final MdxComplexInstance modelInstance = (MdxComplexInstance) model.addInstance(); + modelInstance.setTeamColor(source.getPlayerIndex()); + modelInstance.setScene(War3MapViewer.this.worldScene); + SequenceUtils.randomBirthSequence(modelInstance); + modelInstance.setLocation(targetX, targetY, height); + War3MapViewer.this.projectiles + .add(new RenderAttackInstant(modelInstance, War3MapViewer.this, angleToTarget)); + } + + @Override + public void spawnUnitDamageSound(final CUnit damagedUnit, final String weaponSound, + final String armorType) { + final String key = weaponSound + armorType; + UnitSound combatSound = this.keyToCombatSound.get(key); + if (combatSound == null) { + combatSound = UnitSound.create(War3MapViewer.this.dataSource, + War3MapViewer.this.unitCombatSoundsTable, weaponSound, armorType); + this.keyToCombatSound.put(key, combatSound); + } + combatSound.play(War3MapViewer.this.worldScene.audioContext, damagedUnit.getX(), + damagedUnit.getY()); + } + + @Override + public void removeUnit(final CUnit unit) { + final RenderUnit renderUnit = War3MapViewer.this.unitToRenderPeer.remove(unit); + War3MapViewer.this.units.remove(renderUnit); + War3MapViewer.this.worldScene.removeInstance(renderUnit.instance); + } }, this.terrain.pathingGrid, - new Rectangle(centerOffset[0], centerOffset[1], (mapSize[0] * 128f) - 128, (mapSize[1] * 128f) - 128)); + new Rectangle(centerOffset[0], centerOffset[1], (mapSize[0] * 128f) - 128, (mapSize[1] * 128f) - 128), + this.seededRandom, w3iFile.getPlayers()); if (this.doodadsAndDestructiblesLoaded) { this.loadDoodadsAndDestructibles(modifications); @@ -596,6 +683,7 @@ public class War3MapViewer extends ModelViewer { MutableGameObject row = null; String path = null; Splat unitShadowSplat = null; + BufferedImage buildingPathingPixelMap = null; // Hardcoded? WorldEditorDataType type = null; @@ -693,14 +781,23 @@ public class War3MapViewer extends ModelViewer { } final String pathingTexture = row.getFieldAsString(UNIT_PATHING, 0); if ((pathingTexture != null) && (pathingTexture.length() > 0) && !"_".equals(pathingTexture)) { - BufferedImage bufferedImage = this.filePathToPathingMap.get(pathingTexture.toLowerCase()); - if (bufferedImage == null) { - bufferedImage = TgaFile.readTGA(pathingTexture, - this.mapMpq.getResourceAsStream(pathingTexture)); - this.filePathToPathingMap.put(pathingTexture.toLowerCase(), bufferedImage); + buildingPathingPixelMap = this.filePathToPathingMap.get(pathingTexture.toLowerCase()); + if (buildingPathingPixelMap == null) { + if (pathingTexture.toLowerCase().endsWith(".tga")) { + buildingPathingPixelMap = TgaFile.readTGA(pathingTexture, + this.mapMpq.getResourceAsStream(pathingTexture)); + } + else { + try (InputStream stream = this.mapMpq.getResourceAsStream(pathingTexture)) { + buildingPathingPixelMap = ImageIO.read(stream); + System.out.println("LOADING BLP PATHING: " + pathingTexture); + } + } + this.filePathToPathingMap.put(pathingTexture.toLowerCase(), buildingPathingPixelMap); } this.terrain.pathingGrid.blitPathingOverlayTexture(unit.getLocation()[0], - unit.getLocation()[1], (int) Math.toDegrees(unit.getAngle()), bufferedImage); + unit.getLocation()[1], (int) Math.toDegrees(unit.getAngle()), + buildingPathingPixelMap); } final String soundName = row.getFieldAsString(UNIT_SOUNDSET, 0); @@ -724,19 +821,12 @@ public class War3MapViewer extends ModelViewer { portraitModel = model; } if (type == WorldEditorDataType.UNITS) { - float angle; - if (this.simulation.getUnitData().isBuilding(row.getAlias())) { - // TODO pretty sure 270 is a Gameplay Constants value that should be dynamically - // loaded - angle = 270.0f; - } - else { - angle = (float) Math.toDegrees(unit.getAngle()); - } + final float angle = (float) Math.toDegrees(unit.getAngle()); final CUnit simulationUnit = this.simulation.createUnit(row.getAlias(), unit.getPlayer(), - unit.getLocation()[0], unit.getLocation()[1], angle); + unit.getLocation()[0], unit.getLocation()[1], angle, buildingPathingPixelMap); final RenderUnit renderUnit = new RenderUnit(this, model, row, unit, soundset, portraitModel, simulationUnit); + this.unitToRenderPeer.put(simulationUnit, renderUnit); this.units.add(renderUnit); if (unitShadowSplat != null) { unitShadowSplat.unitMapping.add(new Consumer() { @@ -782,10 +872,10 @@ public class War3MapViewer extends ModelViewer { for (final RenderUnit unit : this.units) { unit.updateAnimations(this); } - final Iterator projectileIterator = this.projectiles.iterator(); + final Iterator projectileIterator = this.projectiles.iterator(); while (projectileIterator.hasNext()) { - final RenderAttackProjectile projectile = projectileIterator.next(); - if (projectile.updateAnimations(this)) { + final RenderEffect projectile = projectileIterator.next(); + if (projectile.updateAnimations(this, Gdx.graphics.getDeltaTime())) { projectileIterator.remove(); } } @@ -793,7 +883,7 @@ public class War3MapViewer extends ModelViewer { final MdxComplexInstance instance = item.instance; final MdxComplexInstance mdxComplexInstance = instance; if (mdxComplexInstance.sequenceEnded || (mdxComplexInstance.sequence == -1)) { - StandSequence.randomStandSequence(mdxComplexInstance); + SequenceUtils.randomStandSequence(mdxComplexInstance); } } for (final Doodad item : this.doodads) { @@ -801,12 +891,13 @@ public class War3MapViewer extends ModelViewer { if ((instance instanceof MdxComplexInstance) && (instance != this.confirmationInstance)) { final MdxComplexInstance mdxComplexInstance = (MdxComplexInstance) instance; if (mdxComplexInstance.sequenceEnded || (mdxComplexInstance.sequence == -1)) { - StandSequence.randomStandSequence(mdxComplexInstance); + SequenceUtils.randomStandSequence(mdxComplexInstance); } } } - this.updateTime += Gdx.graphics.getRawDeltaTime(); + final float rawDeltaTime = Gdx.graphics.getRawDeltaTime(); + this.updateTime += rawDeltaTime; while (this.updateTime >= WarsmashConstants.SIMULATION_STEP_TIME) { this.updateTime -= WarsmashConstants.SIMULATION_STEP_TIME; this.simulation.update(); @@ -942,7 +1033,8 @@ public class War3MapViewer extends ModelViewer { for (final RenderUnit unit : this.units) { final MdxComplexInstance instance = unit.instance; if (instance.isVisible(this.worldScene.camera) - && instance.intersectRayWithCollision(gdxRayHeap, intersectionHeap)) { + && instance.intersectRayWithCollision(gdxRayHeap, intersectionHeap) + && !unit.getSimulationUnit().isDead()) { entity = unit; } } @@ -1081,7 +1173,6 @@ public class War3MapViewer extends ModelViewer { final Vector2 target = PointAbilityTargetCheckReceiver.INSTANCE.getTarget(); if (target != null) { ability.onOrder(this.simulation, unit.getSimulationUnit(), mousePosHeap, false); - unit.soundset.yes.play(this.worldScene.audioContext, unit.location[0], unit.location[1]); ordered = true; } else { @@ -1114,8 +1205,6 @@ public class War3MapViewer extends ModelViewer { final CWidget targetWidget = CWidgetAbilityTargetCheckReceiver.INSTANCE.getTarget(); if (targetWidget != null) { ability.onOrder(this.simulation, unit.getSimulationUnit(), targetWidget, false); - unit.soundset.yesAttack.play(this.worldScene.audioContext, unit.location[0], - unit.location[1]); ordered = true; } else { @@ -1136,8 +1225,8 @@ public class War3MapViewer extends ModelViewer { } public void standOnRepeat(final MdxComplexInstance instance) { - instance.setSequenceLoopMode(2); - StandSequence.randomStandSequence(instance); + instance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + SequenceUtils.randomStandSequence(instance); } private static final class SelectionCircleSize { diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/PathingGrid.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/PathingGrid.java index 9667f49..8345ebc 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/PathingGrid.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/PathingGrid.java @@ -31,8 +31,9 @@ public class PathingGrid { // this blit function is basically copied from HiveWE, maybe remember to mention // that in credits as well: // https://github.com/stijnherfst/HiveWE/blob/master/Base/PathingMap.cpp - public void blitPathingOverlayTexture(final float positionX, final float positionY, final int rotation, + public void blitPathingOverlayTexture(final float positionX, final float positionY, final int rotationInput, final BufferedImage pathingTextureTga) { + final int rotation = (rotationInput + 450) % 360; final int divW = ((rotation % 180) != 0) ? pathingTextureTga.getHeight() : pathingTextureTga.getWidth(); final int divH = ((rotation % 180) != 0) ? pathingTextureTga.getWidth() : pathingTextureTga.getHeight(); for (int j = 0; j < pathingTextureTga.getHeight(); j++) { @@ -65,13 +66,13 @@ public class PathingGrid { final int rgb = pathingTextureTga.getRGB(i, pathingTextureTga.getHeight() - 1 - j); byte data = 0; - if ((rgb & 0xFF) > 250) { + if ((rgb & 0xFF) > 127) { data |= PathingFlags.UNBUILDABLE; } - if (((rgb & 0xFF00) >> 8) > 250) { + if (((rgb & 0xFF00) >>> 8) > 127) { data |= PathingFlags.UNFLYABLE; } - if (((rgb & 0xFF0000) >> 16) > 250) { + if (((rgb & 0xFF0000) >>> 16) > 127) { data |= PathingFlags.UNWALKABLE; } this.dynamicPathingOverlay[(yy * this.pathingGridSizes[0]) + xx] |= data; diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java index c353fe4..4799364 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java @@ -383,6 +383,8 @@ public class Terrain { (this.mapBounds[2] * 128.0f) + this.centerOffset[1], ((this.columns - this.mapBounds[1] - 1) * 128.0f) + this.centerOffset[0], ((this.rows - this.mapBounds[3] - 1) * 128.0f) + this.centerOffset[1] }; + this.shaderMapBoundsRectangle = new Rectangle(this.shaderMapBounds[0], this.shaderMapBounds[1], + this.shaderMapBounds[2] - this.shaderMapBounds[0], this.shaderMapBounds[3] - this.shaderMapBounds[1]); this.mapSize = w3eFile.getMapSize(); this.softwareGroundMesh = new SoftwareGroundMesh(this.groundHeights, this.groundCornerHeights, this.centerOffset, width, height); @@ -1184,6 +1186,7 @@ public class Terrain { static Vector3 tmp3 = new Vector3(); private final WaveBuilder waveBuilder; public PathingGrid pathingGrid; + private final Rectangle shaderMapBoundsRectangle; /** * Intersects the given ray with list of triangles. Returns the nearest @@ -1392,4 +1395,8 @@ public class Terrain { } // TODO why do we use floor if we can use int cast? return this.corners[(int) Math.floor(x)][(int) Math.floor(y)].getBoundary() == 0; } + + public Rectangle getPlayableMapArea() { + return this.shaderMapBoundsRectangle; + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackInstant.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackInstant.java new file mode 100644 index 0000000..bde7317 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackInstant.java @@ -0,0 +1,40 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import java.util.List; + +import com.etheller.warsmash.parsers.mdlx.Sequence; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.IndexedSequence; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public class RenderAttackInstant implements RenderEffect { + private final MdxComplexInstance modelInstance; + + public RenderAttackInstant(final MdxComplexInstance modelInstance, final War3MapViewer war3MapViewer, + final float yaw) { + this.modelInstance = modelInstance; + final MdxModel model = (MdxModel) this.modelInstance.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = SequenceUtils.selectSequence(PrimaryTag.DEATH, SequenceUtils.EMPTY, sequences, + true); + if (sequence != null) { + this.modelInstance.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP); + this.modelInstance.setSequence(sequence.index); + } + this.modelInstance.localRotation.setFromAxisRad(0, 0, 1, yaw); + } + + @Override + public boolean updateAnimations(final War3MapViewer war3MapViewer, final float deltaTime) { + + final boolean everythingDone = this.modelInstance.sequenceEnded; + if (everythingDone) { + war3MapViewer.worldScene.removeInstance(this.modelInstance); + } + return everythingDone; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackProjectile.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackProjectile.java index 4293f71..a96dccb 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackProjectile.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackProjectile.java @@ -2,17 +2,19 @@ package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; import java.util.List; -import com.badlogic.gdx.Gdx; import com.badlogic.gdx.math.Quaternion; import com.etheller.warsmash.parsers.mdlx.Sequence; import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; import com.etheller.warsmash.viewer5.handlers.w3x.IndexedSequence; -import com.etheller.warsmash.viewer5.handlers.w3x.StandSequence; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; -public class RenderAttackProjectile { +public class RenderAttackProjectile implements RenderEffect { private static final Quaternion pitchHeap = new Quaternion(); private final CAttackProjectile simulationProjectile; @@ -29,6 +31,8 @@ public class RenderAttackProjectile { private float yaw; private float pitch; + private boolean done = false; + private float deathTimeElapsed; public RenderAttackProjectile(final CAttackProjectile simulationProjectile, final MdxComplexInstance modelInstance, final float z, final float arc, final War3MapViewer war3MapViewer) { @@ -44,25 +48,32 @@ public class RenderAttackProjectile { final float dyToTarget = targetY - this.y; final float d2DToTarget = (float) StrictMath.sqrt((dxToTarget * dxToTarget) + (dyToTarget * dyToTarget)); final float startingDistance = d2DToTarget + this.totalTravelDistance; + float impactZ = this.simulationProjectile.getTarget().getImpactZ(); + if (simulationProjectile.getUnitAttack().getWeaponType() == CWeaponType.ARTILLERY) { + impactZ = 0; + } this.targetHeight = (war3MapViewer.terrain.getGroundHeight(targetX, targetY) - + this.simulationProjectile.getTarget().getFlyHeight() - + this.simulationProjectile.getTarget().getImpactZ()); + + this.simulationProjectile.getTarget().getFlyHeight() + impactZ); this.arcPeakHeight = arc * startingDistance; this.yaw = (float) StrictMath.atan2(dyToTarget, dxToTarget); } - public boolean updateAnimations(final War3MapViewer war3MapViewer) { - if (this.simulationProjectile.isDone()) { + @Override + public boolean updateAnimations(final War3MapViewer war3MapViewer, final float deltaTime) { + final boolean wasDone = this.done; + if (this.done = this.simulationProjectile.isDone()) { final MdxModel model = (MdxModel) this.modelInstance.model; final List sequences = model.getSequences(); - final IndexedSequence sequence = StandSequence.selectSequence("death", sequences); - if ((sequence != null) && (this.modelInstance.sequence != sequence.index)) { + final IndexedSequence sequence = SequenceUtils.selectSequence(PrimaryTag.DEATH, SequenceUtils.EMPTY, + sequences, true); + if ((sequence != null) && this.done && !wasDone) { + this.modelInstance.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP); this.modelInstance.setSequence(sequence.index); } } else { if (this.modelInstance.sequenceEnded || (this.modelInstance.sequence == -1)) { - StandSequence.randomStandSequence(this.modelInstance); + SequenceUtils.randomStandSequence(this.modelInstance); } } final float simX = this.simulationProjectile.getX(); @@ -70,13 +81,12 @@ public class RenderAttackProjectile { final float simDx = simX - this.x; final float simDy = simY - this.y; final float simD = (float) StrictMath.sqrt((simDx * simDx) + (simDy * simDy)); - final float deltaTime = Gdx.graphics.getDeltaTime(); final float speed = StrictMath.min(simD, this.simulationProjectile.getSpeed() * deltaTime); if (simD > 0) { this.x = this.x + ((speed * simDx) / simD); this.y = this.y + ((speed * simDy) / simD); - final float targetX = this.simulationProjectile.getTarget().getX(); - final float targetY = this.simulationProjectile.getTarget().getY(); + final float targetX = this.simulationProjectile.getTargetX(); + final float targetY = this.simulationProjectile.getTargetY(); final float dxToTarget = targetX - this.x; final float dyToTarget = targetY - this.y; final float d2DToTarget = (float) StrictMath.sqrt((dxToTarget * dxToTarget) + (dyToTarget * dyToTarget)); @@ -94,10 +104,16 @@ public class RenderAttackProjectile { final float arcCurrentHeight = currentHeightPercentage * this.arcPeakHeight; this.z = this.startingHeight + dz + arcCurrentHeight; - this.yaw = (float) StrictMath.atan2(dyToTarget, dxToTarget); + if (!this.done) { + this.yaw = (float) StrictMath.atan2(dyToTarget, dxToTarget); - final float slope = (-2 * (normPeakDist) * this.arcPeakHeight) / halfStartingDistance; - this.pitch = (float) StrictMath.atan2(slope + d1z, 1); + final float slope = (-2 * (normPeakDist) * this.arcPeakHeight) / halfStartingDistance; + this.pitch = (float) StrictMath.atan2(slope + d1z, 1); + } + } + if (this.done) { + this.pitch = 0; + this.deathTimeElapsed += deltaTime; } this.modelInstance.setLocation(this.x, this.y, this.z); @@ -105,7 +121,8 @@ public class RenderAttackProjectile { this.modelInstance.rotate(pitchHeap.setFromAxisRad(0, -1, 0, this.pitch)); war3MapViewer.worldScene.instanceMoved(this.modelInstance, this.x, this.y); - final boolean everythingDone = this.simulationProjectile.isDone() && this.modelInstance.sequenceEnded; + final boolean everythingDone = this.simulationProjectile.isDone() && (this.modelInstance.sequenceEnded + || (this.deathTimeElapsed >= war3MapViewer.simulation.getGameplayConstants().getBulletDeathTime())); if (everythingDone) { war3MapViewer.worldScene.removeInstance(this.modelInstance); } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderEffect.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderEffect.java new file mode 100644 index 0000000..653b006 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderEffect.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public interface RenderEffect { + boolean updateAnimations(final War3MapViewer war3MapViewer, float deltaTime); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnit.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnit.java index 318f948..8691db1 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnit.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnit.java @@ -8,6 +8,7 @@ import java.util.Queue; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.parsers.mdlx.Sequence; import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; import com.etheller.warsmash.util.ImageUtils; import com.etheller.warsmash.util.RenderMathUtils; @@ -17,8 +18,8 @@ import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; -import com.etheller.warsmash.viewer5.handlers.w3x.StandSequence; import com.etheller.warsmash.viewer5.handlers.w3x.UnitSoundset; import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; @@ -40,6 +41,7 @@ public class RenderUnit { private static final War3ID MODEL_SCALE = War3ID.fromString("usca"); private static final War3ID MOVE_HEIGHT = War3ID.fromString("umvh"); private static final War3ID ORIENTATION_INTERPOLATION = War3ID.fromString("uori"); + private static final War3ID ANIM_PROPS = War3ID.fromString("uani"); private static final float[] heapZ = new float[3]; public final MdxComplexInstance instance; public final MutableGameObject row; @@ -59,11 +61,14 @@ public class RenderUnit { private boolean swimming; - private final boolean alreadyPlayedDeath = false; + private boolean dead = false; private final UnitAnimationListenerImpl unitAnimationListenerImpl; private OrientationInterpolation orientationInterpolation; private float currentTurnVelocity = 0; + public long lastUnitResponseEndTimeMillis; + private boolean corpse; + private boolean boneCorpse; public RenderUnit(final War3MapViewer map, final MdxModel model, final MutableGameObject row, final com.etheller.warsmash.parsers.w3x.unitsdoo.Unit unit, final UnitSoundset soundset, @@ -87,6 +92,16 @@ public class RenderUnit { instance.setScene(map.worldScene); this.unitAnimationListenerImpl = new UnitAnimationListenerImpl(instance); simulationUnit.setUnitAnimationListener(this.unitAnimationListenerImpl); + final String requiredAnimationNames = row.getFieldAsString(ANIM_PROPS, 0); + TokenLoop: for (final String animationName : requiredAnimationNames.split(",")) { + final String upperCaseToken = animationName.toUpperCase(); + for (final SecondaryTag secondaryTag : SecondaryTag.values()) { + if (upperCaseToken.equals(secondaryTag.name())) { + this.unitAnimationListenerImpl.addSecondaryTag(secondaryTag); + continue TokenLoop; + } + } + } if (row != null) { heapZ[2] = simulationUnit.getFlyHeight(); @@ -194,6 +209,31 @@ public class RenderUnit { this.unitAnimationListenerImpl.removeSecondaryTag(AnimationTokens.SecondaryTag.SWIM); } this.swimming = swimming; + final boolean dead = this.simulationUnit.isDead(); + final boolean corpse = this.simulationUnit.isCorpse(); + final boolean boneCorpse = this.simulationUnit.isBoneCorpse(); + if (dead && !this.dead) { + this.unitAnimationListenerImpl.playAnimation(true, PrimaryTag.DEATH, SequenceUtils.EMPTY, 1.0f, true); + if (this.shadow != null) { + this.shadow.destroy(Gdx.gl30, map.terrain.centerOffset); + this.shadow = null; + } + if (this.selectionCircle != null) { + this.selectionCircle.destroy(Gdx.gl30, map.terrain.centerOffset); + this.selectionCircle = null; + } + } + if (boneCorpse && !this.boneCorpse) { + this.unitAnimationListenerImpl.playAnimationWithDuration(true, PrimaryTag.DECAY, SequenceUtils.BONE, + map.simulation.getGameplayConstants().getBoneDecayTime(), true); + } + else if (corpse && !this.corpse) { + this.unitAnimationListenerImpl.playAnimationWithDuration(true, PrimaryTag.DECAY, SequenceUtils.FLESH, + map.simulation.getGameplayConstants().getDecayTime(), true); + } + this.dead = dead; + this.corpse = corpse; + this.boneCorpse = boneCorpse; this.location[2] = this.simulationUnit.getFlyHeight() + groundHeight; this.instance.moveTo(this.location); float simulationFacing = this.simulationUnit.getFacing(); @@ -266,6 +306,7 @@ public class RenderUnit { private PrimaryTag currentAnimation; private EnumSet currentAnimationSecondaryTags; private float currentSpeedRatio; + private boolean currentlyAllowingRarityVariations; private final Queue animationQueue = new LinkedList<>(); public UnitAnimationListenerImpl(final MdxComplexInstance instance) { @@ -274,33 +315,59 @@ public class RenderUnit { public void addSecondaryTag(final AnimationTokens.SecondaryTag tag) { this.secondaryAnimationTags.add(tag); - playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, this.currentSpeedRatio); + playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, this.currentSpeedRatio, + this.currentlyAllowingRarityVariations); } public void removeSecondaryTag(final AnimationTokens.SecondaryTag tag) { this.secondaryAnimationTags.remove(tag); - playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, this.currentSpeedRatio); + playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, this.currentSpeedRatio, + this.currentlyAllowingRarityVariations); } @Override public void playAnimation(final boolean force, final PrimaryTag animationName, - final EnumSet secondaryAnimationTags, final float speedRatio) { + final EnumSet secondaryAnimationTags, final float speedRatio, + final boolean allowRarityVariations) { this.animationQueue.clear(); if (force || (animationName != this.currentAnimation)) { this.currentAnimation = animationName; this.currentAnimationSecondaryTags = secondaryAnimationTags; this.currentSpeedRatio = speedRatio; + this.currentlyAllowingRarityVariations = allowRarityVariations; this.recycleSet.clear(); this.recycleSet.addAll(this.secondaryAnimationTags); this.recycleSet.addAll(secondaryAnimationTags); this.instance.setAnimationSpeed(speedRatio); - StandSequence.randomSequence(this.instance, animationName, this.recycleSet); + SequenceUtils.randomSequence(this.instance, animationName, this.recycleSet, allowRarityVariations); + } + } + + public void playAnimationWithDuration(final boolean force, final PrimaryTag animationName, + final EnumSet secondaryAnimationTags, final float duration, + final boolean allowRarityVariations) { + this.animationQueue.clear(); + if (force || (animationName != this.currentAnimation)) { + this.currentAnimation = animationName; + this.currentAnimationSecondaryTags = secondaryAnimationTags; + this.currentlyAllowingRarityVariations = allowRarityVariations; + this.recycleSet.clear(); + this.recycleSet.addAll(this.secondaryAnimationTags); + this.recycleSet.addAll(secondaryAnimationTags); + final Sequence sequence = SequenceUtils.randomSequence(this.instance, animationName, this.recycleSet, + allowRarityVariations); + if (sequence != null) { + this.currentSpeedRatio = ((sequence.getInterval()[1] - sequence.getInterval()[0]) / 1000.0f) + / duration; + this.instance.setAnimationSpeed(this.currentSpeedRatio); + } } } @Override - public void queueAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags) { - this.animationQueue.add(new QueuedAnimation(animationName, secondaryAnimationTags)); + public void queueAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags, + final boolean allowRarityVariations) { + this.animationQueue.add(new QueuedAnimation(animationName, secondaryAnimationTags, allowRarityVariations)); } public void update() { @@ -310,12 +377,13 @@ public class RenderUnit { .get(this.instance.sequence).getFlags() == 0)) { // animation is a looping animation playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, - this.currentSpeedRatio); + this.currentSpeedRatio, this.currentlyAllowingRarityVariations); } else { final QueuedAnimation nextAnimation = this.animationQueue.poll(); if (nextAnimation != null) { - playAnimation(true, nextAnimation.animationName, nextAnimation.secondaryAnimationTags, 1.0f); + playAnimation(true, nextAnimation.animationName, nextAnimation.secondaryAnimationTags, 1.0f, + nextAnimation.allowRarityVariations); } } } @@ -326,10 +394,13 @@ public class RenderUnit { private static final class QueuedAnimation { private final PrimaryTag animationName; private final EnumSet secondaryAnimationTags; + private final boolean allowRarityVariations; - public QueuedAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags) { + public QueuedAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags, + final boolean allowRarityVariations) { this.animationName = animationName; this.secondaryAnimationTags = secondaryAnimationTags; + this.allowRarityVariations = allowRarityVariations; } } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructable.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructable.java index f221c03..4620684 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructable.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructable.java @@ -1,5 +1,10 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + public class CDestructable extends CWidget { public CDestructable(final int handleId, final float x, final float y, final float life) { @@ -15,4 +20,16 @@ public class CDestructable extends CWidget { public float getImpactZ() { return 0; // TODO maybe from DestructableType } + + @Override + public void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage) { + this.life -= damage; + } + + @Override + public boolean canBeTargetedBy(final CSimulation simulation, final CUnit source, + final EnumSet targetsAllowed) { + return false; + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CGameplayConstants.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CGameplayConstants.java index 3567cc6..12a1a9a 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CGameplayConstants.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CGameplayConstants.java @@ -1,7 +1,11 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +import java.util.Arrays; + import com.etheller.warsmash.units.DataTable; import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CDefenseType; /** * Stores some gameplay constants at runtime in a java object (symbol table) to @@ -9,13 +13,71 @@ import com.etheller.warsmash.units.Element; */ public class CGameplayConstants { private final float attackHalfAngle; + private final float[][] damageBonusTable; + private final float maxCollisionRadius; + private final float decayTime; + private final float boneDecayTime; + private final float bulletDeathTime; + private final float closeEnoughRange; public CGameplayConstants(final DataTable parsedDataTable) { final Element miscData = parsedDataTable.get("Misc"); - this.attackHalfAngle = (miscData.getFieldFloatValue("AttackHalfAngle")); // TODO use + // TODO use radians for half angle + this.attackHalfAngle = (float) Math.toDegrees(miscData.getFieldFloatValue("AttackHalfAngle")); + this.maxCollisionRadius = miscData.getFieldFloatValue("MaxCollisionRadius"); + this.decayTime = miscData.getFieldFloatValue("DecayTime"); + this.boneDecayTime = miscData.getFieldFloatValue("BoneDecayTime"); + this.bulletDeathTime = miscData.getFieldFloatValue("BulletDeathTime"); + this.closeEnoughRange = miscData.getFieldFloatValue("CloseEnoughRange"); + + final CDefenseType[] defenseTypeOrder = { CDefenseType.SMALL, CDefenseType.MEDIUM, CDefenseType.LARGE, + CDefenseType.FORT, CDefenseType.NORMAL, CDefenseType.HERO, CDefenseType.DIVINE, CDefenseType.NONE, }; + this.damageBonusTable = new float[CAttackType.values().length][defenseTypeOrder.length]; + for (int i = 0; i < CAttackType.VALUES.length; i++) { + Arrays.fill(this.damageBonusTable[i], 1.0f); + final CAttackType attackType = CAttackType.VALUES[i]; + final String damageBonus = miscData.getField("DamageBonus" + attackType.getDamageKey()); + final String[] damageComponents = damageBonus.split(","); + for (int j = 0; j < damageComponents.length; j++) { + if (damageComponents[j].length() > 0) { + final CDefenseType defenseType = defenseTypeOrder[j]; + try { + this.damageBonusTable[i][defenseType.ordinal()] = Float.parseFloat(damageComponents[j]); +// System.out.println(attackType + ":" + defenseType + ": " + damageComponents[j]); + } + catch (final NumberFormatException e) { + throw new RuntimeException("DamageBonus" + attackType.getDamageKey(), e); + } + } + } + } } public float getAttackHalfAngle() { return this.attackHalfAngle; } + + public float getDamageRatioAgainst(final CAttackType attackType, final CDefenseType defenseType) { + return this.damageBonusTable[attackType.ordinal()][defenseType.ordinal()]; + } + + public float getMaxCollisionRadius() { + return this.maxCollisionRadius; + } + + public float getDecayTime() { + return this.decayTime; + } + + public float getBoneDecayTime() { + return this.boneDecayTime; + } + + public float getBulletDeathTime() { + return this.bulletDeathTime; + } + + public float getCloseEnoughRange() { + return this.closeEnoughRange; + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItem.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItem.java index 81b69b9..3d167a1 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItem.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItem.java @@ -1,6 +1,10 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +import java.util.EnumSet; + import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; public class CItem extends CWidget { @@ -20,4 +24,16 @@ public class CItem extends CWidget { public float getImpactZ() { return 0; // TODO probably from ItemType } + + @Override + public void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage) { + this.life -= damage; + } + + @Override + public boolean canBeTargetedBy(final CSimulation simulation, final CUnit source, + final EnumSet targetsAllowed) { + return targetsAllowed.contains(CTargetType.ITEM); + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayer.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayer.java deleted file mode 100644 index 1afe88a..0000000 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayer.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.etheller.warsmash.viewer5.handlers.w3x.simulation; - -public class CPlayer { - private int id; - private int gold; - private int lumber; - - public CPlayer(final int id) { - this.id = id; - } - - public int getId() { - return this.id; - } - - public int getGold() { - return this.gold; - } - - public int getLumber() { - return this.lumber; - } - - public void setId(final int id) { - this.id = id; - } - - public void setGold(final int gold) { - this.gold = gold; - } - - public void setLumber(final int lumber) { - this.lumber = lumber; - } -} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java index 61ca42c..75c81e7 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java @@ -1,47 +1,82 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Random; import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.parsers.w3x.w3i.Player; import com.etheller.warsmash.units.DataTable; import com.etheller.warsmash.units.manager.MutableObjectData; import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.data.CAbilityData; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.data.CUnitData; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CPathfindingProcessor; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ProjectileCreator; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapControl; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRace; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.SimulationRenderController; public class CSimulation { private final CUnitData unitData; private final CAbilityData abilityData; private final List units; + private final List players; private final List projectiles; + private final List newProjectiles; private final HandleIdAllocator handleIdAllocator; - private transient final ProjectileCreator projectileCreator; + private transient final SimulationRenderController simulationRenderController; private int gameTurnTick = 0; private final PathingGrid pathingGrid; private final CWorldCollision worldCollision; private final CPathfindingProcessor pathfindingProcessor; private final CGameplayConstants gameplayConstants; + private final Random seededRandom; public CSimulation(final DataTable miscData, final MutableObjectData parsedUnitData, - final MutableObjectData parsedAbilityData, final ProjectileCreator projectileCreator, - final PathingGrid pathingGrid, final Rectangle entireMapBounds) { + final MutableObjectData parsedAbilityData, final SimulationRenderController simulationRenderController, + final PathingGrid pathingGrid, final Rectangle entireMapBounds, final Random seededRandom, + final List playerInfos) { this.gameplayConstants = new CGameplayConstants(miscData); - this.projectileCreator = projectileCreator; + this.simulationRenderController = simulationRenderController; this.pathingGrid = pathingGrid; this.unitData = new CUnitData(parsedUnitData); this.abilityData = new CAbilityData(parsedAbilityData); this.units = new ArrayList<>(); this.projectiles = new ArrayList<>(); + this.newProjectiles = new ArrayList<>(); this.handleIdAllocator = new HandleIdAllocator(); - this.worldCollision = new CWorldCollision(entireMapBounds); + this.worldCollision = new CWorldCollision(entireMapBounds, this.gameplayConstants.getMaxCollisionRadius()); this.pathfindingProcessor = new CPathfindingProcessor(pathingGrid, this.worldCollision); + this.seededRandom = seededRandom; + this.players = new ArrayList<>(); + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + if (i < playerInfos.size()) { + final Player playerInfo = playerInfos.get(i); + this.players.add(new CPlayer(playerInfo.getId().getValue(), CMapControl.values()[playerInfo.getType()], + playerInfo.getName(), CRace.parseRace(playerInfo.getRace()), playerInfo.getStartLocation())); + } + else { + this.players.add(new CPlayer(i, CMapControl.NONE, "Default string", CRace.OTHER, new float[] { 0, 0 })); + } + } + this.players.add(new CPlayer(this.players.size(), CMapControl.NEUTRAL, + miscData.getLocalizedString("WESTRING_PLAYER_NA"), CRace.OTHER, new float[] { 0, 0 })); + this.players.add(new CPlayer(this.players.size(), CMapControl.NEUTRAL, + miscData.getLocalizedString("WESTRING_PLAYER_NV"), CRace.OTHER, new float[] { 0, 0 })); + this.players.add(new CPlayer(this.players.size(), CMapControl.NEUTRAL, + miscData.getLocalizedString("WESTRING_PLAYER_NE"), CRace.OTHER, new float[] { 0, 0 })); + this.players.add(new CPlayer(this.players.size(), CMapControl.NEUTRAL, + miscData.getLocalizedString("WESTRING_PLAYER_NP"), CRace.OTHER, new float[] { 0, 0 })); + } public CUnitData getUnitData() { @@ -57,36 +92,48 @@ public class CSimulation { } public CUnit createUnit(final War3ID typeId, final int playerIndex, final float x, final float y, - final float facing) { - final CUnit unit = this.unitData.create(this, this.handleIdAllocator.createId(), playerIndex, typeId, x, y, - facing); + final float facing, final BufferedImage buildingPathingPixelMap) { + final CUnit unit = this.unitData.create(this, playerIndex, this.handleIdAllocator.createId(), typeId, x, y, + facing, buildingPathingPixelMap); this.units.add(unit); - if (!unit.getUnitType().isBuilding()) { - this.worldCollision.addUnit(unit); - } + this.worldCollision.addUnit(unit); return unit; } - public CAttackProjectile createProjectile(final CUnit source, final int attackIndex, final CWidget target) { - final CAttackProjectile projectile = this.projectileCreator.create(this, source, attackIndex, target); - this.projectiles.add(projectile); + public CAttackProjectile createProjectile(final CUnit source, final float launchX, final float launchY, + final float launchFacing, final CUnitAttackMissile attack, final CWidget target, final float damage, + final int bounceIndex) { + final CAttackProjectile projectile = this.simulationRenderController.createAttackProjectile(this, launchX, + launchY, launchFacing, source, attack, target, damage, bounceIndex); + this.newProjectiles.add(projectile); return projectile; } + public void createInstantAttackEffect(final CUnit source, final CUnitAttackInstant attack, final CWidget target) { + this.simulationRenderController.createInstantAttackEffect(this, source, attack, target); + } + public PathingGrid getPathingGrid() { return this.pathingGrid; } - public List findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, final float startX, - final float startY, final Point2D.Float goal, final PathingGrid.MovementType movementType, - final float collisionSize) { - return this.pathfindingProcessor.findNaiveSlowPath(ignoreIntersectionsWithThisUnit, startX, startY, goal, - movementType, collisionSize); + public List findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, + final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, + final boolean allowSmoothing) { + return this.pathfindingProcessor.findNaiveSlowPath(ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, startX, startY, goal, movementType, collisionSize, + allowSmoothing); } public void update() { - for (final CUnit unit : this.units) { - unit.update(this); + final Iterator unitIterator = this.units.iterator(); + while (unitIterator.hasNext()) { + final CUnit unit = unitIterator.next(); + if (unit.update(this)) { + unitIterator.remove(); + this.simulationRenderController.removeUnit(unit); + } } final Iterator projectileIterator = this.projectiles.iterator(); while (projectileIterator.hasNext()) { @@ -95,6 +142,8 @@ public class CSimulation { projectileIterator.remove(); } } + this.projectiles.addAll(this.newProjectiles); + this.newProjectiles.clear(); this.gameTurnTick++; } @@ -109,4 +158,16 @@ public class CSimulation { public CGameplayConstants getGameplayConstants() { return this.gameplayConstants; } + + public Random getSeededRandom() { + return this.seededRandom; + } + + public void unitDamageEvent(final CUnit damagedUnit, final String weaponSound, final String armorType) { + this.simulationRenderController.spawnUnitDamageSound(damagedUnit, weaponSound, armorType); + } + + public CPlayer getPlayer(final int index) { + return this.players.get(index); + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java index 36f83b0..a13b533 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java @@ -1,5 +1,6 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.EnumSet; import java.util.LinkedList; @@ -8,10 +9,15 @@ import java.util.Queue; import com.badlogic.gdx.math.Rectangle; import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitStateListener.CUnitStateNotifier; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CAllianceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; public class CUnit extends CWidget { private War3ID typeId; @@ -35,12 +41,22 @@ public class CUnit extends CWidget { private final EnumSet classifications = EnumSet.noneOf(CUnitClassification.class); + private int deathTurnTick; + private boolean corpse; + private boolean boneCorpse; + private transient CUnitAnimationListener unitAnimationListener; + // if you use triggers for this then the transient tag here becomes really + // questionable -- it already was -- but I meant for those to inform us + // which fields shouldn't be persisted if we do game state save later + private transient CUnitStateNotifier stateNotifier = new CUnitStateNotifier(); + public CUnit(final int handleId, final int playerIndex, final float x, final float y, final float life, final War3ID typeId, final float facing, final float mana, final int maximumLife, final int maximumMana, final int speed, final int defense, final CUnitType unitType) { super(handleId, x, y, life); + this.playerIndex = playerIndex; this.typeId = typeId; this.facing = facing; this.mana = mana; @@ -55,7 +71,7 @@ public class CUnit extends CWidget { public void setUnitAnimationListener(final CUnitAnimationListener unitAnimationListener) { this.unitAnimationListener = unitAnimationListener; - this.unitAnimationListener.playAnimation(true, PrimaryTag.STAND, CUnitAnimationListener.EMPTY, 1.0f); + this.unitAnimationListener.playAnimation(true, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, true); } public CUnitAnimationListener getUnitAnimationListener() { @@ -121,10 +137,48 @@ public class CUnit extends CWidget { } /** - * Updates one tick of simulation logic. + * Updates one tick of simulation logic and return true if it's time to remove + * this unit from the game. */ - public void update(final CSimulation game) { - if (this.currentOrder != null) { + public boolean update(final CSimulation game) { + if (isDead()) { + if (this.collisionRectangle != null) { + // Moved this here because doing it on "kill" was able to happen in some cases + // while also iterating over the units that are in the collision system, and + // then it hit the "writing while iterating" problem. + game.getWorldCollision().removeUnit(this); + } + final int gameTurnTick = game.getGameTurnTick(); + if (!this.corpse) { + if (gameTurnTick > (this.deathTurnTick + + (int) (this.unitType.getDeathTime() / WarsmashConstants.SIMULATION_STEP_TIME))) { + this.corpse = true; + if (!this.unitType.isRaise()) { + this.boneCorpse = true; + // start final phase immediately for "cant raise" case + } + if (!this.unitType.isDecay()) { + // if we dont raise AND dont decay, then now that death anim is over + // we just delete the unit + return true; + } + this.deathTurnTick = gameTurnTick; + } + } + else if (!this.boneCorpse) { + if (game.getGameTurnTick() > (this.deathTurnTick + (int) (game.getGameplayConstants().getDecayTime() + / WarsmashConstants.SIMULATION_STEP_TIME))) { + this.boneCorpse = true; + this.deathTurnTick = gameTurnTick; + } + } + else if (game + .getGameTurnTick() > (this.deathTurnTick + (int) (game.getGameplayConstants().getBoneDecayTime() + / WarsmashConstants.SIMULATION_STEP_TIME))) { + return true; + } + } + else if (this.currentOrder != null) { if (this.currentOrder.update(game)) { // remove current order, because it's completed, polling next // item from order queue @@ -132,12 +186,16 @@ public class CUnit extends CWidget { } if (this.currentOrder == null) { // maybe order "stop" here - this.unitAnimationListener.playAnimation(true, PrimaryTag.STAND, CUnitAnimationListener.EMPTY, 1.0f); + this.unitAnimationListener.playAnimation(true, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, true); } } + return false; } public void order(final COrder order, final boolean queue) { + if (isDead()) { + return; + } if (queue && (this.currentOrder != null)) { this.orderQueue.add(order); } @@ -225,13 +283,199 @@ public class CUnit extends CWidget { return this.defense; } - public void damage(final CUnit source, final CAttackType attackType, final CWeaponType weaponType, - final int damage) { - - } - @Override public float getImpactZ() { return this.unitType.getImpactZ(); } + + public double distance(final CWidget target) { + double dx = Math.abs(target.getX() - getX()); + double dy = Math.abs(target.getY() - getY()); + final float thisCollisionSize = this.unitType.getCollisionSize(); + float targetCollisionSize; + if (target instanceof CUnit) { + final CUnitType targetUnitType = ((CUnit) target).getUnitType(); + targetCollisionSize = targetUnitType.getCollisionSize(); + } + else { + targetCollisionSize = 0; // TODO destructable collision size here + } + if (dx < 0) { + dx = 0; + } + if (dy < 0) { + dy = 0; + } + + double groundDistance = StrictMath.sqrt((dx * dx) + (dy * dy)) - thisCollisionSize - targetCollisionSize; + if (groundDistance < 0) { + groundDistance = 0; + } + return groundDistance; + } + + public double distance(final float x, final float y) { + double dx = Math.abs(x - getX()); + double dy = Math.abs(y - getY()); + final float thisCollisionSize = this.unitType.getCollisionSize(); + if (dx < 0) { + dx = 0; + } + if (dy < 0) { + dy = 0; + } + + double groundDistance = StrictMath.sqrt((dx * dx) + (dy * dy)) - thisCollisionSize; + if (groundDistance < 0) { + groundDistance = 0; + } + return groundDistance; + } + + @Override + public void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage) { + final boolean wasDead = isDead(); + final float damageRatioFromArmorClass = simulation.getGameplayConstants().getDamageRatioAgainst(attackType, + this.unitType.getDefenseType()); + final float damageRatioFromDefense; + if (this.defense >= 0) { + damageRatioFromDefense = 1f - (float) (((this.defense) * 0.06) / (1 + (0.06 * this.defense))); + } + else { + damageRatioFromDefense = 2f - (float) StrictMath.pow(0.94, -this.defense); + } + final float trueDamage = damageRatioFromArmorClass * damageRatioFromDefense * damage; + this.life -= trueDamage; + simulation.unitDamageEvent(this, weaponType, this.unitType.getArmorType()); + this.stateNotifier.lifeChanged(); + if (!wasDead && isDead() && !this.unitType.isBuilding()) { + kill(simulation); + } + } + + private void kill(final CSimulation simulation) { + this.currentOrder = null; + this.orderQueue.clear(); + this.deathTurnTick = simulation.getGameTurnTick(); + } + + public boolean canReach(final CWidget target, final float range) { + final double distance = distance(target); + if (target instanceof CUnit) { + final CUnit targetUnit = (CUnit) target; + final CUnitType targetUnitType = targetUnit.getUnitType(); + if (targetUnitType.isBuilding() && (targetUnitType.getBuildingPathingPixelMap() != null)) { + final float relativeOffsetX = getX() - target.getX(); + final float relativeOffsetY = getY() - target.getY(); + final int rotation = ((int) targetUnit.getFacing() + 450) % 360; + final BufferedImage buildingPathingPixelMap = targetUnitType.getBuildingPathingPixelMap(); + final int gridWidth = ((rotation % 180) != 0) ? buildingPathingPixelMap.getHeight() + : buildingPathingPixelMap.getWidth(); + final int gridHeight = ((rotation % 180) != 0) ? buildingPathingPixelMap.getWidth() + : buildingPathingPixelMap.getHeight(); + final int relativeGridX = (int) Math.floor(relativeOffsetX / 32f) + (gridWidth / 2); + final int relativeGridY = (int) Math.floor(relativeOffsetY / 32f) + (gridHeight / 2); + final int rangeInCells = (int) Math.floor(range / 32f); + final int rangeInCellsSquare = rangeInCells * rangeInCells; + int minCheckX = relativeGridX - rangeInCells; + int minCheckY = relativeGridY - rangeInCells; + int maxCheckX = relativeGridX + rangeInCells; + int maxCheckY = relativeGridY + rangeInCells; + if ((minCheckX < gridWidth) && (maxCheckX >= 0)) { + if ((minCheckY < gridHeight) && (maxCheckY >= 0)) { + if (minCheckX < 0) { + minCheckX = 0; + } + if (minCheckY < 0) { + minCheckY = 0; + } + if (maxCheckX > (gridWidth - 1)) { + maxCheckX = gridWidth - 1; + } + if (maxCheckY > (gridHeight - 1)) { + maxCheckY = gridHeight - 1; + } + for (int checkX = minCheckX; checkX <= maxCheckX; checkX++) { + for (int checkY = minCheckY; checkY <= maxCheckY; checkY++) { + final int dx = relativeGridX - checkX; + final int dy = relativeGridY - checkY; + if (((dx * dx) + (dy * dy)) <= rangeInCellsSquare) { + if (((getRGBFromPixelData(buildingPathingPixelMap, checkX, checkY, rotation) + & 0xFF0000) >>> 16) > 127) { + return true; + } + } + } + } + } + } + } + } + return distance <= range; + } + + private int getRGBFromPixelData(final BufferedImage buildingPathingPixelMap, final int checkX, final int checkY, + final int rotation) { + + // Below: y is downwards (:() + int x; + int y; + switch (rotation) { + case 90: + x = checkY; + y = buildingPathingPixelMap.getWidth() - 1 - checkX; + break; + case 180: + x = buildingPathingPixelMap.getWidth() - 1 - checkX; + y = buildingPathingPixelMap.getHeight() - 1 - checkY; + break; + case 270: + x = buildingPathingPixelMap.getHeight() - 1 - checkY; + y = checkX; + break; + default: + case 0: + x = checkX; + y = checkY; + } + return buildingPathingPixelMap.getRGB(x, buildingPathingPixelMap.getHeight() - 1 - y); + } + + public void addStateListener(final CUnitStateListener listener) { + this.stateNotifier.subscribe(listener); + } + + public void removeStateListener(final CUnitStateListener listener) { + this.stateNotifier.unsubscribe(listener); + } + + public boolean isCorpse() { + return this.corpse; + } + + public boolean isBoneCorpse() { + return this.boneCorpse; + } + + @Override + public boolean canBeTargetedBy(final CSimulation simulation, final CUnit source, + final EnumSet targetsAllowed) { + if (targetsAllowed.containsAll(this.unitType.getTargetedAs())) { + final int sourcePlayerIndex = source.getPlayerIndex(); + final CPlayer sourcePlayer = simulation.getPlayer(sourcePlayerIndex); + if (!targetsAllowed.contains(CTargetType.ENEMIES) + || !sourcePlayer.hasAlliance(this.playerIndex, CAllianceType.PASSIVE)) { + if (isDead()) { + if (this.unitType.isRaise() && this.unitType.isDecay() && isBoneCorpse()) { + return targetsAllowed.contains(CTargetType.DEAD); + } + } + else { + return !targetsAllowed.contains(CTargetType.DEAD) || targetsAllowed.contains(CTargetType.ALIVE); + } + } + } + return false; + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitAnimationListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitAnimationListener.java index db7f9d0..28dd9da 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitAnimationListener.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitAnimationListener.java @@ -6,11 +6,9 @@ import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; public interface CUnitAnimationListener { - EnumSet EMPTY = EnumSet.noneOf(SecondaryTag.class); - EnumSet READY = EnumSet.of(SecondaryTag.READY); - void playAnimation(boolean force, final PrimaryTag animationName, - final EnumSet secondaryAnimationTags, float speedRatio); + final EnumSet secondaryAnimationTags, float speedRatio, boolean allowRarityVariations); - void queueAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags); + void queueAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags, + boolean allowRarityVariations); } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitEnumFunction.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitEnumFunction.java new file mode 100644 index 0000000..1863c2c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitEnumFunction.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +public interface CUnitEnumFunction { + boolean call(CUnit unit); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitStateListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitStateListener.java new file mode 100644 index 0000000..7109037 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitStateListener.java @@ -0,0 +1,17 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import com.etheller.warsmash.util.SubscriberSetNotifier; + +public interface CUnitStateListener { + void lifeChanged(); // hp (current) changes + + public static final class CUnitStateNotifier extends SubscriberSetNotifier + implements CUnitStateListener { + @Override + public void lifeChanged() { + for (final CUnitStateListener listener : set) { + listener.lifeChanged(); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java index 7970023..3d84425 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java @@ -1,11 +1,13 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +import java.awt.image.BufferedImage; import java.util.EnumSet; import java.util.List; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CDefenseType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; /** @@ -25,11 +27,18 @@ public class CUnitType { private final boolean decay; private final CDefenseType defenseType; private final float impactZ; + private final float deathTime; + + // TODO: this should probably not be stored as game state, i.e., is it really + // game data? can we store it in a cleaner way? + private final BufferedImage buildingPathingPixelMap; + private final EnumSet targetedAs; public CUnitType(final String name, final boolean isBldg, final MovementType movementType, final float defaultFlyingHeight, final float collisionSize, final EnumSet classifications, final List attacks, final String armorType, - final boolean raise, final boolean decay, final CDefenseType defenseType, final float impactZ) { + final boolean raise, final boolean decay, final CDefenseType defenseType, final float impactZ, + final BufferedImage buildingPathingPixelMap, final float deathTime, final EnumSet targetedAs) { this.name = name; this.building = isBldg; this.movementType = movementType; @@ -42,6 +51,9 @@ public class CUnitType { this.decay = decay; this.defenseType = defenseType; this.impactZ = impactZ; + this.buildingPathingPixelMap = buildingPathingPixelMap; + this.deathTime = deathTime; + this.targetedAs = targetedAs; } public String getName() { @@ -91,4 +103,16 @@ public class CUnitType { public float getImpactZ() { return this.impactZ; } + + public BufferedImage getBuildingPathingPixelMap() { + return this.buildingPathingPixelMap; + } + + public float getDeathTime() { + return this.deathTime; + } + + public EnumSet getTargetedAs() { + return this.targetedAs; + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidget.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidget.java index 38dd2f9..a78217d 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidget.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidget.java @@ -1,10 +1,15 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + public abstract class CWidget { private final int handleId; private float x; private float y; - private float life; + protected float life; public CWidget(final int handleId, final float x, final float y, final float life) { this.handleId = handleId; @@ -41,12 +46,17 @@ public abstract class CWidget { this.life = life; } - public void damage(final CUnit source, final int damage) { - this.life -= damage; - } + public abstract void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage); public abstract float getFlyHeight(); public abstract float getImpactZ(); + public boolean isDead() { + return this.life <= 0; + } + + public abstract boolean canBeTargetedBy(CSimulation simulation, CUnit source, + final EnumSet targetsAllowed); } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWorldCollision.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWorldCollision.java index ae90c5e..72735c0 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWorldCollision.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWorldCollision.java @@ -1,77 +1,145 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +import java.util.HashSet; +import java.util.Set; + import com.badlogic.gdx.math.Rectangle; import com.etheller.warsmash.util.Quadtree; +import com.etheller.warsmash.util.QuadtreeIntersector; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; public class CWorldCollision { private final Quadtree groundUnitCollision; private final Quadtree airUnitCollision; private final Quadtree seaUnitCollision; + private final Quadtree buildingUnitCollision; + private final float maxCollisionRadius; + private final AnyUnitExceptTwoIntersector anyUnitExceptTwoIntersector; + private final EachUnitOnlyOnceIntersector eachUnitOnlyOnceIntersector; - public CWorldCollision(final Rectangle entireMapBounds) { + public CWorldCollision(final Rectangle entireMapBounds, final float maxCollisionRadius) { this.groundUnitCollision = new Quadtree<>(entireMapBounds); this.airUnitCollision = new Quadtree<>(entireMapBounds); this.seaUnitCollision = new Quadtree<>(entireMapBounds); + this.buildingUnitCollision = new Quadtree<>(entireMapBounds); + this.maxCollisionRadius = maxCollisionRadius; + this.anyUnitExceptTwoIntersector = new AnyUnitExceptTwoIntersector(); + this.eachUnitOnlyOnceIntersector = new EachUnitOnlyOnceIntersector(); } public void addUnit(final CUnit unit) { - if (unit.getUnitType().isBuilding()) { - throw new IllegalArgumentException("Cannot add building to the CWorldCollision"); - } Rectangle bounds = unit.getCollisionRectangle(); if (bounds == null) { - final float collisionSize = unit.getUnitType().getCollisionSize(); + final float collisionSize = Math.min(this.maxCollisionRadius, unit.getUnitType().getCollisionSize()); bounds = new Rectangle(unit.getX() - collisionSize, unit.getY() - collisionSize, collisionSize * 2, collisionSize * 2); unit.setCollisionRectangle(bounds); } - final MovementType movementType = unit.getUnitType().getMovementType(); - if (movementType != null) { - switch (movementType) { - case AMPHIBIOUS: - this.seaUnitCollision.add(unit, bounds); - this.groundUnitCollision.add(unit, bounds); - break; - case FLOAT: - this.seaUnitCollision.add(unit, bounds); - break; - case FLY: - this.airUnitCollision.add(unit, bounds); - break; - default: - case DISABLED: - case FOOT: - case HORSE: - case HOVER: - this.groundUnitCollision.add(unit, bounds); - break; + if (unit.getUnitType().isBuilding()) { + // buildings are here so that we can include them when enumerating all units in + // a rect, but they don't really move dynamically, this is kind of pointless + this.buildingUnitCollision.add(unit, bounds); + } + else { + final MovementType movementType = unit.getUnitType().getMovementType(); + if (movementType != null) { + switch (movementType) { + case AMPHIBIOUS: + this.seaUnitCollision.add(unit, bounds); + this.groundUnitCollision.add(unit, bounds); + break; + case FLOAT: + this.seaUnitCollision.add(unit, bounds); + break; + case FLY: + this.airUnitCollision.add(unit, bounds); + break; + default: + case DISABLED: + case FOOT: + case HORSE: + case HOVER: + this.groundUnitCollision.add(unit, bounds); + break; + } } } } + public void removeUnit(final CUnit unit) { + final Rectangle bounds = unit.getCollisionRectangle(); + if (bounds != null) { + if (unit.getUnitType().isBuilding()) { + this.buildingUnitCollision.remove(unit, bounds); + } + else { + final MovementType movementType = unit.getUnitType().getMovementType(); + if (movementType != null) { + switch (movementType) { + case AMPHIBIOUS: + this.seaUnitCollision.remove(unit, bounds); + this.groundUnitCollision.remove(unit, bounds); + break; + case FLOAT: + this.seaUnitCollision.remove(unit, bounds); + break; + case FLY: + this.airUnitCollision.remove(unit, bounds); + break; + default: + case DISABLED: + case FOOT: + case HORSE: + case HOVER: + this.groundUnitCollision.remove(unit, bounds); + break; + } + } + } + } + unit.setCollisionRectangle(null); + } + + public void enumUnitsInRect(final Rectangle rect, final CUnitEnumFunction callback) { + this.eachUnitOnlyOnceIntersector.reset(callback); + this.groundUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + this.airUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + this.seaUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + this.buildingUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + } + public boolean intersectsAnythingOtherThan(final Rectangle newPossibleRectangle, final CUnit sourceUnitToIgnore, final MovementType movementType) { + return intersectsAnythingOtherThan(newPossibleRectangle, sourceUnitToIgnore, null, movementType); + } + + public boolean intersectsAnythingOtherThan(final Rectangle newPossibleRectangle, final CUnit sourceUnitToIgnore, + final CUnit sourceSecondUnitToIgnore, final MovementType movementType) { if (movementType != null) { switch (movementType) { case AMPHIBIOUS: - if (this.seaUnitCollision.intersectsAnythingOtherThan(sourceUnitToIgnore, newPossibleRectangle)) { + if (this.seaUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore))) { return true; } - if (this.groundUnitCollision.intersectsAnythingOtherThan(sourceUnitToIgnore, newPossibleRectangle)) { + if (this.groundUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore))) { return true; } return false; case FLOAT: - return this.seaUnitCollision.intersectsAnythingOtherThan(sourceUnitToIgnore, newPossibleRectangle); + return this.seaUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore)); case FLY: - return this.airUnitCollision.intersectsAnythingOtherThan(sourceUnitToIgnore, newPossibleRectangle); + return this.airUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore)); default: case DISABLED: case FOOT: case HORSE: case HOVER: - return this.groundUnitCollision.intersectsAnythingOtherThan(sourceUnitToIgnore, newPossibleRectangle); + return this.groundUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore)); } } return false; @@ -109,4 +177,45 @@ public class CWorldCollision { } } } + + private static final class AnyUnitExceptTwoIntersector implements QuadtreeIntersector { + private CUnit firstUnit; + private CUnit secondUnit; + + public AnyUnitExceptTwoIntersector reset(final CUnit firstUnit, final CUnit secondUnit) { + this.firstUnit = firstUnit; + this.secondUnit = secondUnit; + return this; + } + + @Override + public boolean onIntersect(final CUnit intersectingObject) { + return (intersectingObject != this.firstUnit) && (intersectingObject != this.secondUnit); + } + } + + private static final class EachUnitOnlyOnceIntersector implements QuadtreeIntersector { + private CUnitEnumFunction consumerDelegate; + private final Set intersectedUnits = new HashSet<>(); + private boolean done; + + public EachUnitOnlyOnceIntersector reset(final CUnitEnumFunction consumerDelegate) { + this.consumerDelegate = consumerDelegate; + this.intersectedUnits.clear(); + this.done = false; + return this; + } + + @Override + public boolean onIntersect(final CUnit intersectingObject) { + if (this.done) { + return true; + } + if (this.intersectedUnits.add(intersectingObject)) { + this.done = this.consumerDelegate.call(intersectingObject); + return this.done; + } + return false; + } + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityAttack.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityAttack.java index 8cc10d2..f4ad9ef 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityAttack.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityAttack.java @@ -1,11 +1,14 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; import com.badlogic.gdx.math.Vector2; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.COrder; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.StringsToExternalizeLater; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.CAttackOrder; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.CMoveOrder; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver.TargetType; @@ -47,7 +50,17 @@ public class CAbilityAttack implements CAbility { @Override public void onOrder(final CSimulation game, final CUnit caster, final CWidget target, final boolean queue) { - caster.order(new CAttackOrder(caster, caster.getUnitType().getAttacks().get(0), target), queue); + COrder order = null; + for (final CUnitAttack attack : caster.getUnitType().getAttacks()) { + if (target.canBeTargetedBy(game, caster, attack.getTargetsAllowed())) { + order = new CAttackOrder(caster, attack, target); + break; + } + } + if (order == null) { + order = new CMoveOrder(caster, target.getX(), target.getY()); + } + caster.order(order, queue); } @Override diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CAttackType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CAttackType.java index 13c6c68..6da4cbb 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CAttackType.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CAttackType.java @@ -10,14 +10,21 @@ public enum CAttackType implements CodeKeyType { MAGIC, HERO; + public static CAttackType[] VALUES = values(); + private String codeKey; + private String damageKey; private CAttackType() { - String name = name(); - if (name.equals("SPELLS")) { - name = "MAGIC"; + final String name = name(); + final String computedCodeKey = name.charAt(0) + name.substring(1).toLowerCase(); + if (computedCodeKey.equals("Spells")) { + this.codeKey = "Magic"; } - this.codeKey = name.charAt(0) + name.substring(1).toLowerCase(); + else { + this.codeKey = computedCodeKey; + } + this.damageKey = this.codeKey; } @Override @@ -25,6 +32,10 @@ public enum CAttackType implements CodeKeyType { return this.codeKey; } + public String getDamageKey() { + return this.damageKey; + } + public static CAttackType parseAttackType(final String attackTypeString) { return valueOf(attackTypeString.toUpperCase()); } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CDefenseType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CDefenseType.java index b23382b..e300eb3 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CDefenseType.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CDefenseType.java @@ -10,6 +10,8 @@ public enum CDefenseType implements CodeKeyType { HERO, DIVINE; + public static CDefenseType[] VALUES = values(); + private String codeKey; private CDefenseType() { diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttack.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttack.java index d72530a..b979cd5 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttack.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttack.java @@ -2,6 +2,9 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; import java.util.EnumSet; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; @@ -19,7 +22,7 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; * because many of those settings did not exist. So I will attempt to emulate * these attacks as best as possible. */ -public class CUnitAttack { +public abstract class CUnitAttack { private float animationBackswingPoint; private float animationDamagePoint; private CAttackType attackType; @@ -195,4 +198,5 @@ public class CUnitAttack { return this.maxDamage; } + public abstract void launch(CSimulation simulation, CUnit unit, CWidget target, float damage); } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackInstant.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackInstant.java index 8e9ab2e..80df2f1 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackInstant.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackInstant.java @@ -2,6 +2,9 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; import java.util.EnumSet; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; @@ -28,4 +31,10 @@ public class CUnitAttackInstant extends CUnitAttack { this.projectileArt = projectileArt; } + @Override + public void launch(final CSimulation simulation, final CUnit unit, final CWidget target, final float damage) { + simulation.createInstantAttackEffect(unit, this, target); + target.damage(simulation, unit, getAttackType(), getWeaponSound(), damage); + } + } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissile.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissile.java index 9397295..f42c34c 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissile.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissile.java @@ -2,6 +2,9 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; import java.util.EnumSet; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; @@ -59,4 +62,14 @@ public class CUnitAttackMissile extends CUnitAttack { this.projectileSpeed = projectileSpeed; } + @Override + public void launch(final CSimulation simulation, final CUnit unit, final CWidget target, final float damage) { + simulation.createProjectile(unit, unit.getX(), unit.getY(), (float) Math.toRadians(unit.getFacing()), this, + target, damage, 0); + } + + public void doDamage(final CSimulation cSimulation, final CUnit source, final CWidget target, final float damage, + final float x, final float y, final int bounceIndex) { + target.damage(cSimulation, source, getAttackType(), getWeaponSound(), damage); + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileBounce.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileBounce.java index 474ca3f..0014ef2 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileBounce.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileBounce.java @@ -2,6 +2,11 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; import java.util.EnumSet; +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitEnumFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; @@ -9,6 +14,8 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; public class CUnitAttackMissileBounce extends CUnitAttackMissile { private float damageLossFactor; private int maximumNumberOfTargets; + private final int areaOfEffectFullDamage; + private final EnumSet areaOfEffectTargets; public CUnitAttackMissileBounce(final float animationBackswingPoint, final float animationDamagePoint, final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, @@ -16,12 +23,15 @@ public class CUnitAttackMissileBounce extends CUnitAttackMissile { final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, final CWeaponType weaponType, final float projectileArc, final String projectileArt, final boolean projectileHomingEnabled, final int projectileSpeed, final float damageLossFactor, - final int maximumNumberOfTargets) { + final int maximumNumberOfTargets, final int areaOfEffectFullDamage, + final EnumSet areaOfEffectTargets) { super(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, weaponType, projectileArc, projectileArt, projectileHomingEnabled, projectileSpeed); this.damageLossFactor = damageLossFactor; this.maximumNumberOfTargets = maximumNumberOfTargets; + this.areaOfEffectFullDamage = areaOfEffectFullDamage; + this.areaOfEffectTargets = areaOfEffectTargets; } public float getDamageLossFactor() { @@ -40,4 +50,67 @@ public class CUnitAttackMissileBounce extends CUnitAttackMissile { this.maximumNumberOfTargets = maximumNumberOfTargets; } + @Override + public void doDamage(final CSimulation cSimulation, final CUnit source, final CWidget target, final float damage, + final float x, final float y, final int bounceIndex) { + super.doDamage(cSimulation, source, target, damage, x, y, bounceIndex); + final int nextBounceIndex = bounceIndex + 1; + if (nextBounceIndex != this.maximumNumberOfTargets) { + BounceMissileConsumer.INSTANCE.nextBounce(cSimulation, source, target, this, x, y, damage, nextBounceIndex); + } + } + + private static final class BounceMissileConsumer implements CUnitEnumFunction { + private static final BounceMissileConsumer INSTANCE = new BounceMissileConsumer(); + private final Rectangle rect = new Rectangle(); + private CUnitAttackMissileBounce attack; + private CSimulation simulation; + private CUnit source; + private CWidget target; + private float x; + private float y; + private float damage; + private int bounceIndex; + private boolean launched = false; + + public void nextBounce(final CSimulation simulation, final CUnit source, final CWidget target, + final CUnitAttackMissileBounce attack, final float x, final float y, final float damage, + final int bounceIndex) { + this.simulation = simulation; + this.source = source; + this.target = target; + this.attack = attack; + this.x = x; + this.y = y; + this.damage = damage; + this.bounceIndex = bounceIndex; + this.launched = false; + final float doubleMaxArea = attack.areaOfEffectFullDamage + + (this.simulation.getGameplayConstants().getCloseEnoughRange() * 2); + final float maxArea = doubleMaxArea / 2; + this.rect.set(x - maxArea, y - maxArea, doubleMaxArea, doubleMaxArea); + simulation.getWorldCollision().enumUnitsInRect(this.rect, this); + + } + + @Override + public boolean call(final CUnit enumUnit) { + if (enumUnit == this.target) { + return false; + } + if (enumUnit.canBeTargetedBy(this.simulation, this.source, this.attack.areaOfEffectTargets)) { + if (this.launched) { + throw new IllegalStateException("already launched"); + } + final float dx = enumUnit.getX() - this.x; + final float dy = enumUnit.getY() - this.y; + final float angle = (float) Math.atan2(dy, dx); + this.simulation.createProjectile(this.source, this.x, this.y, angle, this.attack, enumUnit, + this.damage * (1.0f - this.attack.damageLossFactor), this.bounceIndex); + this.launched = true; + return true; + } + return false; + } + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileSplash.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileSplash.java index a9e636e..453204e 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileSplash.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileSplash.java @@ -2,6 +2,11 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; import java.util.EnumSet; +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitEnumFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; @@ -82,4 +87,66 @@ public class CUnitAttackMissileSplash extends CUnitAttackMissile { this.damageFactorSmall = damageFactorSmall; } + @Override + public void doDamage(final CSimulation cSimulation, final CUnit source, final CWidget target, final float damage, + final float x, final float y, final int bounceIndex) { + SplashDamageConsumer.INSTANCE.doDamage(cSimulation, source, target, this, x, y, damage); + if ((getWeaponType() != CWeaponType.ARTILLERY) && !SplashDamageConsumer.INSTANCE.hitTarget) { + super.doDamage(cSimulation, source, target, damage * this.damageFactorSmall, x, y, bounceIndex); + } + } + + private static final class SplashDamageConsumer implements CUnitEnumFunction { + private static final SplashDamageConsumer INSTANCE = new SplashDamageConsumer(); + private final Rectangle rect = new Rectangle(); + private CUnitAttackMissileSplash attack; + private CSimulation simulation; + private CUnit source; + private CWidget target; + private float x; + private float y; + private float damage; + private boolean hitTarget; + + public void doDamage(final CSimulation simulation, final CUnit source, final CWidget target, + final CUnitAttackMissileSplash attack, final float x, final float y, final float damage) { + this.simulation = simulation; + this.source = source; + this.target = target; + this.attack = attack; + this.x = x; + this.y = y; + this.damage = damage; + this.hitTarget = false; + final float doubleMaxArea = attack.areaOfEffectSmallDamage + + (this.simulation.getGameplayConstants().getCloseEnoughRange() * 2); + final float maxArea = doubleMaxArea / 2; + this.rect.set(x - maxArea, y - maxArea, doubleMaxArea, doubleMaxArea); + simulation.getWorldCollision().enumUnitsInRect(this.rect, this); + } + + @Override + public boolean call(final CUnit enumUnit) { + if (enumUnit.canBeTargetedBy(this.simulation, this.source, this.attack.areaOfEffectTargets)) { + final double distance = enumUnit.distance(this.x, this.y) + - this.simulation.getGameplayConstants().getCloseEnoughRange(); + if (distance <= (this.attack.areaOfEffectFullDamage / 2)) { + enumUnit.damage(this.simulation, this.source, this.attack.getAttackType(), + this.attack.getWeaponSound(), this.damage); + } + else if (distance <= (this.attack.areaOfEffectMediumDamage / 2)) { + enumUnit.damage(this.simulation, this.source, this.attack.getAttackType(), + this.attack.getWeaponSound(), this.damage * this.attack.damageFactorMedium); + } + else if (distance <= (this.attack.areaOfEffectSmallDamage / 2)) { + enumUnit.damage(this.simulation, this.source, this.attack.getAttackType(), + this.attack.getWeaponSound(), this.damage * this.attack.damageFactorSmall); + } + if (enumUnit == this.target) { + this.hitTarget = true; + } + } + return false; + } + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackNormal.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackNormal.java index 9dca88c..211671e 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackNormal.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackNormal.java @@ -2,6 +2,9 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; import java.util.EnumSet; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; @@ -18,4 +21,9 @@ public class CUnitAttackNormal extends CUnitAttack { weaponType); } + @Override + public void launch(final CSimulation simulation, final CUnit unit, final CWidget target, final float damage) { + target.damage(simulation, unit, getAttackType(), getWeaponSound(), damage); + } + } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/projectile/CAttackProjectile.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/projectile/CAttackProjectile.java index 99d1b71..6b0353a 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/projectile/CAttackProjectile.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/projectile/CAttackProjectile.java @@ -4,29 +4,39 @@ import com.etheller.warsmash.util.WarsmashConstants; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; public class CAttackProjectile { private float x; private float y; + private final float initialTargetX; + private final float initialTargetY; private final float speed; private final CWidget target; private boolean done; private final CUnit source; - private final int damage; + private final float damage; + private final CUnitAttackMissile unitAttack; + private final int bounceIndex; public CAttackProjectile(final float x, final float y, final float speed, final CWidget target, final CUnit source, - final int damage) { + final float damage, final CUnitAttackMissile unitAttack, final int bounceIndex) { this.x = x; this.y = y; this.speed = speed; this.target = target; this.source = source; this.damage = damage; + this.unitAttack = unitAttack; + this.bounceIndex = bounceIndex; + this.initialTargetX = target.getX(); + this.initialTargetY = target.getY(); } public boolean update(final CSimulation cSimulation) { - final float tx = this.target.getX(); - final float ty = this.target.getY(); + final float tx = getTargetX(); + final float ty = getTargetY(); final float sx = this.x; final float sy = this.y; final float dtsx = tx - sx; @@ -39,7 +49,8 @@ public class CAttackProjectile { float travelDistance = Math.min(c, this.speed * WarsmashConstants.SIMULATION_STEP_TIME); if (c <= travelDistance) { if (!this.done) { - this.target.damage(this.source, this.damage); + this.unitAttack.doDamage(cSimulation, this.source, this.target, this.damage, this.x, this.y, + this.bounceIndex); } this.done = true; travelDistance = c; @@ -73,4 +84,26 @@ public class CAttackProjectile { public boolean isDone() { return this.done; } + + public CUnitAttackMissile getUnitAttack() { + return this.unitAttack; + } + + public float getTargetX() { + if (this.unitAttack.isProjectileHomingEnabled() && (this.unitAttack.getWeaponType() != CWeaponType.ARTILLERY)) { + return this.target.getX(); + } + else { + return this.initialTargetX; + } + } + + public float getTargetY() { + if (this.unitAttack.isProjectileHomingEnabled() && (this.unitAttack.getWeaponType() != CWeaponType.ARTILLERY)) { + return this.target.getY(); + } + else { + return this.initialTargetY; + } + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java index ed7bc48..f89f4cf 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java @@ -1,5 +1,6 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.data; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; @@ -113,6 +114,8 @@ public class CUnitData { private static final War3ID MOVE_TYPE = War3ID.fromString("umvt"); private static final War3ID COLLISION_SIZE = War3ID.fromString("ucol"); private static final War3ID CLASSIFICATION = War3ID.fromString("utyp"); + private static final War3ID DEATH_TIME = War3ID.fromString("udtm"); + private static final War3ID TARGETED_AS = War3ID.fromString("utar"); private final MutableObjectData unitData; private final Map unitIdToUnitType = new HashMap<>(); @@ -121,124 +124,137 @@ public class CUnitData { } public CUnit create(final CSimulation simulation, final int playerIndex, final int handleId, final War3ID typeId, - final float x, final float y, final float facing) { + final float x, final float y, final float facing, final BufferedImage buildingPathingPixelMap) { final MutableGameObject unitType = this.unitData.get(typeId); final int life = unitType.getFieldAsInteger(HIT_POINT_MAXIMUM, 0); final int manaInitial = unitType.getFieldAsInteger(MANA_INITIAL_AMOUNT, 0); final int manaMaximum = unitType.getFieldAsInteger(MANA_MAXIMUM, 0); final int speed = unitType.getFieldAsInteger(MOVEMENT_SPEED_BASE, 0); - final float moveHeight = unitType.getFieldAsFloat(MOVE_HEIGHT, 0); - final String movetp = unitType.getFieldAsString(MOVE_TYPE, 0); - final float collisionSize = unitType.getFieldAsFloat(COLLISION_SIZE, 0); - final boolean isBldg = unitType.getFieldAsBoolean(IS_BLDG, 0); - final PathingGrid.MovementType movementType = PathingGrid.getMovementType(movetp); - final String classificationString = unitType.getFieldAsString(CLASSIFICATION, 0); - final String unitName = unitType.getFieldAsString(NAME, 0); - final EnumSet classifications = EnumSet.noneOf(CUnitClassification.class); - if (classificationString != null) { - final String[] classificationValues = classificationString.split(","); - for (final String unitEditorKey : classificationValues) { - final CUnitClassification unitClassification = CUnitClassification - .parseUnitClassification(unitEditorKey); - if (unitClassification != null) { - classifications.add(unitClassification); + final int defense = unitType.getFieldAsInteger(DEFENSE, 0); + + CUnitType unitTypeInstance = this.unitIdToUnitType.get(typeId); + if (unitTypeInstance == null) { + final float moveHeight = unitType.getFieldAsFloat(MOVE_HEIGHT, 0); + final String movetp = unitType.getFieldAsString(MOVE_TYPE, 0); + final float collisionSize = unitType.getFieldAsFloat(COLLISION_SIZE, 0); + final boolean isBldg = unitType.getFieldAsBoolean(IS_BLDG, 0); + final PathingGrid.MovementType movementType = PathingGrid.getMovementType(movetp); + final String unitName = unitType.getFieldAsString(NAME, 0); + final EnumSet targetedAs = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(TARGETED_AS, 0)); + final String classificationString = unitType.getFieldAsString(CLASSIFICATION, 0); + final EnumSet classifications = EnumSet.noneOf(CUnitClassification.class); + if (classificationString != null) { + final String[] classificationValues = classificationString.split(","); + for (final String unitEditorKey : classificationValues) { + final CUnitClassification unitClassification = CUnitClassification + .parseUnitClassification(unitEditorKey); + if (unitClassification != null) { + classifications.add(unitClassification); + } } } + final List attacks = new ArrayList<>(); + final int attacksEnabled = unitType.getFieldAsInteger(ATTACKS_ENABLED, 0); + if ((attacksEnabled & 0x1) != 0) { + // attack one + final float animationBackswingPoint = unitType.getFieldAsFloat(ATTACK1_BACKSWING_POINT, 0); + final float animationDamagePoint = unitType.getFieldAsFloat(ATTACK1_DAMAGE_POINT, 0); + final int areaOfEffectFullDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_FULL_DMG, 0); + final int areaOfEffectMediumDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_HALF_DMG, 0); + final int areaOfEffectSmallDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_QUARTER_DMG, 0); + final EnumSet areaOfEffectTargets = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK1_AREA_OF_EFFECT_TARGETS, 0)); + final CAttackType attackType = CAttackType + .parseAttackType(unitType.getFieldAsString(ATTACK1_ATTACK_TYPE, 0)); + final float cooldownTime = unitType.getFieldAsFloat(ATTACK1_COOLDOWN, 0); + final int damageBase = unitType.getFieldAsInteger(ATTACK1_DMG_BASE, 0); + final float damageFactorMedium = unitType.getFieldAsFloat(ATTACK1_DAMAGE_FACTOR_HALF, 0); + final float damageFactorSmall = unitType.getFieldAsFloat(ATTACK1_DAMAGE_FACTOR_QUARTER, 0); + final float damageLossFactor = unitType.getFieldAsFloat(ATTACK1_DAMAGE_LOSS_FACTOR, 0); + final int damageDice = unitType.getFieldAsInteger(ATTACK1_DMG_DICE, 0); + final int damageSidesPerDie = unitType.getFieldAsInteger(ATTACK1_DMG_SIDES_PER_DIE, 0); + final float damageSpillDistance = unitType.getFieldAsFloat(ATTACK1_DMG_SPILL_DIST, 0); + final float damageSpillRadius = unitType.getFieldAsFloat(ATTACK1_DMG_SPILL_RADIUS, 0); + final int damageUpgradeAmount = unitType.getFieldAsInteger(ATTACK1_DMG_UPGRADE_AMT, 0); + final int maximumNumberOfTargets = unitType.getFieldAsInteger(ATTACK1_TARGET_COUNT, 0); + final float projectileArc = unitType.getFieldAsFloat(ATTACK1_PROJECTILE_ARC, 0); + final String projectileArt = unitType.getFieldAsString(ATTACK1_MISSILE_ART, 0); + final boolean projectileHomingEnabled = unitType.getFieldAsBoolean(ATTACK1_PROJECTILE_HOMING_ENABLED, + 0); + final int projectileSpeed = unitType.getFieldAsInteger(ATTACK1_PROJECTILE_SPEED, 0); + final int range = unitType.getFieldAsInteger(ATTACK1_RANGE, 0); + final float rangeMotionBuffer = unitType.getFieldAsFloat(ATTACK1_RANGE_MOTION_BUFFER, 0); + final boolean showUI = unitType.getFieldAsBoolean(ATTACK1_SHOW_UI, 0); + final EnumSet targetsAllowed = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK1_TARGETS_ALLOWED, 0)); + final String weaponSound = unitType.getFieldAsString(ATTACK1_WEAPON_SOUND, 0); + final CWeaponType weaponType = CWeaponType + .parseWeaponType(unitType.getFieldAsString(ATTACK1_WEAPON_TYPE, 0)); + attacks.add(createAttack(animationBackswingPoint, animationDamagePoint, areaOfEffectFullDamage, + areaOfEffectMediumDamage, areaOfEffectSmallDamage, areaOfEffectTargets, attackType, + cooldownTime, damageBase, damageFactorMedium, damageFactorSmall, damageLossFactor, damageDice, + damageSidesPerDie, damageSpillDistance, damageSpillRadius, damageUpgradeAmount, + maximumNumberOfTargets, projectileArc, projectileArt, projectileHomingEnabled, projectileSpeed, + range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, weaponType)); + } + if ((attacksEnabled & 0x2) != 0) { + // attack two + final float animationBackswingPoint = unitType.getFieldAsFloat(ATTACK2_BACKSWING_POINT, 0); + final float animationDamagePoint = unitType.getFieldAsFloat(ATTACK2_DAMAGE_POINT, 0); + final int areaOfEffectFullDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_FULL_DMG, 0); + final int areaOfEffectMediumDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_HALF_DMG, 0); + final int areaOfEffectSmallDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_QUARTER_DMG, 0); + final EnumSet areaOfEffectTargets = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK2_AREA_OF_EFFECT_TARGETS, 0)); + final CAttackType attackType = CAttackType + .parseAttackType(unitType.getFieldAsString(ATTACK2_ATTACK_TYPE, 0)); + final float cooldownTime = unitType.getFieldAsFloat(ATTACK2_COOLDOWN, 0); + final int damageBase = unitType.getFieldAsInteger(ATTACK2_DMG_BASE, 0); + final float damageFactorMedium = unitType.getFieldAsFloat(ATTACK2_DAMAGE_FACTOR_HALF, 0); + final float damageFactorSmall = unitType.getFieldAsFloat(ATTACK2_DAMAGE_FACTOR_QUARTER, 0); + final float damageLossFactor = unitType.getFieldAsFloat(ATTACK2_DAMAGE_LOSS_FACTOR, 0); + final int damageDice = unitType.getFieldAsInteger(ATTACK2_DMG_DICE, 0); + final int damageSidesPerDie = unitType.getFieldAsInteger(ATTACK2_DMG_SIDES_PER_DIE, 0); + final float damageSpillDistance = unitType.getFieldAsFloat(ATTACK2_DMG_SPILL_DIST, 0); + final float damageSpillRadius = unitType.getFieldAsFloat(ATTACK2_DMG_SPILL_RADIUS, 0); + final int damageUpgradeAmount = unitType.getFieldAsInteger(ATTACK2_DMG_UPGRADE_AMT, 0); + final int maximumNumberOfTargets = unitType.getFieldAsInteger(ATTACK2_TARGET_COUNT, 0); + final float projectileArc = unitType.getFieldAsFloat(ATTACK2_PROJECTILE_ARC, 0); + final String projectileArt = unitType.getFieldAsString(ATTACK2_MISSILE_ART, 0); + final boolean projectileHomingEnabled = unitType.getFieldAsBoolean(ATTACK2_PROJECTILE_HOMING_ENABLED, + 0); + final int projectileSpeed = unitType.getFieldAsInteger(ATTACK2_PROJECTILE_SPEED, 0); + final int range = unitType.getFieldAsInteger(ATTACK2_RANGE, 0); + final float rangeMotionBuffer = unitType.getFieldAsFloat(ATTACK2_RANGE_MOTION_BUFFER, 0); + final boolean showUI = unitType.getFieldAsBoolean(ATTACK2_SHOW_UI, 0); + final EnumSet targetsAllowed = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK2_TARGETS_ALLOWED, 0)); + final String weaponSound = unitType.getFieldAsString(ATTACK2_WEAPON_SOUND, 0); + final CWeaponType weaponType = CWeaponType + .parseWeaponType(unitType.getFieldAsString(ATTACK2_WEAPON_TYPE, 0)); + attacks.add(createAttack(animationBackswingPoint, animationDamagePoint, areaOfEffectFullDamage, + areaOfEffectMediumDamage, areaOfEffectSmallDamage, areaOfEffectTargets, attackType, + cooldownTime, damageBase, damageFactorMedium, damageFactorSmall, damageLossFactor, damageDice, + damageSidesPerDie, damageSpillDistance, damageSpillRadius, damageUpgradeAmount, + maximumNumberOfTargets, projectileArc, projectileArt, projectileHomingEnabled, projectileSpeed, + range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, weaponType)); + } + final int deathType = unitType.getFieldAsInteger(DEATH_TYPE, 0); + final boolean raise = (deathType & 0x1) != 0; + final boolean decay = (deathType & 0x2) != 0; + final String armorType = unitType.getFieldAsString(ARMOR_TYPE, 0); + final float impactZ = unitType.getFieldAsFloat(PROJECTILE_IMPACT_Z, 0); + final CDefenseType defenseType = CDefenseType.parseDefenseType(unitType.getFieldAsString(DEFENSE_TYPE, 0)); + final float deathTime = unitType.getFieldAsFloat(DEATH_TIME, 0); + unitTypeInstance = new CUnitType(unitName, isBldg, movementType, moveHeight, collisionSize, classifications, + attacks, armorType, raise, decay, defenseType, impactZ, buildingPathingPixelMap, deathTime, + targetedAs); + this.unitIdToUnitType.put(typeId, unitTypeInstance); } - final List attacks = new ArrayList<>(); - final int attacksEnabled = unitType.getFieldAsInteger(ATTACKS_ENABLED, 0); - if ((attacksEnabled & 0x1) != 0) { - // attack one - final float animationBackswingPoint = unitType.getFieldAsFloat(ATTACK1_BACKSWING_POINT, 0); - final float animationDamagePoint = unitType.getFieldAsFloat(ATTACK1_DAMAGE_POINT, 0); - final int areaOfEffectFullDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_FULL_DMG, 0); - final int areaOfEffectMediumDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_HALF_DMG, 0); - final int areaOfEffectSmallDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_QUARTER_DMG, 0); - final EnumSet areaOfEffectTargets = CTargetType - .parseTargetTypeSet(unitType.getFieldAsString(ATTACK1_AREA_OF_EFFECT_TARGETS, 0)); - final CAttackType attackType = CAttackType - .parseAttackType(unitType.getFieldAsString(ATTACK1_ATTACK_TYPE, 0)); - final float cooldownTime = unitType.getFieldAsFloat(ATTACK1_COOLDOWN, 0); - final int damageBase = unitType.getFieldAsInteger(ATTACK1_DMG_BASE, 0); - final float damageFactorMedium = unitType.getFieldAsFloat(ATTACK1_DAMAGE_FACTOR_HALF, 0); - final float damageFactorSmall = unitType.getFieldAsFloat(ATTACK1_DAMAGE_FACTOR_QUARTER, 0); - final float damageLossFactor = unitType.getFieldAsFloat(ATTACK1_DAMAGE_LOSS_FACTOR, 0); - final int damageDice = unitType.getFieldAsInteger(ATTACK1_DMG_DICE, 0); - final int damageSidesPerDie = unitType.getFieldAsInteger(ATTACK1_DMG_SIDES_PER_DIE, 0); - final float damageSpillDistance = unitType.getFieldAsFloat(ATTACK1_DMG_SPILL_DIST, 0); - final float damageSpillRadius = unitType.getFieldAsFloat(ATTACK1_DMG_SPILL_RADIUS, 0); - final int damageUpgradeAmount = unitType.getFieldAsInteger(ATTACK1_DMG_UPGRADE_AMT, 0); - final int maximumNumberOfTargets = unitType.getFieldAsInteger(ATTACK1_TARGET_COUNT, 0); - final float projectileArc = unitType.getFieldAsFloat(ATTACK1_PROJECTILE_ARC, 0); - final String projectileArt = unitType.getFieldAsString(ATTACK1_MISSILE_ART, 0); - final boolean projectileHomingEnabled = unitType.getFieldAsBoolean(ATTACK1_PROJECTILE_HOMING_ENABLED, 0); - final int projectileSpeed = unitType.getFieldAsInteger(ATTACK1_PROJECTILE_SPEED, 0); - final int range = unitType.getFieldAsInteger(ATTACK1_RANGE, 0); - final float rangeMotionBuffer = unitType.getFieldAsFloat(ATTACK1_RANGE_MOTION_BUFFER, 0); - final boolean showUI = unitType.getFieldAsBoolean(ATTACK1_SHOW_UI, 0); - final EnumSet targetsAllowed = CTargetType - .parseTargetTypeSet(unitType.getFieldAsString(ATTACK1_TARGETS_ALLOWED, 0)); - final String weaponSound = unitType.getFieldAsString(ATTACK1_WEAPON_SOUND, 0); - final CWeaponType weaponType = CWeaponType - .parseWeaponType(unitType.getFieldAsString(ATTACK1_WEAPON_TYPE, 0)); - attacks.add(createAttack(animationBackswingPoint, animationDamagePoint, areaOfEffectFullDamage, - areaOfEffectMediumDamage, areaOfEffectSmallDamage, areaOfEffectTargets, attackType, cooldownTime, - damageBase, damageFactorMedium, damageFactorSmall, damageLossFactor, damageDice, damageSidesPerDie, - damageSpillDistance, damageSpillRadius, damageUpgradeAmount, maximumNumberOfTargets, projectileArc, - projectileArt, projectileHomingEnabled, projectileSpeed, range, rangeMotionBuffer, showUI, - targetsAllowed, weaponSound, weaponType)); - } - if ((attacksEnabled & 0x2) != 0) { - // attack two - final float animationBackswingPoint = unitType.getFieldAsFloat(ATTACK2_BACKSWING_POINT, 0); - final float animationDamagePoint = unitType.getFieldAsFloat(ATTACK2_DAMAGE_POINT, 0); - final int areaOfEffectFullDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_FULL_DMG, 0); - final int areaOfEffectMediumDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_HALF_DMG, 0); - final int areaOfEffectSmallDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_QUARTER_DMG, 0); - final EnumSet areaOfEffectTargets = CTargetType - .parseTargetTypeSet(unitType.getFieldAsString(ATTACK2_AREA_OF_EFFECT_TARGETS, 0)); - final CAttackType attackType = CAttackType - .parseAttackType(unitType.getFieldAsString(ATTACK2_ATTACK_TYPE, 0)); - final float cooldownTime = unitType.getFieldAsFloat(ATTACK2_COOLDOWN, 0); - final int damageBase = unitType.getFieldAsInteger(ATTACK2_DMG_BASE, 0); - final float damageFactorMedium = unitType.getFieldAsFloat(ATTACK2_DAMAGE_FACTOR_HALF, 0); - final float damageFactorSmall = unitType.getFieldAsFloat(ATTACK2_DAMAGE_FACTOR_QUARTER, 0); - final float damageLossFactor = unitType.getFieldAsFloat(ATTACK2_DAMAGE_LOSS_FACTOR, 0); - final int damageDice = unitType.getFieldAsInteger(ATTACK2_DMG_DICE, 0); - final int damageSidesPerDie = unitType.getFieldAsInteger(ATTACK2_DMG_SIDES_PER_DIE, 0); - final float damageSpillDistance = unitType.getFieldAsFloat(ATTACK2_DMG_SPILL_DIST, 0); - final float damageSpillRadius = unitType.getFieldAsFloat(ATTACK2_DMG_SPILL_RADIUS, 0); - final int damageUpgradeAmount = unitType.getFieldAsInteger(ATTACK2_DMG_UPGRADE_AMT, 0); - final int maximumNumberOfTargets = unitType.getFieldAsInteger(ATTACK2_TARGET_COUNT, 0); - final float projectileArc = unitType.getFieldAsFloat(ATTACK2_PROJECTILE_ARC, 0); - final String projectileArt = unitType.getFieldAsString(ATTACK2_MISSILE_ART, 0); - final boolean projectileHomingEnabled = unitType.getFieldAsBoolean(ATTACK2_PROJECTILE_HOMING_ENABLED, 0); - final int projectileSpeed = unitType.getFieldAsInteger(ATTACK2_PROJECTILE_SPEED, 0); - final int range = unitType.getFieldAsInteger(ATTACK2_RANGE, 0); - final float rangeMotionBuffer = unitType.getFieldAsFloat(ATTACK2_RANGE_MOTION_BUFFER, 0); - final boolean showUI = unitType.getFieldAsBoolean(ATTACK2_SHOW_UI, 0); - final EnumSet targetsAllowed = CTargetType - .parseTargetTypeSet(unitType.getFieldAsString(ATTACK2_TARGETS_ALLOWED, 0)); - final String weaponSound = unitType.getFieldAsString(ATTACK2_WEAPON_SOUND, 0); - final CWeaponType weaponType = CWeaponType - .parseWeaponType(unitType.getFieldAsString(ATTACK2_WEAPON_TYPE, 0)); - attacks.add(createAttack(animationBackswingPoint, animationDamagePoint, areaOfEffectFullDamage, - areaOfEffectMediumDamage, areaOfEffectSmallDamage, areaOfEffectTargets, attackType, cooldownTime, - damageBase, damageFactorMedium, damageFactorSmall, damageLossFactor, damageDice, damageSidesPerDie, - damageSpillDistance, damageSpillRadius, damageUpgradeAmount, maximumNumberOfTargets, projectileArc, - projectileArt, projectileHomingEnabled, projectileSpeed, range, rangeMotionBuffer, showUI, - targetsAllowed, weaponSound, weaponType)); - } - final int deathType = unitType.getFieldAsInteger(DEATH_TYPE, 0); - final boolean raise = (deathType & 0x1) != 0; - final boolean decay = (deathType & 0x2) != 0; - final String armorType = unitType.getFieldAsString(ARMOR_TYPE, 0); - final int defense = unitType.getFieldAsInteger(DEFENSE, 0); - final float impactZ = unitType.getFieldAsFloat(PROJECTILE_IMPACT_Z, 0); - final CDefenseType defenseType = CDefenseType.parseDefenseType(unitType.getFieldAsString(DEFENSE_TYPE, 0)); + final CUnit unit = new CUnit(handleId, playerIndex, x, y, life, typeId, facing, manaInitial, life, manaMaximum, - speed, defense, new CUnitType(unitName, isBldg, movementType, moveHeight, collisionSize, - classifications, attacks, armorType, raise, decay, defenseType, impactZ)); + speed, defense, unitTypeInstance); if (speed > 0) { unit.add(simulation, CAbilityMove.INSTANCE); unit.add(simulation, CAbilityPatrol.INSTANCE); @@ -275,7 +291,8 @@ public class CUnitData { attack = new CUnitAttackMissileBounce(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, weaponType, projectileArc, projectileArt, - projectileHomingEnabled, projectileSpeed, damageLossFactor, maximumNumberOfTargets); + projectileHomingEnabled, projectileSpeed, damageLossFactor, maximumNumberOfTargets, + areaOfEffectFullDamage, areaOfEffectTargets); break; case MSPLASH: case ARTILLERY: diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CAttackOrder.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CAttackOrder.java index 0987753..f3bf6ce 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CAttackOrder.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CAttackOrder.java @@ -2,10 +2,10 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; import com.etheller.warsmash.util.WarsmashConstants; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.COrder; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitAnimationListener; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityAttack; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; @@ -15,16 +15,52 @@ public class CAttackOrder implements COrder { private boolean wasWithinPropWindow = false; private final CUnitAttack unitAttack; private final CWidget target; - private int backswingLaunchTime; + private int damagePointLaunchTime; + private int backSwingTime; + private COrder moveOrder; + private int thisOrderCooldownEndTime; + private boolean wasInRange = false; public CAttackOrder(final CUnit unit, final CUnitAttack unitAttack, final CWidget target) { this.unit = unit; this.unitAttack = unitAttack; this.target = target; + createMoveOrder(unit, target); + } + + private void createMoveOrder(final CUnit unit, final CWidget target) { + if ((target instanceof CUnit) && !(((CUnit) target).getUnitType().isBuilding())) { + this.moveOrder = new CMoveOrder(unit, (CUnit) target); + } + else { + this.moveOrder = new CMoveOrder(unit, target.getX(), target.getY()); + } } @Override public boolean update(final CSimulation simulation) { + if (this.target.isDead() + || !this.target.canBeTargetedBy(simulation, this.unit, this.unitAttack.getTargetsAllowed())) { + return true; + } + float range = this.unitAttack.getRange(); + if ((this.target instanceof CUnit) && (((CUnit) this.target).getCurrentOrder() instanceof CMoveOrder) + && (this.damagePointLaunchTime != 0 /* + * only apply range motion buffer if they were already in range and + * attacked + */)) { + range += this.unitAttack.getRangeMotionBuffer(); + } + if (!this.unit.canReach(this.target, range)) { + if (this.moveOrder.update(simulation)) { + return true; // we just cant reach them + } + this.wasInRange = false; + this.damagePointLaunchTime = 0; + this.thisOrderCooldownEndTime = 0; + return false; + } + this.wasInRange = true; final float prevX = this.unit.getX(); final float prevY = this.unit.getY(); final float deltaY = this.target.getY() - prevY; @@ -70,10 +106,13 @@ public class CAttackOrder implements COrder { final int cooldownEndTime = this.unit.getCooldownEndTime(); final int currentTurnTick = simulation.getGameTurnTick(); if (this.wasWithinPropWindow) { - if (this.backswingLaunchTime != 0) { - if (currentTurnTick >= this.backswingLaunchTime) { - simulation.createProjectile(this.unit, 0, this.target); - this.backswingLaunchTime = 0; + if (this.damagePointLaunchTime != 0) { + if (currentTurnTick >= this.damagePointLaunchTime) { + final int minDamage = this.unitAttack.getMinDamage(); + final int maxDamage = this.unitAttack.getMaxDamage(); + final int damage = simulation.getSeededRandom().nextInt(maxDamage - minDamage) + minDamage; + this.unitAttack.launch(simulation, this.unit, this.target, damage); + this.damagePointLaunchTime = 0; } } else if (currentTurnTick >= cooldownEndTime) { @@ -84,15 +123,21 @@ public class CAttackOrder implements COrder { final int a1DamagePointSteps = (int) (this.unitAttack.getAnimationDamagePoint() / WarsmashConstants.SIMULATION_STEP_TIME); this.unit.setCooldownEndTime(currentTurnTick + a1CooldownSteps); - this.backswingLaunchTime = currentTurnTick + a1DamagePointSteps; - this.unit.getUnitAnimationListener().playAnimation(true, PrimaryTag.ATTACK, - CUnitAnimationListener.EMPTY, 1.0f); - this.unit.getUnitAnimationListener().queueAnimation(PrimaryTag.STAND, CUnitAnimationListener.READY); + this.thisOrderCooldownEndTime = currentTurnTick + a1CooldownSteps; + this.damagePointLaunchTime = currentTurnTick + a1DamagePointSteps; + this.backSwingTime = currentTurnTick + a1DamagePointSteps + a1BackswingSteps; + this.unit.getUnitAnimationListener().playAnimation(true, PrimaryTag.ATTACK, SequenceUtils.EMPTY, 1.0f, + true); + this.unit.getUnitAnimationListener().queueAnimation(PrimaryTag.STAND, SequenceUtils.READY, false); + } + else if ((currentTurnTick >= this.thisOrderCooldownEndTime)) { + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.READY, 1.0f, + false); } } else { - this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, CUnitAnimationListener.READY, - 1.0f); + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.READY, 1.0f, + false); } return false; diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CMoveOrder.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CMoveOrder.java index 1516005..83677cd 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CMoveOrder.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/CMoveOrder.java @@ -7,12 +7,12 @@ import java.util.List; import com.badlogic.gdx.math.Rectangle; import com.etheller.warsmash.util.WarsmashConstants; import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.COrder; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitAnimationListener; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWorldCollision; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityMove; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CPathfindingProcessor; @@ -24,6 +24,8 @@ public class CMoveOrder implements COrder { private List path = null; private final CPathfindingProcessor.GridMapping gridMapping; private final Point2D.Float target; + private int searchCycles = 0; + private CUnit followUnit; public CMoveOrder(final CUnit unit, final float targetX, final float targetY) { this.unit = unit; @@ -33,6 +35,15 @@ public class CMoveOrder implements COrder { this.target = new Point2D.Float(targetX, targetY); } + public CMoveOrder(final CUnit unit, final CUnit followUnit) { + this.unit = unit; + this.gridMapping = CPathfindingProcessor.isCollisionSizeBetterSuitedForCorners( + unit.getUnitType().getCollisionSize()) ? CPathfindingProcessor.GridMapping.CORNERS + : CPathfindingProcessor.GridMapping.CELLS; + this.target = new Point2D.Float(followUnit.getX(), followUnit.getY()); + this.followUnit = followUnit; + } + @Override public boolean update(final CSimulation simulation) { final float prevX = this.unit.getX(); @@ -45,12 +56,15 @@ public class CMoveOrder implements COrder { final float startFloatingX = prevX; final float startFloatingY = prevY; if (this.path == null) { - this.path = simulation.findNaiveSlowPath(this.unit, startFloatingX, startFloatingY, this.target, - movementType == null ? MovementType.FOOT : movementType, collisionSize); + if (this.followUnit != null) { + this.target.x = this.followUnit.getX(); + this.target.y = this.followUnit.getY(); + } + this.path = simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, + this.target, movementType == null ? MovementType.FOOT : movementType, collisionSize, true); System.out.println("init path " + this.path); // check for smoothing if (!this.path.isEmpty()) { - this.path.add(this.target); float lastX = startFloatingX; float lastY = startFloatingY; float smoothingGroupStartX = startFloatingX; @@ -95,16 +109,40 @@ public class CMoveOrder implements COrder { } } } + else if ((this.followUnit != null) && (this.path.size() > 1) && (this.target.distance(this.followUnit.getX(), + this.followUnit.getY()) > (0.1 * this.target.distance(this.unit.getX(), this.unit.getY())))) { + this.target.x = this.followUnit.getX(); + this.target.y = this.followUnit.getY(); + this.path = simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, + this.target, movementType == null ? MovementType.FOOT : movementType, collisionSize, + this.searchCycles < 4); + System.out.println("new path (for target) " + this.path); + if (this.path.isEmpty()) { + return true; + } + } float currentTargetX; float currentTargetY; if (this.path.isEmpty()) { - currentTargetX = this.target.x; - currentTargetY = this.target.y; + if (this.followUnit != null) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + currentTargetX = this.target.x; + currentTargetY = this.target.y; + } } else { - final Point2D.Float nextPathElement = this.path.get(0); - currentTargetX = nextPathElement.x; - currentTargetY = nextPathElement.y; + if ((this.followUnit != null) && (this.path.size() == 1)) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + final Point2D.Float nextPathElement = this.path.get(0); + currentTargetX = nextPathElement.x; + currentTargetY = nextPathElement.y; + } } float deltaX = currentTargetX - prevX; @@ -172,24 +210,46 @@ public class CMoveOrder implements COrder { && !worldCollision.intersectsAnythingOtherThan(tempRect, this.unit, movementType))) { this.unit.setPoint(nextX, nextY, worldCollision); if (done) { + // if we're making headway along the path then it's OK to start thinking fast + // again + if (travelDistance > 0) { + this.searchCycles = 0; + } if (this.path.isEmpty()) { return true; } else { + System.out.println(this.path); final Float removed = this.path.remove(0); System.out.println( - "We think we reached " + removed + " because are at " + nextX + "," + nextY); - if (this.path.isEmpty()) { - currentTargetX = this.target.x; - currentTargetY = this.target.y; + "We think we reached " + removed + " because we are at " + nextX + "," + nextY); + final boolean emptyPath = this.path.isEmpty(); + if (emptyPath) { + if (this.followUnit != null) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + currentTargetX = this.target.x; + currentTargetY = this.target.y; + } } else { - final Point2D.Float firstPathElement = this.path.get(0); - currentTargetX = firstPathElement.x; - currentTargetY = firstPathElement.y; + if ((this.followUnit != null) && (this.path.size() == 1)) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + final Point2D.Float firstPathElement = this.path.get(0); + currentTargetX = firstPathElement.x; + currentTargetY = firstPathElement.y; + } + } + deltaY = currentTargetY - nextY; + deltaX = currentTargetX - nextX; + if ((deltaX == 0.000f) && (deltaY == 0.000f) && this.path.isEmpty()) { + return true; } - deltaY = currentTargetY - prevY; - deltaX = currentTargetX - prevX; System.out.println("new target: " + currentTargetX + "," + currentTargetY); System.out.println("new delta: " + deltaX + "," + deltaY); goalAngleRad = Math.atan2(deltaY, deltaX); @@ -210,7 +270,7 @@ public class CMoveOrder implements COrder { if (absDelta >= propulsionWindow) { if (this.wasWithinPropWindow) { this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, - CUnitAnimationListener.EMPTY, 1.0f); + SequenceUtils.EMPTY, 1.0f, true); } this.wasWithinPropWindow = false; return false; @@ -219,20 +279,21 @@ public class CMoveOrder implements COrder { } } else { - this.path = simulation.findNaiveSlowPath(this.unit, startFloatingX, startFloatingY, this.target, - movementType == null ? MovementType.FOOT : movementType, collisionSize); + if (this.followUnit != null) { + this.target.x = this.followUnit.getX(); + this.target.y = this.followUnit.getY(); + } + this.path = simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, + this.target, movementType == null ? MovementType.FOOT : movementType, collisionSize, + this.searchCycles < 4); + this.searchCycles++; System.out.println("new path " + this.path); - if (this.path.isEmpty()) { + if (this.path.isEmpty() || (this.searchCycles > 5)) { return true; } - else { - this.path.add(this.target); - } - } - if (!this.wasWithinPropWindow) { - this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.WALK, - CUnitAnimationListener.EMPTY, 1.0f); } + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.WALK, SequenceUtils.EMPTY, 1.0f, + true); this.wasWithinPropWindow = true; } while (continueDistance > 0); @@ -241,8 +302,8 @@ public class CMoveOrder implements COrder { // If this happens, the unit is facing the wrong way, and has to turn before // moving. if (this.wasWithinPropWindow) { - this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, - CUnitAnimationListener.EMPTY, 1.0f); + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, + true); } this.wasWithinPropWindow = false; } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java index 885e4d5..c9fe992 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java @@ -1,6 +1,7 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing; import java.awt.geom.Point2D; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; @@ -18,7 +19,8 @@ public class CPathfindingProcessor { private final CWorldCollision worldCollision; private final Node[][] nodes; private final Node[][] cornerNodes; - private Node goal; + private final Node[] goalSet = new Node[4]; + private int goals = 0; public CPathfindingProcessor(final PathingGrid pathingGrid, final CWorldCollision worldCollision) { this.pathingGrid = pathingGrid; @@ -54,12 +56,21 @@ public class CPathfindingProcessor { */ public List findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, final float startX, final float startY, final Point2D.Float goal, final PathingGrid.MovementType movementType, - final float collisionSize) { + final float collisionSize, final boolean allowSmoothing) { + return findNaiveSlowPath(ignoreIntersectionsWithThisUnit, null, startX, startY, goal, movementType, + collisionSize, allowSmoothing); + } + + public List findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, + final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, + final boolean allowSmoothing) { final float goalX = goal.x; final float goalY = goal.y; - if (!this.pathingGrid.isPathable(goalX, goalY, movementType, collisionSize) - || !isPathableDynamically(goalX, goalY, ignoreIntersectionsWithThisUnit, movementType)) { - return Collections.emptyList(); + float weightForHittingWalls = 1E9f; + if (!this.pathingGrid.isPathable(goalX, goalY, movementType, collisionSize) || !isPathableDynamically(goalX, + goalY, ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, movementType)) { + weightForHittingWalls = 5E2f; } System.out.println("beginning findNaiveSlowPath for " + startX + "," + startY + "," + goalX + "," + goalY); if ((startX == goalX) && (startY == goalY)) { @@ -78,7 +89,20 @@ public class CPathfindingProcessor { gridMapping = GridMapping.CELLS; System.out.println("using cells"); } - this.goal = searchGraph[gridMapping.getY(this.pathingGrid, goalY)][gridMapping.getX(this.pathingGrid, goalX)]; + final int goalCellY = gridMapping.getY(this.pathingGrid, goalY); + final int goalCellX = gridMapping.getX(this.pathingGrid, goalX); + final Node mostLikelyGoal = searchGraph[goalCellY][goalCellX]; + final double bestGoalDistance = mostLikelyGoal.point.distance(goalX, goalY); + Arrays.fill(this.goalSet, null); + this.goals = 0; + for (int i = goalCellX - 1; i <= (goalCellX + 1); i++) { + for (int j = goalCellY - 1; j <= (goalCellY + 1); j++) { + final Node possibleGoal = searchGraph[j][i]; + if (possibleGoal.point.distance(goalX, goalY) <= bestGoalDistance) { + this.goalSet[this.goals++] = possibleGoal; + } + } + } final int startGridY = gridMapping.getY(this.pathingGrid, startY); final int startGridX = gridMapping.getX(this.pathingGrid, startX); for (int i = 0; i < searchGraph.length; i++) { @@ -133,8 +157,8 @@ public class CPathfindingProcessor { final Node possibleNode = searchGraph[cellY][cellX]; final float x = possibleNode.point.x; final float y = possibleNode.point.y; - if (pathableBetween(ignoreIntersectionsWithThisUnit, startX, startY, movementType, collisionSize, x, - y)) { + if (pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, startX, + startY, movementType, collisionSize, x, y)) { final double tentativeScore = possibleNode.point.distance(startX, startY); possibleNode.g = tentativeScore; @@ -142,19 +166,33 @@ public class CPathfindingProcessor { openSet.add(possibleNode); } + else { + final double tentativeScore = weightForHittingWalls; + possibleNode.g = tentativeScore; + possibleNode.f = tentativeScore + h(possibleNode); + openSet.add(possibleNode); + + } } } } while (!openSet.isEmpty()) { Node current = openSet.poll(); - if (current == this.goal) { + if (isGoal(current)) { final LinkedList totalPath = new LinkedList<>(); Direction lastCameFromDirection = null; if ((current.cameFrom != null) - && pathableBetween(ignoreIntersectionsWithThisUnit, current.cameFrom.point.x, - current.cameFrom.point.y, movementType, collisionSize, goalX, goalY)) { + && pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + current.point.x, current.point.y, movementType, collisionSize, goalX, goalY) + && pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + current.cameFrom.point.x, current.cameFrom.point.y, movementType, collisionSize, + current.point.x, current.point.y) + && pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + current.cameFrom.point.x, current.cameFrom.point.y, movementType, collisionSize, goalX, + goalY) + && allowSmoothing) { // do some basic smoothing to walk straight to the goal if it is not obstructed, // skipping the last grid location totalPath.addFirst(goal); @@ -172,8 +210,16 @@ public class CPathfindingProcessor { if ((lastCameFromDirection == null) || (current.cameFromDirection != lastCameFromDirection) || (current.cameFromDirection == null)) { if ((current.cameFromDirection != null) || (lastNode == null) - || !pathableBetween(ignoreIntersectionsWithThisUnit, startX, startY, movementType, - collisionSize, lastNode.point.x, lastNode.point.y)) { + || !pathableBetween(ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, startX, startY, movementType, + collisionSize, current.point.x, current.point.y) + || !pathableBetween(ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, + movementType, collisionSize, lastNode.point.x, lastNode.point.y) + || !pathableBetween(ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, startX, startY, movementType, + collisionSize, lastNode.point.x, lastNode.point.y) + || !allowSmoothing) { // Add the point if it's not the first one, or if we can only complete // the journey by specifically walking to the first one totalPath.addFirst(current.point); @@ -187,8 +233,7 @@ public class CPathfindingProcessor { for (final Direction direction : Direction.VALUES) { final float x = current.point.x + (direction.xOffset * 32); final float y = current.point.y + (direction.yOffset * 32); - if (this.pathingGrid.contains(x, y) && pathableBetween(ignoreIntersectionsWithThisUnit, current.point.x, - current.point.y, movementType, collisionSize, x, y)) { + if (this.pathingGrid.contains(x, y)) { double turnCost; if ((current.cameFromDirection != null) && (direction != current.cameFromDirection)) { turnCost = 0.25; @@ -196,7 +241,11 @@ public class CPathfindingProcessor { else { turnCost = 0; } - final double tentativeScore = current.g + ((direction.length + turnCost) * 32); + double tentativeScore = current.g + ((direction.length + turnCost) * 32); + if (!pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + current.point.x, current.point.y, movementType, collisionSize, x, y)) { + tentativeScore += (direction.length) * weightForHittingWalls; + } final Node neighbor = searchGraph[gridMapping.getY(this.pathingGrid, y)][gridMapping .getX(this.pathingGrid, x)]; if (tentativeScore < neighbor.g) { @@ -214,20 +263,24 @@ public class CPathfindingProcessor { return Collections.emptyList(); } - private boolean pathableBetween(final CUnit ignoreIntersectionsWithThisUnit, final float startX, final float startY, + private boolean pathableBetween(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, final PathingGrid.MovementType movementType, final float collisionSize, final float x, final float y) { return this.pathingGrid.isPathable(x, y, movementType, collisionSize) && this.pathingGrid.isPathable(startX, y, movementType, collisionSize) && this.pathingGrid.isPathable(x, startY, movementType, collisionSize) - && isPathableDynamically(x, y, ignoreIntersectionsWithThisUnit, movementType) - && isPathableDynamically(x, startY, ignoreIntersectionsWithThisUnit, movementType) - && isPathableDynamically(startX, y, ignoreIntersectionsWithThisUnit, movementType); + && isPathableDynamically(x, y, ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + movementType) + && isPathableDynamically(x, startY, ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, movementType) + && isPathableDynamically(startX, y, ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, movementType); } private boolean isPathableDynamically(final float x, final float y, final CUnit ignoreIntersectionsWithThisUnit, - final PathingGrid.MovementType movementType) { + final CUnit ignoreIntersectionsWithThisSecondUnit, final PathingGrid.MovementType movementType) { return !this.worldCollision.intersectsAnythingOtherThan(tempRect.setCenter(x, y), - ignoreIntersectionsWithThisUnit, movementType); + ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, movementType); } public static boolean isCollisionSizeBetterSuitedForCorners(final float collisionSize) { @@ -242,8 +295,24 @@ public class CPathfindingProcessor { return n.g; } + private boolean isGoal(final Node n) { + for (int i = 0; i < this.goals; i++) { + if (n == this.goalSet[i]) { + return true; + } + } + return false; + } + public float h(final Node n) { - return (float) n.point.distance(this.goal.point); + float bestDistance = 0; + for (int i = 0; i < this.goals; i++) { + final float possibleDistance = (float) n.point.distance(this.goalSet[i].point); + if (possibleDistance > bestDistance) { + bestDistance = possibleDistance; // always overestimate + } + } + return bestDistance; } public static final class Node { diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CAllianceType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CAllianceType.java new file mode 100644 index 0000000..8587028 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CAllianceType.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CAllianceType { + PASSIVE, + HELP_REQUEST, + HELP_RESPONSE, + SHARED_XP, + SHARED_SPELLS, + SHARED_VISION, + SHARED_CONTROL, + SHARED_ADVANCED_CONTROL, + RESCUABLE, + SHARED_VISION_FORCED; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapControl.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapControl.java new file mode 100644 index 0000000..0d7f4a1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapControl.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CMapControl { + USER, + COMPUTER, + RESCUABLE, + NEUTRAL, + CREEP, + NONE; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayer.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayer.java new file mode 100644 index 0000000..cc3f424 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayer.java @@ -0,0 +1,109 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +import java.util.EnumSet; + +import com.etheller.warsmash.util.WarsmashConstants; + +public class CPlayer { + private final int id; + private int colorIndex; + private final CMapControl controlType; + private String name; + private final CRace race; + private final float[] startLocation; + private final EnumSet racePrefs; + private int gold; + private int lumber; + private final EnumSet[] alliances = new EnumSet[WarsmashConstants.MAX_PLAYERS]; + + public CPlayer(final int id, final CMapControl controlType, final String name, final CRace race, + final float[] startLocation) { + this.id = id; + this.colorIndex = id; + this.controlType = controlType; + this.name = name; + this.race = race; + this.startLocation = startLocation; + this.racePrefs = EnumSet.noneOf(CRacePreference.class); + for (int i = 0; i < this.alliances.length; i++) { + if (i == this.id) { + // player is fully allied with self + this.alliances[i] = EnumSet.allOf(CAllianceType.class); + } + else { + this.alliances[i] = EnumSet.noneOf(CAllianceType.class); + } + } + } + + public int getId() { + return this.id; + } + + public int getColorIndex() { + return this.colorIndex; + } + + public CMapControl getControlType() { + return this.controlType; + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = name; + } + + public CRace getRace() { + return this.race; + } + + public boolean isRacePrefSet(final CRacePreference racePref) { + return this.racePrefs.contains(racePref); + } + + public void setAlliance(final CPlayer otherPlayer, final CAllianceType allianceType, final boolean value) { + final EnumSet alliancesWithOtherPlayer = this.alliances[otherPlayer.getId()]; + if (value) { + alliancesWithOtherPlayer.add(allianceType); + } + else { + alliancesWithOtherPlayer.remove(allianceType); + } + } + + public boolean hasAlliance(final CPlayer otherPlayer, final CAllianceType allianceType) { + return hasAlliance(otherPlayer.getId(), allianceType); + } + + public boolean hasAlliance(final int otherPlayerIndex, final CAllianceType allianceType) { + final EnumSet alliancesWithOtherPlayer = this.alliances[otherPlayerIndex]; + return alliancesWithOtherPlayer.contains(allianceType); + } + + public int getGold() { + return this.gold; + } + + public int getLumber() { + return this.lumber; + } + + public float[] getStartLocation() { + return this.startLocation; + } + + public void setGold(final int gold) { + this.gold = gold; + } + + public void setLumber(final int lumber) { + this.lumber = lumber; + } + + public void setColorIndex(final int colorIndex) { + this.colorIndex = colorIndex; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayerController.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerController.java similarity index 78% rename from core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayerController.java rename to core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerController.java index 5378300..fc482c1 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayerController.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerController.java @@ -1,4 +1,4 @@ -package com.etheller.warsmash.viewer5.handlers.w3x.simulation; +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; public interface CPlayerController { boolean issueTargetOrder(int unitHandleId, int orderId, int targetHandleId); diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRace.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRace.java new file mode 100644 index 0000000..26bb76a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRace.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CRace { + HUMAN(1), + ORC(2), + UNDEAD(3), + NIGHTELF(4), + DEMON(5), + OTHER(7); + + private int id; + + private CRace(final int id) { + this.id = id; + } + + public int getId() { + return this.id; + } + + public static CRace parseRace(final int race) { + // TODO: this is bad time complexity (slow) but we're only doing it on startup + for (final CRace raceEnum : values()) { + if (raceEnum.getId() == race) { + return raceEnum; + } + } + return null; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRacePreference.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRacePreference.java new file mode 100644 index 0000000..7086929 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRacePreference.java @@ -0,0 +1,11 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CRacePreference { + HUMAN, + ORC, + NIGHTELF, + UNDEAD, + DEMON, + RANDOM, + USER_SELECTABLE; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/ProjectileCreator.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/ProjectileCreator.java deleted file mode 100644 index be22c6a..0000000 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/ProjectileCreator.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; - -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; - -public interface ProjectileCreator { - CAttackProjectile create(CSimulation simulation, CUnit source, int attackIndex, CWidget target); -} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/SimulationRenderController.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/SimulationRenderController.java new file mode 100644 index 0000000..43f0993 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/SimulationRenderController.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; + +public interface SimulationRenderController { + CAttackProjectile createAttackProjectile(CSimulation simulation, float launchX, float launchY, float launchFacing, + CUnit source, CUnitAttackMissile attack, CWidget target, float damage, int bounceIndex); + + void createInstantAttackEffect(CSimulation cSimulation, CUnit source, CUnitAttackInstant attack, CWidget target); + + void spawnUnitDamageSound(CUnit damagedUnit, final String weaponSound, String armorType); + + void removeUnit(CUnit unit); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUI.java index 96bd077..e41a302 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUI.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUI.java @@ -2,6 +2,8 @@ package com.etheller.warsmash.viewer5.handlers.w3x.ui; import java.io.IOException; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.GlyphLayout; @@ -21,19 +23,25 @@ import com.etheller.warsmash.parsers.fdf.frames.TextureFrame; import com.etheller.warsmash.parsers.fdf.frames.UIFrame; import com.etheller.warsmash.parsers.jass.Jass2.RootFrameListener; import com.etheller.warsmash.util.FastNumberFormat; +import com.etheller.warsmash.util.ImageUtils; import com.etheller.warsmash.viewer5.Scene; import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; -import com.etheller.warsmash.viewer5.handlers.w3x.StandSequence; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.CommandCardIcon; import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.COrder; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitStateListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityStop; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CDefenseType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CodeKeyType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; -public class MeleeUI { +public class MeleeUI implements CUnitStateListener { private final DataSource dataSource; private final Viewport uiViewport; private final FreeTypeFontGenerator fontGenerator; @@ -43,6 +51,10 @@ public class MeleeUI { private GameUI rootFrame; private UIFrame consoleUI; private UIFrame resourceBar; + private StringFrame resourceBarGoldText; + private StringFrame resourceBarLumberText; + private StringFrame resourceBarSupplyText; + private StringFrame resourceBarUpkeepText; private UIFrame timeIndicator; private UIFrame unitPortrait; private StringFrame unitLifeText; @@ -69,6 +81,10 @@ public class MeleeUI { private StringFrame armorInfoPanelIconLevel; private InfoPanelIconBackdrops damageBackdrops; private InfoPanelIconBackdrops defenseBackdrops; + private RenderUnit selectedUnit; + + // TODO remove this & replace with FDF + private final Texture activeButtonTexture; public MeleeUI(final DataSource dataSource, final Viewport uiViewport, final FreeTypeFontGenerator fontGenerator, final Scene uiScene, final War3MapViewer war3MapViewer, final RootFrameListener rootFrameListener) { @@ -79,6 +95,8 @@ public class MeleeUI { this.war3MapViewer = war3MapViewer; this.rootFrameListener = rootFrameListener; + this.activeButtonTexture = ImageUtils.getBLPTexture(war3MapViewer.mapMpq, + "UI\\Widgets\\Console\\Human\\CommandButton\\human-activebutton.blp"); } /** @@ -89,7 +107,7 @@ public class MeleeUI { // ================================= // Load skins and templates // ================================= - this.rootFrame = new GameUI(this.dataSource, GameUI.loadSkin(this.dataSource, 1), this.uiViewport, + this.rootFrame = new GameUI(this.dataSource, GameUI.loadSkin(this.dataSource, 3), this.uiViewport, this.fontGenerator, this.uiScene, this.war3MapViewer); try { this.rootFrame.loadTOCFile("UI\\FrameDef\\FrameDef.toc"); @@ -118,6 +136,16 @@ public class MeleeUI { // put it in the "TOPRIGHT" corner. this.resourceBar = this.rootFrame.createSimpleFrame("ResourceBarFrame", this.consoleUI, 0); this.resourceBar.addSetPoint(new SetPoint(FramePoint.TOPRIGHT, this.consoleUI, FramePoint.TOPRIGHT, 0, 0)); + this.resourceBarGoldText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarGoldText", 0); + this.resourceBarGoldText.setText("500"); + this.resourceBarLumberText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarLumberText", 0); + this.resourceBarLumberText.setText("150"); + this.resourceBarSupplyText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarSupplyText", 0); + this.resourceBarSupplyText.setText("153/100"); + this.resourceBarSupplyText.setColor(Color.RED); + this.resourceBarUpkeepText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarUpkeepText", 0); + this.resourceBarUpkeepText.setText("High Upkeep"); + this.resourceBarUpkeepText.setColor(Color.RED); // Create the Time Indicator (clock) this.timeIndicator = this.rootFrame.createFrame("TimeOfDayIndicator", this.rootFrame, 0, 0); @@ -174,6 +202,25 @@ public class MeleeUI { public void render(final SpriteBatch batch, final BitmapFont font20, final GlyphLayout glyphLayout) { this.rootFrame.render(batch, font20, glyphLayout); + + if (this.selectedUnit != null) { + font20.setColor(Color.WHITE); + + final COrder currentOrder = this.selectedUnit.getSimulationUnit().getCurrentOrder(); + for (final CommandCardIcon commandCardIcon : this.selectedUnit.getCommandCardIcons()) { + batch.draw(commandCardIcon.getTexture(), 1235 + (86.8f * commandCardIcon.getX()), + 190 - (88 * commandCardIcon.getY()), 78f, 78f); + if (((currentOrder != null) && (currentOrder.getOrderId() == commandCardIcon.getOrderId())) + || ((currentOrder == null) && (commandCardIcon.getOrderId() == CAbilityStop.ORDER_ID))) { + final int blendDstFunc = batch.getBlendDstFunc(); + final int blendSrcFunc = batch.getBlendSrcFunc(); + batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE); + batch.draw(this.activeButtonTexture, 1235 + (86.8f * commandCardIcon.getX()), + 190 - (88 * commandCardIcon.getY()), 78f, 78f); + batch.setBlendFunction(blendSrcFunc, blendDstFunc); + } + } + } } public void portraitTalk() { @@ -196,12 +243,12 @@ public class MeleeUI { this.portraitCameraManager.updateCamera(); if ((this.modelInstance != null) && (this.modelInstance.sequenceEnded || (this.modelInstance.sequence == -1))) { - StandSequence.randomPortraitSequence(this.modelInstance); + SequenceUtils.randomPortraitSequence(this.modelInstance); } } public void talk() { - StandSequence.randomPortraitTalkSequence(this.modelInstance); + SequenceUtils.randomPortraitTalkSequence(this.modelInstance); } public void setSelectedUnit(final RenderUnit unit) { @@ -220,7 +267,7 @@ public class MeleeUI { } this.modelInstance = (MdxComplexInstance) portraitModel.addInstance(); this.portraitCameraManager.setModelInstance(this.modelInstance, portraitModel); - this.modelInstance.setSequenceLoopMode(1); + this.modelInstance.setSequenceLoopMode(SequenceLoopMode.MODEL_LOOP); this.modelInstance.setScene(this.portraitScene); this.modelInstance.setVertexColor(unit.instance.vertexColor); this.modelInstance.setTeamColor(unit.playerIndex); @@ -229,8 +276,15 @@ public class MeleeUI { } } - public void selectUnit(final RenderUnit unit) { + public void selectUnit(RenderUnit unit) { + if ((unit != null) && unit.getSimulationUnit().isDead()) { + unit = null; + } + if (this.selectedUnit != null) { + this.selectedUnit.getSimulationUnit().removeStateListener(this); + } this.portrait.setSelectedUnit(unit); + this.selectedUnit = unit; if (unit == null) { this.simpleNameValue.setText(""); this.unitLifeText.setText(""); @@ -245,6 +299,7 @@ public class MeleeUI { this.armorInfoPanelIconLevel.setText(""); } else { + unit.getSimulationUnit().addStateListener(this); this.simpleNameValue.setText(unit.getSimulationUnit().getUnitType().getName()); String classText = null; for (final CUnitClassification classification : unit.getSimulationUnit().getClassifications()) { @@ -291,8 +346,13 @@ public class MeleeUI { } this.armorIcon.setVisible(true); - this.armorIconBackdrop.setTexture( - this.defenseBackdrops.getTexture(unit.getSimulationUnit().getUnitType().getDefenseType())); + final Texture defenseTexture = this.defenseBackdrops + .getTexture(unit.getSimulationUnit().getUnitType().getDefenseType()); + if (defenseTexture == null) { + throw new RuntimeException( + unit.getSimulationUnit().getUnitType().getDefenseType() + " can't find texture!"); + } + this.armorIconBackdrop.setTexture(defenseTexture); this.armorInfoPanelIconValue.setText(Integer.toString(unit.getSimulationUnit().getDefense())); } } @@ -309,8 +369,8 @@ public class MeleeUI { this.uiViewport.project(this.projectionTemp1); this.uiViewport.project(this.projectionTemp2); - this.tempRect.x = this.projectionTemp1.x; - this.tempRect.y = this.projectionTemp1.y; + this.tempRect.x = this.projectionTemp1.x + this.uiViewport.getScreenX(); + this.tempRect.y = this.projectionTemp1.y + this.uiViewport.getScreenY(); this.tempRect.width = this.projectionTemp2.x - this.projectionTemp1.x; this.tempRect.height = this.projectionTemp2.y - this.projectionTemp1.y; this.portrait.portraitScene.camera.viewport(this.tempRect); @@ -325,10 +385,11 @@ public class MeleeUI { for (int index = 0; index < attackTypes.length; index++) { final CodeKeyType attackType = attackTypes[index]; String skinLookupKey = "InfoPanelIcon" + prefix + attackType.getCodeKey() + suffix; - try { - this.damageBackdropTextures[index] = gameUI.loadTexture(gameUI.getSkinField(skinLookupKey)); + final Texture suffixTexture = gameUI.loadTexture(gameUI.getSkinField(skinLookupKey)); + if (suffixTexture != null) { + this.damageBackdropTextures[index] = suffixTexture; } - catch (final Exception exc) { + else { skinLookupKey = "InfoPanelIcon" + prefix + attackType.getCodeKey(); this.damageBackdropTextures[index] = gameUI.loadTexture(gameUI.getSkinField(skinLookupKey)); } @@ -369,4 +430,20 @@ public class MeleeUI { } } + + @Override + public void lifeChanged() { + if (this.selectedUnit.getSimulationUnit().isDead()) { + selectUnit(null); + } + else { + this.unitLifeText + .setText(FastNumberFormat.formatWholeNumber(this.selectedUnit.getSimulationUnit().getLife()) + " / " + + this.selectedUnit.getSimulationUnit().getMaximumLife()); + } + } + + public RenderUnit getSelectedUnit() { + return this.selectedUnit; + } } diff --git a/desktop/src/com/etheller/warsmash/desktop/DesktopLauncher.java b/desktop/src/com/etheller/warsmash/desktop/DesktopLauncher.java index 2c7fba2..5ad7d58 100644 --- a/desktop/src/com/etheller/warsmash/desktop/DesktopLauncher.java +++ b/desktop/src/com/etheller/warsmash/desktop/DesktopLauncher.java @@ -84,8 +84,14 @@ public class DesktopLauncher { config.gles30ContextMajorVersion = 3; config.gles30ContextMinorVersion = 3; config.samples = 16; - config.fullscreen = false; - if (config.fullscreen) { + config.vSyncEnabled = false; + config.foregroundFPS = 0; + config.backgroundFPS = 0; + if ((arg.length > 0) && "-windowed".equals(arg[0])) { + config.fullscreen = false; + } + else { + config.fullscreen = true; final DisplayMode desktopDisplayMode = LwjglApplicationConfiguration.getDesktopDisplayMode(); config.width = desktopDisplayMode.width; config.height = desktopDisplayMode.height;