feat: add authored history and door interactions
Group level edits across inspector and canvas gestures, preserve legacy maps, and expose opt-in switch/key door bindings without changing wire layouts. Refresh studio and pause UI with UE5-inspired surfaces and content drawer. Verify 162 editor and 307 ASM checks plus renderer parity.
This commit is contained in:
parent
aa64510def
commit
7a56403d65
21 changed files with 675 additions and 97 deletions
|
|
@ -307,6 +307,15 @@ EblanStudio has evolved from a pure level editor into a **Full Game Engine Studi
|
|||
|
||||
|
||||
### K. Authored Lighting Format (EBLN v5)
|
||||
Stage-3 authored history and interaction rules are documented in
|
||||
`docs/STABILIZATION.md`. Level Undo uses nested `LevelEdit` scopes around UI;
|
||||
do not add a second snapshot inside a scope. Sprite switch flags opt into a door
|
||||
binding only when `(flags & 0xFEFF) == 0xA805`: byte 1 is A8 (switch-only) or A9
|
||||
(key-unlockable), bytes 2/3 are door X/Y. One switch per door; validate both C++ and
|
||||
ASM loaders. No EBLN/IPC size change. Runtime unlocks reset in `InitEntities`.
|
||||
F focuses selection, G fills; Ctrl+Space opens Content Drawer. External asset and
|
||||
project-file writes are outside level Undo. Dialog text remains code-authored.
|
||||
|
||||
After the v4 cutscene strings, v5 appends `LightingSettings` (32 bytes), `lightCount * 40` bytes of `LightSource`, and 1024 DWORD baked RGB cells (`00RRGGBB`, 4096 bytes). Settings are eight DWORDs: mode (0..2), ambient (0..255), flashlight (0/1), flashlight radius (32..2048), intensity (0..255), half cone angle (1..96 angle units), baked-valid (1), reserved (0). Source layouts match `EditorLightSource` and MASM `LightSource`. Counts/settings/coordinates/radii/intensities/flags/colors and exact file size are validated before committing a room. v1..v4 load no authored lights and mode NONE; the old reserved header bytes are ignored.
|
||||
|
||||
Sources and baked data are map-local and travel with save/load, undo/redo, F5 and standalone exports. Static lighting is baked in the editor with BakeLightingRGB and in ASM with BakeMapLighting; all 1024 RGB cells are regression-compared. Realtime point/pulse/flicker lights use only the live source array on top of the persisted static map. Shadow flags are respected. Slot 0 belongs to the player flashlight, 1..13 to authored sources, 14 to muzzle flare, 15 to explosions; combat never overwrites authored lights. Both wall stripes and sprites receive RGB coefficients. Flat ceiling/floor colors use a camera-cell light sample, not per-pixel floor projection.
|
||||
|
|
|
|||
|
|
@ -54,6 +54,25 @@ Playing. Playing обновляет игрока, сущности, бой, `Gam
|
|||
представляет кадр; `UpdateScreenFade` обновляет переход, `ApplyScreenFade` меняет
|
||||
только пиксели. Скриншот и счётчик FPS относятся к представлению.
|
||||
|
||||
### Авторские транзакции и взаимодействия (этап 3)
|
||||
|
||||
`EditorApp::LevelEdit` снимает полный snapshot вокруг UI-редактирования. Сравнение
|
||||
авторских данных исключает пустые действия; ActiveId виджета/жест холста объединяет
|
||||
непрерывный ввод, включая запекание на отпускании мыши. Undo/Redo ограничены 64
|
||||
снимками; успешная загрузка очищает историю. Snapshot включает свет, baked RGB,
|
||||
кат-сцены, переходы, спавн, сущности и палитру, но не внешние файлы ассетов/проекта.
|
||||
Настоящие ImGui-поля проверяются в `tests/studio_editor_checks.inl`.
|
||||
|
||||
Опциональная связь рычага с дверью помещается в существующий `SpriteStruct.flags`:
|
||||
тип 5 в младшем байте, `0xA8`/`0xA9` во втором байте, X/Y двери в старших байтах.
|
||||
`0xA9` разрешает также открыть дверь ключом: первый ключ расходуется и сохраняет
|
||||
runtime-флаг отпирания до переинициализации комнаты. Без связи обычные двери
|
||||
бесплатны; старые рычаги вызывают прежний hook/toggle. Оба загрузчика валидируют
|
||||
целевую дверь, границы и уникальность связи. `ToggleDoorCell` не закрывает дверь
|
||||
поверх тела игрока ни при прямом E, ни через рычаг. Прямое взаимодействие с дверью
|
||||
потребляет E до обработки ближайших рычагов. Бинарные размеры EBLN v5 и IPC v2
|
||||
не изменились. Подробный контракт и ограничения: `STABILIZATION.md`, этап 3.
|
||||
|
||||
## 2. Конвейер лучевого рендеринга (2.5D Raycasting)
|
||||
|
||||
Каждый кадр для экрана шириной 320 пикселей выпускается ровно **320 лучей** (по 1 лучу на каждый вертикальный столбец экрана):
|
||||
|
|
|
|||
|
|
@ -168,15 +168,69 @@ cinematic traces and 1024 baked RGB cells agree. Classic/lit IPC v2 frames match
|
|||
direct ASM rendering through the x86/x64/DX11 path. Project assets remain
|
||||
byte-for-byte unchanged. No manual interactive playtest was performed.
|
||||
|
||||
## Stage 3 — authored history, interactions and studio UI
|
||||
|
||||
Implemented:
|
||||
|
||||
- Nested `LevelEdit` scopes compare complete level snapshots around production UI
|
||||
edits. Inspector transforms, sprite texture/type/AI parameters, spawn/angle,
|
||||
palette/presets, portal filenames/destinations/fade, light settings/sources and
|
||||
cinematic events/strings participate in Undo/Redo. Existing explicit `PushUndo`
|
||||
calls are suppressed inside these scopes, avoiding duplicate entries. A stable
|
||||
active widget or canvas stroke groups continuous edits; release-time baking and
|
||||
timeline normalization belong to the same gesture. Selection/view changes and
|
||||
clicks without authored changes create no history. New edits clear Redo; history
|
||||
is limited to 64 snapshots. Successful loads clear history, failed loads preserve
|
||||
it; creating a default level is undoable. Undo/Redo clear active widgets, drag
|
||||
state and selection, restore RGB bake/signature and rescrub cinematic preview.
|
||||
- Sprite drags clamp to map bounds. Inspector duplication returns immediately,
|
||||
avoiding references invalidated by vector growth. Releasing a light drag outside
|
||||
the canvas clears its radius/position drag state.
|
||||
- A switch can opt into one authored door target without changing EBLN v5 or IPC
|
||||
v2 layouts. Flags low byte is 5; byte 1 is `0xA8` (switch-only door) or `0xA9`
|
||||
(also unlockable with a key), bytes 2/3 are door cell X/Y. Other switch flags
|
||||
retain legacy hook/toggle behavior. Each door has at most one bound switch;
|
||||
C++ and ASM loaders reject non-door targets, out-of-bounds targets and duplicate
|
||||
bindings before replacing the room. Painting away a bound door leaves a visible
|
||||
inspector error and blocks save/F5 until the target is repaired or unbound.
|
||||
- Ordinary doors remain free. Bound doors require their switch, or one collected
|
||||
key if explicitly enabled. First direct unlock consumes one key and persists
|
||||
until room reinitialization; subsequent opens/closes are free. Switch use itself
|
||||
never consumes a key. `ToggleDoorCell` shares full-player-body closure protection
|
||||
between direct and remote interactions. An occupied/invalid target does not
|
||||
change switch state. `E` prioritizes the facing door/portal, then the first nearby
|
||||
switch in authored order, consuming the action once. Custom switch hooks run
|
||||
before default door behavior and may handle the action themselves. This stage
|
||||
does not add generic proximity dialogs or switch line-of-sight tests.
|
||||
- Removed fake per-entity dialogue fields and the nonfunctional insertion promise.
|
||||
UI explicitly identifies dialogue as an `engine_gameplay.inc` code hook, not map
|
||||
data. Texture/audio import and project settings remain external file operations,
|
||||
not part of level Undo; IDE text history is separate.
|
||||
- UE5-inspired studio uses charcoal surfaces, compact square panels, Outliner and
|
||||
Details headers, a two-row horizontally scrollable toolbar, resizable views and
|
||||
a bottom Content Drawer (`Ctrl+Space`). Minimum desktop size is 1000x680; narrow
|
||||
mode shortens viewport tabs. No docking framework or alternative renderer was
|
||||
introduced. Workspace geometry ignores stale ImGui layout entries; no existing
|
||||
`imgui.ini` is rewritten by the implementation. F5/F6/F7 remain; F focuses, G
|
||||
fills, resolving the previous F conflict. Runtime pause menu uses the same
|
||||
restrained charcoal/blue visual language with five clearly separated rows.
|
||||
|
||||
Verification (2026-10-31): production builds succeed; editor **162/162**, ASM
|
||||
**307/307**. New checks exercise actual ImGui spawn/sprite/portal inputs and palette
|
||||
preset buttons, history grouping/branching/capacity, workspace/content drawer at
|
||||
1000/1360/1920 widths, real C++/ASM binding validation, key consumption, legacy
|
||||
compatibility, single-action priority and remotely occupied doors. All 320 ray
|
||||
angles, cinematic traces, 1024 RGB bake cells and classic/lit IPC frames still
|
||||
agree. Project assets remain byte-for-byte unchanged. No manual visual/keyboard
|
||||
playtest was performed; automated UI checks do not replace it.
|
||||
|
||||
## Remaining stages
|
||||
|
||||
3. Honest editor/gameplay: complete undo, keys/switches and supported interactions;
|
||||
UE5-inspired editor/runtime UI redesign.
|
||||
4. Project lifecycle and validated, safe standalone export.
|
||||
5. Measurements and targeted renderer/editor optimizations.
|
||||
6. Finished multi-room demo game and release verification.
|
||||
7. Data-driven extensions and subsequent rendering features.
|
||||
|
||||
Stages 1 and 2 do not claim that all audit findings are fixed: AI visibility/melee
|
||||
through walls, full undo and exporter safety remain assigned to subsequent
|
||||
stages. Existing map and texture assets are not migrated or rewritten.
|
||||
Stages 1–3 do not claim that all audit findings are fixed: AI visibility/melee
|
||||
through walls and exporter safety remain open. Existing map and texture assets
|
||||
are not migrated or rewritten.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -92,6 +92,8 @@ EditorApp::~EditorApp() {
|
|||
}
|
||||
|
||||
void EditorApp::InitDefaultLevel() {
|
||||
draggingLightRadiusIdx = -1;
|
||||
strokeSnapshotTaken = false;
|
||||
// Clear map
|
||||
for (int y = 0; y < MAP_GRID_H; ++y) {
|
||||
for (int x = 0; x < MAP_GRID_W; ++x) {
|
||||
|
|
@ -226,6 +228,8 @@ bool EditorApp::LoadLevel(const std::string& filepath) {
|
|||
selectedSpriteIdx = selectedTransitionIdx = draggingSpriteIdx = -1;
|
||||
strokeSnapshotTaken = false;
|
||||
undoStack.clear(); redoStack.clear();
|
||||
++editEpoch; undoGesture = 0;
|
||||
draggingLightRadiusIdx = -1;
|
||||
statusMessage = "Уровень загружен: " + filepath;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -415,6 +419,7 @@ LevelSnapshot EditorApp::CaptureSnapshot() const {
|
|||
}
|
||||
|
||||
void EditorApp::RestoreSnapshot(const LevelSnapshot& s) {
|
||||
if (ImGui::GetCurrentContext()) ImGui::ClearActiveID();
|
||||
std::memcpy(map, s.map, sizeof(map));
|
||||
playerStartX = s.playerStartX;
|
||||
playerStartY = s.playerStartY;
|
||||
|
|
@ -432,6 +437,8 @@ void EditorApp::RestoreSnapshot(const LevelSnapshot& s) {
|
|||
for(int i=0;i<1024;++i) {auto c=bakedLightRGB[i];bakedLightmap[i]=(((c>>16)&255)+((c>>8)&255)+(c&255))/3;}
|
||||
selectedLightIdx = -1;
|
||||
draggingLightIdx = -1;
|
||||
draggingLightRadiusIdx = -1;
|
||||
strokeSnapshotTaken = false;
|
||||
selectedCutsceneEvent = -1; placeCameraPoint = false;
|
||||
ScrubCutscene(cutsceneCursor);
|
||||
selectedSpriteIdx = -1;
|
||||
|
|
@ -440,12 +447,15 @@ void EditorApp::RestoreSnapshot(const LevelSnapshot& s) {
|
|||
}
|
||||
|
||||
void EditorApp::PushUndo() {
|
||||
if (editDepth) return; // UI scope records only real changes, once per gesture.
|
||||
undoGesture = 0;
|
||||
undoStack.push_back(CaptureSnapshot());
|
||||
if (undoStack.size() > MAX_UNDO) undoStack.pop_front();
|
||||
redoStack.clear();
|
||||
}
|
||||
|
||||
void EditorApp::Undo() {
|
||||
++editEpoch; undoGesture = 0;
|
||||
if (undoStack.empty()) return;
|
||||
redoStack.push_back(CaptureSnapshot());
|
||||
RestoreSnapshot(undoStack.back());
|
||||
|
|
@ -454,6 +464,7 @@ void EditorApp::Undo() {
|
|||
}
|
||||
|
||||
void EditorApp::Redo() {
|
||||
++editEpoch; undoGesture = 0;
|
||||
if (redoStack.empty()) return;
|
||||
undoStack.push_back(CaptureSnapshot());
|
||||
RestoreSnapshot(redoStack.back());
|
||||
|
|
@ -461,6 +472,50 @@ void EditorApp::Redo() {
|
|||
statusMessage = "Повтор действия.";
|
||||
}
|
||||
|
||||
bool EditorApp::SameLevel(const LevelSnapshot& a, const LevelSnapshot& b) {
|
||||
auto bytesEqual = [](const auto& x, const auto& y) {
|
||||
return x.size() == y.size() && (x.empty() ||
|
||||
std::memcmp(x.data(), y.data(), x.size() * sizeof(x[0])) == 0);
|
||||
};
|
||||
return !std::memcmp(a.map,b.map,sizeof(a.map)) &&
|
||||
a.playerStartX==b.playerStartX && a.playerStartY==b.playerStartY && a.playerStartAngle==b.playerStartAngle &&
|
||||
a.ceilingColor==b.ceilingColor && a.floorColor==b.floorColor &&
|
||||
bytesEqual(a.sprites,b.sprites) && bytesEqual(a.transitions,b.transitions) &&
|
||||
a.cutscene.flags==b.cutscene.flags && bytesEqual(a.cutscene.events,b.cutscene.events) &&
|
||||
a.cutscene.strings==b.cutscene.strings && bytesEqual(a.lightSources,b.lightSources) &&
|
||||
!std::memcmp(&a.lightingSettings,&b.lightingSettings,sizeof(LightingSettings)) &&
|
||||
a.bakedRGB==b.bakedRGB && a.bakedSignature==b.bakedSignature;
|
||||
}
|
||||
|
||||
void EditorApp::RecordLevelEdit(const LevelSnapshot& before, unsigned gesture) {
|
||||
if (!SameLevel(before,CaptureSnapshot())) {
|
||||
if (!gesture || gesture != undoGesture) {
|
||||
undoStack.push_back(before);
|
||||
if (undoStack.size()>MAX_UNDO) undoStack.pop_front();
|
||||
}
|
||||
redoStack.clear();
|
||||
undoGesture = gesture;
|
||||
} else if (gesture != undoGesture) {
|
||||
undoGesture = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::BeginLevelEdit() {
|
||||
if (editDepth++ == 0) { editBefore=CaptureSnapshot(); scopeEpoch=editEpoch; }
|
||||
}
|
||||
|
||||
void EditorApp::EndLevelEdit() {
|
||||
if (--editDepth) return;
|
||||
if (scopeEpoch!=editEpoch) return; // Load/Undo/Redo replaced the document.
|
||||
unsigned gesture=ImGui::GetCurrentContext()->ActiveId;
|
||||
if (!gesture && ImGui::IsMouseDown(ImGuiMouseButton_Left)) gesture=0xFFFFFFFEu;
|
||||
if (!gesture && ImGui::IsMouseDown(ImGuiMouseButton_Right)) gesture=0xFFFFFFFFu;
|
||||
// Release-frame bake/normalization belongs to the gesture that just ended.
|
||||
const bool released=ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right);
|
||||
RecordLevelEdit(editBefore, !gesture && released ? undoGesture : gesture);
|
||||
if (!gesture) undoGesture=0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// File dialogs (comdlg32)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -1502,6 +1557,7 @@ void EditorApp::HandleHotkeys() {
|
|||
if (io.WantTextInput || ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId)) return;
|
||||
|
||||
if (io.KeyCtrl) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Space, false)) contentDrawerOpen=!contentDrawerOpen;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_N, false)) { PushUndo(); InitDefaultLevel(); }
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_O, false)) {
|
||||
std::string p = BrowseOpenFile();
|
||||
|
|
@ -1529,7 +1585,7 @@ void EditorApp::HandleHotkeys() {
|
|||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_B, false)) currentTool = TOOL_BRUSH;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_E, false)) currentTool = TOOL_ERASER;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F, false)) currentTool = TOOL_FILL;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_G, false)) currentTool = TOOL_FILL;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_P, false)) currentTool = TOOL_PLAYER_SPAWN;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_S, false)) currentTool = TOOL_SPRITE_SPAWN;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_L, false)) currentTool = TOOL_LIGHT_SPAWN;
|
||||
|
|
@ -1633,6 +1689,7 @@ static bool DrawRatioSplitter(const char* id, float* ratio, float totalWidth, fl
|
|||
void EditorApp::RenderUI() {
|
||||
AdvanceCutscenePreview(ImGui::GetIO().DeltaTime);
|
||||
CheckPlaytestProcess();
|
||||
LevelEdit edit(*this);
|
||||
HandleHotkeys();
|
||||
|
||||
RenderMenuBar();
|
||||
|
|
@ -1652,17 +1709,20 @@ void EditorApp::RenderUI() {
|
|||
|
||||
// Main layout
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + 64));
|
||||
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, vp->Size.y - 89));
|
||||
const float drawerHeight=contentDrawerOpen ? (std::min)(240.0f,vp->Size.y*0.32f) : 0;
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + 118));
|
||||
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, vp->Size.y - 143 - drawerHeight));
|
||||
ImGui::Begin("Workspace", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoSavedSettings);
|
||||
|
||||
// Columns: Left = Hierarchy & Tile Palette, Middle = Workspace Viewport/IDE, Right = Inspector
|
||||
float totalAvailX = ImGui::GetContentRegionAvail().x;
|
||||
const float splitterW = 6.0f;
|
||||
const float spacingX = ImGui::GetStyle().ItemSpacing.x;
|
||||
|
||||
const bool compact=totalAvailX<940.0f;
|
||||
if (compact) { leftPanelWidth=180.0f; rightPanelWidth=240.0f; }
|
||||
float maxLeftW = (std::max)(180.0f, totalAvailX - rightPanelWidth - 320.0f);
|
||||
if (leftPanelWidth > maxLeftW) leftPanelWidth = maxLeftW;
|
||||
if (leftPanelWidth < 180.0f) leftPanelWidth = 180.0f;
|
||||
|
|
@ -1677,7 +1737,7 @@ void EditorApp::RenderUI() {
|
|||
// Left Panel (Unity / Unreal Docked Tabs)
|
||||
ImGui::BeginChild("LeftPanel", ImVec2(leftPanelWidth, 0), true);
|
||||
if (ImGui::BeginTabBar("LeftPanelTabs", ImGuiTabBarFlags_None)) {
|
||||
if (ImGui::BeginTabItem("Сцена")) {
|
||||
if (ImGui::BeginTabItem("Объекты")) {
|
||||
RenderHierarchy();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
|
@ -1685,7 +1745,7 @@ void EditorApp::RenderUI() {
|
|||
RenderTilePalette();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Файлы")) {
|
||||
if (ImGui::BeginTabItem("Контент")) {
|
||||
ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), "Браузер ассетов");
|
||||
ImGui::Separator();
|
||||
RenderAssetManagerContents();
|
||||
|
|
@ -1721,12 +1781,12 @@ void EditorApp::RenderUI() {
|
|||
ImGui::PopStyleColor(2);
|
||||
};
|
||||
|
||||
ModeTabButton("2D Карта", 2);
|
||||
ModeTabButton(compact ? "2D" : "2D Карта", 2);
|
||||
ImGui::SameLine();
|
||||
ModeTabButton("3D Сцена", 1);
|
||||
ImGui::SameLine();
|
||||
ModeTabButton("2D + 3D", 0);
|
||||
ModeTabButton(compact ? "3D" : "3D Сцена", 1);
|
||||
ImGui::SameLine();
|
||||
ModeTabButton(compact ? "Оба" : "2D + 3D", 0);
|
||||
if (!compact) ImGui::SameLine();
|
||||
ModeTabButton("Код / IDE", 3);
|
||||
ImGui::SameLine();
|
||||
ModeTabButton("Ассеты", 4);
|
||||
|
|
@ -1801,15 +1861,32 @@ void EditorApp::RenderUI() {
|
|||
|
||||
ImGui::End();
|
||||
|
||||
if (contentDrawerOpen) {
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x,vp->Pos.y+vp->Size.y-25-drawerHeight));
|
||||
ImGui::SetNextWindowSize(ImVec2(vp->Size.x,drawerHeight));
|
||||
ImGui::Begin("ContentDrawer",nullptr,ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoResize|
|
||||
ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::TextUnformatted("КОНТЕНТ / Текстуры и звуки");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Свернуть (Ctrl+Space)")) contentDrawerOpen=false;
|
||||
ImGui::Separator();
|
||||
ImGui::PushID("ContentDrawerAssets");
|
||||
RenderAssetManagerContents();
|
||||
ImGui::PopID();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// Status Bar
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + vp->Size.y - 25));
|
||||
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, 25));
|
||||
ImGui::Begin("StatusBar", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "● %s", statusMessage.c_str());
|
||||
ImGui::SameLine(vp->Size.x - 280);
|
||||
ImGui::Text("FPS: %.1f | Zoom: %.0f%% | Undo: %zu", ImGui::GetIO().Framerate, (canvasZoom / 20.0f) * 100.0f, undoStack.size());
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings);
|
||||
if (ImGui::SmallButton("Контент Ctrl+Space")) contentDrawerOpen=!contentDrawerOpen;
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Undo: %zu | Redo: %zu",undoStack.size(),redoStack.size());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s",statusMessage.c_str());
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
|
@ -1861,7 +1938,7 @@ void EditorApp::RenderMenuBar() {
|
|||
if (ImGui::BeginMenu("Инструменты")) {
|
||||
if (ImGui::MenuItem("Кисть (Стена)", "B", currentTool == TOOL_BRUSH)) currentTool = TOOL_BRUSH;
|
||||
if (ImGui::MenuItem("Ластик (Очистить)", "E", currentTool == TOOL_ERASER)) currentTool = TOOL_ERASER;
|
||||
if (ImGui::MenuItem("Заливка области", "F", currentTool == TOOL_FILL)) currentTool = TOOL_FILL;
|
||||
if (ImGui::MenuItem("Заливка области", "G", currentTool == TOOL_FILL)) currentTool = TOOL_FILL;
|
||||
if (ImGui::MenuItem("Спавн игрока", "P", currentTool == TOOL_PLAYER_SPAWN)) currentTool = TOOL_PLAYER_SPAWN;
|
||||
if (ImGui::MenuItem("Добавить спрайт", "S", currentTool == TOOL_SPRITE_SPAWN)) currentTool = TOOL_SPRITE_SPAWN;
|
||||
if (ImGui::MenuItem("Источник света", "L", currentTool == TOOL_LIGHT_SPAWN)) currentTool = TOOL_LIGHT_SPAWN;
|
||||
|
|
@ -1935,12 +2012,12 @@ void EditorApp::RenderMenuBar() {
|
|||
void EditorApp::RenderToolbar() {
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + 20));
|
||||
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, 42));
|
||||
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, 96));
|
||||
ImGui::Begin("Toolbar", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoSavedSettings);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 2.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(5.0f, 4.0f));
|
||||
|
||||
// 1. PLAY / STOP
|
||||
|
|
@ -2011,18 +2088,21 @@ void EditorApp::RenderToolbar() {
|
|||
if (ImGui::Button(label, ImVec2(0, 26))) currentTool = mode;
|
||||
ImGui::PopStyleColor(2);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip);
|
||||
ImGui::SameLine();
|
||||
};
|
||||
|
||||
ToolButton(" [B] Кисть ", TOOL_BRUSH, "Кисть для рисования стен [B]");
|
||||
ImGui::SameLine();
|
||||
ToolButton(" [E] Ластик ", TOOL_ERASER, "Ластик для очистки ячеек [E]");
|
||||
ToolButton(" [F] Заливка ", TOOL_FILL, "Заливка замкнутой области стен [F]");
|
||||
ImGui::SameLine();
|
||||
ToolButton(" [G] Заливка ", TOOL_FILL, "Заливка замкнутой области стен [G]. F — фокус выделенного объекта.");
|
||||
ImGui::SameLine();
|
||||
ToolButton(" [P] Игрок ", TOOL_PLAYER_SPAWN, "Точка старта игрока [P]");
|
||||
ImGui::SameLine();
|
||||
ToolButton(" [S] Спрайт ", TOOL_SPRITE_SPAWN, "Размещение сущностей и спрайтов [S]");
|
||||
ImGui::SameLine();
|
||||
ToolButton(" [L] Свет ", TOOL_LIGHT_SPAWN, "Размещение источников освещения [L]");
|
||||
|
||||
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
|
||||
ImGui::SameLine();
|
||||
ImGui::Separator();
|
||||
|
||||
// 3. LIGHTING SUBSYSTEM
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.80f, 0.48f, 0.05f, 1.0f));
|
||||
|
|
@ -2053,7 +2133,7 @@ void EditorApp::RenderToolbar() {
|
|||
} else {
|
||||
if (ImGui::Button(" Фонарик: Выкл ", ImVec2(0, 26))) { PushUndo(); isFlashlightOn = true; }
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Включить/выключить фонарик игрока [F]");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Фонарик игрока. В игре переключается клавишей F.");
|
||||
|
||||
ImGui::SameLine();
|
||||
if (showLightHeatmap) {
|
||||
|
|
@ -2429,8 +2509,8 @@ void EditorApp::RenderGridCanvas() {
|
|||
} else if (draggingSpriteIdx >= 0) {
|
||||
float wx = (mousePos.x - gridOrigin.x) / tileSize;
|
||||
float wy = (mousePos.y - gridOrigin.y) / tileSize;
|
||||
sprites[draggingSpriteIdx].x = static_cast<int32_t>(wx * 64.0f);
|
||||
sprites[draggingSpriteIdx].y = static_cast<int32_t>(wy * 64.0f);
|
||||
sprites[draggingSpriteIdx].x = std::clamp(static_cast<int32_t>(wx * 64.0f), 0, MAP_GRID_W*64-1);
|
||||
sprites[draggingSpriteIdx].y = std::clamp(static_cast<int32_t>(wy * 64.0f), 0, MAP_GRID_H*64-1);
|
||||
} else if (strokeSnapshotTaken && inGrid) {
|
||||
ApplyTool(hoverGx, hoverGy); // no extra snapshot during stroke
|
||||
}
|
||||
|
|
@ -2461,8 +2541,11 @@ void EditorApp::RenderGridCanvas() {
|
|||
} else {
|
||||
// Mouse left canvas: end drag / stroke
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
const bool changedDrag=strokeSnapshotTaken || draggingLightIdx>=0 || draggingLightRadiusIdx>=0;
|
||||
draggingSpriteIdx = -1;
|
||||
strokeSnapshotTaken = false;
|
||||
draggingLightIdx = draggingLightRadiusIdx = -1;
|
||||
if (changedDrag && autoBakeLighting && LightingNeedsBake()) BakeLighting();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2580,7 +2663,7 @@ void EditorApp::RenderTilePalette() {
|
|||
}
|
||||
|
||||
void EditorApp::RenderHierarchy() {
|
||||
ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), "ИЕРАРХИЯ СЦЕНЫ (SCENE TREE)");
|
||||
ImGui::TextUnformatted("ОБЪЕКТЫ / OUTLINER");
|
||||
ImGui::Separator();
|
||||
|
||||
// Search filter + Quick Add button (Unity / Unreal style)
|
||||
|
|
@ -2826,12 +2909,13 @@ void EditorApp::RenderHierarchy() {
|
|||
}
|
||||
|
||||
void EditorApp::RenderInspector() {
|
||||
LevelEdit edit(*this);
|
||||
if (showCutsceneTimeline && selectedCutsceneEvent >= 0) {
|
||||
RenderCutsceneInspector();
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), "ИНСПЕКТОР СВОЙСТВ (INSPECTOR)");
|
||||
ImGui::TextUnformatted("СВОЙСТВА / DETAILS");
|
||||
ImGui::Separator();
|
||||
|
||||
if (selectedLightIdx >= 0 && selectedLightIdx < static_cast<int>(lightSources.size())) {
|
||||
|
|
@ -2853,7 +2937,7 @@ void EditorApp::RenderInspector() {
|
|||
|
||||
// Entity Header Card (Unity / Unreal style)
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "Сущность #%d: %s", selectedSpriteIdx + 1, typeName);
|
||||
if (ImGui::SmallButton("Дублировать (Ctrl+D)")) { DuplicateSelectedEntity(); }
|
||||
if (ImGui::SmallButton("Дублировать (Ctrl+D)")) { DuplicateSelectedEntity(); return; }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Фокус (F)")) { FocusSelectedEntity(); }
|
||||
ImGui::SameLine();
|
||||
|
|
@ -2935,6 +3019,22 @@ void EditorApp::RenderInspector() {
|
|||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "● Ключ для открытия запертых дверей");
|
||||
} else if (archType == ENTITY_ARCHETYPE_SWITCH) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.2f, 1.0f), "● Активируется нажатием [E] рядом с объектом");
|
||||
bool bound=HasSwitchDoor(spr);
|
||||
if (ImGui::Checkbox("Связать с дверью", &bound)) {
|
||||
if (bound) SetSwitchDoor(spr, spr.x/64, spr.y/64, false);
|
||||
else SetSpriteEntityData(spr, ENTITY_ARCHETYPE_SWITCH, 100, 0, 0);
|
||||
}
|
||||
if (bound) {
|
||||
int x=(spr.flags>>16)&255, y=(spr.flags>>24)&255;
|
||||
bool key=(spr.flags&SWITCH_DOOR_KEY)!=0;
|
||||
bool changed=ImGui::SliderInt("Дверь X", &x, 0, 31);
|
||||
changed|=ImGui::SliderInt("Дверь Y", &y, 0, 31);
|
||||
changed|=ImGui::Checkbox("Открывать также ключом", &key);
|
||||
if (changed) SetSwitchDoor(spr,x,y,key);
|
||||
if (map[y][x]!=CELL_DOOR && map[y][x]!=CELL_DOOR_OPEN)
|
||||
ImGui::TextWrapped("Ошибка: в целевой клетке должна стоять дверь. Сохранение будет отклонено.");
|
||||
ImGui::TextWrapped("Рычаг переключает дверь. Без ключевого режима дверь открывается только рычагом. Один ключ навсегда отпирает дверь до перезапуска комнаты.");
|
||||
}
|
||||
} else if (archType == ENTITY_ARCHETYPE_BARREL) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.15f, 1.0f), "● Взрывается от пуль, нанося 60 AOE урона вокруг!");
|
||||
}
|
||||
|
|
@ -2946,12 +3046,8 @@ void EditorApp::RenderInspector() {
|
|||
|
||||
// Component 4: Interaction & Lore Notes
|
||||
if (ImGui::CollapsingHeader("Интерактивный диалог / Записка", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::TextDisabled("При взаимодействии [E] вызывается ShowGameDialog:");
|
||||
static char szSpeaker[32] = "ПЕРСОНАЖ";
|
||||
static char szDialog[128] = "ПРИВЕТСТВУЮ В EBLAN ENGINE 3D!";
|
||||
ImGui::InputText("Говорящий", szSpeaker, sizeof(szSpeaker));
|
||||
ImGui::InputTextMultiline("Текст", szDialog, sizeof(szDialog), ImVec2(-1, 45));
|
||||
if (ImGui::Button("Вставить хук диалога в код (F7)")) {
|
||||
ImGui::TextWrapped("Диалоги задаются в engine_gameplay.inc через Game_OnPlayerInteract и ShowGameDialog. Текст диалога не хранится в карте EBLN v5.");
|
||||
if (ImGui::Button("Открыть IDE для редактирования хука (F7)")) {
|
||||
viewportMode = 3;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ public:
|
|||
bool showCodeEditorModal = false;
|
||||
bool showProjectSettingsModal = false;
|
||||
bool showExportGameModal = false;
|
||||
bool contentDrawerOpen = false;
|
||||
|
||||
std::unique_ptr<EblanProject> project;
|
||||
std::string exportStatusMessage;
|
||||
|
|
@ -167,6 +168,18 @@ public:
|
|||
struct ID3D11ShaderResourceView* textureSRVs[16] = { nullptr };
|
||||
|
||||
private:
|
||||
// Nested UI scopes capture authored data, never selection/view preferences.
|
||||
struct LevelEdit {
|
||||
EditorApp& app;
|
||||
explicit LevelEdit(EditorApp& owner) : app(owner) { app.BeginLevelEdit(); }
|
||||
~LevelEdit() { app.EndLevelEdit(); }
|
||||
};
|
||||
void BeginLevelEdit();
|
||||
void EndLevelEdit();
|
||||
void RecordLevelEdit(const LevelSnapshot& before, unsigned gesture);
|
||||
static bool SameLevel(const LevelSnapshot& a, const LevelSnapshot& b);
|
||||
unsigned editDepth = 0, editEpoch = 0, scopeEpoch = 0, undoGesture = 0;
|
||||
LevelSnapshot editBefore;
|
||||
void RenderLightingInspector();
|
||||
void RenderLightingSettings();
|
||||
bool EnsureEnginePreview();
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ void EditorApp::RenderLightingInspector() {
|
|||
float colW2 = (ImGui::GetContentRegionAvail().x - 6.0f) * 0.5f;
|
||||
if (ImGui::Button("Фокус (F)", ImVec2(colW2, 24))) { FocusSelectedEntity(); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Дублировать (Ctrl+D)", ImVec2(colW2, 24))) { DuplicateSelectedEntity(); }
|
||||
if (ImGui::Button("Дублировать (Ctrl+D)", ImVec2(colW2, 24))) { DuplicateSelectedEntity(); return; }
|
||||
|
||||
if (ImGui::Button("К сетке (Snap)", ImVec2(colW2, 24))) { SnapLightToGrid(selectedLightIdx); }
|
||||
ImGui::SameLine();
|
||||
|
|
|
|||
|
|
@ -103,6 +103,17 @@ struct SpriteEntry {
|
|||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// Opt-in switch binding. Existing switch flags keep legacy hook-only behavior.
|
||||
constexpr uint32_t SWITCH_DOOR_BOUND = 0x0000A800;
|
||||
constexpr uint32_t SWITCH_DOOR_KEY = 0x00000100;
|
||||
inline bool HasSwitchDoor(const SpriteEntry& s) {
|
||||
return (s.flags & 0x0000FEFF) == (SWITCH_DOOR_BOUND | ENTITY_ARCHETYPE_SWITCH);
|
||||
}
|
||||
inline void SetSwitchDoor(SpriteEntry& s, int x, int y, bool key) {
|
||||
s.flags = ENTITY_ARCHETYPE_SWITCH | SWITCH_DOOR_BOUND | (key ? SWITCH_DOOR_KEY : 0) |
|
||||
(uint32_t(x & 31) << 16) | (uint32_t(y & 31) << 24);
|
||||
}
|
||||
|
||||
inline uint8_t GetSpriteEntityType(const SpriteEntry& s) {
|
||||
uint8_t t = static_cast<uint8_t>(s.flags & 0xFF);
|
||||
if (s.flags == 0) {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ inline bool ValidateLevel(const BinaryLevel& level, std::string& error) {
|
|||
if (sprite.x < 0 || sprite.x >= MAP_GRID_W * 64 || sprite.y < 0 || sprite.y >= MAP_GRID_H * 64 ||
|
||||
sprite.textureId >= 16 || sprite.scale < 0 || sprite.scale > 16 * 65536)
|
||||
return fail("Неверные координаты, текстура или масштаб спрайта");
|
||||
if (HasSwitchDoor(sprite)) {
|
||||
unsigned x=(sprite.flags>>16)&255, y=(sprite.flags>>24)&255;
|
||||
if (x>=32 || y>=32 || (level.cells[y*32+x]!=CELL_DOOR && level.cells[y*32+x]!=CELL_DOOR_OPEN))
|
||||
return fail("Рычаг должен ссылаться на клетку двери");
|
||||
for (const auto& other : level.sprites)
|
||||
if (&other!=&sprite && HasSwitchDoor(other) && (other.flags>>16)==(sprite.flags>>16))
|
||||
return fail("У двери может быть только один связанный рычаг");
|
||||
}
|
||||
}
|
||||
std::array<bool, MAP_TOTAL_CELLS> triggers{};
|
||||
for (const auto& transition : level.transitions) {
|
||||
|
|
|
|||
|
|
@ -26,22 +26,22 @@ void CreateRenderTarget();
|
|||
void CleanupRenderTarget();
|
||||
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
void SetupGodotStyle() {
|
||||
void SetupUnrealStyle() {
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
ImVec4* colors = style.Colors;
|
||||
|
||||
style.WindowRounding = 8.0f;
|
||||
style.ChildRounding = 6.0f;
|
||||
style.FrameRounding = 5.0f;
|
||||
style.PopupRounding = 8.0f;
|
||||
style.ScrollbarRounding = 7.0f;
|
||||
style.GrabRounding = 5.0f;
|
||||
style.TabRounding = 6.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.ChildRounding = 0.0f;
|
||||
style.FrameRounding = 2.0f;
|
||||
style.PopupRounding = 2.0f;
|
||||
style.ScrollbarRounding = 2.0f;
|
||||
style.GrabRounding = 2.0f;
|
||||
style.TabRounding = 2.0f;
|
||||
style.FrameBorderSize = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.WindowPadding = ImVec2(10.0f, 10.0f);
|
||||
style.FramePadding = ImVec2(8.0f, 5.0f);
|
||||
style.ItemSpacing = ImVec2(8.0f, 6.0f);
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(7.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 5.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
|
||||
// Modern Game Studio Dark Palette (Unreal / Unity / Godot Pro)
|
||||
|
|
@ -75,6 +75,22 @@ void SetupGodotStyle() {
|
|||
colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.16f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.34f, 0.42f, 1.00f);
|
||||
colors[ImGuiCol_TabActive] = ImVec4(0.22f, 0.27f, 0.35f, 1.00f);
|
||||
// UE5-inspired neutral surfaces. Blue denotes selection, not every panel.
|
||||
for (int i=0;i<ImGuiCol_COUNT;++i) {
|
||||
auto& c=colors[i];
|
||||
float gray=(c.x+c.y+c.z)/3.0f;
|
||||
c.x=c.y=c.z=gray;
|
||||
}
|
||||
colors[ImGuiCol_Text]=ImVec4(0.82f,0.82f,0.82f,1);
|
||||
colors[ImGuiCol_WindowBg]=ImVec4(0.075f,0.075f,0.075f,1);
|
||||
colors[ImGuiCol_ChildBg]=ImVec4(0.12f,0.12f,0.12f,1);
|
||||
colors[ImGuiCol_FrameBg]=ImVec4(0.075f,0.075f,0.075f,1);
|
||||
colors[ImGuiCol_PopupBg]=ImVec4(0.13f,0.13f,0.13f,1);
|
||||
colors[ImGuiCol_Header]=ImVec4(0.18f,0.18f,0.18f,1);
|
||||
colors[ImGuiCol_HeaderActive]=ImVec4(0.12f,0.32f,0.49f,1);
|
||||
colors[ImGuiCol_CheckMark]=ImVec4(0.16f,0.56f,0.86f,1);
|
||||
colors[ImGuiCol_SliderGrab]=colors[ImGuiCol_CheckMark];
|
||||
colors[ImGuiCol_TabActive]=ImVec4(0.20f,0.20f,0.20f,1);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
|
||||
|
|
@ -140,7 +156,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||
io.Fonts->AddFontDefault();
|
||||
}
|
||||
|
||||
SetupGodotStyle();
|
||||
SetupUnrealStyle();
|
||||
|
||||
ImGui_ImplWin32_Init(hWnd);
|
||||
ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
|
||||
|
|
@ -244,6 +260,12 @@ LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||
return true;
|
||||
|
||||
switch (msg) {
|
||||
case WM_GETMINMAXINFO: {
|
||||
auto* limits=reinterpret_cast<MINMAXINFO*>(lParam);
|
||||
limits->ptMinTrackSize.x=1000;
|
||||
limits->ptMinTrackSize.y=680;
|
||||
return 0;
|
||||
}
|
||||
case WM_DROPFILES: {
|
||||
HDROP hDrop = reinterpret_cast<HDROP>(wParam);
|
||||
char filePath[MAX_PATH];
|
||||
|
|
|
|||
BIN
engine.exe
BIN
engine.exe
Binary file not shown.
|
|
@ -714,20 +714,25 @@ RenderPauseMenu PROC uses ebx ecx edx
|
|||
cmp isGamePaused, 0
|
||||
je pause_menu_done
|
||||
|
||||
; Centered semi-transparent panel 220x112 at (50, 64)
|
||||
invoke DrawRect, 50, 64, 220, 112, 00141416h
|
||||
invoke DrawRectOutline, 50, 64, 220, 112, COLOR_CYAN
|
||||
invoke DrawRectOutline, 52, 66, 216, 108, COLOR_WHITE
|
||||
; Compact charcoal surfaces, restrained blue accent, consistent option rows.
|
||||
invoke DrawRect, 46, 52, 228, 144, 000B0B0Bh
|
||||
invoke DrawRect, 44, 50, 228, 144, 00202020h
|
||||
invoke DrawRectOutline, 44, 50, 228, 144, 003C3C3Ch
|
||||
invoke DrawRect, 44, 50, 228, 2, 002DA5E0h
|
||||
invoke DrawRect, 45, 53, 226, 24, 00292929h
|
||||
invoke DrawRect, 52, 84, 212, 18, 00353535h
|
||||
|
||||
; Header
|
||||
invoke DrawString8x8, 112, 72, OFFSET strPauseTitle, COLOR_YELLOW
|
||||
invoke DrawString8x8, 112, 61, OFFSET strPauseTitle, 00DEDEDEh
|
||||
|
||||
; Menu Options
|
||||
invoke DrawString8x8, 64, 88, OFFSET strPauseOpt1, COLOR_GREEN
|
||||
invoke DrawString8x8, 64, 102, OFFSET strPauseOpt2, COLOR_WHITE
|
||||
invoke DrawString8x8, 64, 116, OFFSET strPauseOpt3, COLOR_CYAN
|
||||
invoke DrawString8x8, 64, 130, OFFSET strPauseOpt4, COLOR_YELLOW
|
||||
invoke DrawString8x8, 64, 146, OFFSET strPauseOpt5, 00FF4444h
|
||||
invoke DrawRect, 52, 84, 2, 18, 002DA5E0h
|
||||
invoke DrawString8x8, 60, 89, OFFSET strPauseOpt1, 00FFFFFFh
|
||||
invoke DrawString8x8, 60, 109, OFFSET strPauseOpt2, 00CDCDCDh
|
||||
invoke DrawString8x8, 60, 129, OFFSET strPauseOpt3, 00CDCDCDh
|
||||
invoke DrawString8x8, 60, 149, OFFSET strPauseOpt4, 00CDCDCDh
|
||||
invoke DrawRect, 52, 166, 212, 1, 003C3C3Ch
|
||||
invoke DrawString8x8, 60, 177, OFFSET strPauseOpt5, 00D99898h
|
||||
|
||||
pause_menu_done:
|
||||
ret
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ ALIGN 4
|
|||
entityType dd MAX_SPRITES dup(?)
|
||||
entityHealth dd MAX_SPRITES dup(?)
|
||||
entityState dd MAX_SPRITES dup(?)
|
||||
doorUnlocked db MAP_SIZE dup(?)
|
||||
entityTimer dd MAX_SPRITES dup(?)
|
||||
entitySpeed dd MAX_SPRITES dup(?)
|
||||
entityAggro dd MAX_SPRITES dup(?)
|
||||
|
|
@ -326,6 +327,7 @@ szSwitchOnToast db "ПЕРЕКЛЮЧАТЕЛЬ: ВКЛ", 0
|
|||
szSwitchOffToast db "ПЕРЕКЛЮЧАТЕЛЬ: ВЫКЛ", 0
|
||||
szDoorOpenedToast db "ДВЕРЬ ОТКРЫТА", 0
|
||||
szDoorClosedToast db "ДВЕРЬ ЗАКРЫТА", 0
|
||||
szDoorLockedToast db "ДВЕРЬ ЗАПЕРТА", 0
|
||||
szBarrelExplodedToast db "ВЗРЫВ БОЧКИ!", 0
|
||||
szWeaponsEnabledToast db "РЕЖИМ: ОРУЖИЕ ВКЛЮЧЕНО", 0
|
||||
szWeaponsDisabledToast db "РЕЖИМ: БЕЗ ОРУЖИЯ (КВЕСТ)", 0
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ RestartCurrentRoom PROTO
|
|||
; InitEntities: Initialize entity runtime state from spriteList flags/defaults
|
||||
; ------------------------------------------------------------------------------
|
||||
InitEntities PROC uses eax ebx ecx edx esi edi
|
||||
xor eax, eax
|
||||
mov ecx, MAP_SIZE
|
||||
mov edi, OFFSET doorUnlocked
|
||||
rep stosb
|
||||
; 1. Clear runtime arrays
|
||||
xor eax, eax
|
||||
mov ecx, MAX_SPRITES
|
||||
|
|
@ -287,11 +291,33 @@ check_switch_entity:
|
|||
test eax, eax
|
||||
jnz next_entity
|
||||
|
||||
; Default switch behavior: toggle state
|
||||
; Opt-in authored door binding: byte 1 = A8/A9, X/Y in bytes 2/3.
|
||||
mov eax, [ebx].SpriteStruct.flags
|
||||
mov edx, eax
|
||||
and edx, 0FEFFh
|
||||
cmp edx, 0A805h
|
||||
jne legacy_switch_toggle
|
||||
mov edx, eax
|
||||
shr edx, 24
|
||||
shr eax, 16
|
||||
and eax, 0FFh
|
||||
invoke ToggleDoorCell, eax, edx
|
||||
test eax, eax
|
||||
jz next_entity
|
||||
; ToggleDoorCell returns the resulting cell, or zero if blocked/invalid.
|
||||
cmp eax, CELL_DOOR_OPEN
|
||||
sete al
|
||||
movzx eax, al
|
||||
mov ecx, i
|
||||
mov [entityState + ecx*4], eax
|
||||
jmp switch_show_state
|
||||
legacy_switch_toggle:
|
||||
; Legacy switches still toggle state and run the public gameplay hook.
|
||||
mov ecx, i
|
||||
mov eax, [entityState + ecx*4]
|
||||
xor eax, 1
|
||||
mov [entityState + ecx*4], eax
|
||||
switch_show_state:
|
||||
test eax, eax
|
||||
jz switch_off
|
||||
invoke ShowGameToast, OFFSET szSwitchOnToast, 60, COLOR_GREEN
|
||||
|
|
|
|||
|
|
@ -187,6 +187,46 @@ IsSolidCell ENDP
|
|||
; ------------------------------------------------------------------------------
|
||||
; InteractDoor: Checks if player is facing a door or portal and handles interaction
|
||||
; ------------------------------------------------------------------------------
|
||||
; Returns resulting cell or zero. Shared by direct interaction and switches.
|
||||
ToggleDoorCell PROC uses ebx ecx edx, cellX:DWORD, cellY:DWORD
|
||||
invoke GetMapCell, cellX, cellY
|
||||
cmp eax, CELL_DOOR
|
||||
jne toggle_check_close
|
||||
invoke SetMapCell, cellX, cellY, CELL_DOOR_OPEN
|
||||
mov eax, CELL_DOOR_OPEN
|
||||
ret
|
||||
toggle_check_close:
|
||||
cmp eax, CELL_DOOR_OPEN
|
||||
jne toggle_blocked
|
||||
mov eax, playerX
|
||||
sub eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, cellX
|
||||
jg toggle_close
|
||||
mov eax, playerX
|
||||
add eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, cellX
|
||||
jl toggle_close
|
||||
mov eax, playerY
|
||||
sub eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, cellY
|
||||
jg toggle_close
|
||||
mov eax, playerY
|
||||
add eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, cellY
|
||||
jge toggle_blocked
|
||||
toggle_close:
|
||||
invoke SetMapCell, cellX, cellY, CELL_DOOR
|
||||
mov eax, CELL_DOOR
|
||||
ret
|
||||
toggle_blocked:
|
||||
xor eax, eax
|
||||
ret
|
||||
ToggleDoorCell ENDP
|
||||
|
||||
InteractDoor PROC uses ebx ecx edx esi edi
|
||||
LOCAL targetX:DWORD, targetY:DWORD
|
||||
|
||||
|
|
@ -212,42 +252,63 @@ InteractDoor PROC uses ebx ecx edx esi edi
|
|||
|
||||
invoke GetMapCell, targetX, targetY
|
||||
cmp eax, CELL_DOOR
|
||||
jne check_door_close
|
||||
|
||||
; Open the door!
|
||||
invoke SetMapCell, targetX, targetY, CELL_DOOR_OPEN
|
||||
invoke ShowGameToast, OFFSET szDoorOpenedToast, 50, COLOR_GREEN
|
||||
ret
|
||||
|
||||
check_door_close:
|
||||
je interact_door_found
|
||||
cmp eax, CELL_DOOR_OPEN
|
||||
jne check_portal_interact
|
||||
|
||||
; Never close a door over any part of the player's collision body.
|
||||
mov eax, playerX
|
||||
sub eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetX
|
||||
jg close_clear_door
|
||||
mov eax, playerX
|
||||
add eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetX
|
||||
jl close_clear_door
|
||||
mov eax, playerY
|
||||
sub eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetY
|
||||
jg close_clear_door
|
||||
mov eax, playerY
|
||||
add eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetY
|
||||
jge no_door
|
||||
close_clear_door:
|
||||
invoke SetMapCell, targetX, targetY, CELL_DOOR
|
||||
interact_door_found:
|
||||
mov keyAction, 0
|
||||
; A bound door is switch-only, or key-unlockable. Ordinary doors stay free.
|
||||
mov edi, targetY
|
||||
shl edi, CELL_SHIFT-1 ; MAP_W is 32
|
||||
add edi, targetX
|
||||
cmp byte ptr [doorUnlocked+edi], 0
|
||||
jne interact_toggle
|
||||
mov esi, OFFSET spriteList
|
||||
mov ecx, spriteCount
|
||||
interact_binding_loop:
|
||||
test ecx, ecx
|
||||
jz interact_toggle
|
||||
mov eax, [esi].SpriteStruct.flags
|
||||
cmp al, ENTITY_TYPE_SWITCH
|
||||
jne interact_binding_next
|
||||
mov edx, eax
|
||||
and edx, 0FEFFh
|
||||
cmp edx, 0A805h
|
||||
jne interact_binding_next
|
||||
mov edx, eax
|
||||
shr edx, 16
|
||||
and edx, 0FFh
|
||||
cmp edx, targetX
|
||||
jne interact_binding_next
|
||||
mov edx, eax
|
||||
shr edx, 24
|
||||
cmp edx, targetY
|
||||
jne interact_binding_next
|
||||
test eax, 100h
|
||||
jz interact_locked
|
||||
cmp playerKeys, 0
|
||||
je interact_locked
|
||||
dec playerKeys
|
||||
mov byte ptr [doorUnlocked+edi], 1
|
||||
jmp interact_toggle
|
||||
interact_binding_next:
|
||||
add esi, sizeof SpriteStruct
|
||||
dec ecx
|
||||
jmp interact_binding_loop
|
||||
interact_locked:
|
||||
invoke ShowGameToast, OFFSET szDoorLockedToast, 50, COLOR_YELLOW
|
||||
ret
|
||||
interact_toggle:
|
||||
invoke ToggleDoorCell, targetX, targetY
|
||||
cmp eax, CELL_DOOR_OPEN
|
||||
je interact_opened
|
||||
cmp eax, CELL_DOOR
|
||||
jne no_door
|
||||
invoke ShowGameToast, OFFSET szDoorClosedToast, 50, COLOR_YELLOW
|
||||
ret
|
||||
interact_opened:
|
||||
invoke ShowGameToast, OFFSET szDoorOpenedToast, 50, COLOR_GREEN
|
||||
ret
|
||||
|
||||
check_portal_interact:
|
||||
; Check if facing a transition trigger or CELL_PORTAL
|
||||
|
|
@ -267,6 +328,7 @@ portal_find_loop:
|
|||
jne portal_find_next
|
||||
|
||||
; Found transition!
|
||||
mov keyAction, 0
|
||||
movzx eax, [esi].LevelTransition.targetSpawnX
|
||||
movzx ebx, [esi].LevelTransition.targetSpawnY
|
||||
movzx ecx, [esi].LevelTransition.targetAngle
|
||||
|
|
@ -831,6 +893,49 @@ sprite_coords_raw:
|
|||
jl close_load_failed
|
||||
cmp [esi].SpriteStruct.scale, 16 * FIXED_ONE
|
||||
ja close_load_failed
|
||||
mov eax, [esi].SpriteStruct.flags
|
||||
cmp al, ENTITY_TYPE_SWITCH
|
||||
jne validated_sprite_binding
|
||||
mov edx, eax
|
||||
and edx, 0FEFFh
|
||||
cmp edx, 0A805h
|
||||
jne validated_sprite_binding
|
||||
mov edx, eax
|
||||
shr edx, 24
|
||||
cmp edx, MAP_H
|
||||
jae close_load_failed
|
||||
shr eax, 16
|
||||
and eax, 0FFh
|
||||
cmp eax, MAP_W
|
||||
jae close_load_failed
|
||||
shl edx, 5
|
||||
add edx, eax
|
||||
mov al, stagedMap[edx]
|
||||
cmp al, CELL_DOOR
|
||||
je binding_cell_ok
|
||||
cmp al, CELL_DOOR_OPEN
|
||||
jne close_load_failed
|
||||
binding_cell_ok:
|
||||
lea edi, stagedSprites
|
||||
binding_unique_loop:
|
||||
cmp edi, esi
|
||||
jae validated_sprite_binding
|
||||
mov eax, [edi].SpriteStruct.flags
|
||||
cmp al, ENTITY_TYPE_SWITCH
|
||||
jne binding_unique_next
|
||||
mov edx, eax
|
||||
and edx, 0FEFFh
|
||||
cmp edx, 0A805h
|
||||
jne binding_unique_next
|
||||
shr eax, 16
|
||||
mov edx, [esi].SpriteStruct.flags
|
||||
shr edx, 16
|
||||
cmp eax, edx
|
||||
je close_load_failed
|
||||
binding_unique_next:
|
||||
add edi, sizeof SpriteStruct
|
||||
jmp binding_unique_loop
|
||||
validated_sprite_binding:
|
||||
add esi, sizeof SpriteStruct
|
||||
dec ecx
|
||||
jmp validate_sprite
|
||||
|
|
|
|||
BIN
main.obj
BIN
main.obj
Binary file not shown.
|
|
@ -25,6 +25,7 @@ static std::vector<uint8_t> Bytes(const fs::path& path) {
|
|||
#include "lighting_editor_checks.inl"
|
||||
#include "viewport_editor_checks.inl"
|
||||
#include "stability_editor_checks.inl"
|
||||
#include "studio_editor_checks.inl"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
EditorApp app;
|
||||
|
|
@ -156,6 +157,7 @@ int main(int argc, char** argv) {
|
|||
}
|
||||
if(argc>2) RunViewportChecks(app,argv[2]);
|
||||
RunStabilityEditorChecks();
|
||||
RunStudioEditorChecks();
|
||||
std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n";
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ include tests/lighting_engine_checks.inc
|
|||
include tests/stability_engine_checks.inc
|
||||
include tests/integration_engine_checks.inc
|
||||
include tests/simulation_engine_checks.inc
|
||||
include tests/studio_engine_checks.inc
|
||||
|
||||
AuditMain PROC
|
||||
call InitMathTables
|
||||
|
|
@ -374,6 +375,7 @@ flat_depth_ok:
|
|||
call RunCutsceneChecks
|
||||
call RunStabilityChecks
|
||||
call RunSimulationChecks
|
||||
call RunStudioChecks
|
||||
mov eax, resultCount
|
||||
inc eax
|
||||
shl eax, 2
|
||||
|
|
|
|||
106
tests/studio_editor_checks.inl
Normal file
106
tests/studio_editor_checks.inl
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
static void RunStudioEditorChecks() {
|
||||
EditorApp app;
|
||||
app.LoadLevel("../assets/map.dat");
|
||||
auto original=app.CaptureSnapshot();
|
||||
auto before=original;
|
||||
app.RecordLevelEdit(before,71);
|
||||
Check(app.undoStack.empty(),"selection-only gesture creates no undo entry");
|
||||
app.playerStartX=7;
|
||||
app.RecordLevelEdit(before,71);
|
||||
before=app.CaptureSnapshot(); app.playerStartX=8; app.RecordLevelEdit(before,71);
|
||||
Check(app.undoStack.size()==1,"continuous inspector gesture creates exactly one snapshot");
|
||||
app.RecordLevelEdit(app.CaptureSnapshot(),0);
|
||||
before=app.CaptureSnapshot(); app.floorColor=0; app.RecordLevelEdit(before,72);
|
||||
Check(app.undoStack.size()==2,"next property edit gets an independent snapshot");
|
||||
app.Undo(); Check(app.playerStartX==8 && app.floorColor==original.floorColor,"undo palette preserves preceding transform");
|
||||
app.Undo(); Check(EditorApp::SameLevel(app.CaptureSnapshot(),original),"undo continuous transform restores initial state");
|
||||
app.Redo(); app.Redo(); Check(app.playerStartX==8 && app.floorColor==0,"redo restores both authored edits");
|
||||
app.Undo(); before=app.CaptureSnapshot(); app.ceilingColor=123; app.RecordLevelEdit(before,0);
|
||||
Check(app.redoStack.empty(),"new edit after undo invalidates redo branch");
|
||||
|
||||
before=app.CaptureSnapshot();
|
||||
app.sprites[0].x+=10; app.sprites[0].scale=131072; app.sprites[0].textureId=13;
|
||||
SetSpriteEntityData(app.sprites[0],ENTITY_ARCHETYPE_GUARD,55,3,8);
|
||||
app.transitions[0].targetSpawnX=9; app.transitions[0].targetAngle=128;
|
||||
strcpy_s(app.transitions[0].targetMap,"other.dat");
|
||||
app.cutscene.flags=CUTSCENE_AUTOPLAY;
|
||||
app.RecordLevelEdit(before,0);
|
||||
app.draggingLightRadiusIdx=2; app.strokeSnapshotTaken=true; app.Undo();
|
||||
Check(EditorApp::SameLevel(app.CaptureSnapshot(),before),"snapshot restores sprite parameters portal names and cutscene flags together");
|
||||
Check(app.draggingLightRadiusIdx==-1 && !app.strokeSnapshotTaken,"undo cancels light-radius drag and paint stroke");
|
||||
for(int i=0;i<80;++i) {before=app.CaptureSnapshot(); app.floorColor=i+1; app.RecordLevelEdit(before,0);}
|
||||
Check(app.undoStack.size()==64,"history remains bounded to sixty-four snapshots");
|
||||
|
||||
BinaryLevel level; std::string error;
|
||||
ReadLevelFile("../assets/map.dat",level,error);
|
||||
level.cells[10*32+10]=CELL_DOOR;
|
||||
SetSwitchDoor(level.sprites[0],10,10,true);
|
||||
Check(ValidateLevel(level,error) && HasSwitchDoor(level.sprites[0]),"switch binding uses existing v5 sprite bytes");
|
||||
level.cells[10*32+10]=CELL_EMPTY;
|
||||
Check(!ValidateLevel(level,error),"switch binding cannot target a non-door cell");
|
||||
level.cells[10*32+10]=CELL_DOOR;
|
||||
level.sprites.push_back(level.sprites[0]);
|
||||
Check(!ValidateLevel(level,error),"duplicate door bindings are rejected");
|
||||
level.sprites.pop_back(); level.sprites[0].flags|=0x20000000;
|
||||
Check(!ValidateLevel(level,error),"out-of-bounds switch target is rejected");
|
||||
|
||||
// Drive actual ImGui inspector controls, not a duplicate model of UI edits.
|
||||
ImGui::CreateContext();
|
||||
auto& io=ImGui::GetIO(); io.IniFilename=nullptr; io.DisplaySize=ImVec2(1400,1000); io.DeltaTime=1.0f/60;
|
||||
unsigned char* pixels; int w,h; io.Fonts->GetTexDataAsRGBA32(&pixels,&w,&h);
|
||||
app.LoadLevel("../assets/map.dat"); app.selectedSpriteIdx=-1; app.selectedTransitionIdx=-1;
|
||||
auto frame=[&](const char* target, const char* text) {
|
||||
if(text) io.AddInputCharactersUTF8(text);
|
||||
ImGui::NewFrame();
|
||||
ImGui::SetNextWindowPos(ImVec2(0,0)); ImGui::SetNextWindowSize(ImVec2(600,950));
|
||||
ImGui::Begin("InspectorTest",nullptr,ImGuiWindowFlags_NoSavedSettings);
|
||||
if(target) {
|
||||
auto& g=*ImGui::GetCurrentContext();
|
||||
g.NavActivateId=g.NavActivateDownId=ImGui::GetID(target); g.NavActivateFlags=ImGuiActivateFlags_PreferInput;
|
||||
}
|
||||
app.RenderInspector();
|
||||
ImGui::End(); ImGui::Render();
|
||||
};
|
||||
frame(nullptr,nullptr);
|
||||
frame("Spawn X",nullptr);
|
||||
frame(nullptr,"7");
|
||||
io.AddKeyEvent(ImGuiKey_Enter,true); frame(nullptr,nullptr);
|
||||
io.AddKeyEvent(ImGuiKey_Enter,false); frame(nullptr,nullptr);
|
||||
Check(app.playerStartX==7 && app.undoStack.size()==1,"real inspector spawn input records original authored state");
|
||||
app.Undo(); Check(app.playerStartX==2,"real inspector spawn input can be undone");
|
||||
app.Redo(); Check(app.playerStartX==7,"real inspector spawn input can be redone");
|
||||
app.selectedSpriteIdx=0;
|
||||
frame(nullptr,nullptr);
|
||||
auto oldX=app.sprites[0].x; auto count=app.undoStack.size();
|
||||
frame("Позиция X",nullptr); frame(nullptr,"6");
|
||||
io.AddKeyEvent(ImGuiKey_Enter,true); frame(nullptr,nullptr);
|
||||
io.AddKeyEvent(ImGuiKey_Enter,false); frame(nullptr,nullptr);
|
||||
Check(app.sprites[0].x==384 && app.undoStack.size()==count+1,"real sprite inspector transform participates in history");
|
||||
app.Undo(); Check(app.sprites[0].x==oldX,"real sprite inspector undo restores raw world coordinates");
|
||||
app.selectedSpriteIdx=-1; app.selectedTransitionIdx=0;
|
||||
auto oldSpawn=app.transitions[0].targetSpawnX; count=app.undoStack.size();
|
||||
frame(nullptr,nullptr); frame("Целевой Spawn X",nullptr); frame(nullptr,"11");
|
||||
io.AddKeyEvent(ImGuiKey_Enter,true); frame(nullptr,nullptr);
|
||||
io.AddKeyEvent(ImGuiKey_Enter,false); frame(nullptr,nullptr);
|
||||
Check(app.transitions[0].targetSpawnX==11 && app.undoStack.size()==count+1,"real portal destination input records a transaction");
|
||||
app.Undo(); Check(app.transitions[0].targetSpawnX==oldSpawn,"real portal destination undo restores original spawn");
|
||||
app.selectedTransitionIdx=-1;
|
||||
auto oldPalette=app.CaptureSnapshot(); count=app.undoStack.size();
|
||||
frame(nullptr,nullptr); frame("Хоррор",nullptr); frame(nullptr,nullptr);
|
||||
Check(app.ceilingColor==0x003B0710 && app.undoStack.size()==count+1,"real environment preset records both colors once");
|
||||
app.Undo(); Check(app.ceilingColor==oldPalette.ceilingColor && app.floorColor==oldPalette.floorColor,
|
||||
"real environment preset undo restores ceiling and floor");
|
||||
app.viewportMode=2; app.showCutsceneTimeline=false;
|
||||
auto uiBefore=app.CaptureSnapshot(); count=app.undoStack.size();
|
||||
for (float width : {1000.0f,1360.0f,1920.0f}) {
|
||||
io.DisplaySize=ImVec2(width,840);
|
||||
app.contentDrawerOpen=true;
|
||||
ImGui::NewFrame(); app.RenderUI(); ImGui::Render();
|
||||
ImGui::NewFrame(); app.RenderUI(); ImGui::Render();
|
||||
}
|
||||
Check(ImGui::GetDrawData()->TotalVtxCount>0 && app.editDepth==0,
|
||||
"full workspace and content drawer render at supported window widths");
|
||||
Check(EditorApp::SameLevel(uiBefore,app.CaptureSnapshot()) && app.undoStack.size()==count,
|
||||
"workspace layout and content browsing do not dirty authored history");
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
88
tests/studio_engine_checks.inc
Normal file
88
tests/studio_engine_checks.inc
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
.data
|
||||
bindingPath db 'binding.dat',0
|
||||
badBindingPath db 'badbinding.dat',0
|
||||
badBindingBoundsPath db 'badbindingbounds.dat',0
|
||||
badBindingDuplicatePath db 'badbindingduplicate.dat',0
|
||||
.code
|
||||
RunStudioChecks PROC uses ebx esi edi
|
||||
invoke LoadSublevelBinary, OFFSET bindingPath, -1, -1, 0
|
||||
CHECK eax, 1 ; ASM loader accepts valid authored door binding
|
||||
invoke LoadSublevelBinary, OFFSET badBindingPath, -1, -1, 0
|
||||
CHECK eax, 0 ; ASM loader rejects switch binding to a non-door
|
||||
invoke LoadSublevelBinary, OFFSET badBindingBoundsPath, -1, -1, 0
|
||||
CHECK eax, 0 ; ASM loader rejects out-of-bounds switch target
|
||||
invoke LoadSublevelBinary, OFFSET badBindingDuplicatePath, -1, -1, 0
|
||||
CHECK eax, 0 ; ASM loader rejects duplicate switch target bindings
|
||||
call ResetSimulationFixture
|
||||
mov keyForward, 0
|
||||
mov playerX, 160
|
||||
mov playerY, 160
|
||||
mov playerAngle, 0
|
||||
mov spriteCount, 1
|
||||
mov [spriteList].SpriteStruct.x, 160
|
||||
mov [spriteList].SpriteStruct.y, 160
|
||||
mov [spriteList].SpriteStruct.textureId, 12
|
||||
mov [spriteList].SpriteStruct.scale, FIXED_ONE
|
||||
; target (3,2), key-unlockable switch binding
|
||||
mov [spriteList].SpriteStruct.flags, 0203A905h
|
||||
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR
|
||||
call InitEntities
|
||||
mov playerKeys, 0
|
||||
mov keyAction, 1
|
||||
call InteractDoor
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR ; bound door stays locked without a key
|
||||
movzx eax, keyAction
|
||||
CHECK eax, 0 ; locked door consumes action before nearby switches
|
||||
mov playerKeys, 2
|
||||
call InteractDoor
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR_OPEN ; key opens the explicitly authored locked door
|
||||
CHECK playerKeys, 1 ; first unlock consumes exactly one key
|
||||
call InteractDoor
|
||||
call InteractDoor
|
||||
CHECK playerKeys, 1 ; unlocked door never consumes a second key
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR_OPEN ; unlocked door remains reusable
|
||||
call InitEntities
|
||||
movzx eax, BYTE PTR [doorUnlocked+2*MAP_W+3]
|
||||
CHECK eax, 0 ; room entity initialization clears temporary unlocks
|
||||
mov [spriteList].SpriteStruct.flags, 0203A805h
|
||||
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR
|
||||
mov keyAction, 1
|
||||
call InteractDoor
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR ; switch-only door cannot be bypassed with keys
|
||||
mov playerAngle, 256 ; face away from door so E reaches nearby switch
|
||||
mov keyAction, 1
|
||||
call UpdateEntities
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR_OPEN ; authored switch opens its target door
|
||||
CHECK entityState, 1 ; switch state reflects the resulting door state
|
||||
mov keyAction, 1
|
||||
call UpdateEntities
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR ; second switch action closes target door
|
||||
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR_OPEN
|
||||
mov playerX, 3*64-8 ; body overlaps target door
|
||||
mov [spriteList].SpriteStruct.x, 3*64-8
|
||||
mov keyAction, 1
|
||||
call UpdateEntities
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR_OPEN ; remote switch cannot close a door across player body
|
||||
mov playerX, 160
|
||||
mov playerAngle, 0
|
||||
mov [spriteList].SpriteStruct.flags, 6405h ; legacy switch has no binding marker
|
||||
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR
|
||||
call InitEntities
|
||||
mov playerKeys, 0
|
||||
mov keyAction, 1
|
||||
call InteractDoor
|
||||
call UpdateEntities
|
||||
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
|
||||
CHECK eax, CELL_DOOR_OPEN ; ordinary doors remain free on legacy maps
|
||||
CHECK entityState, 0 ; one action cannot open a door and toggle a nearby legacy switch
|
||||
invoke ToggleDoorCell, 32, 32
|
||||
CHECK eax, 0 ; invalid switch targets are bounded at runtime
|
||||
ret
|
||||
RunStudioChecks ENDP
|
||||
|
|
@ -82,6 +82,16 @@ def fixtures(work):
|
|||
orphan = bytearray(grid); orphan[2 * 32 + 4] = 10
|
||||
level("orphan.dat", transitions=[], cells=orphan)
|
||||
level("duplicate.dat", transitions=[(4, 2, b"sublevel_02.dat", 3, 2, 0, 1, 0)] * 2)
|
||||
binding = bytearray(level("binding.dat",count=1,transitions=[]))
|
||||
binding[38+2*32+3]=5
|
||||
struct.pack_into("<I",binding,38+1024+20,0x0203A905)
|
||||
(assets/"binding.dat").write_bytes(binding)
|
||||
broken=bytearray(binding); broken[38+2*32+3]=0
|
||||
(assets/"badbinding.dat").write_bytes(broken)
|
||||
broken=bytearray(binding); struct.pack_into("<I",broken,38+1024+20,0x2003A905)
|
||||
(assets/"badbindingbounds.dat").write_bytes(broken)
|
||||
broken=bytearray(binding); struct.pack_into("<H",broken,16,2); broken+=broken[38+1024:]
|
||||
(assets/"badbindingduplicate.dat").write_bytes(broken)
|
||||
(assets / "badpack.bin").write_bytes(struct.pack("<IHH", 0x58544245, 1, 16) + bytes(12))
|
||||
with wave.open(str(work / "editor/quiet.wav"), "wb") as sound:
|
||||
sound.setnchannels(1); sound.setsampwidth(2); sound.setframerate(8000)
|
||||
|
|
@ -150,7 +160,7 @@ def main():
|
|||
run([str(work / "engine_tests.exe"), "pace map.dat"], work)
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + "".join(
|
||||
(ROOT / "tests" / name).read_text() for name in
|
||||
("lighting_engine_checks.inc", "cutscene_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc", "simulation_engine_checks.inc"))
|
||||
("lighting_engine_checks.inc", "cutscene_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc", "simulation_engine_checks.inc", "studio_engine_checks.inc"))
|
||||
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
|
||||
raw = (work / "engine_results.bin").read_bytes()
|
||||
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue