mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-29 09:18:48 +00:00
Minor: New Game Yu-Gi-Oh! Bandai (#22)
This commit is contained in:
@@ -21,6 +21,7 @@ enum class Game {
|
||||
Pokemon,
|
||||
YuGiOh,
|
||||
DigiBattle99,
|
||||
YuGiOhBandai,
|
||||
JapanesePokemon, // internal Asia sets/preview routing; not in allGames()
|
||||
};
|
||||
|
||||
@@ -70,7 +71,7 @@ std::optional<Condition> conditionFromString(std::string_view s) noexcept;
|
||||
std::optional<Theme> themeFromString(std::string_view s) noexcept;
|
||||
|
||||
// User-facing games (Game menu / Settings). JapanesePokemon is internal-only.
|
||||
const std::array<Game, 4>& allGames() noexcept;
|
||||
const std::array<Game, 5>& allGames() noexcept;
|
||||
const std::array<Language, 10>& allLanguages() noexcept;
|
||||
const std::array<Condition, 7>& allConditions() noexcept;
|
||||
const std::array<Theme, 2>& allThemes() noexcept;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
// YuGiOhBandaiCard - Bandai Carddass (pre-Konami) card model.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/domain/Set.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
struct YuGiOhBandaiCard {
|
||||
std::uint32_t id{0};
|
||||
std::uint8_t amount{1};
|
||||
std::string name;
|
||||
Set set;
|
||||
std::string setNo;
|
||||
std::string rarity;
|
||||
std::string note;
|
||||
std::vector<std::string> images;
|
||||
Language language{Language::Japanese};
|
||||
Condition condition{Condition::NearMint};
|
||||
bool holo{false};
|
||||
bool signed_{false};
|
||||
bool altered{false};
|
||||
|
||||
friend bool operator==(const YuGiOhBandaiCard&, const YuGiOhBandaiCard&) = default;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json& j, const YuGiOhBandaiCard& c);
|
||||
void from_json(const nlohmann::json& j, YuGiOhBandaiCard& c);
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
// YuGiOhBandaiSetCatalog: offline pack → card checklist for Bandai set
|
||||
// completion. Filled from Yugipedia set-gallery wikitext and persisted at
|
||||
// `<dataStorage>/yugiohbandai/set-catalog.json`.
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
struct YuGiOhBandaiCatalogCard {
|
||||
std::string setNo;
|
||||
std::string name;
|
||||
std::string rarity;
|
||||
|
||||
friend bool operator==(const YuGiOhBandaiCatalogCard&,
|
||||
const YuGiOhBandaiCatalogCard&) = default;
|
||||
};
|
||||
|
||||
struct YuGiOhBandaiSetCatalogPack {
|
||||
std::string setId;
|
||||
std::string setName;
|
||||
std::vector<YuGiOhBandaiCatalogCard> cards;
|
||||
|
||||
friend bool operator==(const YuGiOhBandaiSetCatalogPack&,
|
||||
const YuGiOhBandaiSetCatalogPack&) = default;
|
||||
};
|
||||
|
||||
struct YuGiOhBandaiSetCatalog {
|
||||
std::vector<YuGiOhBandaiSetCatalogPack> packs;
|
||||
|
||||
[[nodiscard]] const YuGiOhBandaiSetCatalogPack* findPack(
|
||||
std::string_view setId) const;
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept { return packs.empty(); }
|
||||
|
||||
friend bool operator==(const YuGiOhBandaiSetCatalog&,
|
||||
const YuGiOhBandaiSetCatalog&) = default;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json& j, const YuGiOhBandaiCatalogCard& c);
|
||||
void from_json(const nlohmann::json& j, YuGiOhBandaiCatalogCard& c);
|
||||
void to_json(nlohmann::json& j, const YuGiOhBandaiSetCatalogPack& p);
|
||||
void from_json(const nlohmann::json& j, YuGiOhBandaiSetCatalogPack& p);
|
||||
void to_json(nlohmann::json& j, const YuGiOhBandaiSetCatalog& c);
|
||||
void from_json(const nlohmann::json& j, YuGiOhBandaiSetCatalog& c);
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
// YuGiOhBandaiCardPreviewSource: Yugipedia pageimages + SMW ask for Bandai
|
||||
// Carddass previews and auto-detect (by English name or Bandai number).
|
||||
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/ports/IHttpClient.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class YuGiOhBandaiCardPreviewSource final : public ICardPreviewSource {
|
||||
public:
|
||||
explicit YuGiOhBandaiCardPreviewSource(IHttpClient& http);
|
||||
|
||||
[[nodiscard]] bool supportsAutoDetectPrint() const noexcept override { return true; }
|
||||
|
||||
Result<std::string, PreviewLookupError>
|
||||
fetchImageUrl(std::string_view name,
|
||||
std::string_view setId,
|
||||
std::string_view setNo) override;
|
||||
|
||||
Result<AutoDetectedPrint> detectFirstPrint(std::string_view name,
|
||||
std::string_view setId) override;
|
||||
|
||||
Result<std::vector<AutoDetectedPrint>> detectPrintVariants(std::string_view name,
|
||||
std::string_view setId) override;
|
||||
|
||||
Result<AutoDetectedPrint> detectBySetNo(std::string_view setNo) override;
|
||||
|
||||
Result<std::vector<AutoDetectedPrint>> detectVariantsBySetNo(
|
||||
std::string_view setNo) override;
|
||||
|
||||
// Prefer "<Name> (Bandai)" / English / Sealdass page depending on setId.
|
||||
static std::string preferredPageTitle(std::string_view name,
|
||||
std::string_view setId,
|
||||
std::string_view setNo);
|
||||
|
||||
static std::string buildPageImagesUrl(std::string_view pageTitle);
|
||||
|
||||
static std::string buildAskByNameUrl(std::string_view englishName);
|
||||
|
||||
static std::string buildAskByNumberUrl(std::string_view setNo);
|
||||
|
||||
// True for Jump/Toei promo codes (J1, TA2, …). Yugipedia's SMW
|
||||
// `Bandai number` property is numeric-only, so these must use the
|
||||
// promotional gallery instead of `action=ask`.
|
||||
[[nodiscard]] static bool isAlphanumericPromoNumber(std::string_view setNo);
|
||||
|
||||
static Result<std::vector<AutoDetectedPrint>>
|
||||
parsePromoGalleryResponse(const std::string& body,
|
||||
std::string_view wantedSetNo);
|
||||
|
||||
static Result<std::string, PreviewLookupError>
|
||||
parsePageImagesResponse(const std::string& body);
|
||||
|
||||
static Result<std::vector<AutoDetectedPrint>>
|
||||
parseAskResponse(const std::string& body, std::string_view preferredSetId);
|
||||
|
||||
static AutoDetectedPrint enrichPrint(AutoDetectedPrint print,
|
||||
std::string_view pageTitle);
|
||||
|
||||
private:
|
||||
Result<std::string, PreviewLookupError> fetchPageImage(std::string_view pageTitle);
|
||||
|
||||
Result<std::vector<AutoDetectedPrint>> askByName(std::string_view name,
|
||||
std::string_view setId);
|
||||
|
||||
Result<std::vector<AutoDetectedPrint>> askByNumber(std::string_view setNo);
|
||||
|
||||
IHttpClient& http_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
// YuGiOhBandaiGameModule: Bandai Carddass via Yugipedia.
|
||||
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiCardPreviewSource.hpp"
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiSetSource.hpp"
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class YuGiOhBandaiGameModule final : public IGameModule {
|
||||
public:
|
||||
explicit YuGiOhBandaiGameModule(IHttpClient& http);
|
||||
|
||||
[[nodiscard]] Game id() const noexcept override { return Game::YuGiOhBandai; }
|
||||
[[nodiscard]] std::string dirName() const override { return "yugiohbandai"; }
|
||||
[[nodiscard]] std::string displayName() const override { return "Yu-Gi-Oh! (Bandai)"; }
|
||||
|
||||
ISetSource& setSource() override { return setSource_; }
|
||||
ICardPreviewSource* cardPreviewSource() noexcept override { return &previewSource_; }
|
||||
|
||||
YuGiOhBandaiSetSource& bandaiSetSource() noexcept { return setSource_; }
|
||||
|
||||
private:
|
||||
YuGiOhBandaiSetSource setSource_;
|
||||
YuGiOhBandaiCardPreviewSource previewSource_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
// YuGiOhBandaiSetSource: hardcoded Bandai set manifest + Yugipedia gallery
|
||||
// wikitext catalogs for set completion.
|
||||
|
||||
#include "ccm/domain/Set.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiSetCatalog.hpp"
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/ports/IHttpClient.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class YuGiOhBandaiSetSource final : public ISetSource {
|
||||
public:
|
||||
struct FetchWithCatalog {
|
||||
std::vector<Set> sets;
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
};
|
||||
|
||||
struct SetManifestEntry {
|
||||
const char* id;
|
||||
const char* name;
|
||||
const char* releaseDate; // YYYY/MM/DD
|
||||
const char* galleryPage; // Yugipedia page title (may be shared)
|
||||
// For the shared promo gallery: keep cards whose setNo starts with
|
||||
// this prefix (empty = keep all from that page into this pack).
|
||||
const char* setNoPrefix;
|
||||
};
|
||||
|
||||
explicit YuGiOhBandaiSetSource(IHttpClient& http);
|
||||
|
||||
Result<std::vector<Set>> fetchAll() override;
|
||||
|
||||
Result<FetchWithCatalog> fetchAllWithCatalog();
|
||||
|
||||
[[nodiscard]] static const std::vector<SetManifestEntry>& setManifest();
|
||||
|
||||
static Result<std::vector<Set>> parseResponse(const std::string& /*unused*/);
|
||||
|
||||
// Parse one gallery wikitext body into checklist cards.
|
||||
static Result<std::vector<YuGiOhBandaiCatalogCard>>
|
||||
parseGalleryWikitext(const std::string& wikitext);
|
||||
|
||||
// Map a Bandai number string to a set id (ban1/ban2/ban3/promos/sealdass).
|
||||
static std::string setIdForNumber(std::string_view setNo);
|
||||
|
||||
static std::string setNameForId(std::string_view setId);
|
||||
|
||||
// Normalize printed numbers: strip leading zeros on pure-decimal values;
|
||||
// uppercase letter prefixes (j1 → J1). Sealdass stays unpadded decimal.
|
||||
static std::string normalizeCardNumber(std::string_view setNo);
|
||||
|
||||
static std::string expandRarityCode(std::string_view code);
|
||||
|
||||
static std::string buildGalleryParseUrl(std::string_view pageTitle);
|
||||
|
||||
static std::string englishNameFromGalleryTitle(std::string_view pageTitle);
|
||||
|
||||
private:
|
||||
IHttpClient& http_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -19,6 +19,12 @@ namespace ccm {
|
||||
struct AutoDetectedPrint {
|
||||
std::string setNo;
|
||||
std::string rarity;
|
||||
// Optional fields used by games that resolve set/name/language during
|
||||
// auto-detect (e.g. Yu-Gi-Oh! Bandai). Existing games leave them empty.
|
||||
std::string name;
|
||||
std::string setId;
|
||||
std::string setName;
|
||||
std::string language; // Language enum spelling when known ("Japanese" / "English")
|
||||
};
|
||||
|
||||
// Classified error returned by ICardPreviewSource::fetchImageUrl. The kind
|
||||
@@ -79,6 +85,18 @@ public:
|
||||
return Result<std::vector<AutoDetectedPrint>>::err(
|
||||
"Print variant listing not supported by this game.");
|
||||
}
|
||||
|
||||
// Optional lookup by collector / Bandai number (fills name + set + rarity).
|
||||
virtual Result<AutoDetectedPrint> detectBySetNo(std::string_view /*setNo*/) {
|
||||
return Result<AutoDetectedPrint>::err(
|
||||
"Detect-by-number not supported by this game.");
|
||||
}
|
||||
|
||||
virtual Result<std::vector<AutoDetectedPrint>>
|
||||
detectVariantsBySetNo(std::string_view /*setNo*/) {
|
||||
return Result<std::vector<AutoDetectedPrint>>::err(
|
||||
"Detect-by-number variants not supported by this game.");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "ccm/domain/JapanesePokemonCard.hpp"
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
|
||||
#include <string_view>
|
||||
@@ -47,6 +48,10 @@ namespace ccm {
|
||||
[[nodiscard]] bool matchesDigiBattle99Filter(const DigiBattle99Card& card,
|
||||
std::string_view filter);
|
||||
|
||||
// Bandai: name, set.name, setNo, rarity, language, condition, amount, note.
|
||||
[[nodiscard]] bool matchesYuGiOhBandaiFilter(const YuGiOhBandaiCard& card,
|
||||
std::string_view filter);
|
||||
|
||||
// Japanese Pokemon mirrors Pokemon searchable columns (includes setNo).
|
||||
[[nodiscard]] bool matchesJapanesePokemonFilter(const JapanesePokemonCard& card,
|
||||
std::string_view filter);
|
||||
|
||||
@@ -81,6 +81,11 @@ public:
|
||||
std::string_view name,
|
||||
std::string_view setId);
|
||||
|
||||
Result<AutoDetectedPrint> detectBySetNo(Game game, std::string_view setNo);
|
||||
|
||||
Result<std::vector<AutoDetectedPrint>> detectVariantsBySetNo(Game game,
|
||||
std::string_view setNo);
|
||||
|
||||
// Download image bytes from a fully-qualified URL without going through
|
||||
// per-game preview-source resolution. Cached by URL (same LRU bound).
|
||||
Result<std::string> fetchImageBytesByUrl(std::string_view url);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "ccm/domain/JapanesePokemonCard.hpp"
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
|
||||
#include <vector>
|
||||
@@ -82,6 +83,20 @@ enum class DigiBattle99SortColumn {
|
||||
Note,
|
||||
};
|
||||
|
||||
enum class YuGiOhBandaiSortColumn {
|
||||
Name,
|
||||
SetReleaseDate,
|
||||
SetNo,
|
||||
Rarity,
|
||||
Language,
|
||||
Condition,
|
||||
Amount,
|
||||
Holo,
|
||||
Signed,
|
||||
Altered,
|
||||
Note,
|
||||
};
|
||||
|
||||
// Japanese Pokemon mirrors Pokemon columns.
|
||||
enum class JapanesePokemonSortColumn {
|
||||
Name,
|
||||
@@ -107,6 +122,9 @@ void sortYuGiOhCards(std::vector<YuGiOhCard>& cards, YuGiOhSortColumn column,
|
||||
void sortDigiBattle99Cards(std::vector<DigiBattle99Card>& cards,
|
||||
DigiBattle99SortColumn column,
|
||||
bool ascending);
|
||||
void sortYuGiOhBandaiCards(std::vector<YuGiOhBandaiCard>& cards,
|
||||
YuGiOhBandaiSortColumn column,
|
||||
bool ascending);
|
||||
void sortJapanesePokemonCards(std::vector<JapanesePokemonCard>& cards,
|
||||
JapanesePokemonSortColumn column,
|
||||
bool ascending);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
// YuGiOhBandaiSetCatalogService: load/save yugiohbandai/set-catalog.json.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiSetCatalog.hpp"
|
||||
#include "ccm/ports/IFileSystem.hpp"
|
||||
#include "ccm/services/ConfigService.hpp"
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class YuGiOhBandaiSetCatalogService {
|
||||
public:
|
||||
using DirNameFn = std::function<std::string(Game)>;
|
||||
|
||||
YuGiOhBandaiSetCatalogService(IFileSystem& fs, ConfigService& config, DirNameFn dirName);
|
||||
|
||||
Result<YuGiOhBandaiSetCatalog> load() const;
|
||||
Result<void> save(const YuGiOhBandaiSetCatalog& catalog);
|
||||
|
||||
[[nodiscard]] bool exists() const;
|
||||
|
||||
private:
|
||||
IFileSystem& fs_;
|
||||
ConfigService& config_;
|
||||
DirNameFn dirName_;
|
||||
|
||||
[[nodiscard]] std::filesystem::path catalogPath() const;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
// Pure helpers: Bandai set-completion progress and per-set checklists.
|
||||
// Ownership keys on (set.id, normalized setNo). Never name-only.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiSetCatalog.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
struct YuGiOhBandaiSetCompletionProgress {
|
||||
std::string setId;
|
||||
std::string setName;
|
||||
std::size_t ownedUnique{0};
|
||||
std::size_t total{0};
|
||||
|
||||
[[nodiscard]] int percent() const noexcept {
|
||||
if (total == 0) return 0;
|
||||
return static_cast<int>((ownedUnique * 100) / total);
|
||||
}
|
||||
};
|
||||
|
||||
struct YuGiOhBandaiChecklistEntry {
|
||||
std::string setNo;
|
||||
std::string name;
|
||||
std::string rarity;
|
||||
bool owned{false};
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<Language>
|
||||
yuGiOhBandaiLanguagesInCollection(const std::vector<YuGiOhBandaiCard>& collection);
|
||||
|
||||
[[nodiscard]] std::vector<YuGiOhBandaiSetCompletionProgress>
|
||||
computeYuGiOhBandaiSetCompletion(const std::vector<YuGiOhBandaiCard>& collection,
|
||||
const YuGiOhBandaiSetCatalog& catalog,
|
||||
std::optional<Language> languageFilter = std::nullopt);
|
||||
|
||||
[[nodiscard]] std::vector<YuGiOhBandaiChecklistEntry>
|
||||
yuGiOhBandaiChecklistForSet(const std::vector<YuGiOhBandaiCard>& collection,
|
||||
const YuGiOhBandaiSetCatalog& catalog,
|
||||
std::string_view setId,
|
||||
std::optional<Language> languageFilter = std::nullopt);
|
||||
|
||||
} // namespace ccm
|
||||
Reference in New Issue
Block a user