diff --git a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundAsteroidManager.java b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundAsteroidManager.java index 1059b3b49..1caec41d2 100644 --- a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundAsteroidManager.java +++ b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundAsteroidManager.java @@ -16,6 +16,7 @@ package org.destinationsol.menu.background; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Body; @@ -64,11 +65,11 @@ public MenuBackgroundAsteroidManager(DisplayDimensions displayDimensions, World } } - public void update() { + public void update(float delta) { retainedBackgroundAsteroids.clear(); for (MenuBackgroundObject backgroundObject : backgroundAsteroids) { - backgroundObject.update(); + backgroundObject.update(delta); boolean isInWidth = Math.abs(backgroundObject.getPosition().x) < MenuBackgroundManager.VIEWPORT_HEIGHT * displayDimensions.getRatio() * 0.8f; boolean isInHeight = Math.abs(backgroundObject.getPosition().y) < MenuBackgroundManager.VIEWPORT_HEIGHT * 0.8f; @@ -123,7 +124,7 @@ public MenuBackgroundObject buildAsteroid() { Body body = asteroidMeshLoader.getBodyAndSprite(world, texture, size, BodyDef.BodyType.DynamicBody, position, angle, new ArrayList<>(), 10f, DrawableLevel.BODIES); body.setLinearVelocity(velocity); body.setAngularVelocity(angularVelocity); - MenuBackgroundObject asteroid = new MenuBackgroundObject(texture, size, tint, position, velocity, asteroidMeshLoader.getOrigin(texture.name, size).cpy(), angle, body); + MenuBackgroundObject asteroid = new MenuBackgroundObject(new Animation<>(Float.MAX_VALUE, texture), size, tint, position, velocity, asteroidMeshLoader.getOrigin(texture.name, size).cpy(), angle, body); body.setUserData(asteroid); return asteroid; diff --git a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundManager.java b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundManager.java index 40c5b9762..7a5580446 100644 --- a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundManager.java +++ b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundManager.java @@ -48,9 +48,9 @@ public MenuBackgroundManager(DisplayDimensions displayDimensions) { backgroundCamera = new OrthographicCamera(VIEWPORT_HEIGHT * displayDimensions.getRatio(), -VIEWPORT_HEIGHT); } - public void update() { - asteroidManager.update(); - shipManager.update(); + public void update(float delta) { + asteroidManager.update(delta); + shipManager.update(delta); world.step(Const.REAL_TIME_STEP, 6, 2); } diff --git a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundObject.java b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundObject.java index 4c8a6a9e7..70b5a035a 100644 --- a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundObject.java +++ b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundObject.java @@ -16,6 +16,7 @@ package org.destinationsol.menu.background; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; @@ -27,7 +28,7 @@ * Contains common data holders required for all background objects. */ public class MenuBackgroundObject { - TextureAtlas.AtlasRegion texture; + Animation frames; float scale; @@ -41,8 +42,10 @@ public class MenuBackgroundObject { Body body; - public MenuBackgroundObject(TextureAtlas.AtlasRegion texture, float scale, Color tint, Vector2 position, Vector2 velocity, Vector2 origin, float angle, Body body) { - this.texture = texture; + float animationTime; + + public MenuBackgroundObject(Animation frames, float scale, Color tint, Vector2 position, Vector2 velocity, Vector2 origin, float angle, Body body) { + this.frames = frames; this.scale = scale; this.tint = tint; this.position = position; @@ -50,6 +53,7 @@ public MenuBackgroundObject(TextureAtlas.AtlasRegion texture, float scale, Color this.origin = origin; this.angle = angle; this.body = body; + this.animationTime = 0.0f; } public void setParamsFromBody() { @@ -58,12 +62,13 @@ public void setParamsFromBody() { angle = body.getAngle() * MathUtils.radDeg; } - public void update() { + public void update(float delta) { + this.animationTime += delta; setParamsFromBody(); } public void draw(UiDrawer drawer) { - drawer.draw(texture, scale, scale, origin.x, origin.y, position.x, position.y, angle, tint); + drawer.draw(frames.getKeyFrame(animationTime, true), scale, scale, origin.x, origin.y, position.x, position.y, angle, tint); } public Vector2 getPosition() { diff --git a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundShipManager.java b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundShipManager.java index 32e391c75..1d6ec974f 100644 --- a/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundShipManager.java +++ b/engine/src/main/java/org/destinationsol/menu/background/MenuBackgroundShipManager.java @@ -15,6 +15,7 @@ */ package org.destinationsol.menu.background; +import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Body; @@ -76,6 +77,7 @@ public MenuBackgroundShipManager(DisplayDimensions displayDimensions, World worl public void addShip(String urnString) { + Animation frames = Assets.getAnimation(urnString); TextureAtlas.AtlasRegion texture = Assets.getAtlasRegion(urnString); JSONObject rootNode = Validator.getValidatedJSON(urnString, "engine:schemaHullConfig"); @@ -89,12 +91,12 @@ public void addShip(String urnString) { Body body = shipMeshLoader.getBodyAndSprite(world, texture, scale, BodyDef.BodyType.DynamicBody, position, angle, new ArrayList<>(), Float.MAX_VALUE, DrawableLevel.BODIES); body.setLinearVelocity(velocity); Vector2 origin = shipMeshLoader.getOrigin(texture.name, scale); - MenuBackgroundObject ship = new MenuBackgroundObject(texture, scale, SolColor.WHITE, position, velocity, origin.cpy(), angle, body); + MenuBackgroundObject ship = new MenuBackgroundObject(frames, scale, SolColor.WHITE, position, velocity, origin.cpy(), angle, body); backgroundShips.add(ship); } - public void update() { - backgroundShips.forEach(ship -> ship.update()); + public void update(float deltaTime) { + backgroundShips.forEach(ship -> ship.update(deltaTime)); backgroundShips.removeIf(ship -> ship.getPosition().y >= 3f); if (backgroundShips.isEmpty()) { //Spawn a random ship diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/CreditsScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/CreditsScreen.java index 0210e2ec1..f31a0f4b8 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/CreditsScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/CreditsScreen.java @@ -182,7 +182,7 @@ public void update(float delta) { textColor.setAlpha(Math.clamp(0.0f, 1.0f, alpha)); textSkin.getDefaultStyleFor(creditsText.getFamily()).setTextColor(textColor); - solApplication.getMenuBackgroundManager().update(); + solApplication.getMenuBackgroundManager().update(delta); } @Override diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/InputMapScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/InputMapScreen.java index 6821bdcd1..dffca78c7 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/InputMapScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/InputMapScreen.java @@ -172,7 +172,7 @@ public void update(float delta) { } } - solApplication.getMenuBackgroundManager().update(); + solApplication.getMenuBackgroundManager().update(delta); } @Override diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/MainMenuScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/MainMenuScreen.java index d72e13293..0703291b7 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/MainMenuScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/MainMenuScreen.java @@ -91,7 +91,7 @@ public void onAdded() { @Override public void update(float delta) { super.update(delta); - solApplication.getMenuBackgroundManager().update(); + solApplication.getMenuBackgroundManager().update(delta); } @Override diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewGameScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewGameScreen.java index 307896a3e..3455e4e3e 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewGameScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewGameScreen.java @@ -71,7 +71,7 @@ public void onAdded() { @Override public void update(float delta) { super.update(delta); - solApplication.getMenuBackgroundManager().update(); + solApplication.getMenuBackgroundManager().update(delta); } @Override diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java index d2b6dcdc4..d0e7e4dfa 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java @@ -15,6 +15,8 @@ */ package org.destinationsol.ui.nui.screens.mainMenu; +import com.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; import org.destinationsol.SolApplication; import org.destinationsol.assets.Assets; import org.destinationsol.assets.json.Json; @@ -25,23 +27,23 @@ import org.destinationsol.ui.nui.NUIManager; import org.destinationsol.ui.nui.NUIScreenLayer; import org.destinationsol.ui.nui.widgets.KeyActivatedButton; +import org.destinationsol.ui.nui.widgets.UIAnimatedImage; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.gestalt.assets.ResourceUrn; import org.terasology.gestalt.module.Module; import org.terasology.gestalt.naming.Name; +import org.terasology.joml.geom.Rectanglei; import org.terasology.nui.Canvas; import org.terasology.nui.UITextureRegion; import org.terasology.nui.backends.libgdx.GDXInputUtil; import org.terasology.nui.widgets.UIButton; -import org.terasology.nui.widgets.UIImage; import javax.inject.Inject; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; public class NewShipScreen extends NUIScreenLayer { @@ -50,7 +52,7 @@ public class NewShipScreen extends NUIScreenLayer { private final ModuleManager moduleManager; private int playerSpawnConfigIndex = 0; private List playerSpawnConfigNames = new ArrayList<>(); - private List playerSpawnConfigTextures = new ArrayList<>(); + private List playerSpawnConfigTextures = new ArrayList<>(); private WorldConfig worldConfig; @Inject @@ -82,7 +84,14 @@ public void initialise() { for (String spawnConfigName : playerSpawnConfigs.keySet()) { JSONObject playerSpawnConfig = playerSpawnConfigs.getJSONObject(spawnConfigName); try { - playerSpawnConfigTextures.add(Assets.getDSTexture(playerSpawnConfig.getString("hull")).getUiTexture()); + String shipHullName = playerSpawnConfig.getString("hull"); + Animation shipAnimation = Assets.getAnimation(shipHullName); + UITextureRegion shipAnimationTextureRegion = Assets.getDSTexture(shipHullName).getUiTexture(); + List frames = new ArrayList<>(); + for (TextureAtlas.AtlasRegion frame : shipAnimation.getKeyFrames()) { + frames.add(new Rectanglei(frame.getRegionX(), frame.getRegionY() - frame.getRegionHeight(), frame.getRegionX() + frame.getRegionWidth(), frame.getRegionY())); + } + playerSpawnConfigTextures.add(new ShipSpriteData(shipAnimationTextureRegion, shipAnimation.getFrameDuration(), frames)); } catch (RuntimeException e) { logger.error("Failed to load ship texture!", e); // Null values will not render any texture. @@ -91,15 +100,19 @@ public void initialise() { } } - UIImage shipPreviewImage = find("shipPreviewImage", UIImage.class); - shipPreviewImage.setImage(playerSpawnConfigTextures.get(playerSpawnConfigIndex)); + UIAnimatedImage shipPreviewImage = find("shipPreviewImage", UIAnimatedImage.class); + shipPreviewImage.setSpritesheet(playerSpawnConfigTextures.get(playerSpawnConfigIndex).texture); + shipPreviewImage.setFrameDuration(playerSpawnConfigTextures.get(playerSpawnConfigIndex).frameDuration); + shipPreviewImage.setFrames(playerSpawnConfigTextures.get(playerSpawnConfigIndex).frames); UIButton startingShipButton = find("startingShipButton", UIButton.class); startingShipButton.setText("Starting Ship: " + playerSpawnConfigNames.get(playerSpawnConfigIndex)); startingShipButton.subscribe(button -> { playerSpawnConfigIndex = (playerSpawnConfigIndex + 1) % playerSpawnConfigNames.size(); ((UIButton)button).setText("Starting Ship: " + playerSpawnConfigNames.get(playerSpawnConfigIndex)); - shipPreviewImage.setImage(playerSpawnConfigTextures.get(playerSpawnConfigIndex)); + shipPreviewImage.setSpritesheet(playerSpawnConfigTextures.get(playerSpawnConfigIndex).texture); + shipPreviewImage.setFrameDuration(playerSpawnConfigTextures.get(playerSpawnConfigIndex).frameDuration); + shipPreviewImage.setFrames(playerSpawnConfigTextures.get(playerSpawnConfigIndex).frames); }); UIButton modulesButton = find("modulesButton", UIButton.class); @@ -150,7 +163,7 @@ public void onAdded() { @Override public void update(float delta) { super.update(delta); - solApplication.getMenuBackgroundManager().update(); + solApplication.getMenuBackgroundManager().update(delta); } @Override @@ -166,4 +179,16 @@ public void onDraw(Canvas canvas) { protected boolean escapeCloses() { return false; } + + private static class ShipSpriteData { + public final float frameDuration; + public final List frames; + public final UITextureRegion texture; + + public ShipSpriteData(UITextureRegion texture, float frameDuration, List frames) { + this.texture = texture; + this.frameDuration = frameDuration; + this.frames = frames; + } + } } diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/OptionsScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/OptionsScreen.java index ed8baf27f..8b762ba09 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/OptionsScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/OptionsScreen.java @@ -120,7 +120,7 @@ public void onAdded() { @Override public void update(float delta) { super.update(delta); - solApplication.getMenuBackgroundManager().update(); + solApplication.getMenuBackgroundManager().update(delta); } @Override diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ResolutionScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ResolutionScreen.java index 82d363464..879793212 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ResolutionScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ResolutionScreen.java @@ -115,7 +115,7 @@ public void initialise() { @Override public void update(float delta) { super.update(delta); - solApplication.getMenuBackgroundManager().update(); + solApplication.getMenuBackgroundManager().update(delta); } @Override diff --git a/engine/src/main/java/org/destinationsol/ui/nui/widgets/UIAnimatedImage.java b/engine/src/main/java/org/destinationsol/ui/nui/widgets/UIAnimatedImage.java new file mode 100644 index 000000000..1f8963ea7 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/ui/nui/widgets/UIAnimatedImage.java @@ -0,0 +1,190 @@ +/* + * Copyright 2026 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.destinationsol.ui.nui.widgets; + +import org.joml.Vector2i; +import org.terasology.joml.geom.Rectanglei; +import org.terasology.nui.Canvas; +import org.terasology.nui.Color; +import org.terasology.nui.CoreWidget; +import org.terasology.nui.LayoutConfig; +import org.terasology.nui.ScaleMode; +import org.terasology.nui.UITextureRegion; +import org.terasology.nui.databinding.Binding; +import org.terasology.nui.databinding.DefaultBinding; + +import java.util.List; + +/** + * A widget to display an animated image based on a spritesheet. + */ +public class UIAnimatedImage extends CoreWidget { + @LayoutConfig + private Binding spritesheet = new DefaultBinding<>(); + @LayoutConfig + private Binding frameDuration = new DefaultBinding<>(); + @LayoutConfig + private Binding> frames = new DefaultBinding<>(); + + @LayoutConfig + private Binding tint = new DefaultBinding<>(Color.WHITE); + + @LayoutConfig + private boolean ignoreAspectRatio; + + private float frameTimer; + private int frameNo; + + public UIAnimatedImage() { + } + + public UIAnimatedImage(String id) { + super(id); + } + + public UIAnimatedImage(UITextureRegion spritesheet, float frameDuration, List frames) { + this.spritesheet.set(spritesheet); + this.frameDuration.set(frameDuration); + this.frames.set(frames); + this.frameTimer = 0; + this.frameNo = 0; + } + + public UIAnimatedImage(String id, UITextureRegion spritesheet, float frameDuration, List frames) { + super(id); + this.spritesheet.set(spritesheet); + this.frameDuration.set(frameDuration); + this.frames.set(frames); + this.frameTimer = 0; + this.frameNo = 0; + } + + public UIAnimatedImage(String id, UITextureRegion spritesheet, float frameDuration, List frames, boolean ignoreAspectRatio) { + super(id); + this.spritesheet.set(spritesheet); + this.frameDuration.set(frameDuration); + this.frames.set(frames); + this.frameTimer = 0; + this.frameNo = 0; + this.ignoreAspectRatio = ignoreAspectRatio; + } + + @Override + public void update(float delta) { + frameTimer += delta; + if (frameTimer >= frameDuration.get()) { + frameTimer = 0; + frameNo = (frameNo + 1) % frames.get().size(); + } + super.update(delta); + } + + @Override + public void onDraw(Canvas canvas) { + if (spritesheet.get() != null || frames.get().isEmpty()) { + Rectanglei currentFrame = frames.get().get(frameNo); + if (ignoreAspectRatio) { + ScaleMode scaleMode = canvas.getCurrentStyle().getTextureScaleMode(); + + if (spritesheet.get().getWidth() > (spritesheet.get().getHeight() * 2)) { + canvas.getCurrentStyle().setTextureScaleMode(ScaleMode.STRETCH); + } else { + canvas.getCurrentStyle().setTextureScaleMode(ScaleMode.SCALE_FILL); + } + canvas.drawTextureRaw(spritesheet.get(), canvas.getRegion(), tint.get(), + canvas.getCurrentStyle().getTextureScaleMode(), + (float) currentFrame.minX() / spritesheet.get().getWidth(), + (float) currentFrame.minY() / spritesheet.get().getHeight(), + (float) currentFrame.getSizeX() / spritesheet.get().getWidth(), + (float) currentFrame.getSizeY() / spritesheet.get().getHeight() + ); + canvas.getCurrentStyle().setTextureScaleMode(scaleMode); + } else { + canvas.drawTextureRaw(spritesheet.get(), canvas.getRegion(), tint.get(), + canvas.getCurrentStyle().getTextureScaleMode(), + (float) currentFrame.minX() / spritesheet.get().getWidth(), + (float) currentFrame.minY() / spritesheet.get().getHeight(), + (float) currentFrame.getSizeX() / spritesheet.get().getWidth(), + (float) currentFrame.getSizeY() / spritesheet.get().getHeight() + ); + } + } + } + + @Override + public Vector2i getPreferredContentSize(Canvas canvas, Vector2i sizeHint) { + if (spritesheet.get() != null && !frames.get().isEmpty()) { + if (frameNo >= frames.get().size()) { + frameNo = 0; + } + return frames.get().get(frameNo).getSize(new Vector2i()); + } + return new Vector2i(); + } + + /** + * @return The spritesheet image being used. + */ + public UITextureRegion getSpritesheet() { + return spritesheet.get(); + } + + /** + * @param spritesheet The new spritesheet image to use. + */ + public void setSpritesheet(UITextureRegion spritesheet) { + this.spritesheet.set(spritesheet); + } + + public void bindSpritesheet(Binding binding) { + this.spritesheet = binding; + } + + public void setFrameDuration(float frameDuration) { + this.frameDuration.set(frameDuration); + } + + public void bindFrameDuration(Binding frameDuration) { + this.frameDuration = frameDuration; + } + + public void setFrames(List frames) { + this.frames.set(frames); + } + + public void bindFrame(Binding> frames) { + this.frames = frames; + } + + /** + * @return The Color of the tint. + */ + public Color getTint() { + return tint.get(); + } + + /** + * @param color The new tint to apply. + */ + public void setTint(Color color) { + this.tint.set(color); + } + + public void bindTint(Binding binding) { + this.tint = binding; + } +} diff --git a/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/newShipScreen.ui b/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/newShipScreen.ui index eed5dd8d8..399076197 100644 --- a/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/newShipScreen.ui +++ b/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/newShipScreen.ui @@ -22,7 +22,7 @@ } }, { - "type": "UIImage", + "type": "UIAnimatedImage", "id": "shipPreviewImage", "layoutInfo": { "position-horizontal-center": {},