diff --git a/lib/game/models/season.dart b/lib/game/models/season.dart index c81b2ab..4692778 100644 --- a/lib/game/models/season.dart +++ b/lib/game/models/season.dart @@ -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 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 toJson() => - {'tileSet': tileSet, 'background': background}; + /// Top-to-bottom screen gradient, int ARGB. + final List backgroundGradient; + final int accentColor; + + /// petals | snow | leaves | none + final String particleType; + + /// Optional 8-color tile override; null = built-in candy palette. + final List? tilePalette; + + /// Optional board background override. + final int? boardTint; + + Map 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 diff --git a/test/game/models/season_test.dart b/test/game/models/season_test.dart index ba61c3f..554b291 100644 --- a/test/game/models/season_test.dart +++ b/test/game/models/season_test.dart @@ -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; + 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'); + }); + }); }