diff --git a/api/lib/src/event/event.mapper.dart b/api/lib/src/event/event.mapper.dart index e83a8c72..63a3c2fd 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,197 @@ 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, + def: 1, + ); + + @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, + int? span, + }) => $apply( + FieldCopyWithData({ + if (cell != null) #cell: cell, + if (strategy != $none) #strategy: strategy, + if (span != null) #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..51ee3f60 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 = 1}); +} + @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..c801aaac 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -57,6 +57,8 @@ bool isValidServerEvent(ServerWorldEvent event, WorldState state) => .length - 1, ), + CellMergeStrategyChanged() => + event.span > 0 && event.span <= GameTable.maxMergeSpan, DialogOpened() => event.dialog.isValid(), _ => true, }; @@ -313,6 +315,88 @@ ServerProcessed processServerEvent( teamMembers: Map.from(state.teamMembers)..remove(event.team), ), ); + 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) { + if (strategy is LayoutCellMergeStrategy) 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 = table.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) { + final updatedNeighbor = neighbor.copyWith(merge: null); + if (updatedNeighbor.isEmpty) { + cells.remove(current); + } else { + cells[current] = updatedNeighbor; + } + } + } + } + } + + // Update target cell + var newCell = cell.copyWith(merge: event.strategy); + + // Expand new neighbors + + final strategy = event.strategy; + + final span = event.span; + + if (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); + + final neighbor = cells[current] ?? TableCell(); + if (neighbor.objects.isNotEmpty) { + allObjects.addAll(neighbor.objects); + } + + cells[current] = neighbor.copyWith( + merge: MergedCellStrategy(direction), + objects: [], + ); + } + newCell = newCell.copyWith(objects: allObjects); + } + } + cells[event.cell.position] = newCell; + + 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..1d09917a 100644 --- a/api/lib/src/event/state.dart +++ b/api/lib/src/event/state.dart @@ -124,4 +124,20 @@ final class WorldState with WorldStateMappable { String name, GameTable Function(GameTable) mapper, ) => updateTable(name, mapper(getTableOrDefault(name))); + + int calculateLocalSpan( + VectorDefinition start, + CellMergeDirection direction, + ) => table.calculateSpan(start, direction); + + 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/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 1d24b31b..416fd7c4 100644 --- a/api/lib/src/models/cell.dart +++ b/api/lib/src/models/cell.dart @@ -8,10 +8,11 @@ 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; + bool get isEmpty => objects.isEmpty && tiles.isEmpty && merge == null; } @MappableClass() @@ -30,3 +31,52 @@ class BoardTile with BoardTileMappable { BoardTile(this.asset, this.tile); } + +@MappableEnum() +enum CellMergeDirection { horizontal, vertical } + +@MappableClass() +sealed class CellMergeStrategy with CellMergeStrategyMappable { + 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 LayoutCellMergeStrategy + with StackedCellMergeStrategyMappable { + final int visiblePercentage; + + const StackedCellMergeStrategy({ + this.visiblePercentage = 10, + super.reverse, + super.direction, + }); +} + +@MappableClass() +final class DistributeCellMergeStrategy extends LayoutCellMergeStrategy + with DistributeCellMergeStrategyMappable { + final int maxCards; + final bool fillVariableSpace; + + const DistributeCellMergeStrategy({ + this.maxCards = 5, + this.fillVariableSpace = true, + super.reverse, + super.direction = CellMergeDirection.horizontal, + }); +} diff --git a/api/lib/src/models/cell.mapper.dart b/api/lib/src/models/cell.mapper.dart index e1263898..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 @@ -438,3 +512,657 @@ 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._()); + MergedCellStrategyMapper.ensureInitialized(); + LayoutCellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.ensureInitialized(); + } + return _instance!; + } + + @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 { + #direction: _f$direction, + }; + + 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({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._(); + + static StackedCellMergeStrategyMapper? _instance; + static StackedCellMergeStrategyMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use( + _instance = StackedCellMergeStrategyMapper._(), + ); + LayoutCellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.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, + ); + 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), + ); + } + + @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 LayoutCellMergeStrategyCopyWith<$R, $In, $Out> { + @override + $R call({ + int? visiblePercentage, + bool? reverse, + CellMergeDirection? direction, + }); + 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, + CellMergeDirection? direction, + }) => $apply( + FieldCopyWithData({ + if (visiblePercentage != null) #visiblePercentage: visiblePercentage, + if (reverse != null) #reverse: reverse, + if (direction != null) #direction: direction, + }), + ); + @override + StackedCellMergeStrategy $make(CopyWithData data) => StackedCellMergeStrategy( + visiblePercentage: data.get( + #visiblePercentage, + or: $value.visiblePercentage, + ), + reverse: data.get(#reverse, or: $value.reverse), + direction: data.get(#direction, or: $value.direction), + ); + + @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._(), + ); + LayoutCellMergeStrategyMapper.ensureInitialized(); + CellMergeDirectionMapper.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 _$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', + _$reverse, + opt: true, + def: false, + ); + 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, + #fillVariableSpace: _f$fillVariableSpace, + #reverse: _f$reverse, + #direction: _f$direction, + }; + + static DistributeCellMergeStrategy _instantiate(DecodingData data) { + return DistributeCellMergeStrategy( + maxCards: data.dec(_f$maxCards), + fillVariableSpace: data.dec(_f$fillVariableSpace), + reverse: data.dec(_f$reverse), + direction: data.dec(_f$direction), + ); + } + + @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 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, + ); +} + +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? fillVariableSpace, + bool? reverse, + CellMergeDirection? direction, + }) => $apply( + FieldCopyWithData({ + if (maxCards != null) #maxCards: maxCards, + 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), + fillVariableSpace: data.get( + #fillVariableSpace, + or: $value.fillVariableSpace, + ), + reverse: data.get(#reverse, or: $value.reverse), + direction: data.get(#direction, or: $value.direction), + ); + + @override + DistributeCellMergeStrategyCopyWith<$R2, DistributeCellMergeStrategy, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _DistributeCellMergeStrategyCopyWithImpl<$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.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/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..37252be6 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -60,4 +60,23 @@ final class ClientWorldState with ClientWorldStateMappable { String? get name => world.name; FileMetadata get metadata => world.metadata; SetonixData get data => world.data; + + int calculateLocalSpan( + VectorDefinition start, + CellMergeDirection direction, + ) => world.calculateLocalSpan(start, direction); + int calculateSpan( + GlobalVectorDefinition start, + CellMergeDirection direction, + ) => world.calculateSpan(start, direction); + + 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 && getLocalParentCell(selectedCell!) == cell; + } } 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..82094ce6 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,9 @@ class GameCell extends PositionComponent HandItemDropZone, FlameBlocListenable, ScrollCallbacks { - late final SpriteComponent _selectionComponent; - SpriteComponent? _cardComponent, _boardComponent; + late final NineTileBoxComponent _selectionComponent; TextElementComponent? _waypointComponent; + GameBoardBackground? _backgroundComponent; late final BoardGrid grid; List? _effects; @@ -127,9 +129,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, ); @@ -139,8 +142,8 @@ 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 || @@ -148,7 +151,7 @@ class GameCell extends PositionComponent previousState.showWaypoints != newState.showWaypoints; } - bool get isSelected => isMounted && bloc.state.selectedCell == toDefinition(); + bool get isSelected => isMounted && bloc.state.isCellSelected(toDefinition()); void _fadeIn() => _updateEffects([OpacityEffect.fadeIn(EffectController(duration: 0.2))]); @@ -214,13 +217,14 @@ 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.isCellSelected(toDefinition()); final color = isClaimed(state) ? isAllowed(state) ? state.colorScheme.secondary @@ -245,63 +249,263 @@ 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(); + removeWhere((e) => e is _GameCellObjectComponent); + removeWhere((e) => e is _GameCellTileComponent); + _currentObjects = null; + _currentStrategy = null; + _currentSpan = null; + _currentTile = null; + } + return; + } + + int? newSpan; + if (strategy is LayoutCellMergeStrategy) { + final direction = strategy is StackedCellMergeStrategy + ? strategy.direction + : (strategy as DistributeCellMergeStrategy).direction; + final span = state.calculateLocalSpan(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; + priority = 100; + } + } else { + _currentVisible = true; + if (size != grid.cellSize) { + size = grid.cellSize; + _backgroundComponent?.size = size; + _selectionComponent.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; - 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 } - if (top != null) { - final component = _cardComponent ??= SpriteComponent( - paint: paint, - priority: 1, + 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; + final CellMergeDirection? direction; + if (strategy is LayoutCellMergeStrategy) { + reverse = strategy.reverse; + direction = strategy.direction; + } else { + reverse = false; + direction = null; + } + + 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 double x, y; + switch (strategy) { + case StackedCellMergeStrategy( + visiblePercentage: final visiblePercentage, + ): + 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( + 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 = (i - (count - 1) / 2.0) * 0.4; + } + + 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; + } + + final cRect = Rect.fromCenter( + center: Offset(x, y), + width: grid.cellSize.x, + height: grid.cellSize.y, ); - component - ..anchor = Anchor.center - ..position = size / 2; - component.sprite = - await state.assetManager.loadFigureSprite( - top.asset, - top.hidden || !state.isCellVisible(toGlobalDefinition(state)) - ? null - : top.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); + + // 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; - } - if (!component.isMounted) { + 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 LayoutCellMergeStrategy && 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; + } + } } - } else { - _cardComponent?.removeFromParent(); } } @@ -359,6 +563,34 @@ 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 LayoutCellMergeStrategy) { + return bloc.state.calculateLocalSpan( + toDefinition(), + strategy.direction, + ); + } + return 1; + }(), + ), + ), + ); + }, + ), ContextMenuButtonItem( label: AppLocalizations.of(context).remove, onPressed: () { @@ -477,3 +709,11 @@ class GameCell extends PositionComponent return false; } } + +class _GameCellObjectComponent extends SpriteComponent { + _GameCellObjectComponent({super.paint, super.priority}); +} + +class _GameCellTileComponent extends SpriteComponent { + _GameCellTileComponent({super.paint, super.priority}); +} diff --git a/app/lib/board/grid.dart b/app/lib/board/grid.dart index b5777a43..821bdae7 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.getLocalParentCell(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; } 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"