diff --git a/assets/map/map.json b/assets/map/map.json new file mode 100644 index 0000000..3e9a96b --- /dev/null +++ b/assets/map/map.json @@ -0,0 +1,3 @@ +{ + "mapName": "Some Map" +} \ No newline at end of file diff --git a/src/duskdefs.env b/src/duskdefs.env index bdd1add..b115a02 100644 --- a/src/duskdefs.env +++ b/src/duskdefs.env @@ -11,4 +11,8 @@ TILE_WIDTH = 16.0 TILE_HEIGHT = 16.0 TILE_DEPTH = 16.0 +RPG_CAMERA_FOV = 70 +RPG_CAMERA_PIXELS_PER_UNIT = 1.0 +RPG_CAMERA_Z_OFFSET = 24.0 + ASSET_LANG_CHUNK_CHAR_COUNT = 6144 \ No newline at end of file diff --git a/src/scene/scene/scenemap.c b/src/scene/scene/scenemap.c index 0890c42..5b96ea8 100644 --- a/src/scene/scene/scenemap.c +++ b/src/scene/scene/scenemap.c @@ -15,10 +15,7 @@ #include "display/screen.h" #include "rpg/rpgcamera.h" #include "util/memory.h" - -#define TILE_WIDTH 16.0f -#define TILE_HEIGHT 16.0f -#define TILE_DEPTH 11.36f +#include "duskdefs.h" errorret_t sceneMapInit(scenedata_t *data) { // Init the camera. @@ -26,7 +23,7 @@ errorret_t sceneMapInit(scenedata_t *data) { data->sceneMap.camera.projType = CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED; data->sceneMap.camera.viewType = CAMERA_VIEW_TYPE_LOOKAT_PIXEL_PERFECT; glm_vec3_zero(data->sceneMap.camera.lookatPixelPerfect.offset); - data->sceneMap.camera.lookatPixelPerfect.offset[1] = TILE_HEIGHT; + data->sceneMap.camera.lookatPixelPerfect.offset[1] = RPG_CAMERA_Z_OFFSET; glm_vec3_copy( (vec3){ 0.0f, 0.0f, 0.0f }, data->sceneMap.camera.lookatPixelPerfect.target @@ -35,8 +32,10 @@ errorret_t sceneMapInit(scenedata_t *data) { (vec3){ 0.0f, 1.0f, 0.0f }, data->sceneMap.camera.lookatPixelPerfect.up ); - data->sceneMap.camera.lookatPixelPerfect.pixelsPerUnit = 1.0f; - data->sceneMap.camera.perspective.fov = glm_rad(90.0f); + data->sceneMap.camera.lookatPixelPerfect.pixelsPerUnit = ( + RPG_CAMERA_PIXELS_PER_UNIT + ); + data->sceneMap.camera.perspective.fov = glm_rad(RPG_CAMERA_FOV); errorOk(); } diff --git a/tools/editor.py b/tools/editor.py index d597dd4..196cf4d 100755 --- a/tools/editor.py +++ b/tools/editor.py @@ -9,7 +9,8 @@ from OpenGL.GLU import * from editortool.maptool import MapWindow from editortool.langtool import LangToolWindow -DEFAULT_TOOL = None # Set to "map" or "language" to auto-select for testing +# DEFAULT_TOOL = None +DEFAULT_TOOL = "map" TOOLS = [ ("Map Editor", "map", MapWindow), diff --git a/tools/editortool/map/camera.py b/tools/editortool/map/camera.py new file mode 100644 index 0000000..936ddab --- /dev/null +++ b/tools/editortool/map/camera.py @@ -0,0 +1,28 @@ +import math +from OpenGL.GL import * +from OpenGL.GLU import * +from dusk.defs import defs + +pixelsPerUnit = float(defs.get('RPG_CAMERA_PIXELS_PER_UNIT')) +yOffset = float(defs.get('RPG_CAMERA_Z_OFFSET')) +fov = float(defs.get('RPG_CAMERA_FOV')) +scale = 8.0 + +def setupCamera(vw, vh): + z = (vh / 2.0) / ( + (pixelsPerUnit * scale) * math.tan(math.radians(fov / 2.0)) + ) + lookAt = ( 0, 0, 0 ) + aspectRatio = vw / vh + position = lookAt[0], lookAt[1] - yOffset, lookAt[2] + z + + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPerspective(fov, aspectRatio, 0.1, 1000.0) + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + gluLookAt( + position[0], position[1], position[2], + lookAt[0], lookAt[1], lookAt[2], + 0.0, 1.0, 0.0 + ) \ No newline at end of file diff --git a/tools/editortool/map/chunk.py b/tools/editortool/map/chunk.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/editortool/map/glwidget.py b/tools/editortool/map/glwidget.py index 7485076..49001e4 100644 --- a/tools/editortool/map/glwidget.py +++ b/tools/editortool/map/glwidget.py @@ -2,104 +2,39 @@ from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import QOpenGLWidget from OpenGL.GL import * from OpenGL.GLU import * +from editortool.map.selectbox import drawSelectBox +from editortool.map.tile import drawTile +from editortool.map.camera import setupCamera class GLWidget(QOpenGLWidget): def __init__(self, parent=None): super().__init__(parent) - self.xRot = 20.0 - self.yRot = 30.0 - self.zRot = 0.0 - self.rotation_speed = 2.0 self.timer = QTimer(self) - self.timer.timeout.connect(self.updateRotation) + self.timer.timeout.connect(self.update) + self.timer.start(16) # ~60 FPS def initializeGL(self): - version = glGetString(GL_VERSION) - print("GL version:", version) - glClearColor(0.1, 0.1, 0.1, 1.0) + glClearColor(0.392, 0.584, 0.929, 1.0) glEnable(GL_DEPTH_TEST) - glShadeModel(GL_SMOOTH) - glEnable(GL_COLOR_MATERIAL) - glEnable(GL_LIGHTING) - glEnable(GL_LIGHT0) - light_position = [4.0, 4.0, 8.0, 1.0] - glLightfv(GL_LIGHT0, GL_POSITION, light_position) def resizeGL(self, w, h): - if h == 0: - h = 1 - glViewport(0, 0, w, h) - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - glLoadIdentity() - gluPerspective(45.0, float(w) / float(h), 0.1, 100.0) - glMatrixMode(GL_MODELVIEW) + pass + def paintGL(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glLoadIdentity() - glTranslatef(0.0, 0.0, -7.0) - glRotatef(self.xRot, 1.0, 0.0, 0.0) - glRotatef(self.yRot, 0.0, 1.0, 0.0) - glRotatef(self.zRot, 0.0, 0.0, 1.0) - self.drawCube() - def drawCube(self): - glBegin(GL_QUADS) - # Front (red) - glColor3f(1.0, 0.0, 0.0) - glVertex3f(-1.0, -1.0, 1.0) - glVertex3f( 1.0, -1.0, 1.0) - glVertex3f( 1.0, 1.0, 1.0) - glVertex3f(-1.0, 1.0, 1.0) - # Back (green) - glColor3f(0.0, 1.0, 0.0) - glVertex3f(-1.0, -1.0, -1.0) - glVertex3f(-1.0, 1.0, -1.0) - glVertex3f( 1.0, 1.0, -1.0) - glVertex3f( 1.0, -1.0, -1.0) - # Top (blue) - glColor3f(0.0, 0.0, 1.0) - glVertex3f(-1.0, 1.0, -1.0) - glVertex3f(-1.0, 1.0, 1.0) - glVertex3f( 1.0, 1.0, 1.0) - glVertex3f( 1.0, 1.0, -1.0) - # Bottom (yellow) - glColor3f(1.0, 1.0, 0.0) - glVertex3f(-1.0, -1.0, -1.0) - glVertex3f( 1.0, -1.0, -1.0) - glVertex3f( 1.0, -1.0, 1.0) - glVertex3f(-1.0, -1.0, 1.0) - # Right (magenta) - glColor3f(1.0, 0.0, 1.0) - glVertex3f( 1.0, -1.0, -1.0) - glVertex3f( 1.0, 1.0, -1.0) - glVertex3f( 1.0, 1.0, 1.0) - glVertex3f( 1.0, -1.0, 1.0) - # Left (cyan) - glColor3f(0.0, 1.0, 1.0) - glVertex3f(-1.0, -1.0, -1.0) - glVertex3f(-1.0, -1.0, 1.0) - glVertex3f(-1.0, 1.0, 1.0) - glVertex3f(-1.0, 1.0, -1.0) - glEnd() + w = self.width() + h = self.height() + if h <= 0: + h = 1 + if w <= 0: + w = 1 - def startRotation(self): - if not self.timer.isActive(): - self.timer.start(30) + glViewport(0, 0, w, h) + setupCamera(self.width(), self.height()) # <-- Add this here - def stopRotation(self): - if self.timer.isActive(): - self.timer.stop() - - def resetView(self): - self.xRot = 20.0 - self.yRot = 30.0 - self.zRot = 0.0 - self.update() - - def updateRotation(self): - self.xRot += self.rotation_speed - self.yRot += self.rotation_speed * 0.8 - self.zRot += self.rotation_speed * 0.5 - self.update() + # Draw test quad at 0,0,0 to 1,1,0 + drawTile(0, 0, 0) + drawSelectBox(0, 0, 0) \ No newline at end of file diff --git a/tools/editortool/map/selectbox.py b/tools/editortool/map/selectbox.py new file mode 100644 index 0000000..9df2625 --- /dev/null +++ b/tools/editortool/map/selectbox.py @@ -0,0 +1,48 @@ +import OpenGL.GL as gl +from dusk.defs import defs +import colorsys + +hue = [0.0] # Mutable container for static hue + +def drawSelectBox(x, y, z): + w = float(defs.get('TILE_WIDTH')) + h = float(defs.get('TILE_HEIGHT')) + d = float(defs.get('TILE_DEPTH')) + + x = x * w + y = y * h + z = z * d + + # Center box. + x -= w / 2.0 + y -= h / 2.0 + z -= d / 2.0 + + # Define the 8 vertices of the cube with w=h=d=1 + vertices = [ + (x, y, z), # 0: min corner + (x+w, y, z), # 1 + (x+w, y+h, z), # 2 + (x, y+h, z), # 3 + (x, y, z+d), # 4 + (x+w, y, z+d), # 5 + (x+w, y+h, z+d), # 6 + (x, y+h, z+d) # 7 + ] + # List of edges as pairs of vertex indices + edges = [ + (0, 1), (1, 2), (2, 3), (3, 0), # bottom face + (4, 5), (5, 6), (6, 7), (7, 4), # top face + (0, 4), (1, 5), (2, 6), (3, 7) # vertical edges + ] + + # Cycle hue + hue[0] = (hue[0] + 0.01) % 1.0 + r, g, b = colorsys.hsv_to_rgb(hue[0], 1.0, 1.0) + gl.glColor3f(r, g, b) + gl.glLineWidth(2.0) + gl.glBegin(gl.GL_LINES) + for edge in edges: + for vertex in edge: + gl.glVertex3f(*vertices[vertex]) + gl.glEnd() \ No newline at end of file diff --git a/tools/editortool/map/tile.py b/tools/editortool/map/tile.py new file mode 100644 index 0000000..9d551ef --- /dev/null +++ b/tools/editortool/map/tile.py @@ -0,0 +1,25 @@ +from OpenGL.GL import * +from dusk.defs import defs + +def drawTile(x, y, z): + w = float(defs.get('TILE_WIDTH')) + h = float(defs.get('TILE_HEIGHT')) + d = float(defs.get('TILE_DEPTH')) + + x = x * w + y = y * h + z = z * d + + # Center tile. + x -= w / 2.0 + y -= h / 2.0 + z -= d / 2.0 + + # Draw the tile as a flat square on the X-Y plane at depth z. + glColor3f(1.0, 0.0, 0.0) # Red color + glBegin(GL_QUADS) + glVertex3f(x, y, z) # Bottom-left + glVertex3f(x + w, y, z) # Bottom-right + glVertex3f(x + w, y + h, z) # Top-right + glVertex3f(x, y + h, z) # Top-left + glEnd() \ No newline at end of file diff --git a/tools/editortool/maptool.py b/tools/editortool/maptool.py index cca6a72..c82423e 100644 --- a/tools/editortool/maptool.py +++ b/tools/editortool/maptool.py @@ -1,11 +1,12 @@ import sys import os +import json from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QSlider, QDialog, QRadioButton, QDialogButtonBox, - QAction, QFileDialog, QLineEdit + QAction, QFileDialog, QLineEdit, QMessageBox ) from editortool.map.glwidget import GLWidget @@ -36,6 +37,8 @@ class MapWindow(QMainWindow): action_save_as.triggered.connect(self.saveFileAs) self.current_file = None + self.dirty = False + self.highlighted_pos = [0, 0, 0] # x, y, z central = QWidget() self.setCentralWidget(central) @@ -46,6 +49,22 @@ class MapWindow(QMainWindow): left_panel = QVBoxLayout() chunk_info_label = QLabel("CHUNK INFO") left_panel.addWidget(chunk_info_label) + + # Movement controls + move_label = QLabel("Move Selection") + left_panel.addWidget(move_label) + btn_n = QPushButton("N") + btn_s = QPushButton("S") + btn_e = QPushButton("E") + btn_w = QPushButton("W") + btn_up = QPushButton("Up") + btn_down = QPushButton("Down") + left_panel.addWidget(btn_n) + left_panel.addWidget(btn_s) + left_panel.addWidget(btn_e) + left_panel.addWidget(btn_w) + left_panel.addWidget(btn_up) + left_panel.addWidget(btn_down) left_panel.addStretch() # Right panel @@ -53,9 +72,9 @@ class MapWindow(QMainWindow): map_info_label = QLabel("Map Information") right_panel.addWidget(map_info_label) map_title_label = QLabel("Map Title") - map_title_input = QLineEdit() + self.map_title_input = QLineEdit() right_panel.addWidget(map_title_label) - right_panel.addWidget(map_title_input) + right_panel.addWidget(self.map_title_input) right_panel.addStretch() # Add panels to main layout @@ -73,9 +92,23 @@ class MapWindow(QMainWindow): right_widget.setFixedWidth(250) main_layout.addWidget(right_widget) + self.map_title_input.textChanged.connect(self._on_map_title_changed) + + # Connect movement buttons + btn_n.clicked.connect(lambda: self.move_highlight(0, 1, 0)) + btn_s.clicked.connect(lambda: self.move_highlight(0, -1, 0)) + btn_e.clicked.connect(lambda: self.move_highlight(1, 0, 0)) + btn_w.clicked.connect(lambda: self.move_highlight(-1, 0, 0)) + btn_up.clicked.connect(lambda: self.move_highlight(0, 0, 1)) + btn_down.clicked.connect(lambda: self.move_highlight(0, 0, -1)) + + def _on_map_title_changed(self): + self.dirty = True + def newFile(self): - # Implement new file logic here self.current_file = None + self.map_title_input.setText("") + self.dirty = False # ...clear relevant data... def openFile(self): @@ -83,12 +116,18 @@ class MapWindow(QMainWindow): fname, _ = QFileDialog.getOpenFileName(self, "Open JSON File", default_dir, "JSON Files (*.json)") if fname: self.current_file = fname - # ...load file logic... + try: + with open(fname, "r", encoding="utf-8") as f: + data = json.load(f) + self.map_title_input.setText(data.get("mapName", "")) + self.dirty = False + except Exception as e: + self.map_title_input.setText("") + # Optionally show error dialog def saveFile(self): if self.current_file: - # ...save logic... - pass + self._save_to_file(self.current_file) else: self.saveFileAs() @@ -97,4 +136,43 @@ class MapWindow(QMainWindow): fname, _ = QFileDialog.getSaveFileName(self, "Save JSON File As", default_dir, "JSON Files (*.json)") if fname: self.current_file = fname - # ...save logic... \ No newline at end of file + self._save_to_file(fname) + + def _save_to_file(self, fname): + data = { + "mapName": self.map_title_input.text() + } + try: + with open(fname, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + self.dirty = False + except Exception as e: + # Optionally show error dialog + pass + + def move_highlight(self, dx, dy, dz): + self.highlighted_pos[0] += dx + self.highlighted_pos[1] += dy + self.highlighted_pos[2] += dz + self.gl_widget.set_highlighted_pos(self.highlighted_pos) + self.dirty = True + + def closeEvent(self, event): + if self.dirty: + reply = QMessageBox.question( + self, + "Unsaved Changes", + "You have unsaved changes. Do you want to save before closing?", + QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, + QMessageBox.Save + ) + if reply == QMessageBox.Save: + self.saveFile() + if self.dirty: + event.ignore() + return + elif reply == QMessageBox.Cancel: + event.ignore() + return + # Discard: continue closing + event.accept() \ No newline at end of file