Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/uml-diagrams/imgs/plantuml_class_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/uml-diagrams/imgs/plantuml_class_diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion docs/uml-diagrams/plantuml_class_diagram.puml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ legend right
interfaces such as TurnProcessor and WaveManager
Observer - BattleEventPublisher + BattleEventListener
Factory - BattleSetupFactory and CombatantFactory
SRP - ArenaScenePanel, ArenaSceneModel, ArenaSceneRenderer
SRP - ArenaScenePanel, ArenaSceneModel, ArenaSceneRenderer,
ArenaBackgroundRenderer
Boundary-Control - UI classes delegate battle flow to
BattleController and BattleEngine
endlegend
Expand Down Expand Up @@ -760,10 +761,18 @@ package "UI LAYER - Swing GUI boundary/control" {
}

class ArenaSceneRenderer <<boundary>> {
- backgroundRenderer: ArenaBackgroundRenderer
- fighterRenderer: FighterSpriteRenderer
~ render(g: Graphics2D, model: ArenaSceneModel, width: int, height: int, now: long): void
}

class ArenaBackgroundRenderer <<boundary>> {
{static} ~ BACKGROUND_RESOURCE: String
- source: BufferedImage
- scaledBackground: BufferedImage
~ render(g: Graphics2D, width: int, height: int): void
}

class ArenaSceneLoop <<control>> {
- tickCallback: Runnable
- tickTimer: Timer
Expand Down Expand Up @@ -1053,6 +1062,7 @@ ArenaScenePanel "1" *-- "1" ArenaSceneRenderer
ArenaScenePanel "1" *-- "1" ArenaSceneLoop
ArenaSceneModel "1" o-- "0..*" FighterSpriteDto : sprites
ArenaSceneModel "1" *-- "0..*" FloatingText : floatingTexts
ArenaSceneRenderer "1" o-- "1" ArenaBackgroundRenderer
ArenaSceneRenderer "1" *-- "1" FighterSpriteRenderer
FighterBodyRenderer <|.. WarriorBodyRenderer
FighterBodyRenderer <|.. WizardBodyRenderer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package sc2002.turnbased.ui.gui.view;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Objects;

import javax.imageio.ImageIO;

final class ArenaBackgroundRenderer {
static final String BACKGROUND_RESOURCE = "/sc2002/turnbased/ui/gui/assets/arena-background.png";

private final BufferedImage source;
private BufferedImage scaledBackground;
private int scaledWidth;
private int scaledHeight;

ArenaBackgroundRenderer() {
this(loadResource(BACKGROUND_RESOURCE));
}

ArenaBackgroundRenderer(BufferedImage source) {
this.source = Objects.requireNonNull(source, "source");
if (source.getWidth() <= 0 || source.getHeight() <= 0) {
throw new IllegalArgumentException("Background image must have positive dimensions.");
}
}

void render(Graphics2D g, int width, int height) {
Objects.requireNonNull(g, "g");
if (width <= 0 || height <= 0) {
return;
}
g.drawImage(scaledBackground(width, height), 0, 0, null);
}

private BufferedImage scaledBackground(int width, int height) {
if (scaledBackground == null || width != scaledWidth || height != scaledHeight) {
scaledBackground = createScaledBackground(width, height);
scaledWidth = width;
scaledHeight = height;
}
return scaledBackground;
}

private BufferedImage createScaledBackground(int width, int height) {
BufferedImage target = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D backgroundGraphics = target.createGraphics();
try {
backgroundGraphics.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR
);
backgroundGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
SourceCrop crop = coverCrop(width, height);
backgroundGraphics.drawImage(
source,
0,
0,
width,
height,
crop.x(),
crop.y(),
crop.x() + crop.width(),
crop.y() + crop.height(),
null
);
} finally {
backgroundGraphics.dispose();
}
return target;
}

private SourceCrop coverCrop(int width, int height) {
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
double sourceRatio = sourceWidth / (double) sourceHeight;
double targetRatio = width / (double) height;
if (sourceRatio > targetRatio) {
int cropWidth = Math.max(1, (int) Math.round(sourceHeight * targetRatio));
int cropX = (sourceWidth - cropWidth) / 2;
return new SourceCrop(cropX, 0, cropWidth, sourceHeight);
}
int cropHeight = Math.max(1, (int) Math.round(sourceWidth / targetRatio));
int cropY = (sourceHeight - cropHeight) / 2;
return new SourceCrop(0, cropY, sourceWidth, cropHeight);
}

private static BufferedImage loadResource(String resourceName) {
try (InputStream inputStream = ArenaBackgroundRenderer.class.getResourceAsStream(resourceName)) {
if (inputStream == null) {
throw new IllegalStateException("Missing arena background resource: " + resourceName);
}
BufferedImage image = ImageIO.read(inputStream);
if (image == null) {
throw new IllegalStateException("Unreadable arena background resource: " + resourceName);
}
return image;
} catch (IOException exception) {
throw new UncheckedIOException("Unable to load arena background resource: " + resourceName, exception);
}
}

private record SourceCrop(int x, int y, int width, int height) {
}
}
95 changes: 11 additions & 84 deletions src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,103 +5,34 @@
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
import java.util.Objects;

final class ArenaSceneRenderer {
private static final long FLOATING_TEXT_NANOS = 1_000_000_000L;

private final ArenaBackgroundRenderer backgroundRenderer;
private final FighterSpriteRenderer fighterRenderer = new FighterSpriteRenderer();

ArenaSceneRenderer() {
this(new ArenaBackgroundRenderer());
}

ArenaSceneRenderer(ArenaBackgroundRenderer backgroundRenderer) {
this.backgroundRenderer = Objects.requireNonNull(backgroundRenderer, "backgroundRenderer");
}

void render(Graphics2D g, ArenaSceneModel model, int width, int height, long now) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
drawBackground(g, width, height);
backgroundRenderer.render(g, width, height);
drawTargetPath(g, model);
drawSprites(g, model);
drawFloatingTexts(g, model, now);
drawOverlay(g, model, width);
}

private void drawBackground(Graphics2D g, int width, int height) {
int floorTop = floorTop(height);
g.setPaint(new GradientPaint(0, 0, new Color(34, 107, 124), 0, height, new Color(232, 86, 76)));
g.fillRect(0, 0, width, height);

g.setColor(new Color(255, 210, 99, 185));
g.fillOval(width - 178, 48, 86, 86);

drawMountain(g, -40, floorTop - 160, 270, new Color(60, 102, 91));
drawMountain(g, 190, floorTop - 145, 270, new Color(55, 84, 93));
drawMountain(g, 470, floorTop - 152, 300, new Color(63, 105, 81));
drawForest(g, width, floorTop);
drawRuins(g, width, floorTop);

g.setPaint(new GradientPaint(0, floorTop, new Color(58, 69, 58), 0, height, new Color(34, 48, 45)));
g.fillRect(0, floorTop, width, height - floorTop);

g.setColor(new Color(88, 116, 91, 140));
for (int x = -40; x < width + 90; x += 86) {
g.fillRoundRect(x, floorTop + 22, 58, 18, 8, 8);
g.fillRoundRect(x + 28, floorTop + 116, 74, 22, 8, 8);
}
g.setColor(new Color(18, 29, 31, 90));
for (int y = floorTop + 30; y < height; y += 48) {
g.drawLine(0, y, width, y - 26);
}
}

private void drawMountain(Graphics2D g, int x, int baseY, int size, Color color) {
Polygon mountain = new Polygon();
mountain.addPoint(x, baseY + size);
mountain.addPoint(x + size / 2, baseY);
mountain.addPoint(x + size, baseY + size);
g.setColor(color);
g.fillPolygon(mountain);
g.setColor(new Color(235, 238, 214, 88));
Polygon cap = new Polygon();
cap.addPoint(x + size / 2, baseY);
cap.addPoint(x + size / 2 - 32, baseY + 70);
cap.addPoint(x + size / 2 + 12, baseY + 48);
cap.addPoint(x + size / 2 + 42, baseY + 86);
g.fillPolygon(cap);
}

private void drawForest(Graphics2D g, int width, int floorTop) {
int horizon = floorTop - 40;
for (int x = -20; x < width + 60; x += 42) {
int height = 54 + Math.floorMod(x * 13, 48);
g.setColor(new Color(34, 78, 57, 180));
Path2D tree = new Path2D.Double();
tree.moveTo(x, horizon);
tree.lineTo(x + 20, horizon - height);
tree.lineTo(x + 42, horizon);
tree.closePath();
g.fill(tree);
g.setColor(new Color(50, 63, 48, 190));
g.fillRect(x + 18, horizon - 10, 8, 18);
}
}

private void drawRuins(Graphics2D g, int width, int floorTop) {
int base = floorTop - 26;
Color stoneColor = new Color(74, 77, 72, 155);
Color stripeColor = new Color(120, 58, 58, 150);
for (int x = 44; x < width; x += 245) {
g.setColor(stoneColor);
g.fillRect(x, base - 94, 26, 94);
g.setColor(stoneColor);
g.fillRect(x + 76, base - 118, 28, 118);
g.setColor(stoneColor);
g.fillRect(x - 10, base - 120, 128, 18);
g.setColor(stripeColor);
g.fillRect(x + 24, base - 108, 50, 12);
}
}

private void drawTargetPath(Graphics2D g, ArenaSceneModel model) {
FighterSpriteDto player = model.playerSprite();
FighterSpriteDto target = model.currentSelectedEnemySprite();
Expand Down Expand Up @@ -159,10 +90,6 @@ private void drawOverlay(Graphics2D g, ArenaSceneModel model, int width) {
}
}

private static int floorTop(int height) {
return (int) (height * 0.55);
}

private static String fitText(Graphics2D g, String text, int maxWidth) {
if (g.getFontMetrics().stringWidth(text) <= maxWidth) {
return text;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package sc2002.turnbased.ui.gui.view;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

import javax.imageio.ImageIO;

import org.junit.jupiter.api.Test;

class ArenaBackgroundRendererTest {
private static final byte[] PNG_SIGNATURE = new byte[] {
(byte) 0x89,
'P',
'N',
'G',
'\r',
'\n',
0x1A,
'\n'
};

@Test
void packagesBackgroundAsPngResource() throws IOException {
try (InputStream inputStream = ArenaBackgroundRenderer.class.getResourceAsStream(
ArenaBackgroundRenderer.BACKGROUND_RESOURCE
)) {
assertNotNull(inputStream);
assertArrayEquals(PNG_SIGNATURE, inputStream.readNBytes(PNG_SIGNATURE.length));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

@Test
void decodesPackagedBackgroundResource() throws IOException {
try (InputStream inputStream = ArenaBackgroundRenderer.class.getResourceAsStream(
ArenaBackgroundRenderer.BACKGROUND_RESOURCE
)) {
assertNotNull(inputStream);
BufferedImage image = ImageIO.read(inputStream);

assertNotNull(image);
assertTrue(image.getWidth() > 0);
assertTrue(image.getHeight() > 0);
}
}
}
Loading