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

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
class ChunkPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
self.chunk_info_label = QLabel("Tile Information")
layout.addWidget(self.chunk_info_label)
self.move_label = QLabel("Move Selection")
layout.addWidget(self.move_label)
self.chunkInfoLabel = QLabel("Tile Information")
layout.addWidget(self.chunkInfoLabel)
grid = QGridLayout()
self.btn_up = QPushButton("U")
self.btn_n = QPushButton("N")
self.btn_down = QPushButton("D")
self.btn_w = QPushButton("W")
self.btn_s = QPushButton("S")
self.btn_e = QPushButton("E")
self.btnUp = QPushButton("U")
self.btnN = QPushButton("N")
self.btnDown = QPushButton("D")
self.btnW = QPushButton("W")
self.btnS = QPushButton("S")
self.btnE = QPushButton("E")
# Arrange buttons: U N D on top row, W S E on bottom row
grid.addWidget(self.btn_up, 0, 0)
grid.addWidget(self.btn_n, 0, 1)
grid.addWidget(self.btn_down, 0, 2)
grid.addWidget(self.btn_w, 1, 0)
grid.addWidget(self.btn_s, 1, 1)
grid.addWidget(self.btn_e, 1, 2)
grid.addWidget(self.btnUp, 0, 0)
grid.addWidget(self.btnN, 0, 1)
grid.addWidget(self.btnDown, 0, 2)
grid.addWidget(self.btnW, 1, 0)
grid.addWidget(self.btnS, 1, 1)
grid.addWidget(self.btnE, 1, 2)
layout.addLayout(grid)
layout.addStretch()
self.btn_n.clicked.connect(lambda: map.moveRelative(0, 1, 0))
self.btn_s.clicked.connect(lambda: map.moveRelative(0, -1, 0))
self.btn_e.clicked.connect(lambda: map.moveRelative(1, 0, 0))
self.btn_w.clicked.connect(lambda: map.moveRelative(-1, 0, 0))
self.btn_up.clicked.connect(lambda: map.moveRelative(0, 0, 1))
self.btn_down.clicked.connect(lambda: map.moveRelative(0, 0, -1))
# Add expandable tree list
self.tree = QTreeWidget()
self.tree.setHeaderLabel("Chunks")
# Example tree items
parentItem = QTreeWidgetItem(self.tree, ["Chunk 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:
def __init__(self):
self._position = [0, 0, 0] # x, y, z
self.listeners = []
self.position = [0, 0, 0] # x, y, z
self.positionEvent = Event()
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):
self.position = [x, y, z]
self.positionEvent.invoke(self.position)
def moveRelative(self, x, y, z):
self.moveTo(
self._position[0] + x,
self._position[1] + y,
self._position[2] + z
self.position[0] + x,
self.position[1] + y,
self.position[2] + z
)
# 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 dusk.defs import defs
CHUNK_WIDTH = int(defs.get('CHUNK_WIDTH'))
CHUNK_HEIGHT = int(defs.get('CHUNK_HEIGHT'))
CHUNK_DEPTH = int(defs.get('CHUNK_DEPTH'))
chunkWidth = int(defs.get('CHUNK_WIDTH'))
chunkHeight = int(defs.get('CHUNK_HEIGHT'))
chunkDepth = int(defs.get('CHUNK_DEPTH'))
class MapInfoPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
map_info_label = QLabel("Map Information")
layout.addWidget(map_info_label)
self.parent = parent
map_title_label = QLabel("Map Title")
self.map_title_input = QLineEdit()
layout.addWidget(map_title_label)
layout.addWidget(self.map_title_input)
# Components
layout = QVBoxLayout()
mapTitleLabel = QLabel("Map Title")
self.mapTitleInput = QLineEdit()
layout.addWidget(mapTitleLabel)
layout.addWidget(self.mapTitleInput)
tilePositionLabel = QLabel("Tile Position")
layout.addWidget(tilePositionLabel)
@@ -43,13 +44,18 @@ class MapInfoPanel(QWidget):
layout.addStretch()
self.setLayout(layout)
# Events
self.tileGo.clicked.connect(self.goToPosition)
self.tileXInput.returnPressed.connect(self.goToPosition)
self.tileYInput.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)
map.addPositionListener(self.updatePositionLabels)
def goToPosition(self):
try:
@@ -64,5 +70,12 @@ class MapInfoPanel(QWidget):
self.tileXInput.setText(str(pos[0]))
self.tileYInput.setText(str(pos[1]))
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.chunkLabel.setText(f"Chunk: {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] // 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 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, QMessageBox
)
from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QMessageBox
from editortool.map.glwidget import GLWidget
from editortool.map.chunkpanel import ChunkPanel
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):
def __init__(self):
super().__init__()
# Subclasses
self.mapData = MapData()
# Window setup
self.setWindowTitle("Dusk Map Editor")
self.resize(1280, 720)
# File menu setup
menubar = self.menuBar()
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
# Components
self.toolbar = MapToolbar(self)
central = QWidget()
self.setCentralWidget(central)
main_layout = QHBoxLayout(central)
mainLayout = QHBoxLayout(central)
# Left panel (ChunkPanel)
self.chunk_panel = ChunkPanel()
left_widget = QWidget()
left_widget.setLayout(self.chunk_panel.layout())
left_widget.setFixedWidth(200)
main_layout.addWidget(left_widget)
self.chunkPanel = ChunkPanel()
leftWidget = QWidget()
leftWidget.setLayout(self.chunkPanel.layout())
leftWidget.setFixedWidth(200)
mainLayout.addWidget(leftWidget)
# Center panel (GLWidget + controls)
self.gl_widget = GLWidget(self)
main_layout.addWidget(self.gl_widget, stretch=3)
self.glWidget = GLWidget(self)
mainLayout.addWidget(self.glWidget, stretch=3)
# Right panel (MapInfoPanel)
self.map_info_panel = MapInfoPanel()
right_widget = self.map_info_panel
right_widget.setFixedWidth(250)
main_layout.addWidget(right_widget)
self.mapInfoPanel = MapInfoPanel(self)
rightWidget = self.mapInfoPanel
rightWidget.setFixedWidth(250)
mainLayout.addWidget(rightWidget)
self.map_title_input = self.map_info_panel.map_title_input
self.map_title_input.textChanged.connect(self._on_map_title_changed)
# Status bar
self.statusBar = StatusBar(self)
self.setStatusBar(self.statusBar)
def _on_map_title_changed(self):
self.dirty = True
def newFile(self):
self.current_file = None
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 generateData(self):
objOut = {}
self.fileSaving.invoke(objOut)
self.mapData.data = objOut
return objOut
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:
if not self.mapData.isDirty():
event.accept()
return
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.mapData.isDirty():
event.ignore()
return
# Discard: continue closing
elif reply == QMessageBox.Cancel:
event.ignore()
return
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")