From dbfbde2102f1c5339e93f9e40ab04f024b1966e2 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 4 Oct 2025 13:07:47 +0200 Subject: [PATCH 1/6] Start adding merge strategy, closes #57 --- api/lib/src/models/cell.dart | 41 +++ api/lib/src/models/cell.mapper.dart | 511 ++++++++++++++++++++++++++++ 2 files changed, 552 insertions(+) diff --git a/api/lib/src/models/cell.dart b/api/lib/src/models/cell.dart index 1d24b31b..b43c670f 100644 --- a/api/lib/src/models/cell.dart +++ b/api/lib/src/models/cell.dart @@ -30,3 +30,44 @@ class BoardTile with BoardTileMappable { BoardTile(this.asset, this.tile); } + +@MappableClass() +sealed class CellMergeStrategy with CellMergeStrategyMappable { + const CellMergeStrategy(); +} + +@MappableClass() +final class StackedCellMergeStrategy extends CellMergeStrategy + with StackedCellMergeStrategyMappable { + final int visiblePercentage; + final bool reverse; + + const StackedCellMergeStrategy({ + this.visiblePercentage = 10, + this.reverse = false, + }); +} + +@MappableClass() +final class DistributeCellMergeStrategy extends CellMergeStrategy + with DistributeCellMergeStrategyMappable { + final int maxCards; + final bool reverse; + final bool fillVariableSpace; + + const DistributeCellMergeStrategy({ + this.maxCards = 5, + this.reverse = false, + this.fillVariableSpace = true, + }); +} + +@MappableClass() +enum CellMergeDirection { horizontal, vertical } + +@MappableClass() +final class DirectionalCellMerge extends CellMergeStrategy + with DirectionalCellMergeMappable { + final CellMergeDirection direction; + const DirectionalCellMerge(this.direction); +} diff --git a/api/lib/src/models/cell.mapper.dart b/api/lib/src/models/cell.mapper.dart index e1263898..e570d6e7 100644 --- a/api/lib/src/models/cell.mapper.dart +++ b/api/lib/src/models/cell.mapper.dart @@ -438,3 +438,514 @@ class _BoardTileCopyWithImpl<$R, $Out> ) => _BoardTileCopyWithImpl<$R2, $Out2>($value, $cast, t); } +class CellMergeStrategyMapper extends ClassMapperBase { + CellMergeStrategyMapper._(); + + static CellMergeStrategyMapper? _instance; + static CellMergeStrategyMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = CellMergeStrategyMapper._()); + StackedCellMergeStrategyMapper.ensureInitialized(); + DistributeCellMergeStrategyMapper.ensureInitialized(); + DirectionalCellMergeMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'CellMergeStrategy'; + + @override + final MappableFields fields = const {}; + + static CellMergeStrategy _instantiate(DecodingData data) { + throw MapperException.missingConstructor('CellMergeStrategy'); + } + + @override + final Function instantiate = _instantiate; + + static CellMergeStrategy fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static CellMergeStrategy fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin CellMergeStrategyMappable { + String toJson(); + Map toMap(); + CellMergeStrategyCopyWith< + CellMergeStrategy, + CellMergeStrategy, + CellMergeStrategy + > + get copyWith; +} + +abstract class CellMergeStrategyCopyWith< + $R, + $In extends CellMergeStrategy, + $Out +> + implements ClassCopyWith<$R, $In, $Out> { + $R call(); + CellMergeStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class StackedCellMergeStrategyMapper + extends ClassMapperBase { + StackedCellMergeStrategyMapper._(); + + static StackedCellMergeStrategyMapper? _instance; + static StackedCellMergeStrategyMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use( + _instance = StackedCellMergeStrategyMapper._(), + ); + CellMergeStrategyMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'StackedCellMergeStrategy'; + + static int _$visiblePercentage(StackedCellMergeStrategy v) => + v.visiblePercentage; + static const Field _f$visiblePercentage = + Field('visiblePercentage', _$visiblePercentage, opt: true, def: 10); + static bool _$reverse(StackedCellMergeStrategy v) => v.reverse; + static const Field _f$reverse = Field( + 'reverse', + _$reverse, + opt: true, + def: false, + ); + + @override + final MappableFields fields = const { + #visiblePercentage: _f$visiblePercentage, + #reverse: _f$reverse, + }; + + static StackedCellMergeStrategy _instantiate(DecodingData data) { + return StackedCellMergeStrategy( + visiblePercentage: data.dec(_f$visiblePercentage), + reverse: data.dec(_f$reverse), + ); + } + + @override + final Function instantiate = _instantiate; + + static StackedCellMergeStrategy fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static StackedCellMergeStrategy fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin StackedCellMergeStrategyMappable { + String toJson() { + return StackedCellMergeStrategyMapper.ensureInitialized() + .encodeJson(this as StackedCellMergeStrategy); + } + + Map toMap() { + return StackedCellMergeStrategyMapper.ensureInitialized() + .encodeMap(this as StackedCellMergeStrategy); + } + + StackedCellMergeStrategyCopyWith< + StackedCellMergeStrategy, + StackedCellMergeStrategy, + StackedCellMergeStrategy + > + get copyWith => + _StackedCellMergeStrategyCopyWithImpl< + StackedCellMergeStrategy, + StackedCellMergeStrategy + >(this as StackedCellMergeStrategy, $identity, $identity); + @override + String toString() { + return StackedCellMergeStrategyMapper.ensureInitialized().stringifyValue( + this as StackedCellMergeStrategy, + ); + } + + @override + bool operator ==(Object other) { + return StackedCellMergeStrategyMapper.ensureInitialized().equalsValue( + this as StackedCellMergeStrategy, + other, + ); + } + + @override + int get hashCode { + return StackedCellMergeStrategyMapper.ensureInitialized().hashValue( + this as StackedCellMergeStrategy, + ); + } +} + +extension StackedCellMergeStrategyValueCopy<$R, $Out> + on ObjectCopyWith<$R, StackedCellMergeStrategy, $Out> { + StackedCellMergeStrategyCopyWith<$R, StackedCellMergeStrategy, $Out> + get $asStackedCellMergeStrategy => $base.as( + (v, t, t2) => _StackedCellMergeStrategyCopyWithImpl<$R, $Out>(v, t, t2), + ); +} + +abstract class StackedCellMergeStrategyCopyWith< + $R, + $In extends StackedCellMergeStrategy, + $Out +> + implements CellMergeStrategyCopyWith<$R, $In, $Out> { + @override + $R call({int? visiblePercentage, bool? reverse}); + StackedCellMergeStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _StackedCellMergeStrategyCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, StackedCellMergeStrategy, $Out> + implements + StackedCellMergeStrategyCopyWith<$R, StackedCellMergeStrategy, $Out> { + _StackedCellMergeStrategyCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + StackedCellMergeStrategyMapper.ensureInitialized(); + @override + $R call({int? visiblePercentage, bool? reverse}) => $apply( + FieldCopyWithData({ + if (visiblePercentage != null) #visiblePercentage: visiblePercentage, + if (reverse != null) #reverse: reverse, + }), + ); + @override + StackedCellMergeStrategy $make(CopyWithData data) => StackedCellMergeStrategy( + visiblePercentage: data.get( + #visiblePercentage, + or: $value.visiblePercentage, + ), + reverse: data.get(#reverse, or: $value.reverse), + ); + + @override + StackedCellMergeStrategyCopyWith<$R2, StackedCellMergeStrategy, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _StackedCellMergeStrategyCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + +class DistributeCellMergeStrategyMapper + extends ClassMapperBase { + DistributeCellMergeStrategyMapper._(); + + static DistributeCellMergeStrategyMapper? _instance; + static DistributeCellMergeStrategyMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use( + _instance = DistributeCellMergeStrategyMapper._(), + ); + CellMergeStrategyMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'DistributeCellMergeStrategy'; + + static int _$maxCards(DistributeCellMergeStrategy v) => v.maxCards; + static const Field _f$maxCards = Field( + 'maxCards', + _$maxCards, + opt: true, + def: 5, + ); + static bool _$reverse(DistributeCellMergeStrategy v) => v.reverse; + static const Field _f$reverse = Field( + 'reverse', + _$reverse, + opt: true, + def: false, + ); + static bool _$fillVariableSpace(DistributeCellMergeStrategy v) => + v.fillVariableSpace; + static const Field _f$fillVariableSpace = + Field('fillVariableSpace', _$fillVariableSpace, opt: true, def: true); + + @override + final MappableFields fields = const { + #maxCards: _f$maxCards, + #reverse: _f$reverse, + #fillVariableSpace: _f$fillVariableSpace, + }; + + static DistributeCellMergeStrategy _instantiate(DecodingData data) { + return DistributeCellMergeStrategy( + maxCards: data.dec(_f$maxCards), + reverse: data.dec(_f$reverse), + fillVariableSpace: data.dec(_f$fillVariableSpace), + ); + } + + @override + final Function instantiate = _instantiate; + + static DistributeCellMergeStrategy fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static DistributeCellMergeStrategy fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin DistributeCellMergeStrategyMappable { + String toJson() { + return DistributeCellMergeStrategyMapper.ensureInitialized() + .encodeJson( + this as DistributeCellMergeStrategy, + ); + } + + Map toMap() { + return DistributeCellMergeStrategyMapper.ensureInitialized() + .encodeMap( + this as DistributeCellMergeStrategy, + ); + } + + DistributeCellMergeStrategyCopyWith< + DistributeCellMergeStrategy, + DistributeCellMergeStrategy, + DistributeCellMergeStrategy + > + get copyWith => + _DistributeCellMergeStrategyCopyWithImpl< + DistributeCellMergeStrategy, + DistributeCellMergeStrategy + >(this as DistributeCellMergeStrategy, $identity, $identity); + @override + String toString() { + return DistributeCellMergeStrategyMapper.ensureInitialized().stringifyValue( + this as DistributeCellMergeStrategy, + ); + } + + @override + bool operator ==(Object other) { + return DistributeCellMergeStrategyMapper.ensureInitialized().equalsValue( + this as DistributeCellMergeStrategy, + other, + ); + } + + @override + int get hashCode { + return DistributeCellMergeStrategyMapper.ensureInitialized().hashValue( + this as DistributeCellMergeStrategy, + ); + } +} + +extension DistributeCellMergeStrategyValueCopy<$R, $Out> + on ObjectCopyWith<$R, DistributeCellMergeStrategy, $Out> { + DistributeCellMergeStrategyCopyWith<$R, DistributeCellMergeStrategy, $Out> + get $asDistributeCellMergeStrategy => $base.as( + (v, t, t2) => _DistributeCellMergeStrategyCopyWithImpl<$R, $Out>(v, t, t2), + ); +} + +abstract class DistributeCellMergeStrategyCopyWith< + $R, + $In extends DistributeCellMergeStrategy, + $Out +> + implements CellMergeStrategyCopyWith<$R, $In, $Out> { + @override + $R call({int? maxCards, bool? reverse, bool? fillVariableSpace}); + DistributeCellMergeStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _DistributeCellMergeStrategyCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, DistributeCellMergeStrategy, $Out> + implements + DistributeCellMergeStrategyCopyWith< + $R, + DistributeCellMergeStrategy, + $Out + > { + _DistributeCellMergeStrategyCopyWithImpl( + super.value, + super.then, + super.then2, + ); + + @override + late final ClassMapperBase $mapper = + DistributeCellMergeStrategyMapper.ensureInitialized(); + @override + $R call({int? maxCards, bool? reverse, bool? fillVariableSpace}) => $apply( + FieldCopyWithData({ + if (maxCards != null) #maxCards: maxCards, + if (reverse != null) #reverse: reverse, + if (fillVariableSpace != null) #fillVariableSpace: fillVariableSpace, + }), + ); + @override + DistributeCellMergeStrategy $make(CopyWithData data) => + DistributeCellMergeStrategy( + maxCards: data.get(#maxCards, or: $value.maxCards), + reverse: data.get(#reverse, or: $value.reverse), + fillVariableSpace: data.get( + #fillVariableSpace, + or: $value.fillVariableSpace, + ), + ); + + @override + DistributeCellMergeStrategyCopyWith<$R2, DistributeCellMergeStrategy, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _DistributeCellMergeStrategyCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + +class DirectionalCellMergeMapper extends ClassMapperBase { + DirectionalCellMergeMapper._(); + + static DirectionalCellMergeMapper? _instance; + static DirectionalCellMergeMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = DirectionalCellMergeMapper._()); + CellMergeStrategyMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'DirectionalCellMerge'; + + static CellMergeDirection _$direction(DirectionalCellMerge v) => v.direction; + static const Field _f$direction = + Field('direction', _$direction); + + @override + final MappableFields fields = const { + #direction: _f$direction, + }; + + static DirectionalCellMerge _instantiate(DecodingData data) { + return DirectionalCellMerge(data.dec(_f$direction)); + } + + @override + final Function instantiate = _instantiate; + + static DirectionalCellMerge fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static DirectionalCellMerge fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin DirectionalCellMergeMappable { + String toJson() { + return DirectionalCellMergeMapper.ensureInitialized() + .encodeJson(this as DirectionalCellMerge); + } + + Map toMap() { + return DirectionalCellMergeMapper.ensureInitialized() + .encodeMap(this as DirectionalCellMerge); + } + + DirectionalCellMergeCopyWith< + DirectionalCellMerge, + DirectionalCellMerge, + DirectionalCellMerge + > + get copyWith => + _DirectionalCellMergeCopyWithImpl< + DirectionalCellMerge, + DirectionalCellMerge + >(this as DirectionalCellMerge, $identity, $identity); + @override + String toString() { + return DirectionalCellMergeMapper.ensureInitialized().stringifyValue( + this as DirectionalCellMerge, + ); + } + + @override + bool operator ==(Object other) { + return DirectionalCellMergeMapper.ensureInitialized().equalsValue( + this as DirectionalCellMerge, + other, + ); + } + + @override + int get hashCode { + return DirectionalCellMergeMapper.ensureInitialized().hashValue( + this as DirectionalCellMerge, + ); + } +} + +extension DirectionalCellMergeValueCopy<$R, $Out> + on ObjectCopyWith<$R, DirectionalCellMerge, $Out> { + DirectionalCellMergeCopyWith<$R, DirectionalCellMerge, $Out> + get $asDirectionalCellMerge => $base.as( + (v, t, t2) => _DirectionalCellMergeCopyWithImpl<$R, $Out>(v, t, t2), + ); +} + +abstract class DirectionalCellMergeCopyWith< + $R, + $In extends DirectionalCellMerge, + $Out +> + implements CellMergeStrategyCopyWith<$R, $In, $Out> { + @override + $R call({CellMergeDirection? direction}); + DirectionalCellMergeCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _DirectionalCellMergeCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, DirectionalCellMerge, $Out> + implements DirectionalCellMergeCopyWith<$R, DirectionalCellMerge, $Out> { + _DirectionalCellMergeCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + DirectionalCellMergeMapper.ensureInitialized(); + @override + $R call({CellMergeDirection? direction}) => + $apply(FieldCopyWithData({if (direction != null) #direction: direction})); + @override + DirectionalCellMerge $make(CopyWithData data) => + DirectionalCellMerge(data.get(#direction, or: $value.direction)); + + @override + DirectionalCellMergeCopyWith<$R2, DirectionalCellMerge, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _DirectionalCellMergeCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + From 840c2e4e1367340dcb687654df03af61ed27504f Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 16 Feb 2026 22:29:03 +0100 Subject: [PATCH 2/6] Implement cell merging system --- api/lib/src/event/event.mapper.dart | 192 ++++++++ api/lib/src/event/hybrid.dart | 10 + api/lib/src/event/process/server.dart | 69 +++ api/lib/src/event/state.dart | 35 ++ api/lib/src/event/state.mapper.dart | 1 + api/lib/src/helpers/equality.mapper.dart | 1 + api/lib/src/models/background.mapper.dart | 1 + api/lib/src/models/cell.dart | 45 +- api/lib/src/models/cell.mapper.dart | 519 +++++++++++++++------ api/lib/src/models/chat.mapper.dart | 1 + api/lib/src/models/config.mapper.dart | 1 + api/lib/src/models/deck.mapper.dart | 1 + api/lib/src/models/definition.mapper.dart | 1 + api/lib/src/models/dialog.mapper.dart | 1 + api/lib/src/models/info.mapper.dart | 1 + api/lib/src/models/kick.mapper.dart | 1 + api/lib/src/models/meta.mapper.dart | 1 + api/lib/src/models/mode.mapper.dart | 1 + api/lib/src/models/server.mapper.dart | 1 + api/lib/src/models/table.mapper.dart | 1 + api/lib/src/models/toolbar.mapper.dart | 1 + api/lib/src/models/translation.mapper.dart | 1 + api/lib/src/models/vector.mapper.dart | 1 + api/lib/src/models/visual.mapper.dart | 1 + api/lib/src/models/waypoint.mapper.dart | 1 + api/lib/src/services/user.mapper.dart | 1 + app/lib/bloc/multiplayer.mapper.dart | 1 + app/lib/bloc/settings.mapper.dart | 1 + app/lib/bloc/world/local.mapper.dart | 1 + app/lib/bloc/world/state.dart | 6 + app/lib/bloc/world/state.mapper.dart | 1 + app/lib/board/background.dart | 37 +- app/lib/board/cell.dart | 275 +++++++++-- app/lib/l10n/app_en.arb | 15 +- app/lib/pages/game/merge.dart | 182 ++++++++ plugin/pubspec.lock | 4 +- server/pubspec.lock | 4 +- 37 files changed, 1199 insertions(+), 218 deletions(-) create mode 100644 app/lib/pages/game/merge.dart diff --git a/api/lib/src/event/event.mapper.dart b/api/lib/src/event/event.mapper.dart index e83a8c72..0cc9af7b 100644 --- a/api/lib/src/event/event.mapper.dart +++ b/api/lib/src/event/event.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter @@ -4476,6 +4477,7 @@ class HybridWorldEventMapper extends SubClassMapperBase { ObjectIndexChangedMapper.ensureInitialized(); TeamChangedMapper.ensureInitialized(); TeamRemovedMapper.ensureInitialized(); + CellMergeStrategyChangedMapper.ensureInitialized(); MetadataChangedMapper.ensureInitialized(); ObjectsRemovedMapper.ensureInitialized(); TableRenamedMapper.ensureInitialized(); @@ -5791,6 +5793,196 @@ class _TeamRemovedCopyWithImpl<$R, $Out> ) => _TeamRemovedCopyWithImpl<$R2, $Out2>($value, $cast, t); } +class CellMergeStrategyChangedMapper + extends SubClassMapperBase { + CellMergeStrategyChangedMapper._(); + + static CellMergeStrategyChangedMapper? _instance; + static CellMergeStrategyChangedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use( + _instance = CellMergeStrategyChangedMapper._(), + ); + HybridWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + GlobalVectorDefinitionMapper.ensureInitialized(); + CellMergeStrategyMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'CellMergeStrategyChanged'; + + static GlobalVectorDefinition _$cell(CellMergeStrategyChanged v) => v.cell; + static const Field _f$cell = + Field('cell', _$cell); + static CellMergeStrategy? _$strategy(CellMergeStrategyChanged v) => + v.strategy; + static const Field _f$strategy = + Field('strategy', _$strategy); + static int? _$span(CellMergeStrategyChanged v) => v.span; + static const Field _f$span = Field( + 'span', + _$span, + opt: true, + ); + + @override + final MappableFields fields = const { + #cell: _f$cell, + #strategy: _f$strategy, + #span: _f$span, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'CellMergeStrategyChanged'; + @override + late final ClassMapperBase superMapper = + HybridWorldEventMapper.ensureInitialized(); + + static CellMergeStrategyChanged _instantiate(DecodingData data) { + return CellMergeStrategyChanged( + data.dec(_f$cell), + data.dec(_f$strategy), + span: data.dec(_f$span), + ); + } + + @override + final Function instantiate = _instantiate; + + static CellMergeStrategyChanged fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static CellMergeStrategyChanged fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin CellMergeStrategyChangedMappable { + String toJson() { + return CellMergeStrategyChangedMapper.ensureInitialized() + .encodeJson(this as CellMergeStrategyChanged); + } + + Map toMap() { + return CellMergeStrategyChangedMapper.ensureInitialized() + .encodeMap(this as CellMergeStrategyChanged); + } + + CellMergeStrategyChangedCopyWith< + CellMergeStrategyChanged, + CellMergeStrategyChanged, + CellMergeStrategyChanged + > + get copyWith => + _CellMergeStrategyChangedCopyWithImpl< + CellMergeStrategyChanged, + CellMergeStrategyChanged + >(this as CellMergeStrategyChanged, $identity, $identity); + @override + String toString() { + return CellMergeStrategyChangedMapper.ensureInitialized().stringifyValue( + this as CellMergeStrategyChanged, + ); + } + + @override + bool operator ==(Object other) { + return CellMergeStrategyChangedMapper.ensureInitialized().equalsValue( + this as CellMergeStrategyChanged, + other, + ); + } + + @override + int get hashCode { + return CellMergeStrategyChangedMapper.ensureInitialized().hashValue( + this as CellMergeStrategyChanged, + ); + } +} + +extension CellMergeStrategyChangedValueCopy<$R, $Out> + on ObjectCopyWith<$R, CellMergeStrategyChanged, $Out> { + CellMergeStrategyChangedCopyWith<$R, CellMergeStrategyChanged, $Out> + get $asCellMergeStrategyChanged => $base.as( + (v, t, t2) => _CellMergeStrategyChangedCopyWithImpl<$R, $Out>(v, t, t2), + ); +} + +abstract class CellMergeStrategyChangedCopyWith< + $R, + $In extends CellMergeStrategyChanged, + $Out +> + implements HybridWorldEventCopyWith<$R, $In, $Out> { + GlobalVectorDefinitionCopyWith< + $R, + GlobalVectorDefinition, + GlobalVectorDefinition + > + get cell; + CellMergeStrategyCopyWith<$R, CellMergeStrategy, CellMergeStrategy>? + get strategy; + @override + $R call({ + GlobalVectorDefinition? cell, + CellMergeStrategy? strategy, + int? span, + }); + CellMergeStrategyChangedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _CellMergeStrategyChangedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, CellMergeStrategyChanged, $Out> + implements + CellMergeStrategyChangedCopyWith<$R, CellMergeStrategyChanged, $Out> { + _CellMergeStrategyChangedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + CellMergeStrategyChangedMapper.ensureInitialized(); + @override + GlobalVectorDefinitionCopyWith< + $R, + GlobalVectorDefinition, + GlobalVectorDefinition + > + get cell => $value.cell.copyWith.$chain((v) => call(cell: v)); + @override + CellMergeStrategyCopyWith<$R, CellMergeStrategy, CellMergeStrategy>? + get strategy => $value.strategy?.copyWith.$chain((v) => call(strategy: v)); + @override + $R call({ + GlobalVectorDefinition? cell, + Object? strategy = $none, + Object? span = $none, + }) => $apply( + FieldCopyWithData({ + if (cell != null) #cell: cell, + if (strategy != $none) #strategy: strategy, + if (span != $none) #span: span, + }), + ); + @override + CellMergeStrategyChanged $make(CopyWithData data) => CellMergeStrategyChanged( + data.get(#cell, or: $value.cell), + data.get(#strategy, or: $value.strategy), + span: data.get(#span, or: $value.span), + ); + + @override + CellMergeStrategyChangedCopyWith<$R2, CellMergeStrategyChanged, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _CellMergeStrategyChangedCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + class MetadataChangedMapper extends SubClassMapperBase { MetadataChangedMapper._(); diff --git a/api/lib/src/event/hybrid.dart b/api/lib/src/event/hybrid.dart index 591ede6d..7f3cb7bf 100644 --- a/api/lib/src/event/hybrid.dart +++ b/api/lib/src/event/hybrid.dart @@ -170,6 +170,16 @@ final class TeamRemoved extends HybridWorldEvent with TeamRemovedMappable { TeamRemoved(this.team); } +@MappableClass() +final class CellMergeStrategyChanged extends HybridWorldEvent + with CellMergeStrategyChangedMappable { + final GlobalVectorDefinition cell; + final CellMergeStrategy? strategy; + final int? span; + + CellMergeStrategyChanged(this.cell, this.strategy, {this.span}); +} + @MappableClass() final class MetadataChanged extends HybridWorldEvent with MetadataChangedMappable { diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index 0107a718..595a74f5 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -313,6 +313,75 @@ ServerProcessed processServerEvent( teamMembers: Map.from(state.teamMembers)..remove(event.team), ), ); + case CellMergeStrategyChanged(): + return ServerProcessed( + state.mapTableOrDefault(event.cell.table, (table) { + final cells = Map.from(table.cells); + + CellMergeDirection? getDirection(CellMergeStrategy? strategy) { + if (strategy is StackedCellMergeStrategy) return strategy.direction; + if (strategy is DistributeCellMergeStrategy) { + return strategy.direction; + } + return null; + } + + // Cleanup old neighbors + final oldCell = cells[event.cell.position]; + final oldStrategy = oldCell?.merge; + final oldDirection = getDirection(oldStrategy); + if (oldDirection != null) { + final oldSpan = state.calculateSpan( + event.cell.position, + oldDirection, + ); + if (oldSpan > 1) { + var current = event.cell.position; + for (var i = 1; i < oldSpan; i++) { + current = + oldDirection == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); + + final neighbor = cells[current] ?? TableCell(); + if (neighbor.merge is MergedCellStrategy && + (neighbor.merge as MergedCellStrategy).direction == + oldDirection) { + cells[current] = neighbor.copyWith(merge: null); + } + } + } + } + + // Update target cell + final cell = cells[event.cell.position] ?? TableCell(); + final newCell = cell.copyWith(merge: event.strategy); + cells[event.cell.position] = newCell; + + // Expand new neighbors + final strategy = event.strategy; + final span = event.span; + if (span != null && span > 1 && strategy != null) { + final direction = getDirection(strategy); + if (direction != null) { + var current = event.cell.position; + for (var i = 1; i < span; i++) { + current = + direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); + + final neighbor = cells[current] ?? TableCell(); + cells[current] = neighbor.copyWith( + merge: MergedCellStrategy(direction), + ); + } + } + } + + return table.copyWith.cellsBox(content: cells); + }), + ); case MetadataChanged(): return ServerProcessed(state.copyWith(metadata: event.metadata)); case MessageSent(): diff --git a/api/lib/src/event/state.dart b/api/lib/src/event/state.dart index 44405bfa..77de625e 100644 --- a/api/lib/src/event/state.dart +++ b/api/lib/src/event/state.dart @@ -124,4 +124,39 @@ final class WorldState with WorldStateMappable { String name, GameTable Function(GameTable) mapper, ) => updateTable(name, mapper(getTableOrDefault(name))); + + int calculateSpan(VectorDefinition start, CellMergeDirection direction) { + var span = 1; + var current = start; + while (true) { + current = + direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); + final cell = table.cells[current]; + final strategy = cell?.merge; + if (strategy is MergedCellStrategy && strategy.direction == direction) { + span++; + } else { + break; + } + } + return span; + } + + VectorDefinition getParentCell(VectorDefinition position) { + var current = position; + while (true) { + final cell = table.cells[current]; + final strategy = cell?.merge; + if (strategy is MergedCellStrategy) { + current = + strategy.direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x - 1, current.y) + : VectorDefinition(current.x, current.y - 1); + } else { + return current; + } + } + } } diff --git a/api/lib/src/event/state.mapper.dart b/api/lib/src/event/state.mapper.dart index eae1b477..319318bb 100644 --- a/api/lib/src/event/state.mapper.dart +++ b/api/lib/src/event/state.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/helpers/equality.mapper.dart b/api/lib/src/helpers/equality.mapper.dart index ecb32430..3da11831 100644 --- a/api/lib/src/helpers/equality.mapper.dart +++ b/api/lib/src/helpers/equality.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/background.mapper.dart b/api/lib/src/models/background.mapper.dart index 6255054f..c8dedc0b 100644 --- a/api/lib/src/models/background.mapper.dart +++ b/api/lib/src/models/background.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/cell.dart b/api/lib/src/models/cell.dart index b43c670f..bca49b9b 100644 --- a/api/lib/src/models/cell.dart +++ b/api/lib/src/models/cell.dart @@ -8,8 +8,9 @@ part 'cell.mapper.dart'; class TableCell with TableCellMappable { final List objects; final List tiles; + final CellMergeStrategy? merge; - TableCell({this.objects = const [], this.tiles = const []}); + TableCell({this.objects = const [], this.tiles = const [], this.merge}); bool get isEmpty => objects.isEmpty && tiles.isEmpty; } @@ -31,43 +32,51 @@ class BoardTile with BoardTileMappable { BoardTile(this.asset, this.tile); } +@MappableEnum() +enum CellMergeDirection { horizontal, vertical } + @MappableClass() sealed class CellMergeStrategy with CellMergeStrategyMappable { - const CellMergeStrategy(); + final CellMergeDirection direction; + const CellMergeStrategy({this.direction = CellMergeDirection.vertical}); +} + +@MappableClass() +final class MergedCellStrategy extends CellMergeStrategy + with MergedCellStrategyMappable { + const MergedCellStrategy(CellMergeDirection direction) + : super(direction: direction); +} + +@MappableClass() +sealed class LayoutCellMergeStrategy extends CellMergeStrategy + with LayoutCellMergeStrategyMappable { + final bool reverse; + const LayoutCellMergeStrategy({super.direction, this.reverse = false}); } @MappableClass() -final class StackedCellMergeStrategy extends CellMergeStrategy +final class StackedCellMergeStrategy extends LayoutCellMergeStrategy with StackedCellMergeStrategyMappable { final int visiblePercentage; - final bool reverse; const StackedCellMergeStrategy({ this.visiblePercentage = 10, - this.reverse = false, + super.reverse, + super.direction, }); } @MappableClass() -final class DistributeCellMergeStrategy extends CellMergeStrategy +final class DistributeCellMergeStrategy extends LayoutCellMergeStrategy with DistributeCellMergeStrategyMappable { final int maxCards; - final bool reverse; final bool fillVariableSpace; const DistributeCellMergeStrategy({ this.maxCards = 5, - this.reverse = false, this.fillVariableSpace = true, + super.reverse, + super.direction = CellMergeDirection.horizontal, }); } - -@MappableClass() -enum CellMergeDirection { horizontal, vertical } - -@MappableClass() -final class DirectionalCellMerge extends CellMergeStrategy - with DirectionalCellMergeMappable { - final CellMergeDirection direction; - const DirectionalCellMerge(this.direction); -} diff --git a/api/lib/src/models/cell.mapper.dart b/api/lib/src/models/cell.mapper.dart index e570d6e7..a42ec36b 100644 --- a/api/lib/src/models/cell.mapper.dart +++ b/api/lib/src/models/cell.mapper.dart @@ -2,11 +2,58 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter part of 'cell.dart'; +class CellMergeDirectionMapper extends EnumMapper { + CellMergeDirectionMapper._(); + + static CellMergeDirectionMapper? _instance; + static CellMergeDirectionMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = CellMergeDirectionMapper._()); + } + return _instance!; + } + + static CellMergeDirection fromValue(dynamic value) { + ensureInitialized(); + return MapperContainer.globals.fromValue(value); + } + + @override + CellMergeDirection decode(dynamic value) { + switch (value) { + case r'horizontal': + return CellMergeDirection.horizontal; + case r'vertical': + return CellMergeDirection.vertical; + default: + throw MapperException.unknownEnumValue(value); + } + } + + @override + dynamic encode(CellMergeDirection self) { + switch (self) { + case CellMergeDirection.horizontal: + return r'horizontal'; + case CellMergeDirection.vertical: + return r'vertical'; + } + } +} + +extension CellMergeDirectionMapperExtension on CellMergeDirection { + String toValue() { + CellMergeDirectionMapper.ensureInitialized(); + return MapperContainer.globals.toValue(this) as String; + } +} + class TableCellMapper extends ClassMapperBase { TableCellMapper._(); @@ -16,6 +63,7 @@ class TableCellMapper extends ClassMapperBase { MapperContainer.globals.use(_instance = TableCellMapper._()); GameObjectMapper.ensureInitialized(); BoardTileMapper.ensureInitialized(); + CellMergeStrategyMapper.ensureInitialized(); } return _instance!; } @@ -37,15 +85,26 @@ class TableCellMapper extends ClassMapperBase { opt: true, def: const [], ); + static CellMergeStrategy? _$merge(TableCell v) => v.merge; + static const Field _f$merge = Field( + 'merge', + _$merge, + opt: true, + ); @override final MappableFields fields = const { #objects: _f$objects, #tiles: _f$tiles, + #merge: _f$merge, }; static TableCell _instantiate(DecodingData data) { - return TableCell(objects: data.dec(_f$objects), tiles: data.dec(_f$tiles)); + return TableCell( + objects: data.dec(_f$objects), + tiles: data.dec(_f$tiles), + merge: data.dec(_f$merge), + ); } @override @@ -111,7 +170,13 @@ abstract class TableCellCopyWith<$R, $In extends TableCell, $Out> get objects; ListCopyWith<$R, BoardTile, BoardTileCopyWith<$R, BoardTile, BoardTile>> get tiles; - $R call({List? objects, List? tiles}); + CellMergeStrategyCopyWith<$R, CellMergeStrategy, CellMergeStrategy>? + get merge; + $R call({ + List? objects, + List? tiles, + CellMergeStrategy? merge, + }); TableCellCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -138,16 +203,25 @@ class _TableCellCopyWithImpl<$R, $Out> (v) => call(tiles: v), ); @override - $R call({List? objects, List? tiles}) => $apply( + CellMergeStrategyCopyWith<$R, CellMergeStrategy, CellMergeStrategy>? + get merge => $value.merge?.copyWith.$chain((v) => call(merge: v)); + @override + $R call({ + List? objects, + List? tiles, + Object? merge = $none, + }) => $apply( FieldCopyWithData({ if (objects != null) #objects: objects, if (tiles != null) #tiles: tiles, + if (merge != $none) #merge: merge, }), ); @override TableCell $make(CopyWithData data) => TableCell( objects: data.get(#objects, or: $value.objects), tiles: data.get(#tiles, or: $value.tiles), + merge: data.get(#merge, or: $value.merge), ); @override @@ -445,9 +519,9 @@ class CellMergeStrategyMapper extends ClassMapperBase { static CellMergeStrategyMapper ensureInitialized() { if (_instance == null) { MapperContainer.globals.use(_instance = CellMergeStrategyMapper._()); - StackedCellMergeStrategyMapper.ensureInitialized(); - DistributeCellMergeStrategyMapper.ensureInitialized(); - DirectionalCellMergeMapper.ensureInitialized(); + MergedCellStrategyMapper.ensureInitialized(); + LayoutCellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.ensureInitialized(); } return _instance!; } @@ -455,8 +529,19 @@ class CellMergeStrategyMapper extends ClassMapperBase { @override final String id = 'CellMergeStrategy'; + static CellMergeDirection _$direction(CellMergeStrategy v) => v.direction; + static const Field _f$direction = + Field( + 'direction', + _$direction, + opt: true, + def: CellMergeDirection.vertical, + ); + @override - final MappableFields fields = const {}; + final MappableFields fields = const { + #direction: _f$direction, + }; static CellMergeStrategy _instantiate(DecodingData data) { throw MapperException.missingConstructor('CellMergeStrategy'); @@ -491,12 +576,224 @@ abstract class CellMergeStrategyCopyWith< $Out > implements ClassCopyWith<$R, $In, $Out> { - $R call(); + $R call({CellMergeDirection? direction}); CellMergeStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, ); } +class MergedCellStrategyMapper extends ClassMapperBase { + MergedCellStrategyMapper._(); + + static MergedCellStrategyMapper? _instance; + static MergedCellStrategyMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = MergedCellStrategyMapper._()); + CellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'MergedCellStrategy'; + + static CellMergeDirection _$direction(MergedCellStrategy v) => v.direction; + static const Field _f$direction = + Field('direction', _$direction); + + @override + final MappableFields fields = const { + #direction: _f$direction, + }; + + static MergedCellStrategy _instantiate(DecodingData data) { + return MergedCellStrategy(data.dec(_f$direction)); + } + + @override + final Function instantiate = _instantiate; + + static MergedCellStrategy fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static MergedCellStrategy fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin MergedCellStrategyMappable { + String toJson() { + return MergedCellStrategyMapper.ensureInitialized() + .encodeJson(this as MergedCellStrategy); + } + + Map toMap() { + return MergedCellStrategyMapper.ensureInitialized() + .encodeMap(this as MergedCellStrategy); + } + + MergedCellStrategyCopyWith< + MergedCellStrategy, + MergedCellStrategy, + MergedCellStrategy + > + get copyWith => + _MergedCellStrategyCopyWithImpl( + this as MergedCellStrategy, + $identity, + $identity, + ); + @override + String toString() { + return MergedCellStrategyMapper.ensureInitialized().stringifyValue( + this as MergedCellStrategy, + ); + } + + @override + bool operator ==(Object other) { + return MergedCellStrategyMapper.ensureInitialized().equalsValue( + this as MergedCellStrategy, + other, + ); + } + + @override + int get hashCode { + return MergedCellStrategyMapper.ensureInitialized().hashValue( + this as MergedCellStrategy, + ); + } +} + +extension MergedCellStrategyValueCopy<$R, $Out> + on ObjectCopyWith<$R, MergedCellStrategy, $Out> { + MergedCellStrategyCopyWith<$R, MergedCellStrategy, $Out> + get $asMergedCellStrategy => $base.as( + (v, t, t2) => _MergedCellStrategyCopyWithImpl<$R, $Out>(v, t, t2), + ); +} + +abstract class MergedCellStrategyCopyWith< + $R, + $In extends MergedCellStrategy, + $Out +> + implements CellMergeStrategyCopyWith<$R, $In, $Out> { + @override + $R call({CellMergeDirection? direction}); + MergedCellStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _MergedCellStrategyCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, MergedCellStrategy, $Out> + implements MergedCellStrategyCopyWith<$R, MergedCellStrategy, $Out> { + _MergedCellStrategyCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + MergedCellStrategyMapper.ensureInitialized(); + @override + $R call({CellMergeDirection? direction}) => + $apply(FieldCopyWithData({if (direction != null) #direction: direction})); + @override + MergedCellStrategy $make(CopyWithData data) => + MergedCellStrategy(data.get(#direction, or: $value.direction)); + + @override + MergedCellStrategyCopyWith<$R2, MergedCellStrategy, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ) => _MergedCellStrategyCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + +class LayoutCellMergeStrategyMapper + extends ClassMapperBase { + LayoutCellMergeStrategyMapper._(); + + static LayoutCellMergeStrategyMapper? _instance; + static LayoutCellMergeStrategyMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use( + _instance = LayoutCellMergeStrategyMapper._(), + ); + CellMergeStrategyMapper.ensureInitialized(); + StackedCellMergeStrategyMapper.ensureInitialized(); + DistributeCellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'LayoutCellMergeStrategy'; + + static CellMergeDirection _$direction(LayoutCellMergeStrategy v) => + v.direction; + static const Field _f$direction = + Field( + 'direction', + _$direction, + opt: true, + def: CellMergeDirection.vertical, + ); + static bool _$reverse(LayoutCellMergeStrategy v) => v.reverse; + static const Field _f$reverse = Field( + 'reverse', + _$reverse, + opt: true, + def: false, + ); + + @override + final MappableFields fields = const { + #direction: _f$direction, + #reverse: _f$reverse, + }; + + static LayoutCellMergeStrategy _instantiate(DecodingData data) { + throw MapperException.missingConstructor('LayoutCellMergeStrategy'); + } + + @override + final Function instantiate = _instantiate; + + static LayoutCellMergeStrategy fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static LayoutCellMergeStrategy fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin LayoutCellMergeStrategyMappable { + String toJson(); + Map toMap(); + LayoutCellMergeStrategyCopyWith< + LayoutCellMergeStrategy, + LayoutCellMergeStrategy, + LayoutCellMergeStrategy + > + get copyWith; +} + +abstract class LayoutCellMergeStrategyCopyWith< + $R, + $In extends LayoutCellMergeStrategy, + $Out +> + implements CellMergeStrategyCopyWith<$R, $In, $Out> { + @override + $R call({CellMergeDirection? direction, bool? reverse}); + LayoutCellMergeStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + class StackedCellMergeStrategyMapper extends ClassMapperBase { StackedCellMergeStrategyMapper._(); @@ -507,7 +804,8 @@ class StackedCellMergeStrategyMapper MapperContainer.globals.use( _instance = StackedCellMergeStrategyMapper._(), ); - CellMergeStrategyMapper.ensureInitialized(); + LayoutCellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.ensureInitialized(); } return _instance!; } @@ -526,17 +824,28 @@ class StackedCellMergeStrategyMapper opt: true, def: false, ); + static CellMergeDirection _$direction(StackedCellMergeStrategy v) => + v.direction; + static const Field + _f$direction = Field( + 'direction', + _$direction, + opt: true, + def: CellMergeDirection.vertical, + ); @override final MappableFields fields = const { #visiblePercentage: _f$visiblePercentage, #reverse: _f$reverse, + #direction: _f$direction, }; static StackedCellMergeStrategy _instantiate(DecodingData data) { return StackedCellMergeStrategy( visiblePercentage: data.dec(_f$visiblePercentage), reverse: data.dec(_f$reverse), + direction: data.dec(_f$direction), ); } @@ -609,9 +918,13 @@ abstract class StackedCellMergeStrategyCopyWith< $In extends StackedCellMergeStrategy, $Out > - implements CellMergeStrategyCopyWith<$R, $In, $Out> { + implements LayoutCellMergeStrategyCopyWith<$R, $In, $Out> { @override - $R call({int? visiblePercentage, bool? reverse}); + $R call({ + int? visiblePercentage, + bool? reverse, + CellMergeDirection? direction, + }); StackedCellMergeStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, ); @@ -627,10 +940,15 @@ class _StackedCellMergeStrategyCopyWithImpl<$R, $Out> late final ClassMapperBase $mapper = StackedCellMergeStrategyMapper.ensureInitialized(); @override - $R call({int? visiblePercentage, bool? reverse}) => $apply( + $R call({ + int? visiblePercentage, + bool? reverse, + CellMergeDirection? direction, + }) => $apply( FieldCopyWithData({ if (visiblePercentage != null) #visiblePercentage: visiblePercentage, if (reverse != null) #reverse: reverse, + if (direction != null) #direction: direction, }), ); @override @@ -640,6 +958,7 @@ class _StackedCellMergeStrategyCopyWithImpl<$R, $Out> or: $value.visiblePercentage, ), reverse: data.get(#reverse, or: $value.reverse), + direction: data.get(#direction, or: $value.direction), ); @override @@ -658,7 +977,8 @@ class DistributeCellMergeStrategyMapper MapperContainer.globals.use( _instance = DistributeCellMergeStrategyMapper._(), ); - CellMergeStrategyMapper.ensureInitialized(); + LayoutCellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.ensureInitialized(); } return _instance!; } @@ -673,6 +993,10 @@ class DistributeCellMergeStrategyMapper opt: true, def: 5, ); + static bool _$fillVariableSpace(DistributeCellMergeStrategy v) => + v.fillVariableSpace; + static const Field _f$fillVariableSpace = + Field('fillVariableSpace', _$fillVariableSpace, opt: true, def: true); static bool _$reverse(DistributeCellMergeStrategy v) => v.reverse; static const Field _f$reverse = Field( 'reverse', @@ -680,23 +1004,30 @@ class DistributeCellMergeStrategyMapper opt: true, def: false, ); - static bool _$fillVariableSpace(DistributeCellMergeStrategy v) => - v.fillVariableSpace; - static const Field _f$fillVariableSpace = - Field('fillVariableSpace', _$fillVariableSpace, opt: true, def: true); + static CellMergeDirection _$direction(DistributeCellMergeStrategy v) => + v.direction; + static const Field + _f$direction = Field( + 'direction', + _$direction, + opt: true, + def: CellMergeDirection.horizontal, + ); @override final MappableFields fields = const { #maxCards: _f$maxCards, - #reverse: _f$reverse, #fillVariableSpace: _f$fillVariableSpace, + #reverse: _f$reverse, + #direction: _f$direction, }; static DistributeCellMergeStrategy _instantiate(DecodingData data) { return DistributeCellMergeStrategy( maxCards: data.dec(_f$maxCards), - reverse: data.dec(_f$reverse), fillVariableSpace: data.dec(_f$fillVariableSpace), + reverse: data.dec(_f$reverse), + direction: data.dec(_f$direction), ); } @@ -773,9 +1104,14 @@ abstract class DistributeCellMergeStrategyCopyWith< $In extends DistributeCellMergeStrategy, $Out > - implements CellMergeStrategyCopyWith<$R, $In, $Out> { - @override - $R call({int? maxCards, bool? reverse, bool? fillVariableSpace}); + implements LayoutCellMergeStrategyCopyWith<$R, $In, $Out> { + @override + $R call({ + int? maxCards, + bool? fillVariableSpace, + bool? reverse, + CellMergeDirection? direction, + }); DistributeCellMergeStrategyCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, ); @@ -799,22 +1135,29 @@ class _DistributeCellMergeStrategyCopyWithImpl<$R, $Out> late final ClassMapperBase $mapper = DistributeCellMergeStrategyMapper.ensureInitialized(); @override - $R call({int? maxCards, bool? reverse, bool? fillVariableSpace}) => $apply( + $R call({ + int? maxCards, + bool? fillVariableSpace, + bool? reverse, + CellMergeDirection? direction, + }) => $apply( FieldCopyWithData({ if (maxCards != null) #maxCards: maxCards, - if (reverse != null) #reverse: reverse, if (fillVariableSpace != null) #fillVariableSpace: fillVariableSpace, + if (reverse != null) #reverse: reverse, + if (direction != null) #direction: direction, }), ); @override DistributeCellMergeStrategy $make(CopyWithData data) => DistributeCellMergeStrategy( maxCards: data.get(#maxCards, or: $value.maxCards), - reverse: data.get(#reverse, or: $value.reverse), fillVariableSpace: data.get( #fillVariableSpace, or: $value.fillVariableSpace, ), + reverse: data.get(#reverse, or: $value.reverse), + direction: data.get(#direction, or: $value.direction), ); @override @@ -823,129 +1166,3 @@ class _DistributeCellMergeStrategyCopyWithImpl<$R, $Out> _DistributeCellMergeStrategyCopyWithImpl<$R2, $Out2>($value, $cast, t); } -class DirectionalCellMergeMapper extends ClassMapperBase { - DirectionalCellMergeMapper._(); - - static DirectionalCellMergeMapper? _instance; - static DirectionalCellMergeMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = DirectionalCellMergeMapper._()); - CellMergeStrategyMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'DirectionalCellMerge'; - - static CellMergeDirection _$direction(DirectionalCellMerge v) => v.direction; - static const Field _f$direction = - Field('direction', _$direction); - - @override - final MappableFields fields = const { - #direction: _f$direction, - }; - - static DirectionalCellMerge _instantiate(DecodingData data) { - return DirectionalCellMerge(data.dec(_f$direction)); - } - - @override - final Function instantiate = _instantiate; - - static DirectionalCellMerge fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static DirectionalCellMerge fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin DirectionalCellMergeMappable { - String toJson() { - return DirectionalCellMergeMapper.ensureInitialized() - .encodeJson(this as DirectionalCellMerge); - } - - Map toMap() { - return DirectionalCellMergeMapper.ensureInitialized() - .encodeMap(this as DirectionalCellMerge); - } - - DirectionalCellMergeCopyWith< - DirectionalCellMerge, - DirectionalCellMerge, - DirectionalCellMerge - > - get copyWith => - _DirectionalCellMergeCopyWithImpl< - DirectionalCellMerge, - DirectionalCellMerge - >(this as DirectionalCellMerge, $identity, $identity); - @override - String toString() { - return DirectionalCellMergeMapper.ensureInitialized().stringifyValue( - this as DirectionalCellMerge, - ); - } - - @override - bool operator ==(Object other) { - return DirectionalCellMergeMapper.ensureInitialized().equalsValue( - this as DirectionalCellMerge, - other, - ); - } - - @override - int get hashCode { - return DirectionalCellMergeMapper.ensureInitialized().hashValue( - this as DirectionalCellMerge, - ); - } -} - -extension DirectionalCellMergeValueCopy<$R, $Out> - on ObjectCopyWith<$R, DirectionalCellMerge, $Out> { - DirectionalCellMergeCopyWith<$R, DirectionalCellMerge, $Out> - get $asDirectionalCellMerge => $base.as( - (v, t, t2) => _DirectionalCellMergeCopyWithImpl<$R, $Out>(v, t, t2), - ); -} - -abstract class DirectionalCellMergeCopyWith< - $R, - $In extends DirectionalCellMerge, - $Out -> - implements CellMergeStrategyCopyWith<$R, $In, $Out> { - @override - $R call({CellMergeDirection? direction}); - DirectionalCellMergeCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ); -} - -class _DirectionalCellMergeCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, DirectionalCellMerge, $Out> - implements DirectionalCellMergeCopyWith<$R, DirectionalCellMerge, $Out> { - _DirectionalCellMergeCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - DirectionalCellMergeMapper.ensureInitialized(); - @override - $R call({CellMergeDirection? direction}) => - $apply(FieldCopyWithData({if (direction != null) #direction: direction})); - @override - DirectionalCellMerge $make(CopyWithData data) => - DirectionalCellMerge(data.get(#direction, or: $value.direction)); - - @override - DirectionalCellMergeCopyWith<$R2, DirectionalCellMerge, $Out2> - $chain<$R2, $Out2>(Then<$Out2, $R2> t) => - _DirectionalCellMergeCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/api/lib/src/models/chat.mapper.dart b/api/lib/src/models/chat.mapper.dart index b980face..1664dcb3 100644 --- a/api/lib/src/models/chat.mapper.dart +++ b/api/lib/src/models/chat.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/config.mapper.dart b/api/lib/src/models/config.mapper.dart index 6e4d12c6..f1c2ca03 100644 --- a/api/lib/src/models/config.mapper.dart +++ b/api/lib/src/models/config.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/deck.mapper.dart b/api/lib/src/models/deck.mapper.dart index cb98cda2..4dba4849 100644 --- a/api/lib/src/models/deck.mapper.dart +++ b/api/lib/src/models/deck.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/definition.mapper.dart b/api/lib/src/models/definition.mapper.dart index d863bff5..5eaadaff 100644 --- a/api/lib/src/models/definition.mapper.dart +++ b/api/lib/src/models/definition.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/dialog.mapper.dart b/api/lib/src/models/dialog.mapper.dart index a93dbd6d..b9b25903 100644 --- a/api/lib/src/models/dialog.mapper.dart +++ b/api/lib/src/models/dialog.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/info.mapper.dart b/api/lib/src/models/info.mapper.dart index 12f56c75..a4b28c03 100644 --- a/api/lib/src/models/info.mapper.dart +++ b/api/lib/src/models/info.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/kick.mapper.dart b/api/lib/src/models/kick.mapper.dart index c25d4f1c..675b3375 100644 --- a/api/lib/src/models/kick.mapper.dart +++ b/api/lib/src/models/kick.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/meta.mapper.dart b/api/lib/src/models/meta.mapper.dart index 6af978da..ffcdcbce 100644 --- a/api/lib/src/models/meta.mapper.dart +++ b/api/lib/src/models/meta.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/mode.mapper.dart b/api/lib/src/models/mode.mapper.dart index e6af0e9a..84c9e91c 100644 --- a/api/lib/src/models/mode.mapper.dart +++ b/api/lib/src/models/mode.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/server.mapper.dart b/api/lib/src/models/server.mapper.dart index 0714e47e..7e43e03e 100644 --- a/api/lib/src/models/server.mapper.dart +++ b/api/lib/src/models/server.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/table.mapper.dart b/api/lib/src/models/table.mapper.dart index 0c90febc..469ab5c7 100644 --- a/api/lib/src/models/table.mapper.dart +++ b/api/lib/src/models/table.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/toolbar.mapper.dart b/api/lib/src/models/toolbar.mapper.dart index c3f789bb..d6d6570f 100644 --- a/api/lib/src/models/toolbar.mapper.dart +++ b/api/lib/src/models/toolbar.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/translation.mapper.dart b/api/lib/src/models/translation.mapper.dart index 702fd88c..7ff8c83e 100644 --- a/api/lib/src/models/translation.mapper.dart +++ b/api/lib/src/models/translation.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/vector.mapper.dart b/api/lib/src/models/vector.mapper.dart index ccb891f5..da61b0fc 100644 --- a/api/lib/src/models/vector.mapper.dart +++ b/api/lib/src/models/vector.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/visual.mapper.dart b/api/lib/src/models/visual.mapper.dart index faecd6ac..0f9d6ebd 100644 --- a/api/lib/src/models/visual.mapper.dart +++ b/api/lib/src/models/visual.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/models/waypoint.mapper.dart b/api/lib/src/models/waypoint.mapper.dart index 4de1b350..bb2ac659 100644 --- a/api/lib/src/models/waypoint.mapper.dart +++ b/api/lib/src/models/waypoint.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/api/lib/src/services/user.mapper.dart b/api/lib/src/services/user.mapper.dart index 0fc2e7a3..f9341b39 100644 --- a/api/lib/src/services/user.mapper.dart +++ b/api/lib/src/services/user.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/app/lib/bloc/multiplayer.mapper.dart b/app/lib/bloc/multiplayer.mapper.dart index 6e7d3add..fd21ee8c 100644 --- a/app/lib/bloc/multiplayer.mapper.dart +++ b/app/lib/bloc/multiplayer.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/app/lib/bloc/settings.mapper.dart b/app/lib/bloc/settings.mapper.dart index cf404aed..b3c53a4d 100644 --- a/app/lib/bloc/settings.mapper.dart +++ b/app/lib/bloc/settings.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/app/lib/bloc/world/local.mapper.dart b/app/lib/bloc/world/local.mapper.dart index 3ac397e9..9a3b5b02 100644 --- a/app/lib/bloc/world/local.mapper.dart +++ b/app/lib/bloc/world/local.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/app/lib/bloc/world/state.dart b/app/lib/bloc/world/state.dart index 9cdbe338..1f976a75 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -60,4 +60,10 @@ final class ClientWorldState with ClientWorldStateMappable { String? get name => world.name; FileMetadata get metadata => world.metadata; SetonixData get data => world.data; + + int calculateSpan(VectorDefinition start, CellMergeDirection direction) => + world.calculateSpan(start, direction); + + VectorDefinition getParentCell(VectorDefinition position) => + world.getParentCell(position); } diff --git a/app/lib/bloc/world/state.mapper.dart b/app/lib/bloc/world/state.mapper.dart index 1ea8f43e..53679d14 100644 --- a/app/lib/bloc/world/state.mapper.dart +++ b/app/lib/bloc/world/state.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/app/lib/board/background.dart b/app/lib/board/background.dart index ca7028c4..3a7b6e65 100644 --- a/app/lib/board/background.dart +++ b/app/lib/board/background.dart @@ -1,19 +1,27 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flutter/rendering.dart'; import 'package:setonix/bloc/world/bloc.dart'; import 'package:setonix/bloc/world/state.dart'; +import 'package:setonix/board/grid.dart'; import 'package:setonix_api/setonix_api.dart'; class GameBoardBackground extends PositionComponent with FlameBlocListenable { - SpriteComponent? _sprite; + Sprite? _sprite; bool _isDirty = true; + late final BoardGrid grid; GameBoardBackground({super.size}); + @override + void onLoad() { + super.onLoad(); + grid = findParent()!; + } + @override void onInitialState(ClientWorldState state) => _isDirty = true; @@ -34,6 +42,22 @@ class GameBoardBackground extends PositionComponent } } + @override + void render(Canvas canvas) { + final sprite = _sprite; + if (sprite != null) { + paintImage( + canvas: canvas, + rect: size.toRect(), + image: sprite.image, + repeat: ImageRepeat.repeat, + scale: sprite.image.width / grid.cellSize.x, + alignment: Alignment.topLeft, + filterQuality: FilterQuality.none, + ); + } + } + Future _loadSprite( ClientWorldState state, PackItem? item, @@ -60,13 +84,6 @@ class GameBoardBackground extends PositionComponent .nonNulls .firstOrNull, ); - if (background == null) return; - final shouldAdd = _sprite == null; - final sprite = _sprite ??= SpriteComponent( - size: size, - paint: Paint()..isAntiAlias = false, - ); - sprite.sprite = background; - if (shouldAdd) add(sprite); + _sprite = background; } } diff --git a/app/lib/board/cell.dart b/app/lib/board/cell.dart index 2d951140..5d59e671 100644 --- a/app/lib/board/cell.dart +++ b/app/lib/board/cell.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; @@ -9,6 +10,7 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:setonix/pages/game/waypoint.dart'; +import 'package:setonix/pages/game/merge.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:setonix/bloc/world/bloc.dart'; @@ -37,9 +39,10 @@ class GameCell extends PositionComponent HandItemDropZone, FlameBlocListenable, ScrollCallbacks { - late final SpriteComponent _selectionComponent; - SpriteComponent? _cardComponent, _boardComponent; + late final NineTileBoxComponent _selectionComponent; + SpriteComponent? _boardComponent; TextElementComponent? _waypointComponent; + GameBoardBackground? _backgroundComponent; late final BoardGrid grid; List? _effects; @@ -127,9 +130,10 @@ class GameCell extends PositionComponent void onLoad() { super.onLoad(); grid = findParent()!; - add(GameBoardBackground(size: size)); - _selectionComponent = SpriteComponent( - sprite: game.selectionSprite, + _backgroundComponent = GameBoardBackground(size: size); + add(_backgroundComponent!); + _selectionComponent = NineTileBoxComponent( + nineTileBox: NineTileBox(game.selectionSprite, tileSize: 12), size: size, priority: 1, ); @@ -145,10 +149,20 @@ class GameCell extends PositionComponent newState.table.cells[definition] || previousState.teamMembers != newState.teamMembers || previousState.colorScheme != newState.colorScheme || - previousState.showWaypoints != newState.showWaypoints; + previousState.showWaypoints != newState.showWaypoints || + (newState.selectedCell != null && + newState.getParentCell(newState.selectedCell!) == definition) != + (previousState.selectedCell != null && + previousState.getParentCell(previousState.selectedCell!) == + definition); } - bool get isSelected => isMounted && bloc.state.selectedCell == toDefinition(); + bool get isSelected => + isMounted && + (bloc.state.selectedCell == toDefinition() || + (bloc.state.selectedCell != null && + bloc.state.getParentCell(bloc.state.selectedCell!) == + toDefinition())); void _fadeIn() => _updateEffects([OpacityEffect.fadeIn(EffectController(duration: 0.2))]); @@ -214,13 +228,17 @@ class GameCell extends PositionComponent false, ); - GameObject? _currentTop; + List? _currentObjects; + CellMergeStrategy? _currentStrategy; BoardTile? _currentTile; bool _currentVisible = true; @override void onNewState(ClientWorldState state) { - final selected = state.selectedCell == toDefinition(); + final selected = + state.selectedCell == toDefinition() || + (state.selectedCell != null && + state.getParentCell(state.selectedCell!) == toDefinition()); final color = isClaimed(state) ? isAllowed(state) ? state.colorScheme.secondary @@ -245,18 +263,75 @@ class GameCell extends PositionComponent _buildWaypointComponent(state); } + int? _currentSpan; + Future _updateTop() async { final state = bloc.state; - final cell = state.table.cells[toDefinition()]; - final top = cell?.objects.firstOrNull; + final cellDefinition = toDefinition(); + final cell = state.table.cells[cellDefinition]; + final objects = cell?.objects ?? const []; + final strategy = cell?.merge; final visible = state.isCellVisible(toGlobalDefinition(state)); final tile = cell?.tiles.lastOrNull; - if (top == _currentTop && + + if (strategy is MergedCellStrategy) { + if (_currentVisible) { + _currentVisible = false; + size = Vector2.zero(); + _backgroundComponent?.size = Vector2.zero(); + _selectionComponent.size = Vector2.zero(); + _boardComponent?.removeFromParent(); + _boardComponent = null; + removeWhere((e) => e is _GameCellObjectComponent); + _currentObjects = null; + _currentStrategy = null; + _currentSpan = null; + _currentTile = null; + } + return; + } + + int? newSpan; + if (strategy is StackedCellMergeStrategy || + strategy is DistributeCellMergeStrategy) { + final direction = strategy is StackedCellMergeStrategy + ? strategy.direction + : (strategy as DistributeCellMergeStrategy).direction; + final span = state.calculateSpan(cellDefinition, direction); + newSpan = span; + final s = grid.cellSize.clone(); + if (direction == CellMergeDirection.horizontal) { + s.x *= span; + } else { + s.y *= span; + } + if (size != s) { + size = s; + _backgroundComponent?.size = s; + _selectionComponent.size = s; + _boardComponent?.size = s; + priority = 100; + } + } else { + if (size != grid.cellSize) { + size = grid.cellSize; + _backgroundComponent?.size = size; + _selectionComponent.size = size; + _boardComponent?.size = size; + priority = 0; + } + } + + if (const ListEquality().equals(objects, _currentObjects) && + strategy == _currentStrategy && + newSpan == _currentSpan && visible == _currentVisible && tile == _currentTile) { return; } - _currentTop = top; + _currentObjects = objects; + _currentStrategy = strategy; + _currentSpan = newSpan; _currentVisible = visible; _currentTile = tile; final paint = Paint()..isAntiAlias = false; @@ -274,34 +349,129 @@ class GameCell extends PositionComponent } else { _boardComponent?.removeFromParent(); } - if (top != null) { - final component = _cardComponent ??= SpriteComponent( - paint: paint, - priority: 1, - ); - component - ..anchor = Anchor.center - ..position = size / 2; - component.sprite = + removeWhere((e) => e is _GameCellObjectComponent); + if (objects.isEmpty) return; + + var displayObjects = switch (strategy) { + DistributeCellMergeStrategy(maxCards: final maxCards) => objects.take( + maxCards, + ), + _ => strategy == null ? [objects.first] : objects, + }.toList(); + + final bool reverse; + if (strategy is StackedCellMergeStrategy) { + reverse = strategy.reverse; + } else if (strategy is DistributeCellMergeStrategy) { + reverse = strategy.reverse; + } else { + reverse = false; + } + + final renderObjects = displayObjects + .asMap() + .entries + .toList() + .reversed + .toList(); + final cellRect = size.toRect(); + + for (final entry in renderObjects) { + final i = entry.key; + final object = entry.value; + final component = _GameCellObjectComponent(paint: paint, priority: 1); + final sprite = await state.assetManager.loadFigureSprite( - top.asset, - top.hidden || !state.isCellVisible(toGlobalDefinition(state)) + object.asset, + object.hidden || !state.isCellVisible(toGlobalDefinition(state)) ? null - : top.variation, + : object.variation, ) ?? game.blankSprite; - final sprite = component.sprite; - if (sprite != null) { - final scale = (size.x / sprite.srcSize.x) < (size.y / sprite.srcSize.y) - ? (size.x / sprite.srcSize.x) - : (size.y / sprite.srcSize.y); - component.size = sprite.srcSize * scale; + component.sprite = sprite; + + final scale = + (grid.cellSize.x / sprite.srcSize.x) < + (grid.cellSize.y / sprite.srcSize.y) + ? (grid.cellSize.x / sprite.srcSize.x) + : (grid.cellSize.y / sprite.srcSize.y); + component.size = sprite.srcSize * scale; + component.anchor = Anchor.center; + + final double x, y; + switch (strategy) { + case StackedCellMergeStrategy( + visiblePercentage: final visiblePercentage, + direction: final direction, + ): + final offsetStep = visiblePercentage / 100.0; + final count = displayObjects.length; + + if (direction == CellMergeDirection.vertical) { + x = size.x / 2; + if (!reverse) { + final startY = grid.cellSize.y / 2; + y = startY + i * offsetStep * grid.cellSize.y; + } else { + final startY = size.y - grid.cellSize.y / 2; + y = startY - (count - 1 - i) * offsetStep * grid.cellSize.y; + } + } else { + y = size.y / 2; + if (!reverse) { + final startX = grid.cellSize.x / 2; + x = startX + i * offsetStep * grid.cellSize.x; + } else { + final startX = size.x - grid.cellSize.x / 2; + x = startX - (count - 1 - i) * offsetStep * grid.cellSize.x; + } + } + case DistributeCellMergeStrategy( + direction: final direction, + fillVariableSpace: final fillVariableSpace, + ): + final count = displayObjects.length; + if (count == 1) { + x = size.x / 2; + y = size.y / 2; + } else { + var factor = i / (count - 1); + // Center factor to -0.5 ... 0.5 range + factor -= 0.5; + if (reverse) factor = -factor; + + if (fillVariableSpace) { + factor *= 2; // -1 to 1 + } else { + factor = 0; // Fallback or fixed spacing logic could go here + } + + if (direction == CellMergeDirection.vertical) { + x = size.x / 2; + y = size.y / 2 + (size.y - grid.cellSize.y) / 2 * factor; + } else { + x = size.x / 2 + (size.x - grid.cellSize.x) / 2 * factor; + y = size.y / 2; + } + } + default: + x = size.x / 2; + y = size.y / 2; } - if (!component.isMounted) { + component.position = Vector2(x, y); + + final cRect = Rect.fromCenter( + center: Offset(x, y), + width: component.width, + height: component.height, + ); + + // Relaxed check to handle floating point precision + final bounds = cellRect.inflate(1); + if (bounds.contains(cRect.topLeft) && + bounds.contains(cRect.bottomRight)) { add(component); } - } else { - _cardComponent?.removeFromParent(); } } @@ -359,6 +529,39 @@ class GameCell extends PositionComponent onClose(); }, ), + ContextMenuButtonItem( + label: AppLocalizations.of(context).merge, + onPressed: () { + onClose(); + showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: bloc, + child: MergeDialog( + cell: toGlobalDefinition(bloc.state), + initialStrategy: + bloc.state.table.cells[toDefinition()]?.merge, + initialSpan: () { + final strategy = + bloc.state.table.cells[toDefinition()]?.merge; + if (strategy is StackedCellMergeStrategy) { + return bloc.state.calculateSpan( + toDefinition(), + strategy.direction, + ); + } else if (strategy is DistributeCellMergeStrategy) { + return bloc.state.calculateSpan( + toDefinition(), + strategy.direction, + ); + } + return 1; + }(), + ), + ), + ); + }, + ), ContextMenuButtonItem( label: AppLocalizations.of(context).remove, onPressed: () { @@ -477,3 +680,7 @@ class GameCell extends PositionComponent return false; } } + +class _GameCellObjectComponent extends SpriteComponent { + _GameCellObjectComponent({super.paint, super.priority}); +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index aa345d71..c81c00ae 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -295,5 +295,18 @@ "editWaypoint": "Edit waypoint", "team": "Team", "public": "Public", - "noWaypoints": "There are no waypoints available" + "noWaypoints": "There are no waypoints available", + "merge": "Merge", + "mergeStrategy": "Merge strategy", + "stacked": "Stacked", + "distribute": "Distribute", + "visiblePercentage": "Visible percentage", + "reverse": "Reverse", + "direction": "Direction", + "horizontal": "Horizontal", + "vertical": "Vertical", + "maxCards": "Max cards", + "fillVariableSpace": "Fill variable space", + "none": "None", + "span": "Span" } diff --git a/app/lib/pages/game/merge.dart b/app/lib/pages/game/merge.dart new file mode 100644 index 00000000..7e27475d --- /dev/null +++ b/app/lib/pages/game/merge.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:setonix/bloc/world/bloc.dart'; +import 'package:setonix/src/generated/i18n/app_localizations.dart'; +import 'package:setonix_api/setonix_api.dart'; + +class MergeDialog extends StatefulWidget { + final GlobalVectorDefinition cell; + final CellMergeStrategy? initialStrategy; + final int initialSpan; + + const MergeDialog({ + super.key, + required this.cell, + this.initialStrategy, + this.initialSpan = 1, + }); + + @override + State createState() => _MergeDialogState(); +} + +class _MergeDialogState extends State { + late CellMergeStrategy? _strategy = widget.initialStrategy; + late int _span = widget.initialSpan; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final bloc = context.read(); + + return ResponsiveAlertDialog( + title: Text(loc.mergeStrategy), + constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), + content: ListView( + shrinkWrap: true, + children: [ + DropdownMenu( + expandedInsets: EdgeInsets.zero, + label: Text(loc.merge), + initialSelection: switch (_strategy) { + StackedCellMergeStrategy() => 'stacked', + DistributeCellMergeStrategy() => 'distribute', + _ => null, + }, + onSelected: (value) { + setState(() { + if (value == 'stacked') { + _strategy = const StackedCellMergeStrategy(); + } else if (value == 'distribute') { + _strategy = const DistributeCellMergeStrategy(); + } else { + _strategy = null; + } + }); + }, + dropdownMenuEntries: [ + DropdownMenuEntry(value: null, label: loc.none), + DropdownMenuEntry(value: 'stacked', label: loc.stacked), + DropdownMenuEntry(value: 'distribute', label: loc.distribute), + ], + ), + if (_strategy != null && _strategy is! MergedCellStrategy) ...[ + const SizedBox(height: 16), + ExactSlider( + label: loc.span, + value: _span.toDouble(), + min: 1, + max: 10, + divide: true, + fractionDigits: 0, + onChanged: (value) { + setState(() { + _span = value.toInt(); + }); + }, + ), + ], + if (_strategy is StackedCellMergeStrategy) ...[ + const SizedBox(height: 16), + ExactSlider( + label: '${loc.visiblePercentage} (%)', + value: (_strategy as StackedCellMergeStrategy).visiblePercentage + .toDouble(), + min: 0, + max: 100, + onChanged: (value) { + setState(() { + _strategy = (_strategy as StackedCellMergeStrategy).copyWith( + visiblePercentage: value.toInt(), + ); + }); + }, + ), + ], + if (_strategy is DistributeCellMergeStrategy) ...[ + const SizedBox(height: 16), + ExactSlider( + label: loc.maxCards, + value: (_strategy as DistributeCellMergeStrategy).maxCards + .toDouble(), + min: 1, + max: 20, + divide: true, + fractionDigits: 0, + onChanged: (value) { + setState(() { + _strategy = (_strategy as DistributeCellMergeStrategy) + .copyWith(maxCards: value.toInt()); + }); + }, + ), + SwitchListTile( + title: Text(loc.fillVariableSpace), + value: + (_strategy as DistributeCellMergeStrategy).fillVariableSpace, + onChanged: (value) { + setState(() { + _strategy = (_strategy as DistributeCellMergeStrategy) + .copyWith(fillVariableSpace: value); + }); + }, + ), + ], + if (_strategy != null && _strategy is! MergedCellStrategy) ...[ + SwitchListTile( + title: Text(loc.reverse), + value: _strategy is LayoutCellMergeStrategy + ? (_strategy as LayoutCellMergeStrategy).reverse + : false, + onChanged: (value) { + setState(() { + final s = _strategy; + if (s is StackedCellMergeStrategy) { + _strategy = s.copyWith(reverse: value); + } else if (s is DistributeCellMergeStrategy) { + _strategy = s.copyWith(reverse: value); + } + }); + }, + ), + const SizedBox(height: 8), + SegmentedButton( + segments: [ + ButtonSegment( + value: CellMergeDirection.horizontal, + label: Text(loc.horizontal), + ), + ButtonSegment( + value: CellMergeDirection.vertical, + label: Text(loc.vertical), + ), + ], + selected: {_strategy?.direction ?? CellMergeDirection.vertical}, + onSelectionChanged: (value) { + setState(() { + _strategy = _strategy?.copyWith(direction: value.first); + }); + }, + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(loc.cancel), + ), + ElevatedButton( + onPressed: () { + bloc.process( + CellMergeStrategyChanged(widget.cell, _strategy, span: _span), + ); + Navigator.of(context).pop(); + }, + child: Text(loc.save), + ), + ], + ); + } +} diff --git a/plugin/pubspec.lock b/plugin/pubspec.lock index 64b01dcc..0f41ffa6 100644 --- a/plugin/pubspec.lock +++ b/plugin/pubspec.lock @@ -310,8 +310,8 @@ packages: dependency: transitive description: path: "packages/lw_file_system_api" - ref: ddd0761c3ed5a48108bddd0448df76d77eeb9da0 - resolved-ref: ddd0761c3ed5a48108bddd0448df76d77eeb9da0 + ref: "94a914c4bc3d3163bad28dfc24a65bf7a1ac06bc" + resolved-ref: "94a914c4bc3d3163bad28dfc24a65bf7a1ac06bc" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" diff --git a/server/pubspec.lock b/server/pubspec.lock index 05c6a6d2..06bbab3f 100644 --- a/server/pubspec.lock +++ b/server/pubspec.lock @@ -375,8 +375,8 @@ packages: dependency: transitive description: path: "packages/lw_file_system_api" - ref: ddd0761c3ed5a48108bddd0448df76d77eeb9da0 - resolved-ref: ddd0761c3ed5a48108bddd0448df76d77eeb9da0 + ref: "94a914c4bc3d3163bad28dfc24a65bf7a1ac06bc" + resolved-ref: "94a914c4bc3d3163bad28dfc24a65bf7a1ac06bc" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" From 24f553e4b892fb49e18c16ccd468e08afc608274 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 16 Feb 2026 22:52:10 +0100 Subject: [PATCH 3/6] Enhance cell merging with improved rendering, server-side logic, and UI refinements --- api/lib/src/event/process/server.dart | 38 ++++++++++++------ api/lib/src/event/state.dart | 14 +++---- app/lib/bloc/world/state.dart | 5 +++ app/lib/board/cell.dart | 58 +++++++++++++-------------- app/lib/board/grid.dart | 53 +++++++++++++++++++++--- 5 files changed, 113 insertions(+), 55 deletions(-) diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index 595a74f5..fed20b5b 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -319,10 +319,7 @@ ServerProcessed processServerEvent( final cells = Map.from(table.cells); CellMergeDirection? getDirection(CellMergeStrategy? strategy) { - if (strategy is StackedCellMergeStrategy) return strategy.direction; - if (strategy is DistributeCellMergeStrategy) { - return strategy.direction; - } + if (strategy is LayoutCellMergeStrategy) return strategy.direction; return null; } @@ -338,10 +335,9 @@ ServerProcessed processServerEvent( if (oldSpan > 1) { var current = event.cell.position; for (var i = 1; i < oldSpan; i++) { - current = - oldDirection == CellMergeDirection.horizontal - ? VectorDefinition(current.x + 1, current.y) - : VectorDefinition(current.x, current.y + 1); + current = oldDirection == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); final neighbor = cells[current] ?? TableCell(); if (neighbor.merge is MergedCellStrategy && @@ -359,23 +355,41 @@ ServerProcessed processServerEvent( cells[event.cell.position] = newCell; // Expand new neighbors + final strategy = event.strategy; + final span = event.span; + if (span != null && span > 1 && strategy != null) { final direction = getDirection(strategy); + if (direction != null) { var current = event.cell.position; + + final allObjects = (cells[current] ?? TableCell()).objects + .toList(); + for (var i = 1; i < span; i++) { - current = - direction == CellMergeDirection.horizontal - ? VectorDefinition(current.x + 1, current.y) - : VectorDefinition(current.x, current.y + 1); + current = direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); final neighbor = cells[current] ?? TableCell(); + + if (neighbor.objects.isNotEmpty) { + allObjects.addAll(neighbor.objects); + } + cells[current] = neighbor.copyWith( merge: MergedCellStrategy(direction), + + objects: [], ); } + + cells[event.cell.position] = cells[event.cell.position]!.copyWith( + objects: allObjects, + ); } } diff --git a/api/lib/src/event/state.dart b/api/lib/src/event/state.dart index 77de625e..7c7b37c3 100644 --- a/api/lib/src/event/state.dart +++ b/api/lib/src/event/state.dart @@ -129,10 +129,9 @@ final class WorldState with WorldStateMappable { var span = 1; var current = start; while (true) { - current = - direction == CellMergeDirection.horizontal - ? VectorDefinition(current.x + 1, current.y) - : VectorDefinition(current.x, current.y + 1); + current = direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); final cell = table.cells[current]; final strategy = cell?.merge; if (strategy is MergedCellStrategy && strategy.direction == direction) { @@ -150,10 +149,9 @@ final class WorldState with WorldStateMappable { final cell = table.cells[current]; final strategy = cell?.merge; if (strategy is MergedCellStrategy) { - current = - strategy.direction == CellMergeDirection.horizontal - ? VectorDefinition(current.x - 1, current.y) - : VectorDefinition(current.x, current.y - 1); + current = strategy.direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x - 1, current.y) + : VectorDefinition(current.x, current.y - 1); } else { return current; } diff --git a/app/lib/bloc/world/state.dart b/app/lib/bloc/world/state.dart index 1f976a75..01b6f22e 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -66,4 +66,9 @@ final class ClientWorldState with ClientWorldStateMappable { VectorDefinition getParentCell(VectorDefinition position) => world.getParentCell(position); + + bool isCellSelected(VectorDefinition cell) { + if (selectedCell == cell) return true; + return selectedCell != null && getParentCell(selectedCell!) == cell; + } } diff --git a/app/lib/board/cell.dart b/app/lib/board/cell.dart index 5d59e671..fb109d9c 100644 --- a/app/lib/board/cell.dart +++ b/app/lib/board/cell.dart @@ -143,26 +143,16 @@ class GameCell extends PositionComponent @override bool listenWhen(ClientWorldState previousState, ClientWorldState newState) { final definition = toDefinition(); - return (previousState.selectedCell == definition) != - (newState.selectedCell == definition) || + return previousState.isCellSelected(definition) != + newState.isCellSelected(definition) || previousState.table.cells[definition] != newState.table.cells[definition] || previousState.teamMembers != newState.teamMembers || previousState.colorScheme != newState.colorScheme || - previousState.showWaypoints != newState.showWaypoints || - (newState.selectedCell != null && - newState.getParentCell(newState.selectedCell!) == definition) != - (previousState.selectedCell != null && - previousState.getParentCell(previousState.selectedCell!) == - definition); + previousState.showWaypoints != newState.showWaypoints; } - bool get isSelected => - isMounted && - (bloc.state.selectedCell == toDefinition() || - (bloc.state.selectedCell != null && - bloc.state.getParentCell(bloc.state.selectedCell!) == - toDefinition())); + bool get isSelected => isMounted && bloc.state.isCellSelected(toDefinition()); void _fadeIn() => _updateEffects([OpacityEffect.fadeIn(EffectController(duration: 0.2))]); @@ -235,10 +225,7 @@ class GameCell extends PositionComponent @override void onNewState(ClientWorldState state) { - final selected = - state.selectedCell == toDefinition() || - (state.selectedCell != null && - state.getParentCell(state.selectedCell!) == toDefinition()); + final selected = state.isCellSelected(toDefinition()); final color = isClaimed(state) ? isAllowed(state) ? state.colorScheme.secondary @@ -292,8 +279,7 @@ class GameCell extends PositionComponent } int? newSpan; - if (strategy is StackedCellMergeStrategy || - strategy is DistributeCellMergeStrategy) { + if (strategy is LayoutCellMergeStrategy) { final direction = strategy is StackedCellMergeStrategy ? strategy.direction : (strategy as DistributeCellMergeStrategy).direction; @@ -360,9 +346,8 @@ class GameCell extends PositionComponent }.toList(); final bool reverse; - if (strategy is StackedCellMergeStrategy) { - reverse = strategy.reverse; - } else if (strategy is DistributeCellMergeStrategy) { + final direction = cell?.merge?.direction; + if (strategy is LayoutCellMergeStrategy) { reverse = strategy.reverse; } else { reverse = false; @@ -471,6 +456,26 @@ class GameCell extends PositionComponent if (bounds.contains(cRect.topLeft) && bounds.contains(cRect.bottomRight)) { add(component); + } else { + // Optimization: Break if we are moving away from bounds + bool decreasing = true; + if (strategy is DistributeCellMergeStrategy && reverse) { + decreasing = false; + } + + if (decreasing) { + if (direction == CellMergeDirection.horizontal) { + if (cRect.right < bounds.left) break; + } else { + if (cRect.bottom < bounds.top) break; + } + } else { + if (direction == CellMergeDirection.horizontal) { + if (cRect.left > bounds.right) break; + } else { + if (cRect.top > bounds.bottom) break; + } + } } } } @@ -544,12 +549,7 @@ class GameCell extends PositionComponent initialSpan: () { final strategy = bloc.state.table.cells[toDefinition()]?.merge; - if (strategy is StackedCellMergeStrategy) { - return bloc.state.calculateSpan( - toDefinition(), - strategy.direction, - ); - } else if (strategy is DistributeCellMergeStrategy) { + if (strategy is LayoutCellMergeStrategy) { return bloc.state.calculateSpan( toDefinition(), strategy.direction, diff --git a/app/lib/board/grid.dart b/app/lib/board/grid.dart index b5777a43..4b7a01ac 100644 --- a/app/lib/board/grid.dart +++ b/app/lib/board/grid.dart @@ -1,14 +1,34 @@ import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:setonix/bloc/world/bloc.dart'; +import 'package:setonix/bloc/world/state.dart'; import 'package:setonix/board/cell.dart'; +import 'package:setonix/board/game.dart'; +import 'package:setonix/helpers/vector.dart'; -class BoardGrid extends PositionComponent with HasGameReference { +class BoardGrid extends PositionComponent + with + HasGameReference, + FlameBlocListenable { final Vector2 cellSize; static const _padding = 3.0; Rect? _lastViewport; + bool _forceUpdate = false; BoardGrid({required this.cellSize}); + @override + bool listenWhen(ClientWorldState previousState, ClientWorldState newState) { + return previousState.table != newState.table; + } + + @override + void onNewState(ClientWorldState state) { + _forceUpdate = true; + _lastViewport = null; + } + Rect get viewport { final Rect viewport = game.camera.visibleWorldRect; final currentSize = cellSize; @@ -25,11 +45,12 @@ class BoardGrid extends PositionComponent with HasGameReference { final Rect viewport = this.viewport; final Rect lastViewport = _lastViewport ?? Rect.zero; final bool shouldReset = viewport != lastViewport; - return shouldReset; + return shouldReset || _forceUpdate; } void _updateGrid() { if (!shouldReset()) return; + _forceUpdate = false; final viewport = this.viewport; final currentSize = cellSize; // Remove components that are out of the viewport @@ -41,25 +62,45 @@ class BoardGrid extends PositionComponent with HasGameReference { }), ); final last = _lastViewport ?? Rect.zero; + final existingPositions = children + .whereType() + .map((e) => e.position) + .fold>({}, (set, pos) { + set.add(pos); + return set; + }); + + void tryAddCell(Vector2 position) { + final definition = (position.clone()..divide(cellSize)).toDefinition(); + final parentDefinition = bloc.state.getParentCell(definition); + final parentPosition = parentDefinition.toVector()..multiply(cellSize); + + if (!existingPositions.contains(parentPosition)) { + add(_createCell(position: parentPosition, size: currentSize)); + existingPositions.add(parentPosition); + } + } + // Add components that are in the viewport // Top and bottom for (var x = viewport.left; x < viewport.right; x += currentSize.x) { for (var y = viewport.top; y < last.top; y += currentSize.y) { - add(_createCell(position: Vector2(x, y), size: currentSize)); + tryAddCell(Vector2(x, y)); } for (var y = last.bottom; y < viewport.bottom; y += currentSize.y) { - add(_createCell(position: Vector2(x, y), size: currentSize)); + tryAddCell(Vector2(x, y)); } } // Left and right for (var y = last.top; y < last.bottom; y += currentSize.y) { for (var x = viewport.left; x < last.left; x += currentSize.x) { - add(_createCell(position: Vector2(x, y), size: currentSize)); + tryAddCell(Vector2(x, y)); } for (var x = last.right; x < viewport.right; x += currentSize.x) { - add(_createCell(position: Vector2(x, y), size: currentSize)); + tryAddCell(Vector2(x, y)); } } + _lastViewport = viewport; } From 54cf85e0490b6ac04585b990020ec57cc3fb0898 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 16 Feb 2026 23:09:47 +0100 Subject: [PATCH 4/6] Address request review --- api/lib/src/event/process/server.dart | 6 ++++++ api/lib/src/event/state.dart | 16 ++++++++-------- api/lib/src/models/cell.dart | 2 +- app/lib/board/cell.dart | 3 ++- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index fed20b5b..362d9a57 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -57,6 +57,12 @@ bool isValidServerEvent(ServerWorldEvent event, WorldState state) => .length - 1, ), + CellMergeStrategyChanged() => + (event.span ?? 1) > 0 && + state + .getTableOrDefault(event.cell.table) + .cells + .containsKey(event.cell.position), DialogOpened() => event.dialog.isValid(), _ => true, }; diff --git a/api/lib/src/event/state.dart b/api/lib/src/event/state.dart index 7c7b37c3..4977980f 100644 --- a/api/lib/src/event/state.dart +++ b/api/lib/src/event/state.dart @@ -125,27 +125,26 @@ final class WorldState with WorldStateMappable { GameTable Function(GameTable) mapper, ) => updateTable(name, mapper(getTableOrDefault(name))); + static const int maxMergeSpan = 1000; + int calculateSpan(VectorDefinition start, CellMergeDirection direction) { - var span = 1; var current = start; - while (true) { + for (var span = 1; span < maxMergeSpan; span++) { current = direction == CellMergeDirection.horizontal ? VectorDefinition(current.x + 1, current.y) : VectorDefinition(current.x, current.y + 1); final cell = table.cells[current]; final strategy = cell?.merge; - if (strategy is MergedCellStrategy && strategy.direction == direction) { - span++; - } else { - break; + if (strategy is! MergedCellStrategy || strategy.direction != direction) { + return span; } } - return span; + return maxMergeSpan; } VectorDefinition getParentCell(VectorDefinition position) { var current = position; - while (true) { + for (var depth = 0; depth < maxMergeSpan; depth++) { final cell = table.cells[current]; final strategy = cell?.merge; if (strategy is MergedCellStrategy) { @@ -156,5 +155,6 @@ final class WorldState with WorldStateMappable { return current; } } + return current; } } diff --git a/api/lib/src/models/cell.dart b/api/lib/src/models/cell.dart index bca49b9b..416fd7c4 100644 --- a/api/lib/src/models/cell.dart +++ b/api/lib/src/models/cell.dart @@ -12,7 +12,7 @@ class TableCell with TableCellMappable { TableCell({this.objects = const [], this.tiles = const [], this.merge}); - bool get isEmpty => objects.isEmpty && tiles.isEmpty; + bool get isEmpty => objects.isEmpty && tiles.isEmpty && merge == null; } @MappableClass() diff --git a/app/lib/board/cell.dart b/app/lib/board/cell.dart index fb109d9c..1ffb1858 100644 --- a/app/lib/board/cell.dart +++ b/app/lib/board/cell.dart @@ -299,6 +299,7 @@ class GameCell extends PositionComponent priority = 100; } } else { + _currentVisible = true; if (size != grid.cellSize) { size = grid.cellSize; _backgroundComponent?.size = size; @@ -428,7 +429,7 @@ class GameCell extends PositionComponent if (fillVariableSpace) { factor *= 2; // -1 to 1 } else { - factor = 0; // Fallback or fixed spacing logic could go here + factor = (i - (count - 1) / 2.0) * 0.4; } if (direction == CellMergeDirection.vertical) { From 9cc83df7ee09126c99acc77a36a08aa785bbffd1 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 17 Feb 2026 18:41:16 +0100 Subject: [PATCH 5/6] Implement cell merge improvements: optimize rendering, centralize logic, and enhance safety --- api/lib/src/event/event.mapper.dart | 7 ++-- api/lib/src/event/hybrid.dart | 4 +-- api/lib/src/event/process/server.dart | 17 +++++----- api/lib/src/event/state.dart | 45 ++++++++----------------- api/lib/src/models/table.dart | 32 ++++++++++++++++++ app/lib/bloc/world/state.dart | 20 ++++++++--- app/lib/board/cell.dart | 48 ++++++++++++--------------- app/lib/board/grid.dart | 2 +- 8 files changed, 100 insertions(+), 75 deletions(-) diff --git a/api/lib/src/event/event.mapper.dart b/api/lib/src/event/event.mapper.dart index 0cc9af7b..63a3c2fd 100644 --- a/api/lib/src/event/event.mapper.dart +++ b/api/lib/src/event/event.mapper.dart @@ -5820,11 +5820,12 @@ class CellMergeStrategyChangedMapper v.strategy; static const Field _f$strategy = Field('strategy', _$strategy); - static int? _$span(CellMergeStrategyChanged v) => v.span; + static int _$span(CellMergeStrategyChanged v) => v.span; static const Field _f$span = Field( 'span', _$span, opt: true, + def: 1, ); @override @@ -5962,12 +5963,12 @@ class _CellMergeStrategyChangedCopyWithImpl<$R, $Out> $R call({ GlobalVectorDefinition? cell, Object? strategy = $none, - Object? span = $none, + int? span, }) => $apply( FieldCopyWithData({ if (cell != null) #cell: cell, if (strategy != $none) #strategy: strategy, - if (span != $none) #span: span, + if (span != null) #span: span, }), ); @override diff --git a/api/lib/src/event/hybrid.dart b/api/lib/src/event/hybrid.dart index 7f3cb7bf..51ee3f60 100644 --- a/api/lib/src/event/hybrid.dart +++ b/api/lib/src/event/hybrid.dart @@ -175,9 +175,9 @@ final class CellMergeStrategyChanged extends HybridWorldEvent with CellMergeStrategyChangedMappable { final GlobalVectorDefinition cell; final CellMergeStrategy? strategy; - final int? span; + final int span; - CellMergeStrategyChanged(this.cell, this.strategy, {this.span}); + CellMergeStrategyChanged(this.cell, this.strategy, {this.span = 1}); } @MappableClass() diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index 362d9a57..31fc4995 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -58,11 +58,7 @@ bool isValidServerEvent(ServerWorldEvent event, WorldState state) => 1, ), CellMergeStrategyChanged() => - (event.span ?? 1) > 0 && - state - .getTableOrDefault(event.cell.table) - .cells - .containsKey(event.cell.position), + event.span > 0 && event.span <= GameTable.maxMergeSpan, DialogOpened() => event.dialog.isValid(), _ => true, }; @@ -334,7 +330,7 @@ ServerProcessed processServerEvent( final oldStrategy = oldCell?.merge; final oldDirection = getDirection(oldStrategy); if (oldDirection != null) { - final oldSpan = state.calculateSpan( + final oldSpan = table.calculateSpan( event.cell.position, oldDirection, ); @@ -349,7 +345,12 @@ ServerProcessed processServerEvent( if (neighbor.merge is MergedCellStrategy && (neighbor.merge as MergedCellStrategy).direction == oldDirection) { - cells[current] = neighbor.copyWith(merge: null); + final updatedNeighbor = neighbor.copyWith(merge: null); + if (updatedNeighbor.isEmpty) { + cells.remove(current); + } else { + cells[current] = updatedNeighbor; + } } } } @@ -366,7 +367,7 @@ ServerProcessed processServerEvent( final span = event.span; - if (span != null && span > 1 && strategy != null) { + if (strategy != null) { final direction = getDirection(strategy); if (direction != null) { diff --git a/api/lib/src/event/state.dart b/api/lib/src/event/state.dart index 4977980f..1d09917a 100644 --- a/api/lib/src/event/state.dart +++ b/api/lib/src/event/state.dart @@ -125,36 +125,19 @@ final class WorldState with WorldStateMappable { GameTable Function(GameTable) mapper, ) => updateTable(name, mapper(getTableOrDefault(name))); - static const int maxMergeSpan = 1000; - - int calculateSpan(VectorDefinition start, CellMergeDirection direction) { - var current = start; - for (var span = 1; span < maxMergeSpan; span++) { - current = direction == CellMergeDirection.horizontal - ? VectorDefinition(current.x + 1, current.y) - : VectorDefinition(current.x, current.y + 1); - final cell = table.cells[current]; - final strategy = cell?.merge; - if (strategy is! MergedCellStrategy || strategy.direction != direction) { - return span; - } - } - return maxMergeSpan; - } + int calculateLocalSpan( + VectorDefinition start, + CellMergeDirection direction, + ) => table.calculateSpan(start, direction); - VectorDefinition getParentCell(VectorDefinition position) { - var current = position; - for (var depth = 0; depth < maxMergeSpan; depth++) { - final cell = table.cells[current]; - final strategy = cell?.merge; - if (strategy is MergedCellStrategy) { - current = strategy.direction == CellMergeDirection.horizontal - ? VectorDefinition(current.x - 1, current.y) - : VectorDefinition(current.x, current.y - 1); - } else { - return current; - } - } - return current; - } + int calculateSpan( + GlobalVectorDefinition start, + CellMergeDirection direction, + ) => getTableOrDefault(start.table).calculateSpan(start.position, direction); + + VectorDefinition getLocalParentCell(VectorDefinition start) => + table.getParentCell(start); + + VectorDefinition getParentCell(GlobalVectorDefinition position) => + getTableOrDefault(position.table).getParentCell(position.position); } diff --git a/api/lib/src/models/table.dart b/api/lib/src/models/table.dart index 63bbbbb7..37f0295c 100644 --- a/api/lib/src/models/table.dart +++ b/api/lib/src/models/table.dart @@ -7,6 +7,7 @@ part 'table.mapper.dart'; @MappableClass() class GameTable with GameTableMappable { + static const int maxMergeSpan = 1000; @MappableField(key: "cells") final IgnoreEqualityBox> cellsBox; final ItemLocation? background; @@ -20,4 +21,35 @@ class GameTable with GameTableMappable { TableCell getCell(VectorDefinition position) => cells[position] ?? TableCell(); + + int calculateSpan(VectorDefinition start, CellMergeDirection direction) { + var current = start; + for (var span = 1; span < maxMergeSpan; span++) { + current = direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); + final cell = cells[current]; + final strategy = cell?.merge; + if (strategy is! MergedCellStrategy || strategy.direction != direction) { + return span; + } + } + return maxMergeSpan; + } + + VectorDefinition getParentCell(VectorDefinition position) { + var current = position; + for (var depth = 0; depth < maxMergeSpan; depth++) { + final cell = cells[current]; + final strategy = cell?.merge; + if (strategy is MergedCellStrategy) { + current = strategy.direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x - 1, current.y) + : VectorDefinition(current.x, current.y - 1); + } else { + return current; + } + } + return current; + } } diff --git a/app/lib/bloc/world/state.dart b/app/lib/bloc/world/state.dart index 01b6f22e..9f5dd3b2 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -61,14 +61,26 @@ final class ClientWorldState with ClientWorldStateMappable { FileMetadata get metadata => world.metadata; SetonixData get data => world.data; - int calculateSpan(VectorDefinition start, CellMergeDirection direction) => - world.calculateSpan(start, direction); + int calculateLocalSpan( + VectorDefinition start, + CellMergeDirection direction, + ) => world.calculateLocalSpan(start, direction); + int calculateSpan( + GlobalVectorDefinition start, + CellMergeDirection direction, + ) => world.calculateSpan(start, direction); - VectorDefinition getParentCell(VectorDefinition position) => + VectorDefinition getLocalParentCell(VectorDefinition position) => + world.getLocalParentCell(position); + VectorDefinition getParentCell(GlobalVectorDefinition position) => world.getParentCell(position); bool isCellSelected(VectorDefinition cell) { if (selectedCell == cell) return true; - return selectedCell != null && getParentCell(selectedCell!) == cell; + return selectedCell != null && + getParentCell( + GlobalVectorDefinition.fromLocal(tableName, selectedCell!), + ) == + cell; } } diff --git a/app/lib/board/cell.dart b/app/lib/board/cell.dart index 1ffb1858..5fd55e2e 100644 --- a/app/lib/board/cell.dart +++ b/app/lib/board/cell.dart @@ -283,7 +283,7 @@ class GameCell extends PositionComponent final direction = strategy is StackedCellMergeStrategy ? strategy.direction : (strategy as DistributeCellMergeStrategy).direction; - final span = state.calculateSpan(cellDefinition, direction); + final span = state.calculateLocalSpan(cellDefinition, direction); newSpan = span; final s = grid.cellSize.clone(); if (direction == CellMergeDirection.horizontal) { @@ -365,30 +365,11 @@ class GameCell extends PositionComponent for (final entry in renderObjects) { final i = entry.key; final object = entry.value; - final component = _GameCellObjectComponent(paint: paint, priority: 1); - final sprite = - await state.assetManager.loadFigureSprite( - object.asset, - object.hidden || !state.isCellVisible(toGlobalDefinition(state)) - ? null - : object.variation, - ) ?? - game.blankSprite; - component.sprite = sprite; - - final scale = - (grid.cellSize.x / sprite.srcSize.x) < - (grid.cellSize.y / sprite.srcSize.y) - ? (grid.cellSize.x / sprite.srcSize.x) - : (grid.cellSize.y / sprite.srcSize.y); - component.size = sprite.srcSize * scale; - component.anchor = Anchor.center; final double x, y; switch (strategy) { case StackedCellMergeStrategy( visiblePercentage: final visiblePercentage, - direction: final direction, ): final offsetStep = visiblePercentage / 100.0; final count = displayObjects.length; @@ -413,7 +394,6 @@ class GameCell extends PositionComponent } } case DistributeCellMergeStrategy( - direction: final direction, fillVariableSpace: final fillVariableSpace, ): final count = displayObjects.length; @@ -444,23 +424,39 @@ class GameCell extends PositionComponent x = size.x / 2; y = size.y / 2; } - component.position = Vector2(x, y); final cRect = Rect.fromCenter( center: Offset(x, y), - width: component.width, - height: component.height, + width: grid.cellSize.x, + height: grid.cellSize.y, ); // Relaxed check to handle floating point precision final bounds = cellRect.inflate(1); if (bounds.contains(cRect.topLeft) && bounds.contains(cRect.bottomRight)) { + final component = _GameCellObjectComponent(paint: paint, priority: 1); + final sprite = + await state.assetManager.loadFigureSprite( + object.asset, + object.hidden || !visible ? null : object.variation, + ) ?? + game.blankSprite; + component.sprite = sprite; + + final scale = + (grid.cellSize.x / sprite.srcSize.x) < + (grid.cellSize.y / sprite.srcSize.y) + ? (grid.cellSize.x / sprite.srcSize.x) + : (grid.cellSize.y / sprite.srcSize.y); + component.size = sprite.srcSize * scale; + component.anchor = Anchor.center; + component.position = Vector2(x, y); add(component); } else { // Optimization: Break if we are moving away from bounds bool decreasing = true; - if (strategy is DistributeCellMergeStrategy && reverse) { + if (strategy is LayoutCellMergeStrategy && reverse) { decreasing = false; } @@ -551,7 +547,7 @@ class GameCell extends PositionComponent final strategy = bloc.state.table.cells[toDefinition()]?.merge; if (strategy is LayoutCellMergeStrategy) { - return bloc.state.calculateSpan( + return bloc.state.calculateLocalSpan( toDefinition(), strategy.direction, ); diff --git a/app/lib/board/grid.dart b/app/lib/board/grid.dart index 4b7a01ac..821bdae7 100644 --- a/app/lib/board/grid.dart +++ b/app/lib/board/grid.dart @@ -72,7 +72,7 @@ class BoardGrid extends PositionComponent void tryAddCell(Vector2 position) { final definition = (position.clone()..divide(cellSize)).toDefinition(); - final parentDefinition = bloc.state.getParentCell(definition); + final parentDefinition = bloc.state.getLocalParentCell(definition); final parentPosition = parentDefinition.toVector()..multiply(cellSize); if (!existingPositions.contains(parentPosition)) { From f58d3fc93c960a2f72c033c129b44e7fffec80fa Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 17 Feb 2026 19:32:23 +0100 Subject: [PATCH 6/6] Fix tiles not correctly rendering if merged --- api/lib/src/event/process/server.dart | 14 ++---- app/lib/bloc/world/state.dart | 6 +-- app/lib/board/cell.dart | 66 +++++++++++++++++++++------ 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index 31fc4995..c801aaac 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -318,6 +318,7 @@ ServerProcessed processServerEvent( case CellMergeStrategyChanged(): return ServerProcessed( state.mapTableOrDefault(event.cell.table, (table) { + final cell = table.getCell(event.cell.position); final cells = Map.from(table.cells); CellMergeDirection? getDirection(CellMergeStrategy? strategy) { @@ -357,9 +358,7 @@ ServerProcessed processServerEvent( } // Update target cell - final cell = cells[event.cell.position] ?? TableCell(); - final newCell = cell.copyWith(merge: event.strategy); - cells[event.cell.position] = newCell; + var newCell = cell.copyWith(merge: event.strategy); // Expand new neighbors @@ -372,7 +371,6 @@ ServerProcessed processServerEvent( if (direction != null) { var current = event.cell.position; - final allObjects = (cells[current] ?? TableCell()).objects .toList(); @@ -382,23 +380,19 @@ ServerProcessed processServerEvent( : VectorDefinition(current.x, current.y + 1); final neighbor = cells[current] ?? TableCell(); - if (neighbor.objects.isNotEmpty) { allObjects.addAll(neighbor.objects); } cells[current] = neighbor.copyWith( merge: MergedCellStrategy(direction), - objects: [], ); } - - cells[event.cell.position] = cells[event.cell.position]!.copyWith( - objects: allObjects, - ); + newCell = newCell.copyWith(objects: allObjects); } } + cells[event.cell.position] = newCell; return table.copyWith.cellsBox(content: cells); }), diff --git a/app/lib/bloc/world/state.dart b/app/lib/bloc/world/state.dart index 9f5dd3b2..37252be6 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -77,10 +77,6 @@ final class ClientWorldState with ClientWorldStateMappable { bool isCellSelected(VectorDefinition cell) { if (selectedCell == cell) return true; - return selectedCell != null && - getParentCell( - GlobalVectorDefinition.fromLocal(tableName, selectedCell!), - ) == - cell; + return selectedCell != null && getLocalParentCell(selectedCell!) == cell; } } diff --git a/app/lib/board/cell.dart b/app/lib/board/cell.dart index 5fd55e2e..82094ce6 100644 --- a/app/lib/board/cell.dart +++ b/app/lib/board/cell.dart @@ -40,7 +40,6 @@ class GameCell extends PositionComponent FlameBlocListenable, ScrollCallbacks { late final NineTileBoxComponent _selectionComponent; - SpriteComponent? _boardComponent; TextElementComponent? _waypointComponent; GameBoardBackground? _backgroundComponent; late final BoardGrid grid; @@ -267,9 +266,8 @@ class GameCell extends PositionComponent size = Vector2.zero(); _backgroundComponent?.size = Vector2.zero(); _selectionComponent.size = Vector2.zero(); - _boardComponent?.removeFromParent(); - _boardComponent = null; removeWhere((e) => e is _GameCellObjectComponent); + removeWhere((e) => e is _GameCellTileComponent); _currentObjects = null; _currentStrategy = null; _currentSpan = null; @@ -295,7 +293,6 @@ class GameCell extends PositionComponent size = s; _backgroundComponent?.size = s; _selectionComponent.size = s; - _boardComponent?.size = s; priority = 100; } } else { @@ -304,7 +301,6 @@ class GameCell extends PositionComponent size = grid.cellSize; _backgroundComponent?.size = size; _selectionComponent.size = size; - _boardComponent?.size = size; priority = 0; } } @@ -322,19 +318,53 @@ class GameCell extends PositionComponent _currentVisible = visible; _currentTile = tile; final paint = Paint()..isAntiAlias = false; - if (tile != null) { - final component = _boardComponent ??= SpriteComponent( - size: size, - paint: paint, - ); + + removeWhere((e) => e is _GameCellTileComponent); + + if (newSpan != null && newSpan > 1 && strategy is LayoutCellMergeStrategy) { + final direction = strategy.direction; + var current = cellDefinition; + for (var i = 0; i < newSpan; i++) { + if (i > 0) { + current = direction == CellMergeDirection.horizontal + ? VectorDefinition(current.x + 1, current.y) + : VectorDefinition(current.x, current.y + 1); + } + + final targetCell = state.table.cells[current]; + final targetTile = targetCell?.tiles.lastOrNull; + + if (targetTile != null) { + final component = _GameCellTileComponent(paint: paint, priority: 0); + component.sprite = + await state.assetManager.loadBoardSprite( + targetTile.asset, + targetTile.tile, + ) ?? + game.blankSprite; + component.size = grid.cellSize; + + double tx = 0; + double ty = 0; + if (direction == CellMergeDirection.horizontal) { + tx = i * grid.cellSize.x; + } else { + ty = i * grid.cellSize.y; + } + component.position = Vector2(tx, ty); + add(component); + } + } + } else if (tile != null) { + final component = _GameCellTileComponent(paint: paint, priority: 0); component.sprite = await state.assetManager.loadBoardSprite(tile.asset, tile.tile) ?? game.blankSprite; - if (!component.isMounted) { - add(component); - } + component.size = grid.cellSize; + component.position = Vector2.zero(); + add(component); } else { - _boardComponent?.removeFromParent(); + // Clear if no tile } removeWhere((e) => e is _GameCellObjectComponent); if (objects.isEmpty) return; @@ -347,11 +377,13 @@ class GameCell extends PositionComponent }.toList(); final bool reverse; - final direction = cell?.merge?.direction; + final CellMergeDirection? direction; if (strategy is LayoutCellMergeStrategy) { reverse = strategy.reverse; + direction = strategy.direction; } else { reverse = false; + direction = null; } final renderObjects = displayObjects @@ -681,3 +713,7 @@ class GameCell extends PositionComponent class _GameCellObjectComponent extends SpriteComponent { _GameCellObjectComponent({super.paint, super.priority}); } + +class _GameCellTileComponent extends SpriteComponent { + _GameCellTileComponent({super.paint, super.priority}); +}