Prepping editor more...
All checks were successful
Build Dusk / build-linux (push) Successful in 39s
Build Dusk / build-psp (push) Successful in 1m6s

This commit is contained in:
2025-11-16 14:43:29 -06:00
parent cf59989167
commit 750e8840f0
10 changed files with 285 additions and 167 deletions

View File

@@ -1,3 +1,3 @@
{ {
"mapName": "Some Map" "mapName": "Test"
} }

18
tools/dusk/event.py Normal file
View File

@@ -0,0 +1,18 @@
class Event:
def __init__(self):
self._subscribers = []
def sub(self, callback):
"""Subscribe a callback to the event."""
if callback not in self._subscribers:
self._subscribers.append(callback)
def unsub(self, callback):
"""Unsubscribe a callback from the event."""
if callback in self._subscribers:
self._subscribers.remove(callback)
def invoke(self, *args, **kwargs):
"""Invoke all subscribers with the given arguments."""
for callback in self._subscribers:
callback(*args, **kwargs)

View File

@@ -0,0 +1,3 @@
{
"lastFile": "/home/yourwishes/htdocs/dusk/assets/map/map.json"
}

View File

@@ -1,36 +1,54 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QGridLayout from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QGridLayout, QTreeWidget, QTreeWidgetItem
from editortool.map.map import map from editortool.map.map import map
class ChunkPanel(QWidget): class ChunkPanel(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
self.chunk_info_label = QLabel("Tile Information") self.chunkInfoLabel = QLabel("Tile Information")
layout.addWidget(self.chunk_info_label) layout.addWidget(self.chunkInfoLabel)
self.move_label = QLabel("Move Selection")
layout.addWidget(self.move_label)
grid = QGridLayout() grid = QGridLayout()
self.btn_up = QPushButton("U") self.btnUp = QPushButton("U")
self.btn_n = QPushButton("N") self.btnN = QPushButton("N")
self.btn_down = QPushButton("D") self.btnDown = QPushButton("D")
self.btn_w = QPushButton("W") self.btnW = QPushButton("W")
self.btn_s = QPushButton("S") self.btnS = QPushButton("S")
self.btn_e = QPushButton("E") self.btnE = QPushButton("E")
# Arrange buttons: U N D on top row, W S E on bottom row # Arrange buttons: U N D on top row, W S E on bottom row
grid.addWidget(self.btn_up, 0, 0) grid.addWidget(self.btnUp, 0, 0)
grid.addWidget(self.btn_n, 0, 1) grid.addWidget(self.btnN, 0, 1)
grid.addWidget(self.btn_down, 0, 2) grid.addWidget(self.btnDown, 0, 2)
grid.addWidget(self.btn_w, 1, 0) grid.addWidget(self.btnW, 1, 0)
grid.addWidget(self.btn_s, 1, 1) grid.addWidget(self.btnS, 1, 1)
grid.addWidget(self.btn_e, 1, 2) grid.addWidget(self.btnE, 1, 2)
layout.addLayout(grid) layout.addLayout(grid)
layout.addStretch()
self.btn_n.clicked.connect(lambda: map.moveRelative(0, 1, 0)) # Add expandable tree list
self.btn_s.clicked.connect(lambda: map.moveRelative(0, -1, 0)) self.tree = QTreeWidget()
self.btn_e.clicked.connect(lambda: map.moveRelative(1, 0, 0)) self.tree.setHeaderLabel("Chunks")
self.btn_w.clicked.connect(lambda: map.moveRelative(-1, 0, 0)) # Example tree items
self.btn_up.clicked.connect(lambda: map.moveRelative(0, 0, 1)) parentItem = QTreeWidgetItem(self.tree, ["Chunk 1"])
self.btn_down.clicked.connect(lambda: map.moveRelative(0, 0, -1)) childItem1 = QTreeWidgetItem(parentItem, ["Tile A"])
childItem2 = QTreeWidgetItem(parentItem, ["Tile B"])
parentItem2 = QTreeWidgetItem(self.tree, ["Chunk 2"])
QTreeWidgetItem(parentItem2, ["Tile C"])
QTreeWidgetItem(parentItem2, ["Tile D"])
self.tree.expandAll() # Expand by default, remove if you want collapsed
layout.addWidget(self.tree, 1) # Give tree stretch factor so it expands
self.btnN.clicked.connect(lambda: map.moveRelative(0, 1, 0))
self.btnS.clicked.connect(lambda: map.moveRelative(0, -1, 0))
self.btnE.clicked.connect(lambda: map.moveRelative(1, 0, 0))
self.btnW.clicked.connect(lambda: map.moveRelative(-1, 0, 0))
self.btnUp.clicked.connect(lambda: map.moveRelative(0, 0, 1))
self.btnDown.clicked.connect(lambda: map.moveRelative(0, 0, -1))
# Subscribe to parent's fileSaving event
if parent is not None and hasattr(parent, 'fileSaving'):
parent.fileSaving.subscribe(self.onFileSaving)
def onFileSaving(self, objOut):
# Inject 'name' into the object
objOut['name'] = 'ChunkPanel'

View File

@@ -1,36 +1,24 @@
from dusk.event import Event
MAP_WIDTH = 5
MAP_HEIGHT = 5
MAP_DEPTH = 3
class Map: class Map:
def __init__(self): def __init__(self):
self._position = [0, 0, 0] # x, y, z self.position = [0, 0, 0] # x, y, z
self.listeners = [] self.positionEvent = Event()
self.chunks = [] self.chunks = []
self.width = 5
self.height = 5
self.depth = 3
@property
def position(self):
return self._position
@position.setter
def position(self, value):
self._position = value
self.notifyPositionListeners()
def addPositionListener(self, listener):
self.listeners.append(listener)
def notifyPositionListeners(self):
for listener in self.listeners:
listener(self._position)
def moveTo(self, x, y, z): def moveTo(self, x, y, z):
self.position = [x, y, z] self.position = [x, y, z]
self.positionEvent.invoke(self.position)
def moveRelative(self, x, y, z): def moveRelative(self, x, y, z):
self.moveTo( self.moveTo(
self._position[0] + x, self.position[0] + x,
self._position[1] + y, self.position[1] + y,
self._position[2] + z self.position[2] + z
) )
# Singleton instance # Singleton instance

View File

@@ -0,0 +1,78 @@
import json
from dusk.event import Event
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import QTimer
import os
MAP_DEFAULT_PATH = os.path.join(os.path.dirname(__file__), '../../../assets/map/')
EDITOR_CONFIG_PATH = os.path.join(os.path.dirname(__file__), '.editor')
class MapData:
def __init__(self):
self.data = {}
self.dataOriginal = {}
self.onMapData = Event()
self.mapFileName = None
QTimer.singleShot(16, self.loadLastFile)
def loadLastFile(self):
if os.path.exists(EDITOR_CONFIG_PATH):
try:
with open(EDITOR_CONFIG_PATH, 'r') as f:
config = json.load(f)
lastFile = config.get('lastFile')
if lastFile and os.path.exists(lastFile):
self.load(lastFile)
except Exception:
pass
def updateEditorConfig(self):
try:
config = {'lastFile': self.mapFileName if self.mapFileName else ""}
with open(EDITOR_CONFIG_PATH, 'w') as f:
json.dump(config, f, indent=2)
except Exception:
pass
def newFile(self):
self.data = {}
self.dataOriginal = {}
self.mapFileName = None
self.onMapData.invoke(self.data)
self.updateEditorConfig()
def save(self, fname=None):
if not self.mapFileName and fname is None:
filePath, _ = QFileDialog.getSaveFileName(None, "Save Map File", MAP_DEFAULT_PATH, "Map Files (*.json)")
if not filePath:
return
self.mapFileName = filePath
if fname:
self.mapFileName = fname
if not self.isMapFileDirty():
return
try:
with open(self.mapFileName, 'w') as f:
json.dump(self.data, f, indent=2)
self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy
self.updateEditorConfig()
except Exception as e:
QMessageBox.critical(None, "Save Error", f"Failed to save map file:\n{e}")
def load(self, fileName):
try:
with open(fileName, 'r') as f:
self.data = json.load(f)
self.mapFileName = fileName
self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy
self.onMapData.invoke(self.data)
self.updateEditorConfig()
except Exception as e:
QMessageBox.critical(None, "Load Error", f"Failed to load map file:\n{e}")
def isMapFileDirty(self):
return json.dumps(self.data, sort_keys=True) != json.dumps(self.dataOriginal, sort_keys=True)
def isDirty(self):
return self.isMapFileDirty()

View File

@@ -2,22 +2,23 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton
from editortool.map.map import map from editortool.map.map import map
from dusk.defs import defs from dusk.defs import defs
CHUNK_WIDTH = int(defs.get('CHUNK_WIDTH')) chunkWidth = int(defs.get('CHUNK_WIDTH'))
CHUNK_HEIGHT = int(defs.get('CHUNK_HEIGHT')) chunkHeight = int(defs.get('CHUNK_HEIGHT'))
CHUNK_DEPTH = int(defs.get('CHUNK_DEPTH')) chunkDepth = int(defs.get('CHUNK_DEPTH'))
class MapInfoPanel(QWidget): class MapInfoPanel(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
layout = QVBoxLayout() self.parent = parent
map_info_label = QLabel("Map Information")
layout.addWidget(map_info_label)
map_title_label = QLabel("Map Title") # Components
self.map_title_input = QLineEdit() layout = QVBoxLayout()
layout.addWidget(map_title_label)
layout.addWidget(self.map_title_input) mapTitleLabel = QLabel("Map Title")
self.mapTitleInput = QLineEdit()
layout.addWidget(mapTitleLabel)
layout.addWidget(self.mapTitleInput)
tilePositionLabel = QLabel("Tile Position") tilePositionLabel = QLabel("Tile Position")
layout.addWidget(tilePositionLabel) layout.addWidget(tilePositionLabel)
@@ -43,13 +44,18 @@ class MapInfoPanel(QWidget):
layout.addStretch() layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
# Events
self.tileGo.clicked.connect(self.goToPosition) self.tileGo.clicked.connect(self.goToPosition)
self.tileXInput.returnPressed.connect(self.goToPosition) self.tileXInput.returnPressed.connect(self.goToPosition)
self.tileYInput.returnPressed.connect(self.goToPosition) self.tileYInput.returnPressed.connect(self.goToPosition)
self.tileZInput.returnPressed.connect(self.goToPosition) self.tileZInput.returnPressed.connect(self.goToPosition)
map.positionEvent.sub(self.updatePositionLabels)
parent.mapData.onMapData.sub(self.onMapData)
self.mapTitleInput.textChanged.connect(self.onMapNameChange)
# Initial label setting
self.updatePositionLabels(map.position) self.updatePositionLabels(map.position)
map.addPositionListener(self.updatePositionLabels)
def goToPosition(self): def goToPosition(self):
try: try:
@@ -64,5 +70,12 @@ class MapInfoPanel(QWidget):
self.tileXInput.setText(str(pos[0])) self.tileXInput.setText(str(pos[0]))
self.tileYInput.setText(str(pos[1])) self.tileYInput.setText(str(pos[1]))
self.tileZInput.setText(str(pos[2])) self.tileZInput.setText(str(pos[2]))
self.chunkPosLabel.setText(f"Chunk Position: {pos[0] % CHUNK_WIDTH}, {pos[1] % CHUNK_HEIGHT}, {pos[2] % CHUNK_DEPTH}") self.chunkPosLabel.setText(f"Chunk Position: {pos[0] % chunkWidth}, {pos[1] % chunkHeight}, {pos[2] % chunkDepth}")
self.chunkLabel.setText(f"Chunk: {pos[0] // CHUNK_WIDTH}, {pos[1] // CHUNK_HEIGHT}, {pos[2] // CHUNK_DEPTH}") self.chunkLabel.setText(f"Chunk: {pos[0] // chunkWidth}, {pos[1] // chunkHeight}, {pos[2] // chunkDepth}")
def onMapData(self, data):
self.updatePositionLabels(map.position)
self.mapTitleInput.setText(data.get("mapName", ""))
def onMapNameChange(self, text):
self.parent.mapData.data['mapName'] = text

View File

@@ -0,0 +1,39 @@
import os
from PyQt5.QtWidgets import QAction, QMenuBar, QFileDialog
from editortool.map.mapdata import MAP_DEFAULT_PATH
class MapToolbar:
def __init__(self, parent):
self.parent = parent
self.menubar = parent.menuBar()
self.fileMenu = self.menubar.addMenu("File")
self.actionNew = QAction("New", parent)
self.actionOpen = QAction("Open", parent)
self.actionSave = QAction("Save", parent)
self.actionSaveAs = QAction("Save As", parent)
self.actionNew.triggered.connect(self.newFile)
self.actionOpen.triggered.connect(self.openFile)
self.actionSave.triggered.connect(self.saveFile)
self.actionSaveAs.triggered.connect(self.saveAsFile)
self.fileMenu.addAction(self.actionNew)
self.fileMenu.addAction(self.actionOpen)
self.fileMenu.addAction(self.actionSave)
self.fileMenu.addAction(self.actionSaveAs)
def newFile(self):
self.parent.mapData.newFile()
def openFile(self):
filePath, _ = QFileDialog.getOpenFileName(self.menubar, "Open Map File", MAP_DEFAULT_PATH, "Map Files (*.json)")
if filePath:
self.parent.mapData.load(filePath)
def saveFile(self):
self.parent.mapData.save()
def saveAsFile(self):
filePath, _ = QFileDialog.getSaveFileName(self.menubar, "Save Map File As", MAP_DEFAULT_PATH, "Map Files (*.json)")
if filePath:
self.parent.mapData.save(filePath)

View File

@@ -1,134 +1,77 @@
import sys
import os import os
import json from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QMessageBox
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget,
QHBoxLayout, QVBoxLayout, QPushButton,
QLabel, QSlider, QDialog, QRadioButton, QDialogButtonBox,
QAction, QFileDialog, QLineEdit, QMessageBox
)
from editortool.map.glwidget import GLWidget from editortool.map.glwidget import GLWidget
from editortool.map.chunkpanel import ChunkPanel from editortool.map.chunkpanel import ChunkPanel
from editortool.map.mapinfopanel import MapInfoPanel from editortool.map.mapinfopanel import MapInfoPanel
from editortool.map.mapdata import MapData
from editortool.map.toolbar import MapToolbar
from editortool.statusbar import StatusBar
class MapWindow(QMainWindow): class MapWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# Subclasses
self.mapData = MapData()
# Window setup
self.setWindowTitle("Dusk Map Editor") self.setWindowTitle("Dusk Map Editor")
self.resize(1280, 720) self.resize(1280, 720)
# File menu setup # Components
menubar = self.menuBar() self.toolbar = MapToolbar(self)
file_menu = menubar.addMenu("File")
action_new = QAction("New", self)
action_open = QAction("Open", self)
action_save = QAction("Save", self)
action_save_as = QAction("Save As", self)
file_menu.addAction(action_new)
file_menu.addAction(action_open)
file_menu.addAction(action_save)
file_menu.addAction(action_save_as)
action_new.triggered.connect(self.newFile)
action_open.triggered.connect(self.openFile)
action_save.triggered.connect(self.saveFile)
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() central = QWidget()
self.setCentralWidget(central) self.setCentralWidget(central)
main_layout = QHBoxLayout(central) mainLayout = QHBoxLayout(central)
# Left panel (ChunkPanel) # Left panel (ChunkPanel)
self.chunk_panel = ChunkPanel() self.chunkPanel = ChunkPanel()
left_widget = QWidget() leftWidget = QWidget()
left_widget.setLayout(self.chunk_panel.layout()) leftWidget.setLayout(self.chunkPanel.layout())
left_widget.setFixedWidth(200) leftWidget.setFixedWidth(200)
main_layout.addWidget(left_widget) mainLayout.addWidget(leftWidget)
# Center panel (GLWidget + controls) # Center panel (GLWidget + controls)
self.gl_widget = GLWidget(self) self.glWidget = GLWidget(self)
main_layout.addWidget(self.gl_widget, stretch=3) mainLayout.addWidget(self.glWidget, stretch=3)
# Right panel (MapInfoPanel) # Right panel (MapInfoPanel)
self.map_info_panel = MapInfoPanel() self.mapInfoPanel = MapInfoPanel(self)
right_widget = self.map_info_panel rightWidget = self.mapInfoPanel
right_widget.setFixedWidth(250) rightWidget.setFixedWidth(250)
main_layout.addWidget(right_widget) mainLayout.addWidget(rightWidget)
self.map_title_input = self.map_info_panel.map_title_input # Status bar
self.map_title_input.textChanged.connect(self._on_map_title_changed) self.statusBar = StatusBar(self)
self.setStatusBar(self.statusBar)
def _on_map_title_changed(self): def generateData(self):
self.dirty = True objOut = {}
self.fileSaving.invoke(objOut)
def newFile(self): self.mapData.data = objOut
self.current_file = None return objOut
self.map_title_input.setText("")
self.dirty = False
def openFile(self):
default_dir = os.path.join(os.path.dirname(__file__), '../../assets/map/')
fname, _ = QFileDialog.getOpenFileName(self, "Open JSON File", default_dir, "JSON Files (*.json)")
if fname:
self.current_file = fname
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("")
QMessageBox.critical(self, "Error", f"Failed to load file:\n{e}")
def saveFile(self):
if self.current_file:
self._save_to_file(self.current_file)
else:
self.saveFileAs()
def saveFileAs(self):
default_dir = os.path.join(os.path.dirname(__file__), '../../assets/map/')
fname, _ = QFileDialog.getSaveFileName(self, "Save JSON File As", default_dir, "JSON Files (*.json)")
if fname:
self.current_file = fname
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:
QMessageBox.critical(self, "Error", f"Failed to save file:\n{e}")
def closeEvent(self, event): def closeEvent(self, event):
if self.dirty: if not self.mapData.isDirty():
reply = QMessageBox.question( event.accept()
self, return
"Unsaved Changes",
"You have unsaved changes. Do you want to save before closing?", reply = QMessageBox.question(
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, self,
QMessageBox.Save "Unsaved Changes",
) "You have unsaved changes. Do you want to save before closing?",
if reply == QMessageBox.Save: QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
self.saveFile() QMessageBox.Save
if self.dirty: )
event.ignore() if reply == QMessageBox.Save:
return self.saveFile()
elif reply == QMessageBox.Cancel: if self.mapData.isDirty():
event.ignore() event.ignore()
return return
# Discard: continue closing elif reply == QMessageBox.Cancel:
event.ignore()
return
event.accept() event.accept()

View File

@@ -0,0 +1,18 @@
from PyQt5.QtWidgets import QStatusBar, QLabel
class StatusBar(QStatusBar):
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.leftLabel = QLabel("Ready")
self.rightLabel = QLabel("")
self.addWidget(self.leftLabel, 1)
self.addPermanentWidget(self.rightLabel)
parent.mapData.onMapData.sub(self.onMapData)
def setStatus(self, message):
self.leftLabel.setText(message)
def onMapData(self, data):
self.rightLabel.setText(self.parent.mapData.mapFileName if self.parent.mapData.mapFileName else "Untitled.json")