diff --git a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/maps/MapRA.groovy b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/maps/MapRA.groovy index f8346bf9..9ec78d89 100644 --- a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/maps/MapRA.groovy +++ b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/maps/MapRA.groovy @@ -419,19 +419,19 @@ class MapRA extends Node implements GraphicsElement { } var unitImages = resourceManager.loadFile("${unitLine.type}.shp", ShpFile) - var unit = new Vehicle(unitConfig, unitImages, palette) - - // TODO: Country to faction map - unit.faction = switch (unitLine.faction) { - case "Greece" -> Faction.BLUE - case "USSR" -> Faction.RED - default -> Faction.GOLD - } - - unit.heading = unitLine.heading - unit.translate(unitLine.coords.asWorldCoords()) - - elements << unit +// var unit = new Vehicle(unitConfig, unitImages, palette) +// +// // TODO: Country to faction map +// unit.faction = switch (unitLine.faction) { +// case "Greece" -> Faction.BLUE +// case "USSR" -> Faction.RED +// default -> Faction.GOLD +// } +// +// unit.heading = unitLine.heading +// unit.translate(unitLine.coords.asWorldCoords()) +// +// elements << unit } catch (IllegalArgumentException ignored) { // Ignore unknown units @@ -462,21 +462,21 @@ class MapRA extends Node implements GraphicsElement { var unitConfig = jsonSlurper.parseText(unitConfigJson) as UnitData var infantryImages = resourceManager.loadFile("${infantryLine.type}.shp", ShpFile) - var infantry = new Infantry(unitConfig, infantryImages, palette).tap { it -> - - // TODO: Country to faction map - it.faction = switch (infantryLine.faction) { - case "Greece" -> Faction.BLUE - case "USSR" -> Faction.RED - default -> Faction.GOLD - } - it.heading = infantryLine.heading - - // TODO: Sub positions within cells - it.translate(infantryLine.coords.asWorldCoords()) - } - - elements << infantry +// var infantry = new Infantry(unitConfig, infantryImages, palette).tap { it -> +// +// // TODO: Country to faction map +// it.faction = switch (infantryLine.faction) { +// case "Greece" -> Faction.BLUE +// case "USSR" -> Faction.RED +// default -> Faction.GOLD +// } +// it.heading = infantryLine.heading +// +// // TODO: Sub positions within cells +// it.translate(infantryLine.coords.asWorldCoords()) +// } +// +// elements << infantry } catch (IllegalArgumentException ignored) { // Ignore unknown units diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/SandboxCli.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/SandboxCli.groovy index d7f162ce..8b6312b3 100644 --- a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/SandboxCli.groovy +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/SandboxCli.groovy @@ -49,7 +49,7 @@ class SandboxCli implements Callable { Thread.currentThread().name = 'Sandbox [main]' logger.info('Red Horizon Sandbox {}', commandSpec.parent().version()[0]) - new Sandbox(touchpadInput).start() + new Sandbox(touchpadInput) return 0 } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ControlsOverlayRenderPass.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ControlsOverlayRenderPass.groovy index 7a63cee0..b59d0486 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ControlsOverlayRenderPass.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ControlsOverlayRenderPass.groovy @@ -27,6 +27,8 @@ import imgui.ImGui import imgui.type.ImBoolean import static imgui.flag.ImGuiWindowFlags.* +import java.util.concurrent.CopyOnWriteArrayList + /** * Small overlay for displaying registered controls in the application. * @@ -34,7 +36,7 @@ import static imgui.flag.ImGuiWindowFlags.* */ class ControlsOverlayRenderPass implements OverlayRenderPass { - private List controls = [] + private List controls = new CopyOnWriteArrayList<>() private int controlsWindowSizeX = 350 private int controlsWindowSizeY = 200 diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/Control.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/Control.groovy index af469db7..e78044af 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/Control.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/Control.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2022, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * 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 - * + * * http://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. @@ -19,20 +19,37 @@ package nz.net.ultraq.redhorizon.engine.input import nz.net.ultraq.redhorizon.events.Event import nz.net.ultraq.redhorizon.events.EventListener +import org.lwjgl.glfw.GLFW + import groovy.transform.TupleConstructor +import java.lang.reflect.Field +import java.lang.reflect.Modifier /** * An input binding with a description so that what it does can be displayed. - * + * * @author Emanuel Rabina */ -@TupleConstructor +@TupleConstructor(defaults = false) abstract class Control implements EventListener { + protected static final Field[] glfwFields = GLFW.getDeclaredFields() + final Class event final String name final String binding + /** + * Return a string representing the name of a modifier key. + */ + protected static String determineModifierName(int modifier) { + + var modifierField = glfwFields.find { field -> + return Modifier.isStatic(field.modifiers) && field.name.startsWith("GLFW_MOD_") && field.getInt(null) == modifier + } + return modifierField ? "${modifierField.name.substring(9).toLowerCase().capitalize()} + " : "" + } + @Override String toString() { diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/KeyControl.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/KeyControl.groovy index d5f87129..dbe491b5 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/KeyControl.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/KeyControl.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2022, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * 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 - * + * * http://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. @@ -16,8 +16,6 @@ package nz.net.ultraq.redhorizon.engine.input - -import org.lwjgl.glfw.GLFW import static org.lwjgl.glfw.GLFW.GLFW_PRESS import static org.lwjgl.glfw.GLFW.GLFW_REPEAT @@ -30,38 +28,35 @@ import java.lang.reflect.Modifier */ class KeyControl extends Control { + private final int modifier private final int key private Closure handler - /** - * Constructor, build a key-handling control around the given key. - * - * @param key - * @param name - * @param handler - */ KeyControl(int key, String name, Closure handler) { - super(KeyEvent, name, determineKeyBindingName(key)) + this(-1, key, name, handler) + } + + KeyControl(int modifier, int key, String name, Closure handler) { + + super(KeyEvent, name, determineBindingName(modifier, key)) + this.modifier = modifier this.key = key this.handler = handler } /** * Return a string representing the name of the key binding. - * - * @param key - * @return */ - private static String determineKeyBindingName(int key) { + private static String determineBindingName(int modifier, int key) { - var field = GLFW.getDeclaredFields().find { field -> + var modifierName = determineModifierName(modifier) + var buttonField = glfwFields.find { field -> return Modifier.isStatic(field.modifiers) && field.name.startsWith("GLFW_KEY_") && field.getInt(null) == key } - if (field) { - return field.name.substring(9).toLowerCase().capitalize() - } - return key.toString() + return buttonField ? + modifierName + buttonField.name.substring(9).toLowerCase().capitalize() : + key.toString() } @Override diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/MouseControl.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/MouseControl.groovy new file mode 100644 index 00000000..165017ac --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/MouseControl.groovy @@ -0,0 +1,66 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * 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 + * + * http://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 nz.net.ultraq.redhorizon.engine.input + +import java.lang.reflect.Modifier + +/** + * Mouse-specific control class. + * + * @author Emanuel Rabina + */ +class MouseControl extends Control { + + private final int modifier + private final int button + private Closure handler + + MouseControl(int button, String name, Closure handler) { + + this(-1, button, name, handler) + } + + MouseControl(int modifier, int button, String name, Closure handler) { + + super(MouseButtonEvent, name, determineBindingName(modifier, button)) + this.modifier = modifier + this.button = button + this.handler = handler + } + + /** + * Return a string representing the name of the key binding. + */ + private static String determineBindingName(int modifier, int button) { + + var modifierName = determineModifierName(modifier) + var buttonField = glfwFields.find { field -> + return Modifier.isStatic(field.modifiers) && field.name.startsWith("GLFW_MOUSE_BUTTON_") && field.getInt(null) == button + } + return buttonField ? + "${modifierName}Mouse ${buttonField.name.substring(18).toLowerCase().capitalize()}" : + button.toString() + } + + @Override + void handleEvent(MouseButtonEvent event) { + + if (event.button == button && event.mods == modifier) { + handler() + } + } +} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Outline.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Outline.groovy deleted file mode 100644 index 22791af3..00000000 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Outline.groovy +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) - * - * 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 - * - * http://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 nz.net.ultraq.redhorizon.engine.scenegraph.nodes - -import nz.net.ultraq.redhorizon.engine.graphics.Colour -import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRenderer -import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRequests.MeshRequest -import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRequests.ShaderRequest -import nz.net.ultraq.redhorizon.engine.graphics.Mesh -import nz.net.ultraq.redhorizon.engine.graphics.MeshType -import nz.net.ultraq.redhorizon.engine.graphics.Shader -import nz.net.ultraq.redhorizon.engine.graphics.VertexBufferLayout -import nz.net.ultraq.redhorizon.engine.graphics.VertexBufferLayoutPart -import nz.net.ultraq.redhorizon.engine.graphics.opengl.Shaders -import nz.net.ultraq.redhorizon.engine.scenegraph.GraphicsElement -import nz.net.ultraq.redhorizon.engine.scenegraph.Node -import nz.net.ultraq.redhorizon.engine.scenegraph.Scene - -import org.joml.Vector2f - -/** - * Draws a line at the bounds of its parent node. Mainly used for debugging. - * - * @author Emanuel Rabina - */ -class Outline extends Node implements GraphicsElement { - - private Mesh mesh - private Shader shader - - @Override - void onSceneAdded(Scene scene) { - - bounds.set( - parent.bounds.minX + 1 as float, - parent.bounds.minY + 1 as float, - parent.bounds.maxX - 1 as float, - parent.bounds.maxY - 1 as float - ) - mesh = scene - .requestCreateOrGet(new MeshRequest( - MeshType.LINE_LOOP, - new VertexBufferLayout(VertexBufferLayoutPart.COLOUR, VertexBufferLayoutPart.POSITION), - Colour.GREEN, - bounds as Vector2f[] - )) - .get() - shader = scene - .requestCreateOrGet(new ShaderRequest(Shaders.primitivesShader)) - .get() - } - - @Override - void onSceneRemoved(Scene scene) { - - scene.requestDelete(mesh) - } - - @Override - void render(GraphicsRenderer renderer) { - - if (!mesh || !shader) { - return - } - - renderer.draw(mesh, transform, shader) - } -} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Lines.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Primitive.groovy similarity index 80% rename from redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Lines.groovy rename to redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Primitive.groovy index 1f705929..6a4fba38 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Lines.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Primitive.groovy @@ -33,14 +33,15 @@ import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import org.joml.Vector2f /** - * A node for the {@code LINES} primitive of OpenGL. + * A node for creating a mesh using any of the OpenGL primitives. * * @author Emanuel Rabina */ -class Lines extends Node implements GraphicsElement { +class Primitive extends Node implements GraphicsElement { + final MeshType type final Colour colour - final Vector2f[] lines + final Vector2f[] points private Mesh mesh private Shader shader @@ -50,23 +51,22 @@ class Lines extends Node implements GraphicsElement { * method. The first describes the line start, the second describes the line * end. */ - Lines(Colour colour, Vector2f... lines) { + Primitive(MeshType type, Colour colour, Vector2f... points) { + this.type = type this.colour = colour + this.points = points - assert lines.length % 2 == 0 : 'Uneven number of points provided' - this.lines = lines - - // Set bounds to the min/max X/Y points across all lines + // Set bounds to the min/max X/Y points var minX = Float.MAX_VALUE var minY = Float.MAX_VALUE var maxX = Float.MIN_VALUE var maxY = Float.MIN_VALUE - lines.each { line -> - minX = Math.min(minX, line.x()) - minY = Math.min(minY, line.y()) - maxX = Math.max(maxX, line.x()) - maxY = Math.max(maxY, line.y()) + points.each { point -> + minX = Math.min(minX, point.x()) + minY = Math.min(minY, point.y()) + maxX = Math.max(maxX, point.x()) + maxY = Math.max(maxY, point.y()) } bounds.set(minX, minY, maxX, maxY) } @@ -75,8 +75,8 @@ class Lines extends Node implements GraphicsElement { void onSceneAdded(Scene scene) { mesh = scene - .requestCreateOrGet(new MeshRequest(MeshType.LINES, - new VertexBufferLayout(VertexBufferLayoutPart.COLOUR, VertexBufferLayoutPart.POSITION), colour, lines)) + .requestCreateOrGet(new MeshRequest(type, + new VertexBufferLayout(VertexBufferLayoutPart.COLOUR, VertexBufferLayoutPart.POSITION), colour, this.points)) .get() shader = scene .requestCreateOrGet(new ShaderRequest(Shaders.primitivesShader)) diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy index 5f66b032..0467b929 100644 --- a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy @@ -17,6 +17,7 @@ package nz.net.ultraq.redhorizon.explorer import nz.net.ultraq.preferences.Preferences +import nz.net.ultraq.redhorizon.classic.filetypes.MapFile import nz.net.ultraq.redhorizon.classic.filetypes.MixFile import nz.net.ultraq.redhorizon.classic.filetypes.ShpFile import nz.net.ultraq.redhorizon.classic.units.Unit @@ -26,12 +27,15 @@ import nz.net.ultraq.redhorizon.engine.geometry.Dimension import nz.net.ultraq.redhorizon.engine.graphics.Colour import nz.net.ultraq.redhorizon.engine.graphics.GraphicsConfiguration import nz.net.ultraq.redhorizon.engine.graphics.WindowMaximizedEvent +import nz.net.ultraq.redhorizon.engine.resources.ResourceManager import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Animation import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.FullScreenContainer import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sound import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sprite import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Video +import nz.net.ultraq.redhorizon.explorer.objects.Map +import nz.net.ultraq.redhorizon.explorer.scripts.MapViewerScript import nz.net.ultraq.redhorizon.explorer.scripts.PlaybackScript import nz.net.ultraq.redhorizon.explorer.scripts.UnitShowcaseScript import nz.net.ultraq.redhorizon.filetypes.AnimationFile @@ -261,10 +265,12 @@ class Explorer { logger.info('File details: {}', file) var mediaNode = switch (file) { + // Objects - case ShpFile -> { + case ShpFile -> + preview(file, objectId) + case MapFile -> preview(file, objectId) - } // Media case ImageFile -> @@ -316,4 +322,18 @@ class Explorer { scene << unit } } + + /** + * Attempt to load up a map from its map file. + */ + private void preview(MapFile mapFile, String objectId) { + + // Assume the directory in which file resides is where we can search for items + new ResourceManager(currentDirectory, + 'nz.net.ultraq.redhorizon.filetypes', + 'nz.net.ultraq.redhorizon.classic.filetypes').withCloseable { resourceManager -> + + scene << new Map(mapFile, resourceManager).attachScript(new MapViewerScript(true)) + } + } } diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/objects/Map.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/objects/Map.groovy new file mode 100644 index 00000000..676aa988 --- /dev/null +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/objects/Map.groovy @@ -0,0 +1,97 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * 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 + * + * http://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 nz.net.ultraq.redhorizon.explorer.objects + +import nz.net.ultraq.redhorizon.classic.filetypes.MapFile +import nz.net.ultraq.redhorizon.classic.filetypes.PalFile +import nz.net.ultraq.redhorizon.classic.maps.Theater +import nz.net.ultraq.redhorizon.engine.resources.ResourceManager +import nz.net.ultraq.redhorizon.engine.scenegraph.Node +import nz.net.ultraq.redhorizon.filetypes.Palette + +import org.joml.Vector2f +import org.joml.primitives.Rectanglef +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * A map on which a mission or skirmish can take place. + * + * @author Emanuel Rabina + */ +class Map extends Node { + + private static final Logger logger = LoggerFactory.getLogger(Map) + private static final int TILES_X = 128 + private static final int TILES_Y = 128 + private static final int TILE_WIDTH = 24 + private static final int TILE_HEIGHT = 24 + + final MapFile mapFile + final String name + final Theater theater + final Rectanglef boundary + final Vector2f initialPosition + + private final Palette palette + + /** + * Constructor, create a new map from a map file. + */ + Map(MapFile mapFile, ResourceManager resourceManager) { + + this.mapFile = mapFile + + name = mapFile.name + + var mapSection = mapFile.mapSection + theater = Theater.valueOf(mapSection.theater()) + palette = getResourceAsStream("ra-${theater.label.toLowerCase()}.pal").withBufferedStream { inputStream -> + return new PalFile(inputStream).withAlphaMask() + } + + var mapXY = new Vector2f(mapSection.x(), mapSection.y()) + var mapWH = new Vector2f(mapXY).add(mapSection.width(), mapSection.height()) + boundary = new Rectanglef(mapXY.asWorldCoords(), mapWH.asWorldCoords()).makeValid() + + var waypoints = mapFile.waypointsData + var waypoint98 = waypoints[98] + initialPosition = waypoint98.asCellCoords().asWorldCoords() + + var halfMapWidth = (TILES_X * TILE_WIDTH) / 2 as float + var halfMapHeight = (TILES_Y * TILE_HEIGHT) / 2 as float + bounds.set(-halfMapWidth, -halfMapHeight, halfMapWidth, halfMapHeight) + + addChild(new MapLines(this)) + } + + /** + * Return some information about this map. + * + * @return Map info. + */ + @Override + String toString() { + + return """ + Red Alert map + - Name: ${name} + - Theater: ${theater} + - Bounds: x=${boundary.minX},y=${boundary.minY},w=${boundary.lengthX()},h=${boundary.lengthY()} + """.stripIndent() + } +} diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/objects/MapLines.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/objects/MapLines.groovy new file mode 100644 index 00000000..464c941b --- /dev/null +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/objects/MapLines.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2021, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * 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 + * + * http://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 nz.net.ultraq.redhorizon.explorer.objects + +import nz.net.ultraq.redhorizon.engine.graphics.Colour +import nz.net.ultraq.redhorizon.engine.graphics.MeshType +import nz.net.ultraq.redhorizon.engine.scenegraph.Node +import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Primitive + +import org.joml.Vector2f + +/** + * Map overlays and lines to help with debugging maps. + * + * @author Emanuel Rabina + */ +class MapLines extends Node { + + private static final Vector2f X_AXIS_MIN = new Vector2f(-3600, 0) + private static final Vector2f X_AXIS_MAX = new Vector2f(3600, 0) + private static final Vector2f Y_AXIS_MIN = new Vector2f(0, -3600) + private static final Vector2f Y_AXIS_MAX = new Vector2f(0, 3600) + + MapLines(Map map) { + + addChild(new Primitive(MeshType.LINES, Colour.RED.withAlpha(0.5), X_AXIS_MIN, X_AXIS_MAX, Y_AXIS_MIN, Y_AXIS_MAX)) + addChild(new Primitive(MeshType.LINE_LOOP, Colour.YELLOW.withAlpha(0.5), map.boundary as Vector2f[])) + } +} diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/scripts/MapViewerScript.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/scripts/MapViewerScript.groovy new file mode 100644 index 00000000..a9ed7315 --- /dev/null +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/scripts/MapViewerScript.groovy @@ -0,0 +1,167 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * 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 + * + * http://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 nz.net.ultraq.redhorizon.explorer.scripts + +import nz.net.ultraq.redhorizon.engine.input.CursorPositionEvent +import nz.net.ultraq.redhorizon.engine.input.KeyControl +import nz.net.ultraq.redhorizon.engine.input.KeyEvent +import nz.net.ultraq.redhorizon.engine.input.MouseButtonEvent +import nz.net.ultraq.redhorizon.engine.input.MouseControl +import nz.net.ultraq.redhorizon.engine.input.RemoveControlFunction +import nz.net.ultraq.redhorizon.engine.input.ScrollEvent +import nz.net.ultraq.redhorizon.engine.scenegraph.Scene +import nz.net.ultraq.redhorizon.engine.scenegraph.scripting.Script +import nz.net.ultraq.redhorizon.events.DeregisterEventFunction +import nz.net.ultraq.redhorizon.explorer.objects.Map + +import org.joml.Vector2f +import static org.lwjgl.glfw.GLFW.* + +import groovy.transform.TupleConstructor + +/** + * Controls for viewing a map in the explorer. + * + * @author Emanuel Rabina + */ +@TupleConstructor(defaults = false) +class MapViewerScript extends Script { + + private static final int TICK = 48 + + private final float initialScale = 1.0f + private final float[] scaleRange = (1.0..2.0).by(0.1) + private final List deregisterEventFunctions = [] + private final List removeControlFunctions = [] + + final boolean touchpadInput + + @Delegate + Map applyDelegate() { + return scriptable + } + + @Override + void onSceneAdded(Scene scene) { + + var inputEventStream = scene.inputEventStream + var window = scene.window + var camera = scene.camera + + var scaleIndex = scaleRange.findIndexOf { it == initialScale } + var mouseMovementModifier = 1f + var renderResolution = window.renderResolution + var targetResolution = window.targetResolution + mouseMovementModifier = renderResolution.width / targetResolution.width + + // Add options so it's not hard-coded to my weird inverted setup 😅 + if (touchpadInput) { + var ctrl = false + deregisterEventFunctions << inputEventStream.on(KeyEvent) { event -> + if (event.key == GLFW_KEY_LEFT_CONTROL) { + ctrl = event.action == GLFW_PRESS || event.action == GLFW_REPEAT + } + } + deregisterEventFunctions << inputEventStream.on(ScrollEvent) { event -> + + // Zoom in/out using CTRL + scroll up/down + if (ctrl) { + if (event.yOffset < 0) { + scaleIndex = Math.clamp(scaleIndex - 1, 0, scaleRange.length - 1) + } + else if (event.yOffset > 0) { + scaleIndex = Math.clamp(scaleIndex + 1, 0, scaleRange.length - 1) + } + camera.scale(scaleRange[scaleIndex]) + } + // Use scroll input to move around the map + else { + camera.translate(Math.round(3 * event.xOffset) as float, Math.round(3 * -event.yOffset) as float) + } + } + removeControlFunctions << inputEventStream.addControl( + new MouseControl(GLFW_MOD_CONTROL, GLFW_MOUSE_BUTTON_RIGHT, 'Reset scale', { -> + camera.resetScale() + }) + ) + } + else { + + // Use click-and-drag to move around + var cursorPosition = new Vector2f() + var dragging = false + deregisterEventFunctions << inputEventStream.on(CursorPositionEvent) { event -> + if (dragging) { + var diffX = (cursorPosition.x - event.xPos) * mouseMovementModifier as float + var diffY = (cursorPosition.y - event.yPos) * mouseMovementModifier as float + camera.translate(-diffX, diffY) + } + cursorPosition.set(event.xPos as float, event.yPos as float) + } + deregisterEventFunctions << inputEventStream.on(MouseButtonEvent) { event -> + if (event.button == GLFW_MOUSE_BUTTON_LEFT) { + if (event.action == GLFW_PRESS) { + dragging = true + } + else if (event.action == GLFW_RELEASE) { + dragging = false + } + } + } + + // Zoom in/out using the scroll wheel + deregisterEventFunctions << inputEventStream.on(ScrollEvent) { event -> + if (event.yOffset < 0) { + scaleIndex = Math.clamp(scaleIndex - 1, 0, scaleRange.length - 1) + } + else if (event.yOffset > 0) { + scaleIndex = Math.clamp(scaleIndex + 1, 0, scaleRange.length - 1) + } + camera.scale(scaleRange[scaleIndex]) + } + removeControlFunctions << inputEventStream.addControl( + new MouseControl(GLFW_MOD_CONTROL, GLFW_MOUSE_BUTTON_MIDDLE, 'Reset scale', { -> + camera.resetScale() + }) + ) + } + + // Custom inputs + removeControlFunctions << inputEventStream.addControl(new KeyControl(GLFW_KEY_UP, 'Scroll up', { -> + camera.translate(0, -TICK) + })) + removeControlFunctions << inputEventStream.addControl(new KeyControl(GLFW_KEY_DOWN, 'Scroll down', { -> + camera.translate(0, TICK) + })) + removeControlFunctions << inputEventStream.addControl(new KeyControl(GLFW_KEY_LEFT, 'Scroll left', { -> + camera.translate(TICK, 0) + })) + removeControlFunctions << inputEventStream.addControl(new KeyControl(GLFW_KEY_RIGHT, 'Scroll right', { -> + camera.translate(-TICK, 0) + })) + removeControlFunctions << inputEventStream.addControl(new KeyControl(GLFW_KEY_SPACE, 'Reset camera position', { -> + camera.center(initialPosition.x(), initialPosition.y()) + })) + } + + @Override + void onSceneRemoved(Scene scene) { + + deregisterEventFunctions*.deregister() + removeControlFunctions*.remove() + } +} diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/scripts/UnitShowcaseScript.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/scripts/UnitShowcaseScript.groovy index 492aa50a..b48db08d 100644 --- a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/scripts/UnitShowcaseScript.groovy +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/scripts/UnitShowcaseScript.groovy @@ -50,7 +50,7 @@ class UnitShowcaseScript extends Script { // TODO: Have it so that the render window is the desktop resolution and the // camera scales things so that things are the size they were back // when the game was 640x480 - scene.camera.scale(2.0f) + scene.camera.scale(4.0f) logger.info("Showing ${state} state") removeControlFunctions << scene.inputEventStream.addControl(new KeyControl(GLFW_KEY_LEFT, 'Rotate left', { -> @@ -94,5 +94,6 @@ class UnitShowcaseScript extends Script { void onSceneRemoved(Scene scene) { scene.camera.resetScale() + removeControlFunctions*.remove() } }