Minor: Add Asian Pokemon Card Support (#18)

This commit is contained in:
Sebastian Dine
2026-07-22 11:13:42 +02:00
committed by GitHub
parent e5c830e945
commit c9e6bc2b6b
87 changed files with 78068 additions and 162 deletions
+2
View File
@@ -27,6 +27,8 @@ struct AppContext {
IGameModule& pokemonModule;
IGameModule& yuGiOhModule;
IGameModule& digiBattle99Module;
// Asia Pokemon sets/preview backend (not a separate Game menu entry).
IGameModule& japanesePokemonModule;
// Active per-game UI bundles. The order is the order shown in the
// Game menu; the composition root constructs them and hands raw
// pointers in. `MainFrame` does not own these — `app/main.cpp` does.
+50 -7
View File
@@ -3,7 +3,8 @@
// BaseCardEditDialog<TCard>
//
// Header-only template for the modal create/edit form. Owns the parts every
// game shares — Name, Set picker (read-only combo with prefix typeahead),
// game shares — Name, Set picker (read-only combo with typeahead: prefix first,
// then substring, with ASCII-fold so "Pokemon"/"Jungle" match "Pokémon Jungle"),
// Amount spin, Language and Condition choices, Note, Image list with
// Add/Remove/double-click-to-view, OK/Cancel — and exposes hooks the
// subclass uses to:
@@ -48,6 +49,7 @@
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <span>
#include <string>
#include <utility>
#include <vector>
@@ -102,6 +104,9 @@ protected:
// wants; the base only owns the surrounding label.
virtual void buildFlagsRow(wxBoxSizer* flagsBox) = 0;
// Subclass adds any labelled rows between Name and Set. Default does nothing.
virtual void appendPreSetRows(wxFlexGridSizer* /*grid*/) {}
// Subclass adds any extra game-specific labelled rows just below the
// standard rows but above the Note row, by calling `appendRow(label, ctrl)`
// (provided as a parameter). Default does nothing.
@@ -114,6 +119,11 @@ protected:
// Subclass copies the extra fields it owns from its widgets back into `card_`.
virtual void writeExtraToCard() {}
// Languages offered in the Language choice. Default: allLanguages().
[[nodiscard]] virtual std::span<const Language> languagesForChoice() const {
return allLanguages();
}
[[nodiscard]] virtual std::string updateMenuName() const { return "Update Sets"; }
// Display name passed into errors and the dialog title hints.
@@ -151,6 +161,12 @@ protected:
return preloadedSets_ != nullptr ? *preloadedSets_ : sets_;
}
void setPreloadedSetsPointer(const std::vector<Set>* sets) noexcept {
preloadedSets_ = sets;
}
void refreshSetAndLanguageChoices() { populateChoices(); }
// Default: combo only. Yu-Gi-Oh! overrides to add set-code entry + toggle.
virtual void customizeSetPickerRow(wxBoxSizer& row, wxComboBox* combo) {
row.Add(combo, 1, wxEXPAND);
@@ -193,6 +209,9 @@ private:
});
appendRow(grid, "Name", nameCtrl_);
// Optional rows between Name and Set (e.g. Pokemon West/Asia region).
appendPreSetRows(grid);
// `setCombo_` must be parented to `setHost` so every control in the Set row
// shares the same `wxPanel`; otherwise the combo stays a direct child of the
// dialog while the sizer lives on `setHost`, which corrupts layout on MSW.
@@ -311,9 +330,10 @@ private:
languageChoice_->Clear();
int langIdx = 0;
int i = 0;
const auto langsForChoice = languagesForChoice();
wxArrayString langs;
langs.Alloc(allLanguages().size());
for (auto l : allLanguages()) {
langs.Alloc(langsForChoice.size());
for (auto l : langsForChoice) {
const std::string lang = std::string(to_string(l));
langs.Add(wxString::FromUTF8(lang.c_str()));
if (l == card_.language) langIdx = i;
@@ -486,20 +506,43 @@ private:
#endif
}
// Fold a few Latin-1 diacritics so typing ASCII "Pokemon" matches "Pokémon".
[[nodiscard]] static wxString foldAsciiForTypeahead(wxString s) {
s.MakeLower();
s.Replace(wxString::FromUTF8("\xc3\xa9"), wxT("e")); // é
s.Replace(wxString::FromUTF8("\xc3\x89"), wxT("e")); // É (after lower: é)
s.Replace(wxString::FromUTF8("\xc3\xa8"), wxT("e")); // è
s.Replace(wxString::FromUTF8("\xc3\xaa"), wxT("e")); // ê
s.Replace(wxString::FromUTF8("\xc3\xa0"), wxT("a")); // à
s.Replace(wxString::FromUTF8("\xc3\xa1"), wxT("a")); // á
s.Replace(wxString::FromUTF8("\xc3\xb1"), wxT("n")); // ñ
s.Replace(wxString::FromUTF8("\xc3\xbc"), wxT("u")); // ü
s.Replace(wxString::FromUTF8("\xc3\xb6"), wxT("o")); // ö
return s;
}
void applySetTypeaheadSelection() {
const auto& available = availableSets();
if (!setCombo_ || available.empty()) return;
wxString pref = setTypeaheadPrefix_;
pref.MakeLower();
const wxString pref = foldAsciiForTypeahead(setTypeaheadPrefix_);
if (pref.empty()) return;
// Prefer prefix matches, then substring (so "Jungle" finds "Pokémon Jungle").
for (std::size_t i = 0; i < available.size(); ++i) {
wxString name(wxString::FromUTF8(available[i].name));
name.MakeLower();
const wxString name =
foldAsciiForTypeahead(wxString::FromUTF8(available[i].name));
if (name.StartsWith(pref)) {
setCombo_->SetSelection(static_cast<int>(i));
return;
}
}
for (std::size_t i = 0; i < available.size(); ++i) {
const wxString name =
foldAsciiForTypeahead(wxString::FromUTF8(available[i].name));
if (name.Contains(pref)) {
setCombo_->SetSelection(static_cast<int>(i));
return;
}
}
}
void onSetComboChar(wxKeyEvent& ev) {
+10 -1
View File
@@ -217,6 +217,12 @@ protected:
[[nodiscard]] virtual Game gameId() const noexcept = 0;
// Preview / card-back routing. Defaults to `gameId()`; Pokemon overrides
// so Asia cards use the JapanesePokemon preview source + card back.
[[nodiscard]] virtual Game previewGameFor(const TCard& /*card*/) const noexcept {
return gameId();
}
// Construction ------------------------------------------------------------
BaseSelectedCardPanel(wxWindow* parent,
@@ -292,6 +298,9 @@ private:
case Game::Pokemon:
// Mirrors CCM2's unresolved-preview fallback image.
return "https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg";
case Game::JapanesePokemon:
// Japanese TCG back (distinct from the Western Cardback.jpg).
return "https://archives.bulbagarden.net/media/upload/2/2a/TCG_Card_Back_Japanese.jpg";
case Game::YuGiOh:
// Yugipedia English TCG backing (thumbnail — smaller than full scan).
return "https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/250px-Back-EN.png";
@@ -379,7 +388,7 @@ private:
auto state = state_;
CardPreviewService* svcPtr = &cardPreview_;
auto [name, setId, setNo] = previewKey(card);
const Game game = gameId();
const Game game = previewGameFor(card);
const std::string exeDirCopy = exeDirForBundledAssets_;
std::thread([state, gen, svcPtr, name = std::move(name),
+39 -6
View File
@@ -1,24 +1,28 @@
#pragma once
// PokemonCardEditDialog: typed Add/Edit form for a `PokemonCard`. Inherits
// the shared layout, set picker, and image management from
// `BaseCardEditDialog<PokemonCard>` and adds:
// - a `Set #` text input (between the Set picker and the Amount spin)
// - `Holo`, `1. Edition`, `Signed`, `Altered` check boxes in the flags row
// PokemonCardEditDialog: Add/Edit form for a unified West/Asia PokemonCard.
// West/Asia switch drives set lists, language choices, preview APIs, and the
// Asia-only UnnumberedPromo print UX (from the former Japanese dialog).
#include "ccm/domain/PokemonCard.hpp"
#include "ccm/ports/ICardPreviewSource.hpp"
#include "ccm/services/CardPreviewService.hpp"
#include "ccm/ui/BaseCardEditDialog.hpp"
#include "ccm/ui/SwitchCtrl.hpp"
#include <wx/button.h>
#include <wx/stattext.h>
#include <atomic>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace ccm::ui {
class VariantImagePreviewDialog;
class PokemonCardEditDialog final : public BaseCardEditDialog<PokemonCard> {
public:
PokemonCardEditDialog(wxWindow* parent,
@@ -27,22 +31,27 @@ public:
CardPreviewService& cardPreview,
EditMode mode,
PokemonCard initial,
const std::vector<Set>* preloadedSets = nullptr);
const std::vector<Set>* westSets = nullptr,
const std::vector<Set>* asiaSets = nullptr);
~PokemonCardEditDialog() override;
protected:
void appendPreSetRows(wxFlexGridSizer* grid) override;
void buildFlagsRow(wxBoxSizer* flagsBox) override;
void appendExtraRows(wxFlexGridSizer* grid) override;
void readExtraFromCard() override;
void writeExtraToCard() override;
[[nodiscard]] std::string updateMenuName() const override { return "Update Pokemon"; }
void onCardLookupContextChanged() override;
[[nodiscard]] std::span<const Language> languagesForChoice() const override;
private:
struct VariantFetchState {
std::atomic<bool> alive{true};
};
void onRegionSwitch(wxCommandEvent&);
void applyRegion(PokemonRegion region, bool clearSetIfMissing);
void onAutoDetectSetNo(wxCommandEvent&);
void onNextSetNo(wxCommandEvent&);
void onSetSelectionChanged(wxCommandEvent&);
@@ -60,15 +69,37 @@ private:
void rebuildVariantRingFromCache();
void syncRingPositionToControls();
void refreshVariantNextControls();
void refreshSetNoRowMode();
void applySelectedSetNo(std::string setNo);
void stepVariantRing(int delta);
void scheduleDeferredVariantPrefetch();
void prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch);
void closeUnnumberedPreview();
void ensureUnnumberedPreviewOpen();
void refreshUnnumberedPreview();
void requestUnnumberedPreviewAsync(unsigned capturedEpoch,
std::string name,
std::string setId,
std::string setNo,
std::size_t ringIndex,
std::size_t ringCount);
[[nodiscard]] bool isUnnumberedPromoSelected() const;
[[nodiscard]] std::string currentRingSetNo() const;
[[nodiscard]] Game backendGame() const noexcept;
[[nodiscard]] PokemonRegion currentRegion() const noexcept;
[[nodiscard]] static std::string storedSetNoFromControls(const wxTextCtrl* ctrl);
[[nodiscard]] static std::string normalizedStoredSetNo(std::string_view setNo);
EditMode dialogMode_;
unsigned variantFetchEpoch_{0};
unsigned previewFetchEpoch_{0};
CardPreviewService& cardPreview_;
const std::vector<Set>* westSets_{nullptr};
const std::vector<Set>* asiaSets_{nullptr};
std::shared_ptr<VariantFetchState> variantFetchState_;
SwitchCtrl* regionSwitch_{nullptr};
wxStaticText* setNoLabel_{nullptr};
wxTextCtrl* setNoCtrl_{nullptr};
wxButton* autoSetNoBtn_{nullptr};
wxButton* nextSetNoBtn_{nullptr};
@@ -76,7 +107,9 @@ private:
wxCheckBox* firstEditionCheck_{nullptr};
wxCheckBox* signedCheck_{nullptr};
wxCheckBox* alteredCheck_{nullptr};
VariantImagePreviewDialog* unnumberedPreview_{nullptr};
std::string selectedSetNo_;
std::vector<AutoDetectedPrint> cachedVariants_;
std::vector<std::string> uniqueSetNos_;
std::size_t setNoRingPos_{0};
+5 -6
View File
@@ -1,9 +1,7 @@
#pragma once
// PokemonGameView: IGameView for the Pokemon TCG. Mirrors `MagicGameView` —
// owns the Pokemon-typed list, selected, and edit-dialog widgets and
// delegates persistence to a `CollectionService<PokemonCard>` reference
// supplied by the composition root.
// PokemonGameView: unified West + Asia Pokemon UI. One collection file;
// separate West/Asia set caches; Sets > Update Pokemon refreshes both.
#include "ccm/domain/PokemonCard.hpp"
#include "ccm/games/IGameModule.hpp"
@@ -49,7 +47,7 @@ public:
private:
void ensureSetsLoaded();
const std::vector<Set>& setsForDialog();
const std::vector<Set>& setsForDialog(PokemonRegion region);
ConfigService& config_;
CollectionService<PokemonCard>& collection_;
@@ -60,7 +58,8 @@ private:
PokemonCardListPanel* listPanel_{nullptr};
PokemonSelectedCardPanel* selectedPanel_{nullptr};
std::vector<Set> setsCache_;
std::vector<Set> setsCacheWest_;
std::vector<Set> setsCacheAsia_;
bool attemptedInitialSetLoad_{false};
};
@@ -25,6 +25,9 @@ protected:
[[nodiscard]] std::tuple<std::string, std::string, std::string>
previewKey(const PokemonCard& card) const override;
[[nodiscard]] Game gameId() const noexcept override { return Game::Pokemon; }
[[nodiscard]] Game previewGameFor(const PokemonCard& card) const noexcept override {
return pokemonBackendGame(card.region);
}
};
} // namespace ccm::ui
@@ -0,0 +1,45 @@
#pragma once
// VariantImagePreviewDialog: small modeless popup that shows a single card
// preview image (bytes decoded as wxImage). Used by Japanese Pokémon Add/Edit
// when cycling UnnumberedPromo prints. Prev/Next fire custom events so the
// edit dialog owns the variant ring.
#include <wx/button.h>
#include <wx/dialog.h>
#include <wx/event.h>
#include <wx/image.h>
#include <wx/panel.h>
#include <wx/stattext.h>
#include <wx/string.h>
#include <string_view>
namespace ccm::ui {
wxDECLARE_EVENT(EVT_VARIANT_PREVIEW_PREV, wxCommandEvent);
wxDECLARE_EVENT(EVT_VARIANT_PREVIEW_NEXT, wxCommandEvent);
class VariantImagePreviewDialog : public wxDialog {
public:
explicit VariantImagePreviewDialog(wxWindow* parent);
void setCaption(const wxString& caption);
void setImageBytes(std::string_view bytes);
void clearImage();
void setNavigationEnabled(bool enabled);
void repositionBesideParent();
private:
class ImageCanvas;
void onPrev(wxCommandEvent&);
void onNext(wxCommandEvent&);
ImageCanvas* imageHost_{nullptr};
wxStaticText* caption_{nullptr};
wxButton* prevButton_{nullptr};
wxButton* nextButton_{nullptr};
};
} // namespace ccm::ui