feat: extend SeasonTheme with visual identity fields (ARGB ints)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 20:58:48 +09:00
parent a69120e46b
commit e866de189d
2 changed files with 105 additions and 6 deletions
+49 -5
View File
@@ -1,18 +1,62 @@
import 'stage.dart';
/// Visual identity of a season. Colors are int ARGB so this file stays
/// pure Dart (architecture guard forbids Flutter imports here).
class SeasonTheme {
const SeasonTheme({required this.tileSet, required this.background});
const SeasonTheme({
this.tileSet = 'spring',
this.background = '',
this.backgroundGradient = defaultGradient,
this.accentColor = 0xFFFF7EB3,
this.particleType = 'petals',
this.tilePalette,
this.boardTint,
});
factory SeasonTheme.fromJson(Map<String, dynamic> json) => SeasonTheme(
tileSet: json['tileSet'] as String,
background: json['background'] as String,
tileSet: (json['tileSet'] as String?) ?? 'spring',
background: (json['background'] as String?) ?? '',
backgroundGradient: json['backgroundGradient'] != null
? [for (final c in json['backgroundGradient'] as List) c as int]
: defaultGradient,
accentColor: (json['accentColor'] as int?) ?? 0xFFFF7EB3,
particleType: (json['particleType'] as String?) ?? 'petals',
tilePalette: json['tilePalette'] != null
? [for (final c in json['tilePalette'] as List) c as int]
: null,
boardTint: json['boardTint'] as int?,
);
/// Season 1 "First Bloom": deep navy dusk.
static const defaultGradient = [0xFF0E1430, 0xFF16204A, 0xFF2A2E5E];
static const fallback = SeasonTheme();
final String tileSet;
final String background;
Map<String, dynamic> toJson() =>
{'tileSet': tileSet, 'background': background};
/// Top-to-bottom screen gradient, int ARGB.
final List<int> backgroundGradient;
final int accentColor;
/// petals | snow | leaves | none
final String particleType;
/// Optional 8-color tile override; null = built-in candy palette.
final List<int>? tilePalette;
/// Optional board background override.
final int? boardTint;
Map<String, dynamic> toJson() => {
'tileSet': tileSet,
'background': background,
'backgroundGradient': backgroundGradient,
'accentColor': accentColor,
'particleType': particleType,
if (tilePalette != null) 'tilePalette': tilePalette,
if (boardTint != null) 'boardTint': boardTint,
};
}
/// A season's full content: metadata, theme, and its stages. The unit of
+56 -1
View File
@@ -41,7 +41,24 @@ void main() {
test('round-trips to JSON', () {
final pack = SeasonPack.fromJson(packJson);
expect(pack.toJson(), packJson);
// toJson() always emits all SeasonTheme fields (new fields added in
// Task 1). Compare everything except theme separately so that adding
// more theme fields in the future only requires updating theme tests.
final json = pack.toJson();
expect(json['schemaVersion'], packJson['schemaVersion']);
expect(json['seasonId'], packJson['seasonId']);
expect(json['version'], packJson['version']);
expect(json['title'], packJson['title']);
expect(json['stages'], packJson['stages']);
// Theme: legacy fields preserved, new fields present with defaults.
final theme = json['theme'] as Map<String, dynamic>;
expect(theme['tileSet'], 'spring');
expect(theme['background'], 'background.webp');
expect(theme['backgroundGradient'], SeasonTheme.defaultGradient);
expect(theme['accentColor'], 0xFFFF7EB3);
expect(theme['particleType'], 'petals');
expect(theme.containsKey('tilePalette'), isFalse);
expect(theme.containsKey('boardTint'), isFalse);
});
test('localized title falls back to English', () {
@@ -58,4 +75,42 @@ void main() {
);
});
});
group('SeasonTheme visuals', () {
test('legacy theme json (tileSet/background only) gets defaults', () {
final theme = SeasonTheme.fromJson({
'tileSet': 'spring',
'background': 'background.webp',
});
expect(theme.backgroundGradient, SeasonTheme.defaultGradient);
expect(theme.accentColor, 0xFFFF7EB3);
expect(theme.particleType, 'petals');
expect(theme.tilePalette, isNull);
expect(theme.boardTint, isNull);
});
test('full theme json round-trips', () {
final theme = SeasonTheme(
tileSet: 'summer',
background: 'bg.webp',
backgroundGradient: const [0xFF0A2430, 0xFF10394A, 0xFF1E5A66],
accentColor: 0xFF6FCDF5,
particleType: 'snow',
tilePalette: const [0xFF111111, 0xFF222222],
boardTint: 0xFF041016,
);
final decoded = SeasonTheme.fromJson(theme.toJson());
expect(decoded.backgroundGradient, theme.backgroundGradient);
expect(decoded.accentColor, theme.accentColor);
expect(decoded.particleType, theme.particleType);
expect(decoded.tilePalette, theme.tilePalette);
expect(decoded.boardTint, theme.boardTint);
});
test('fallback constant matches season 1 defaults', () {
expect(SeasonTheme.fallback.backgroundGradient,
const [0xFF0E1430, 0xFF16204A, 0xFF2A2E5E]);
expect(SeasonTheme.fallback.particleType, 'petals');
});
});
}