Menu screens: rock background, centred frame, logo, 3D hero

- Spielen hero panel shows the selected hero in 3D (cUI_NetGranny,
  game/ui/granny.h).
- All menu screens draw on the options' rock wall; the main menu's 1024
  frame is drawn centred over it, mouse and cursor shifted to match.
- The main menu's and options' own pictures and the main menu's 3D scene
  are left out (textures hidden by surface, scene call redirected).
- Logo top left: FX_SACREDLOGO.TGA, drawn added.
- Game textures by name (game/ui/texture.h), textured and added quads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-21 00:05:55 +02:00
1 parent 34a119433a
commit db87e276bf
16 files changed
+620 -69

No files matched your search

+70 -3
View File
@@ -49,6 +49,7 @@
#include "game/ui/button.h"
#include "game/ui/control.h"
#include "game/ui/events.h"
#include "game/ui/granny.h"
#include "game/ui/text.h"
#include "game/ui/texts.h"
#include "game/ui/windows.h"
@@ -386,6 +387,8 @@ std::atomic<bool> g_autosavePending{false};
bool g_inGame = false; // a game runs (the load window opens from the Esc menu too)
std::atomic<uint64_t> g_lastSaveAt{0}; // GetTickCount64 of the last save of any kind
void ForgetHeroModel(); // the files may have changed: read the hero anew
std::string Description(const json& meta) {
return Convert(FromUtf8(meta.value("name", std::string())) + L" - " + ClassName(meta.value("class", 0u)),
CP_ACP);
@@ -417,6 +420,7 @@ void RebuildList() {
DeleteFileA((work + file).c_str());
}
g_rows.clear();
ForgetHeroModel();
for (const std::string& name : FilesIn(CharactersDir(), "*.sacredK")) {
if (EndsWith(name, kAutosaveExtension) || g_rows.size() >= kMaxRows) {
continue;
@@ -616,8 +620,10 @@ std::atomic<int> g_loadRow{-1};
// Layout on the canvas (1365x768): three panels side by side (sizes in the
// frames' 64 steps, screen::kFrameStep), centred, a row of buttons below.
constexpr int kPanelTop = 96; // the title above the frames' ornament
constexpr int kPanelHeight = 8 * screen::kFrameStep;
// Top: room for the heading and the frames' ornament; the buttons end
// above the screen's bottom.
constexpr int kPanelTop = 164 + screen::kFrameOrnament;
constexpr int kPanelHeight = 7 * screen::kFrameStep;
constexpr int kListWidth = 5 * screen::kFrameStep;
constexpr int kHeroWidth = 6 * screen::kFrameStep;
constexpr int kWorldsWidth = 6 * screen::kFrameStep;
@@ -1329,6 +1335,64 @@ void DrawList(void* device) {
}
}
// The selected character's hero in 3D (game/ui/granny.h): its hero file is
// unpacked to the game's save folder (_work) and read as a hero info. A
// migrated character without the shared hero has none until it is played.
constexpr float kHeroModelScale = 3.8f; // the game's 2.5 fills a third of the panel
constexpr int kHeroModelFeetAboveBottom = 24;
constexpr char kWorkPreviewHero[] = "preview.pax";
constexpr char kGamePreviewHero[] = "Save\\preview.pax";
struct HeroModel {
uint8_t* control = nullptr;
std::string file; // whose hero it shows
bool hasHero = false;
} g_heroModel;
void ForgetHeroModel() {
g_heroModel.file.clear();
}
void ShowHeroOf(const Row& row) {
if (row.file == g_heroModel.file) {
return;
}
g_heroModel.file = row.file;
g_heroModel.hasHero = false;
CharacterFile character(row.file);
std::optional<Bytes> hero = character.Open() ? character.Read("hero") : std::nullopt;
std::vector<uint8_t> info(game::kHeroInfoSize);
if (hero && WriteFileReplacing(WorkDir() + kWorkPreviewHero, *hero) &&
game::ReadHeroInfo(kGamePreviewHero, info.data())) {
game::SetNetGrannyHero(g_heroModel.control, info.data());
g_heroModel.hasHero = true;
} else {
game::ClearNetGrannyHero(g_heroModel.control);
}
DeleteFileA((WorkDir() + kWorkPreviewHero).c_str());
}
// The model stands centred in `rect` (canvas coordinates). The control centres
// it as in a 1024 wide layout (x - 512 around the screen centre); the canvas
// is centred too, so its x is moved by the difference.
void DrawHeroModel(void* device, const Row& row, game::UiRect rect) {
rect.x -= (screen::kWidth - 1024) / 2;
if (!g_heroModel.control) {
g_heroModel.control = game::NewNetGranny(rect);
if (!g_heroModel.control) {
return;
}
g_heroModel.control[game::kNetGrannyWhiteLight] = 1;
*reinterpret_cast<float*>(g_heroModel.control + game::kNetGrannyScale) = kHeroModelScale;
game::SetControlShown(g_heroModel.control, true);
}
*reinterpret_cast<game::UiRect*>(g_heroModel.control + game::kControlRect) = rect;
ShowHeroOf(row);
if (g_heroModel.hasHero) {
game::RenderNetGranny(g_heroModel.control, device);
}
}
void DrawHero(void* device) {
const game::UiRect content = g_heroPanel.Draw(device, kHeroRect);
const Row* row = SelectedCharacter();
@@ -1348,6 +1412,9 @@ void DrawHero(void* device) {
line(CampaignName(last->campaign) + L" · " + DifficultyName(last->difficulty), screen::kTextColour);
line(FormatTime(last->savedAt), screen::kDimColour);
}
// The control puts the feet at 5/6 of its rect's height (0x88f04c).
const int feet = content.y + content.h - kHeroModelFeetAboveBottom;
DrawHeroModel(device, *row, {content.x, y, content.w, static_cast<int16_t>((feet - y) * 6 / 5)});
}
void DrawCell(void* device, const Row& row, int campaign, int difficulty) {
@@ -1375,7 +1442,7 @@ void DrawCell(void* device, const Row& row, int campaign, int difficulty) {
if (cell.autosave && cell.world) {
screen::Text(device, bottom, L"+ Autosave", screen::kHeadingColour, game::kTextCenter);
} else if (row.storyCompleted[difficulty]) {
screen::Text(device, bottom, L"Story abgeschlossen", screen::kDimColour, game::kTextCenter);
screen::Text(device, bottom, L"abgeschlossen", screen::kDimColour, game::kTextCenter);
}
}