diff --git a/packages/tiled/lib/src/common/property.dart b/packages/tiled/lib/src/common/property.dart index 3360b27..b52e9ac 100644 --- a/packages/tiled/lib/src/common/property.dart +++ b/packages/tiled/lib/src/common/property.dart @@ -12,11 +12,10 @@ part of tiled; /// (default string is “”, default number is 0, default boolean is “false”, /// default color is #00000000, default file is “.” (the current file’s /// parent directory)) -class Property { +class Property { String name; PropertyType type; - // TODO(luan): support other property types - String value; + T value; Property({ required this.name, @@ -24,17 +23,192 @@ class Property { required this.value, }); - Property.parse(Parser parser) - : this( - name: parser.getString('name'), - type: parser.getPropertyType('type', defaults: PropertyType.string), - value: parser.getString('value'), + static Property parse(Parser parser) { + final name = parser.getString('name'); + final type = parser.getPropertyType('type', defaults: PropertyType.string); + + switch (type) { + case PropertyType.object: + return ObjectProperty( + name: name, + value: parser.getInt('value', defaults: 0), + ); + + case PropertyType.color: + return ColorProperty( + name: name, + value: parser.getColor('value', defaults: const Color(0x00000000)), + hexValue: parser.getString('value', defaults: '#00000000'), + ); + + case PropertyType.bool: + return BoolProperty( + name: name, + value: parser.getBool('value', defaults: false), + ); + + case PropertyType.float: + return FloatProperty( + name: name, + value: parser.getDouble('value', defaults: 0), + ); + + case PropertyType.int: + return IntProperty( + name: name, + value: parser.getInt('value', defaults: 0), + ); + + case PropertyType.file: + return FileProperty( + name: name, + value: parser.getString('value', defaults: '.'), + ); + + case PropertyType.string: + final value = parser.formatSpecificParsing((json) { + return json.getString('value', defaults: ''); + }, (xml) { + final attrString = parser.getStringOrNull('value'); + if (attrString != null) { + return attrString; + } else { + // In tmx files, multi-line text property values can be stored + // inside the node itself instead of in the 'value' + // attribute + return xml.element.innerText; + } + }); + + return StringProperty( + name: name, + value: value, ); + } + } +} + +/// A wrapper for a Tiled property set +/// +/// Accessing an int value +/// ```dart +/// properties.get('foo') == 3 +/// ``` +/// +/// Accessing an int property: +/// ```dart +/// properties.getProperty('foo') == +/// IntProperty(name: 'foo', value: 3); +/// ``` +/// +/// You can also use an accessor: +/// ```dart +/// properties['foo'] == IntProperty(name: 'foo', value: 3); +/// ``` +class CustomProperties extends Iterable> { + static const empty = CustomProperties({}); + + /// The properties, indexed by name + final Map> byName; + + const CustomProperties(this.byName); + + /// Get a property value by its name. + /// + /// [T] must be the type of the value, which depends on the property type. + /// The following Tiled properties map to the follow Dart types: + /// - int property -> int + /// - float property -> double + /// - boolean property -> bool + /// - string property -> string + /// - color property -> ui.Color + /// - file property -> string (path) + /// - object property -> int (ID) + T? getValue(String name) { + return getProperty(name)?.value as T?; + } + + /// Get a typed property by its name + T? getProperty>(String name) { + return byName[name] as T?; + } + + /// Get a property by its name + Property? operator [](String name) { + return byName[name]; + } + + /// Returns whether or not a property with [name] exists. + bool has(String name) { + return byName.containsKey(name); + } + + @override + Iterator> get iterator => byName.values.iterator; +} + +/// [value] is the ID of the object +class ObjectProperty extends Property { + ObjectProperty({ + required super.name, + required super.value, + }) : super(type: PropertyType.object); +} + +/// [value] is the color +class ColorProperty extends Property { + final String hexValue; + + ColorProperty({ + required super.name, + required super.value, + required this.hexValue, + }) : super(type: PropertyType.color); +} + +/// [value] is the string text +class StringProperty extends Property { + StringProperty({ + required super.name, + required super.value, + }) : super(type: PropertyType.string); +} + +/// [value] is the path to the file +class FileProperty extends Property { + FileProperty({ + required super.name, + required super.value, + }) : super(type: PropertyType.file); +} + +/// [value] is the integer number +class IntProperty extends Property { + IntProperty({ + required super.name, + required super.value, + }) : super(type: PropertyType.int); +} + +/// [value] is the double-percision floating-point number +class FloatProperty extends Property { + FloatProperty({ + required super.name, + required super.value, + }) : super(type: PropertyType.float); +} + +/// [value] is the boolean +class BoolProperty extends Property { + BoolProperty({ + required super.name, + required super.value, + }) : super(type: PropertyType.bool); } extension PropertiesParser on Parser { - List getProperties() { - return formatSpecificParsing( + CustomProperties getProperties() { + final properties = formatSpecificParsing( (json) => json.getChildrenAs('properties', Property.parse), (xml) => xml @@ -42,5 +216,14 @@ extension PropertiesParser on Parser { ?.getChildrenAs('property', Property.parse) ?? [], ); + + // NOTE: two properties should never have the same name, if they do + // one will simply override the other + final byName = + properties.groupFoldBy((prop) => prop.name, (previous, element) { + return element; + }); + + return CustomProperties(byName); } } diff --git a/packages/tiled/lib/src/layer.dart b/packages/tiled/lib/src/layer.dart index 4a73de1..ccff08d 100644 --- a/packages/tiled/lib/src/layer.dart +++ b/packages/tiled/lib/src/layer.dart @@ -90,7 +90,7 @@ abstract class Layer { bool visible; /// List of [Property]. - List properties; + CustomProperties properties; Layer({ this.id, @@ -109,7 +109,7 @@ abstract class Layer { this.tintColor, this.opacity = 1, this.visible = true, - this.properties = const [], + this.properties = CustomProperties.empty, }); static Layer parse(Parser parser) { diff --git a/packages/tiled/lib/src/objects/tiled_object.dart b/packages/tiled/lib/src/objects/tiled_object.dart index 239b1bb..8cf4ce8 100644 --- a/packages/tiled/lib/src/objects/tiled_object.dart +++ b/packages/tiled/lib/src/objects/tiled_object.dart @@ -65,7 +65,7 @@ class TiledObject { List polygon; List polyline; - List properties; + CustomProperties properties; /// The "Class" property, a.k.a "Type" prior to Tiled 1.9. /// Will be same as [type]. @@ -89,7 +89,7 @@ class TiledObject { this.visible = true, this.polygon = const [], this.polyline = const [], - this.properties = const [], + this.properties = CustomProperties.empty, }); bool get isPolyline => polyline.isNotEmpty; diff --git a/packages/tiled/lib/src/tiled_map.dart b/packages/tiled/lib/src/tiled_map.dart index e1a4e29..7e2edf7 100644 --- a/packages/tiled/lib/src/tiled_map.dart +++ b/packages/tiled/lib/src/tiled_map.dart @@ -89,7 +89,7 @@ class TiledMap { RenderOrder renderOrder; List editorSettings; - List properties; + CustomProperties properties; // only for hexagonal maps: int? hexSideLength; @@ -118,7 +118,7 @@ class TiledMap { this.staggerAxis, this.staggerIndex, this.editorSettings = const [], - this.properties = const [], + this.properties = CustomProperties.empty, }); /// Takes a string [contents] and converts it to a [TiledMap] with the help of diff --git a/packages/tiled/lib/src/tileset/terrain.dart b/packages/tiled/lib/src/tileset/terrain.dart index c27ad4e..4c78940 100644 --- a/packages/tiled/lib/src/tileset/terrain.dart +++ b/packages/tiled/lib/src/tileset/terrain.dart @@ -12,12 +12,12 @@ part of tiled; class Terrain { String name; int tile; - List properties; + CustomProperties properties; Terrain({ required this.name, required this.tile, - this.properties = const [], + this.properties = CustomProperties.empty, }); Terrain.parse(Parser parser) diff --git a/packages/tiled/lib/src/tileset/tile.dart b/packages/tiled/lib/src/tileset/tile.dart index 1addaa4..9a925de 100644 --- a/packages/tiled/lib/src/tileset/tile.dart +++ b/packages/tiled/lib/src/tileset/tile.dart @@ -29,7 +29,7 @@ class Tile { TiledImage? image; Layer? objectGroup; List animation; - List properties; + CustomProperties properties; Tile({ required this.localId, @@ -39,7 +39,7 @@ class Tile { this.image, this.objectGroup, this.animation = const [], - this.properties = const [], + this.properties = CustomProperties.empty, }); bool get isEmpty => localId == -1; diff --git a/packages/tiled/lib/src/tileset/tileset.dart b/packages/tiled/lib/src/tileset/tileset.dart index bdaa1bc..dd27510 100644 --- a/packages/tiled/lib/src/tileset/tileset.dart +++ b/packages/tiled/lib/src/tileset/tileset.dart @@ -60,7 +60,7 @@ class Tileset { TiledImage? image; TileOffset? tileOffset; Grid? grid; - List properties = []; + CustomProperties properties; List terrains = []; List wangSets = []; @@ -85,7 +85,7 @@ class Tileset { this.image, this.tileOffset, this.grid, - this.properties = const [], + this.properties = CustomProperties.empty, this.terrains = const [], this.wangSets = const [], this.version = '1.0', @@ -185,7 +185,7 @@ class Tileset { tileWidth = tileset.tileWidth ?? tileWidth; transparentColor = tileset.transparentColor ?? transparentColor; // Add List-Attributes - properties.addAll(tileset.properties); + properties.byName.addAll(tileset.properties.byName); terrains.addAll(tileset.terrains); tiles.addAll(tileset.tiles); wangSets.addAll(tileset.wangSets); diff --git a/packages/tiled/lib/src/tileset/wang/wang_color.dart b/packages/tiled/lib/src/tileset/wang/wang_color.dart index f8ef3f0..c5661f3 100644 --- a/packages/tiled/lib/src/tileset/wang/wang_color.dart +++ b/packages/tiled/lib/src/tileset/wang/wang_color.dart @@ -19,14 +19,14 @@ class WangColor { int tile; double probability; - List properties; + CustomProperties properties; WangColor({ required this.name, required this.color, required this.tile, this.probability = 0, - this.properties = const [], + this.properties = CustomProperties.empty, }); WangColor.parse(Parser parser) diff --git a/packages/tiled/lib/src/tileset/wang/wang_set.dart b/packages/tiled/lib/src/tileset/wang/wang_set.dart index c87bb54..4a25355 100644 --- a/packages/tiled/lib/src/tileset/wang/wang_set.dart +++ b/packages/tiled/lib/src/tileset/wang/wang_set.dart @@ -21,7 +21,7 @@ class WangSet { List cornerColors; List edgeColors; List wangTiles; - List properties; + CustomProperties properties; WangSet({ required this.name, @@ -29,7 +29,7 @@ class WangSet { this.cornerColors = const [], this.edgeColors = const [], this.wangTiles = const [], - this.properties = const [], + this.properties = CustomProperties.empty, }); factory WangSet.parse(Parser parser) { diff --git a/packages/tiled/test/fixtures/test.tmx b/packages/tiled/test/fixtures/test.tmx index 10bd60e..bcadba0 100644 --- a/packages/tiled/test/fixtures/test.tmx +++ b/packages/tiled/test/fixtures/test.tmx @@ -2,7 +2,15 @@ - + + Hello, +World + + + + + + diff --git a/packages/tiled/test/map_test.dart b/packages/tiled/test/map_test.dart index 3525347..bad9a42 100644 --- a/packages/tiled/test/map_test.dart +++ b/packages/tiled/test/map_test.dart @@ -41,13 +41,12 @@ void main() { Tile(localId: 0), Tile( localId: 2, - properties: [ - Property( + properties: CustomProperties({ + 'name': StringProperty( name: 'name', - type: PropertyType.string, - value: 'value', + value: 'tile2-prop-value', ), - ], + }), ), ], ), @@ -66,7 +65,11 @@ void main() { final tile = map.tileByGid(7); expect(tile?.localId, equals(2)); - expect(tile?.properties.first.name, equals('name')); + expect(tile?.properties['name'], isA()); + expect( + tile?.properties.getValue('name'), + equals('tile2-prop-value'), + ); }); }); diff --git a/packages/tiled/test/parser_test.dart b/packages/tiled/test/parser_test.dart index bae27cb..ae22cf0 100644 --- a/packages/tiled/test/parser_test.dart +++ b/packages/tiled/test/parser_test.dart @@ -79,11 +79,42 @@ void main() { }); group('populates its properties correctly and', () { - late List properties; + late CustomProperties properties; setUp(() => properties = tileset.properties); test('has a key of "test_property" = "test_value"', () { - expect(properties[0].name, equals('test_property')); - expect(properties[0].value, equals('test_value')); + expect(properties.first.name, equals('string property')); + expect( + properties.getValue('string property'), + equals('test_value'), + ); + expect( + properties.getValue('multiline string'), + equals('Hello,\nWorld'), + ); + expect( + properties.getValue('integer property'), + equals(42), + ); + expect( + properties.getProperty('color property')!.hexValue, + equals('#00112233'), + ); + expect( + properties.getValue('color property'), + equals(const Color(0x00112233)), + ); + expect( + properties.getValue('float property'), + equals(1.56), + ); + expect( + properties.getValue('file property'), + equals('./icons.png'), + ); + expect( + properties.getValue('object property'), + equals(32), + ); }); }); @@ -105,18 +136,26 @@ void main() { }); group('populates its child tile properties correctly by', () { - late List tile1Properties; - late List tile2Properties; + late CustomProperties tile1Properties; + late CustomProperties tile2Properties; setUp(() { tile1Properties = tileset.tiles[0].properties; tile2Properties = tileset.tiles[1].properties; }); test('inserting properties into tileProperties based on Tile GID', () { - expect(tile1Properties[0].name, equals('tile_0_property_name')); - expect(tile1Properties[0].value, equals('tile_0_property_value')); - expect(tile2Properties[0].name, equals('tile_1_property_name')); - expect(tile2Properties[0].value, equals('tile_1_property_value')); + expect( + tile1Properties.has('tile_0_property_name'), + isTrue, + ); + expect( + tile1Properties.getValue('tile_0_property_name'), + equals('tile_0_property_value'), + ); + expect( + tile2Properties.getValue('tile_1_property_name'), + equals('tile_1_property_value'), + ); }); }); }); diff --git a/packages/tiled/test/tile_test.dart b/packages/tiled/test/tile_test.dart index bc91e68..9472b1c 100644 --- a/packages/tiled/test/tile_test.dart +++ b/packages/tiled/test/tile_test.dart @@ -23,6 +23,6 @@ void main() { test('Tile.properties is present', () { final tile = Tile(localId: -1); - expect(tile.properties, isA()); + expect(tile.properties, isA()); }); } diff --git a/packages/tiled/test/tileset_test.dart b/packages/tiled/test/tileset_test.dart index cd94163..ad659e4 100644 --- a/packages/tiled/test/tileset_test.dart +++ b/packages/tiled/test/tileset_test.dart @@ -17,7 +17,7 @@ void main() { test('spacing == 0', () => expect(tileset.spacing, equals(0))); test('margin == 0', () => expect(tileset.margin, equals(0))); test('tileProperties == {}', () { - expect(tileset.properties, equals([])); + expect(tileset.properties.byName, equals({})); }); }); diff --git a/packages/tiled/test/tmx_object_test.dart b/packages/tiled/test/tmx_object_test.dart index 4e3a244..8579ed6 100644 --- a/packages/tiled/test/tmx_object_test.dart +++ b/packages/tiled/test/tmx_object_test.dart @@ -48,8 +48,11 @@ void main() { test('sets properties', () { final props = tiledObject.properties; - expect(props[0].name, equals('property_name')); - expect(props[0].value, equals('property_value')); + expect(props.first.name, equals('property_name')); + expect( + props.getValue('property_name'), + equals('property_value'), + ); }); test('sets isEllipse to true', () => expect(tiledObject.ellipse, isTrue));