Characters: 16:9 "Spielen" screen, autosave interval option

- Shared screen kit (mods/screen, docs/screens.md): a 1365x768 canvas
  centred in the render width on our own dark background, the game's
  frames and buttons; the main menu renders full width while it is shown.
- The load window (cUI_Savegame) is replaced by the Spielen screen:
  character list, hero panel, campaign x difficulty grid, save/autosave
  choice, rename/delete/reset/new character dialogs.
- Migrated characters without a shared hero load their hero's world
  first, then start the new world.
- Options, Allgemein: autosave interval (Aus/5/10/15/30 minutes).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-20 23:20:45 +02:00
1 parent 732d9c2abb
commit 34a119433a
16 files changed
+1391 -468

No files matched your search

+154
View File
@@ -0,0 +1,154 @@
// 16:9 menu screens (mods/screen.h, docs/screens.md).
#include "mods/screen.h"
#include <algorithm>
#include <atomic>
#include <mutex>
#include <set>
#include <windows.h>
#include "game/ui/button.h"
#include "game/ui/control.h"
#include "game/ui/draw.h"
#include "game/ui/events.h"
#include "game/ui/frame.h"
#include "game/ui/texts.h"
#include "mods/widescreen.h"
namespace mods::screen {
namespace {
// Background (decided 2026-10-20: our own, no picture): dark, a little warm
// at the top, darker towards the bottom and the sides.
constexpr uint32_t kBackgroundTop = 0xff1c1712;
constexpr uint32_t kBackgroundBottom = 0xff070605;
constexpr uint32_t kVignette = 0xa0000000;
constexpr int kVignetteWidth = 240;
constexpr uint32_t kPanelBackdrop = 0xb4000000;
constexpr int kHeadingHeight = 30;
// Frames drawn so far, to tell a button drawn this frame (events come between
// frames).
std::atomic<unsigned> g_frame{1};
// The windows whose screen is shown: the menu thread renders the full width
// while any is.
std::mutex g_shownLock;
std::set<const void*> g_shown;
void NoteShown(const void* window, bool shown) {
std::lock_guard guard(g_shownLock);
if (shown) {
g_shown.insert(window);
} else {
g_shown.erase(window);
}
widescreen::SetWideMenuScreen(!g_shown.empty());
}
void DrawBackground(void* device) {
const float width = static_cast<float>(std::max(widescreen::RenderWidth(), kWidth));
const float height = static_cast<float>(kHeight);
game::FillVertical(device, 0, 0, width, height, kBackgroundTop, kBackgroundBottom);
game::FillHorizontal(device, 0, 0, kVignetteWidth, height, kVignette, 0);
game::FillHorizontal(device, width - kVignetteWidth, 0, width, height, 0, kVignette);
}
} // namespace
int Render(void* window, void* device, int (*body)(void* context), void* context) {
const bool shown = game::ControlShown(window);
NoteShown(window, shown);
if (!shown || !device) {
return 0;
}
++g_frame;
DrawBackground(device);
return hud::RenderCanvas(window, body, context);
}
int Handle(void* event, int (*body)(void* context), void* context) {
return hud::HandleCanvas(event, body, context);
}
void Fill(void* device, const UiRect& r, uint32_t colour) {
game::FillRect(device, static_cast<float>(r.x), static_cast<float>(r.y), static_cast<float>(r.x + r.w),
static_cast<float>(r.y + r.h), colour);
}
void Text(void* device, const UiRect& rect, const std::wstring& text, uint32_t colour, uint32_t flags) {
game::DrawText(device, kFont, game::CenterLineVertically(kFont, rect), text.c_str(), colour, flags);
}
UiRect Inset(const UiRect& r, int by) {
return {r.x + by, r.y + by, static_cast<int16_t>(r.w - 2 * by), static_cast<int16_t>(r.h - 2 * by)};
}
UiRect Panel::Draw(void* device, const UiRect& rect, const std::wstring& heading) {
if (!frame_ || rect.x != rect_.x || rect.y != rect_.y || rect.w != rect_.w || rect.h != rect_.h) {
if (frame_) {
game::DeleteControl(frame_);
}
frame_ = game::NewFrame(name_, rect);
rect_ = rect;
}
Fill(device, rect, kPanelBackdrop);
if (frame_) {
game::ControlRender(frame_, device);
}
UiRect content = Inset(rect, kPanelPadding);
if (!heading.empty()) {
Text(device, {content.x, content.y, content.w, kHeadingHeight}, heading, kHeadingColour);
content.y += kHeadingHeight;
content.h = static_cast<int16_t>(content.h - kHeadingHeight);
}
return content;
}
void Button::Draw(void* device, const uint8_t* model, const UiRect& rect) {
if (!control_) {
captionId_ = game::RegisterText(caption_.c_str());
control_ = game::NewButtonLike(model, rect, captionId_, name_);
if (!control_) {
return;
}
}
rect_ = rect;
*reinterpret_cast<UiRect*>(control_ + game::kControlRect) = rect;
game::ControlRender(control_, device);
drawnFrame_ = g_frame;
}
bool Button::Clicked(void* event) {
if (!control_ || drawnFrame_ != g_frame) {
return false;
}
game::ControlHandleEvent(control_, event);
return game::MouseEventKind(event) == game::MouseEvent::ButtonDown &&
game::EventField(event, game::kMouseButton) == game::kLeftButton &&
game::Inside(rect_, game::EventField(event, game::kMouseButtonX),
game::EventField(event, game::kMouseButtonY));
}
void Button::SetCaption(const std::wstring& caption) {
caption_ = caption;
if (captionId_) {
game::SetTextOverride(captionId_, caption_);
}
}
int16_t ButtonHeight(const uint8_t* model) {
return reinterpret_cast<const UiRect*>(model + game::kControlRect)->h;
}
bool DoubleClick::Click(int target) {
const unsigned long now = GetTickCount();
const bool twice = target == last_ && now - at_ <= GetDoubleClickTime();
last_ = twice ? -1 : target;
at_ = now;
return twice;
}
} // namespace mods::screen