Merge pull request #7 from DungeonPaper/v3.0

V3.0
This commit is contained in:
Chen Asraf
2023-09-07 20:42:22 +03:00
committed by GitHub
82 changed files with 20048 additions and 8860 deletions

5
.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

26
.gitignore vendored
View File

@@ -1,3 +1,9 @@
#========================================================================
# Dart
#========================================================================
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
@@ -9,18 +15,20 @@ pubspec.lock
# If you don't generate documentation locally you can remove this line.
doc/api/
# dotenv environment variables file
.env*
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
# extras
doc/
.dart_tool/pub/bin/test
output.dart
npm_package/
npm_package/package.json
.flutter-plugins
.flutter-plugins-dependencies
web/dw_data.json
.DS_Store

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"printWidth": 100,
"semi": false,
"singleQuote": false,
"trailingComma": "all"
}

View File

@@ -1 +0,0 @@
language: dart

61
.vscode/launch.json vendored
View File

@@ -4,28 +4,41 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "All Tests",
"type": "dart",
"request": "launch",
"program": "test/"
},
{
"name": "Main",
"program": "bin/dw_data.dart",
"request": "launch",
"type": "dart"
},
{
"name": "Example",
"program": "example/example.dart",
"request": "launch",
"type": "dart"
},
{
"name": "Dump Dart",
"program": "scripts/dump/dump.dart",
"request": "launch",
"type": "dart"
}, ]
{
"name": "Dump json -> dart",
"type": "dart",
"request": "launch",
"program": "scripts/parsers/json_to_dart.dart"
},
{
"name": "Dump dart -> json",
"type": "dart",
"request": "launch",
"program": "scripts/parsers/dart_to_json.dart"
},
{
"name": "All Tests",
"type": "dart",
"request": "launch",
"program": "test/"
},
{
"name": "Main",
"program": "bin/dw_data.dart",
"request": "launch",
"type": "dart"
},
{
"name": "Example",
"program": "example/example.dart",
"request": "launch",
"type": "dart"
},
{
"name": "Dump Dart",
"program": "scripts/dump/dump.dart",
"request": "launch",
"type": "dart"
},
]
}

150
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,150 @@
{
"cSpell.words": [
"aboleth",
"alflings",
"ankheg",
"arcanist",
"arcanists",
"aronwe",
"aswang",
"avra'hal",
"backstab",
"bakunawa",
"barkskin",
"battlemoore",
"berserkers",
"blech",
"bloodlust",
"bloodwarrior",
"bloodweed",
"braaaaaains",
"brightspear",
"bucksberg",
"bulette",
"burrin",
"cephalo",
"chainmail",
"chuul",
"cloaker",
"counterspell",
"coutal",
"crickety",
"croc",
"cthonic",
"cullaina",
"demonhood",
"derro",
"diabolist",
"dragonbone",
"draugr",
"duelistss",
"dwarven",
"ekek",
"elven",
"enkindler",
"ensorcelled",
"ettin",
"exterminatus",
"fjornnvald",
"formcrafter",
"formian",
"formians",
"formshaper",
"girallon",
"gnoll",
"gnolls",
"goldenroot",
"golemist",
"halfling",
"halflings",
"helferth",
"hordeland",
"horselords",
"hospitaller",
"housecat",
"hungerer",
"hurlant",
"hwyn",
"insectoids",
"ironflanks",
"ironscale",
"jahad",
"krugon",
"lescia",
"lich",
"liches",
"lizardman",
"lizardmen",
"lorekeepers",
"machano",
"magicks",
"magmin",
"majilis",
"manticore",
"mechano",
"mirrorshield",
"mohrg",
"multiclass",
"namiah",
"nightwing",
"nightwings",
"nordemark",
"ntreants",
"ogdru",
"orcish",
"orkaster",
"otyugh",
"overgod",
"owlbear",
"ozruk",
"packmates",
"pipeleaf",
"pipeleafs",
"pyractomena",
"quasit",
"regnus",
"rockslide",
"sahuagin",
"shadowhunter",
"shambler",
"shamblers",
"shapeshifted",
"shapeshifter",
"shapeshifting",
"sigben",
"smaug",
"smaugs",
"spellbook",
"spellcasting",
"spiderlord",
"stormhoof",
"sucellus",
"summonings",
"svenloff",
"swordmaster",
"swordmasters",
"tagit",
"tarrasque",
"thak",
"thuu",
"tidecaller",
"tidecallers",
"travelled",
"traveller",
"treant",
"treants",
"trogs",
"unlife",
"victis",
"warchief",
"warfleet",
"warhammer",
"whitemane",
"willem",
"worg",
"xorn",
"xorn",
"zorica",
"zuko",
]
}

49
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,49 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "dart",
"command": "dart",
"cwd": "",
"args": [
"doc",
"."
],
"problemMatcher": [],
"label": "dart: dart doc .",
"detail": ""
},
{
"label": "Auto-fix all fixable problems",
"type": "dart",
"command": "dart",
"detail": "dart fix --apply",
"args": [
"fix",
"--apply"
],
"problemMatcher": []
},
{
"label": "npm publish",
"type": "shell",
"command": "npm publish -d ../build",
"dependsOn": [
"npm build",
],
"options": {
"cwd": "./web/src"
},
"problemMatcher": []
},
{
"label": "npm build",
"type": "shell",
"command": "yarn build",
"options": {
"cwd": "./web/src"
},
"problemMatcher": []
},
]
}

239
CHANGELOG.md Normal file → Executable file
View File

@@ -1,132 +1,187 @@
# v2.0.5+8
* Unified "Cast A Spell" from separate Cleric/Wizard moves to one move
# CHANGELOG
# v2.0.5+7
* Added modifier to `Dice.toString()` (e.g. 2d6+3)
## v2.0.5-nullsafety
# v2.0.5+6
* Fixed Barbarian move "Khan Of Khans"
- Improve types
# v2.0.5+5
* Fixed Barbarian advanced moves
## v2.0.5+10
# v2.0.5+4
* Added convenience methods to Dice
- Fix cleric move "Deity" description
# v2.0.5+3
* Added `getByKey` helper method to DWEntity
* Added modifiers to Dice
## v2.0.5+9
# v2.0.5+2
* Doc updates
- Reverted: Unified "Cast A Spell" from separate Cleric/Wizard moves to one move
# v2.0.5+1
* Doc updates
## v2.0.5+8
# v2.0.5
* DW data updates/fixes
- Unified "Cast A Spell" from separate Cleric/Wizard moves to one move
# v2.0.4
* File structure changes
* Bugfixes
## v2.0.5+7
# v2.0.3+2
* Bugfix in exporting spell tags
- Added modifier to `Dice.toString()` (e.g. 2d6+3)
# v2.0.3+1
* Bugfix in player classes advanced move
## v2.0.5+6
# v2.0.2
* Bugfix in tag parsing
- Fixed Barbarian move "Khan Of Khans"
# v2.0.1
* Implemented base entity equality comparitor
* Moved source files to `lib/src`, and selectively expose on `lib/` root.
* Added build steps to NPM package containing all the static data.
## v2.0.5+5
# v2.0.0+4
* Fixed player class export JSON keys
- Fixed Barbarian advanced moves
# v2.0.0+3
* Fixed exported alignment value
## v2.0.5+4
# v2.0.0+2
* Fixed exported alignment key
- Added convenience methods to Dice
# v2.0.0+1
* Updated code to improve static analysis score.
## v2.0.5+3
# v2.0.0
* The imported data is now not parsed from JSON, but directly declared in Dart.
* Better key generation
* The data has been reorganized to lists, and data redundancy has been removed.
- Added `getByKey` helper method to DWEntity
- Added modifiers to Dice
# v1.5.0
* Added `copy` method to every DWEntity subclass
* Unified public API for easier use
* Updated `key`s to use `Uuid().v4()` fallback
## v2.0.5+2
# v1.4.5
* Fixed case sensitivity in info tag parsing
* Improved tag parsing from starting gear options
- Doc updates
# v1.4.4
* Fixed tag toJSON method
## v2.0.5+1
# v1.4.3
* Fixed info tag mapping in main data
- Doc updates
# v1.4.2
* Updated tags to allow description
## v2.0.5
# v1.4.1
* Fixed Tags toString() behavior
- DW data updates/fixes
# v1.4.0
* Simplified Tags behavior. This will break some Tag usages
## v2.0.4
# v1.3.4
* Removed test dump file from source
- File structure changes
- Bugfixes
# v1.3.3
* Removed test package from dependencies
## v2.0.3+2
# v1.3.2
* Fixed 'looks' sections in some classes being concatenated and not split
* Added 2 tests
- Bugfix in exporting spell tags
# v1.3.1
* Added missing toJSON() on GearOption
## v2.0.3+1
# v1.3.0
* Updated gear choice and option mapping
- Bugfix in player classes advanced move
# v1.2.6
* Fixed tag JSON output in other classes
## v2.0.2
# v1.2.5
* Turned fields to non final, so they're changable for users
- Bugfix in tag parsing
# v1.2.4
* Fix JSON output for Equipment item
## v2.0.1
# v1.2.1-v1.2.3
* Tag bugfixes
- Implemented base entity equality comparitor
- Moved source files to `lib/src`, and selectively expose on `lib/` root.
- Added build steps to NPM package containing all the static data.
# v1.2.0
* Improved equipment loading (breaks interface for equipment items)
## v2.0.0+4
# v1.1.7
* Fixed spell loading
- Fixed player class export JSON keys
# v1.1.6
* Reduced min version of `meta` to increase compatibility
## v2.0.0+3
# v1.1.4, v1.1.5
* Added `key` property to some missing classes
- Fixed exported alignment value
# v1.1.2, v1.1.3
* Documentation updates
## v2.0.0+2
# v1.1.1
* Working release
- Fixed exported alignment key
## v2.0.0+1
- Updated code to improve static analysis score.
## v2.0.0
- The imported data is now not parsed from JSON, but directly declared in Dart.
- Better key generation
- The data has been reorganized to lists, and data redundancy has been removed.
## v1.5.0
- Added `copy` method to every DWEntity subclass
- Unified public API for easier use
- Updated `key`s to use `Uuid().v4()` fallback
## v1.4.5
- Fixed case sensitivity in info tag parsing
- Improved tag parsing from starting gear options
## v1.4.4
- Fixed tag toJSON method
## v1.4.3
- Fixed info tag mapping in main data
## v1.4.2
- Updated tags to allow description
## v1.4.1
- Fixed Tags toString() behavior
## v1.4.0
- Simplified Tags behavior. This will break some Tag usages
## v1.3.4
- Removed test dump file from source
## v1.3.3
- Removed test package from dependencies
## v1.3.2
- Fixed 'looks' sections in some classes being concatenated and not split
- Added 2 tests
## v1.3.1
- Added missing toJSON() on GearOption
## v1.3.0
- Updated gear choice and option mapping
## v1.2.6
- Fixed tag JSON output in other classes
## v1.2.5
- Turned fields to non final, so they're changable for users
## v1.2.4
- Fix JSON output for Equipment item
## v1.2.1-v1.2.3
- Tag bugfixes
## v1.2.0
- Improved equipment loading (breaks interface for equipment items)
## v1.1.7
- Fixed spell loading
## v1.1.6
- Reduced min version of `meta` to increase compatibility
## v1.1.4, v1.1.5
- Added `key` property to some missing classes
## v1.1.2, v1.1.3
- Documentation updates
## v1.1.1
- Working release

View File

@@ -1,7 +1,7 @@
# Defines a default set of lint rules enforced for
# projects at Google. For details and rationale,
# see https://github.com/dart-lang/pedantic#enabled-lints.
include: package:pedantic/analysis_options.yaml
include: package:lints/recommended.yaml
analyzer:
exclude:
- example/*.dart

View File

@@ -1,6 +1,6 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/dungeon_world_data.dart';
void main() {
print('Data version: ${dungeonWorld.version}');
print('Data version: ${dungeonWorldData.version}');
print('Please import this library and access `dungeonWorld`.');
}

View File

@@ -1,19 +0,0 @@
# targets:
# $default:
# builders:
# entrypoint:
# enabled: false
# generate_for:
# - web/package.json
# build_web_compilers|entrypoint:
# # enabled: false
# generate_for:
# - web/scripts/code.dart
# options:
# compiler: dart2js
# build_node_compilers|entrypoint:
# # enabled: false
# generate_for:
# - web/scripts/build.dart
# options:
# compiler: dart2js

View File

@@ -1,4 +1,4 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/_old/dw_data.dart';
void main() {
// Get bard class

View File

@@ -1,4 +1,4 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/_old/dw_data.dart';
void main() {
// Get inventory items

View File

@@ -1,4 +1,4 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/_old/dw_data.dart';
void main() {
// Get all monsters

View File

@@ -1,4 +1,4 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/_old/dw_data.dart';
void main() {
var bard = DWEntity.getByKey('bard', dungeonWorld.classes);

View File

@@ -1,4 +1,4 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/_old/dw_data.dart';
void main() {
// Get all spells

View File

@@ -1,4 +1,4 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/_old/dw_data.dart';
void main() {
// Parse tags from objects or strings

120
lib/_repository.dart Normal file
View File

@@ -0,0 +1,120 @@
import 'package:dungeon_world_data/dungeon_world_data.dart';
import 'package:meta/meta.dart';
class RepositoryItem<T> {
String? _currentLocale;
@protected
final _cache = <String, T>{};
RepositoryItem([String? initialLocale]) : _currentLocale = initialLocale {
if (_currentLocale != null) {
initLocale(_currentLocale!);
changeLocale(_currentLocale!);
}
}
String get currentLocale => _currentLocale!;
void initLocale(String locale) {
_cache[locale] ??= create();
}
void changeLocale(String locale) {
_currentLocale = locale;
}
T create() {
throw UnimplementedError();
}
void setForLocale(String locale, T item) {
_cache[locale] = item;
}
T getForLocale(String locale) => _cache[locale]!;
T get() => getForLocale(currentLocale);
}
class RepositoryList<T> extends RepositoryItem<List<T>> {
RepositoryList([String? initialLocale]) : super(initialLocale);
@override
List<T> create() => <T>[];
void addForLocale(String locale, T item) => _cache[locale]!.add(item);
void add(T item) => addForLocale(currentLocale, item);
}
class RepositoryMap<K, V> extends RepositoryItem<Map<K, V>> {
RepositoryMap([String? initialLocale]) : super(initialLocale);
@override
Map<K, V> create() => <K, V>{};
V getItemFor(String locale, String key) => _cache[locale]![key]!;
V getItem(String key) => getItemFor(currentLocale, key);
void addForLocale(String locale, K key, V item) => _cache[locale]![key] = item;
void add(K key, V item) => addForLocale(currentLocale, key, item);
void addAllForLocale(String locale, Map<K, V> items) => _cache[locale]!.addAll(items);
void addAll(Map<K, V> items) => addAllForLocale(currentLocale, items);
Iterable<V> get values => _cache[currentLocale]!.values;
Iterable<K> get keys => _cache[currentLocale]!.keys;
}
class DungeonWorldRepository extends RepositoryMap<String, dynamic> {
final version = '3.0.0';
DungeonWorldRepository([String? initialLocale]) : super(initialLocale);
@override
create() => <String, Map<String, dynamic>>{
'CharacterClasses': <String, CharacterClass>{},
'Items': <String, Item>{},
'Monsters': <String, Monster>{},
'Moves': <String, Move>{},
'Races': <String, Race>{},
'Spells': <String, Spell>{},
'Tags': <String, Tag>{},
};
Map<String, CharacterClass> get characterClasses =>
getItem('CharacterClasses').cast<String, CharacterClass>();
Map<String, Item> get items => getItem('Items').cast<String, Item>();
Map<String, Monster> get monsters => getItem('Monsters').cast<String, Monster>();
Map<String, Move> get moves => getItem('Moves').cast<String, Move>();
Map<String, Race> get races => getItem('Races').cast<String, Race>();
Map<String, Spell> get spells => getItem('Spells').cast<String, Spell>();
Map<String, Tag> get tags => getItem('Tags').cast<String, Tag>();
Map<String, dynamic> toJson() => toJsonAsMaps();
Map<String, dynamic> toJsonAsMaps() => {
"CharacterClasses": characterClasses.map(((key, value) => MapEntry(key, value.toJson()))),
"Items": items.map(((key, value) => MapEntry(key, value.toJson()))),
"Monsters": monsters.map(((key, value) => MapEntry(key, value.toJson()))),
"Moves": moves.map(((key, value) => MapEntry(key, value.toJson()))),
"Races": races.map(((key, value) => MapEntry(key, value.toJson()))),
"Spells": spells.map(((key, value) => MapEntry(key, value.toJson()))),
"Tags": tags.map(((key, value) => MapEntry(key, value.toJson()))),
};
Map<String, dynamic> toJsonAsLists() => {
"CharacterClasses": characterClasses.values.map(((value) => value.toJson())).toList(),
"Items": items.values.map(((value) => value.toJson())).toList(),
"Monsters": monsters.values.map(((value) => value.toJson())).toList(),
"Moves": moves.values.map(((value) => value.toJson())).toList(),
"Races": races.values.map(((value) => value.toJson())).toList(),
"Spells": spells.values.map(((value) => value.toJson())).toList(),
"Tags": tags.values.map(((value) => value.toJson())).toList(),
};
}

View File

@@ -1,32 +1,149 @@
import 'package:meta/meta.dart';
import 'dw_entity.dart';
import 'dart:convert';
class Alignment extends DWEntity {
/// Alignment name
String name;
import 'base.dart';
/// Alignment description
String description;
enum AlignmentType {
good,
lawful,
neutral,
chaotic,
evil,
}
/// Describes a character's alignment.
class Alignment with KeyMixin {
Alignment({
String key,
@required this.name,
@required this.description,
}) : super(key: key ?? DWEntity.generateKey(name));
required this.meta,
required this.type,
required this.description,
});
factory Alignment.fromJSON(Map map) => Alignment(
key: map['key'],
name: map['name'],
description: map['description'],
);
final dynamic meta;
/// The alignment being described
///
/// See [AlignmentType]
final AlignmentType type;
@override
Map toJSON() => {
'key': key,
'name': name,
'description': description,
String get key => type.name;
/// The alignment's description
final String description;
Alignment copyWith({
AlignmentType? type,
String? description,
}) =>
Alignment(
meta: meta,
type: type ?? this.type,
description: description ?? this.description,
);
factory Alignment.fromRawJson(String str) => Alignment.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Alignment.fromJson(Map<String, dynamic> json) => Alignment(
meta: json['_meta'],
type: AlignmentType.values.firstWhere((e) => e == json["key"]),
description: json["description"],
);
Map<String, dynamic> toJson() => {
"_meta": meta,
"key": key,
"description": description,
};
@override
Alignment copy() => Alignment.fromJSON(toJSON());
String get displayName => description;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Alignment &&
runtimeType == other.runtimeType &&
type == other.type &&
description == other.description;
@override
int get hashCode => Object.hashAll([type, description]);
String get debugProperties => 'type: $type, description: $description';
@override
String toString() => 'Alignment($debugProperties)';
}
class AlignmentValues {
AlignmentValues({
required this.good,
required this.evil,
required this.lawful,
required this.neutral,
required this.chaotic,
});
final String good;
final String evil;
final String lawful;
final String neutral;
final String chaotic;
AlignmentValues copyWith({
String? good,
String? evil,
String? lawful,
String? neutral,
String? chaotic,
}) =>
AlignmentValues(
good: good ?? this.good,
evil: evil ?? this.evil,
lawful: lawful ?? this.lawful,
neutral: neutral ?? this.neutral,
chaotic: chaotic ?? this.chaotic,
);
factory AlignmentValues.fromRawJson(String str) => AlignmentValues.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory AlignmentValues.fromJson(Map<String, dynamic> json) => AlignmentValues(
good: json["good"],
evil: json["evil"],
lawful: json["lawful"],
neutral: json["neutral"],
chaotic: json["chaotic"],
);
Map<String, dynamic> toJson() => {
"good": good,
"evil": evil,
"lawful": lawful,
"neutral": neutral,
"chaotic": chaotic,
};
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AlignmentValues &&
runtimeType == other.runtimeType &&
good == other.good &&
evil == other.evil &&
lawful == other.lawful &&
neutral == other.neutral &&
chaotic == other.chaotic;
@override
int get hashCode => Object.hashAll([good, evil, lawful, neutral, chaotic]);
String get debugProperties =>
'good: $good, evil: $evil, lawful: $lawful, neutral: $neutral, chaotic: $chaotic';
@override
String toString() => 'AlignmentValues($debugProperties)';
}

14
lib/base.dart Normal file
View File

@@ -0,0 +1,14 @@
import 'package:dungeon_world_data/dungeon_world_data.dart';
mixin KeyMixin {
/// This entity's unique key
abstract final String key;
String get displayName;
EntityReference get reference => EntityReference(
key: key,
name: displayName,
type: runtimeType.toString(),
);
}

168
lib/character_class.dart Normal file
View File

@@ -0,0 +1,168 @@
import 'dart:convert';
import 'alignment.dart';
import 'base.dart';
import 'dice.dart';
import 'gear_choice.dart';
/// Describes a Dungeon World character class
class CharacterClass with KeyMixin {
CharacterClass({
required this.meta,
required this.name,
required this.key,
required this.description,
required this.damageDice,
required this.load,
required this.hp,
required this.alignments,
required this.bonds,
required this.flags,
required this.gearChoices,
required this.isSpellcaster,
});
/// Dynamic metadata
final dynamic meta;
/// Class name, such as 'Wizard', 'Paladin'
final String name;
@override
final String key;
/// Class description
final String description;
/// Dice used for damage calculation
final Dice damageDice;
/// CLass's base load, which is added to the character's STR modifier to calculate Max Load
final int load;
/// CLass's base HP, which is added to the character's CON modifier to calculate Max HP
final int hp;
/// This class's set of alignments along with their descriptions
final AlignmentValues alignments;
/// This class's default bonds
final List<String> bonds;
/// This class's default flags
final List<String> flags;
/// This class's starting gear options
final List<GearChoice> gearChoices;
/// Whether this class is a Magic User
final bool isSpellcaster;
CharacterClass copyWith({
dynamic meta,
String? name,
String? key,
String? description,
Dice? damageDice,
int? load,
int? hp,
AlignmentValues? alignments,
List<String>? bonds,
List<String>? flags,
List<GearChoice>? gearChoices,
bool? isSpellcaster,
}) =>
CharacterClass(
meta: meta ?? this.meta,
name: name ?? this.name,
key: key ?? this.key,
description: description ?? this.description,
damageDice: damageDice ?? this.damageDice,
load: load ?? this.load,
hp: hp ?? this.hp,
alignments: alignments ?? this.alignments,
bonds: bonds ?? this.bonds,
flags: flags ?? this.flags,
gearChoices: gearChoices ?? this.gearChoices,
isSpellcaster: isSpellcaster ?? this.isSpellcaster,
);
factory CharacterClass.fromRawJson(String str) => CharacterClass.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory CharacterClass.fromJson(Map<String, dynamic> json) => CharacterClass(
meta: json['_meta'],
name: json["name"],
key: json["key"],
description: json["description"],
damageDice: Dice.fromJson(json["damageDice"]),
load: json["load"],
hp: json["hp"],
alignments: AlignmentValues.fromJson(json["alignments"]),
bonds: List<String>.from(json["bonds"]),
flags: List<String>.from(json["flags"]),
gearChoices: List<GearChoice>.from(
json["gearChoices"].map((x) => GearChoice.fromJson(x)),
),
isSpellcaster: json["isSpellcaster"] ?? false,
);
Map<String, dynamic> toJson() => {
"_meta": meta,
"name": name,
"key": key,
"description": description,
"damageDice": damageDice.toJson(),
"load": load,
"hp": hp,
"alignments": alignments.toJson(),
"bonds": List<dynamic>.from(bonds),
"flags": List<dynamic>.from(flags),
"gearChoices": List<dynamic>.from(gearChoices.map((x) => x.toJson())),
"isSpellcaster": isSpellcaster,
};
@override
String get displayName => name;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CharacterClass &&
runtimeType == other.runtimeType &&
meta == other.meta &&
name == other.name &&
key == other.key &&
description == other.description &&
damageDice == other.damageDice &&
load == other.load &&
hp == other.hp &&
alignments == other.alignments &&
bonds == other.bonds &&
flags == other.flags &&
gearChoices == other.gearChoices &&
isSpellcaster == other.isSpellcaster;
@override
int get hashCode => Object.hashAll([
meta,
name,
key,
description,
damageDice,
load,
hp,
alignments,
bonds,
flags,
gearChoices,
isSpellcaster
]);
String get debugProperties =>
'meta: $meta, name: $name, key: $key, description: $description, damageDice: $damageDice, load: $load, hp: $hp, alignments: $alignments, bonds: $bonds, flags: $flags, gearChoices: $gearChoices, isSpellcaster: $isSpellcaster';
@override
String toString() => 'CharacterClass($debugProperties)';
}

2960
lib/data/classes.dart Normal file

File diff suppressed because it is too large Load Diff

2098
lib/data/items.dart Normal file

File diff suppressed because it is too large Load Diff

1676
lib/data/monsters.dart Normal file

File diff suppressed because it is too large Load Diff

3544
lib/data/moves.dart Normal file

File diff suppressed because it is too large Load Diff

269
lib/data/races.dart Normal file
View File

@@ -0,0 +1,269 @@
import 'package:dungeon_world_data/race.dart';
List<Race> getRaceList() => [
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "c0193cdc-e318-44e5-8229-fbbaf3a98936",
"name": "Elf",
"description":
"When you enter an important location (your call) you can ask the GM for one fact from the history of that location.",
"explanation": "",
"classKeys": [
{"key": "bard", "name": "Bard", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "6ff63a18-529e-47d8-902e-8093808a9afa",
"name": "Human",
"description":
"When you first enter a civilized settlement someone who respects the custom of hospitality to minstrels will take you in as their guest.",
"explanation": "",
"classKeys": [
{"key": "bard", "name": "Bard", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "a300fc64-19b4-4615-98f1-87dea6048cf2",
"name": "Dwarf",
"description":
"You are one with stone. When you Commune you are also granted a special version of Words of the Unspeaking as a rote which only works on stone.",
"explanation": "",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "35a7f478-a172-48d4-904c-24235fac4425",
"name": "Human",
"description":
"Your faith is diverse. Choose one wizard spell. You can cast and be granted that spell as if it was a cleric spell.",
"explanation": "",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "4988b58e-8b1e-4204-a94c-e4416bfd0f7a",
"name": "Elf",
"description":
"The sap of the elder trees flows within you. In addition to any other attunements, the Great Forest is always considered your land.",
"explanation": "",
"classKeys": [
{"key": "druid", "name": "Druid", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "85bbb517-d3a3-4918-832c-bf2a428ab588",
"name": "Human",
"description":
"As your people learned to bind animals to field and farm, so too are you bound to them. You may always take the shape of any domesticated animal, in addition to your normal options.",
"explanation": "",
"classKeys": [
{"key": "druid", "name": "Druid", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "b3758f36-1b0e-4ddb-bdd8-ef00cf9b8295",
"name": "Halfling",
"description":
"You sing the healing songs of spring and brook. When you Make Camp, you and your allies heal +1d6.",
"explanation": "",
"classKeys": [
{"key": "druid", "name": "Druid", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "a545c44b-7a25-4224-82d0-1b33173e901b",
"name": "Dwarf",
"description":
"When you share a drink with someone, you may Parley with them using CON instead of CHA.",
"explanation": "",
"classKeys": [
{"key": "fighter", "name": "Fighter", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "57600856-b738-426e-bedd-a3175f824c00",
"name": "Elf",
"description":
"Choose one weapon-you can always treat weapons of that type as if they had the precise tag.",
"explanation": "",
"classKeys": [
{"key": "fighter", "name": "Fighter", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "ceeca68d-0e0e-480c-9a8e-a3b50bb4111a",
"name": "Halfling",
"description": "When you Defy Danger and use your small size to your advantage, take +1.",
"explanation": "",
"classKeys": [
{"key": "fighter", "name": "Fighter", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "2177dfa4-4b69-45b2-b495-041664015808",
"name": "Human",
"description":
"Once per battle you may reroll a single damage roll (yours or someone else's).",
"explanation": "",
"classKeys": [
{"key": "fighter", "name": "Fighter", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "faf9eb5f-3f91-4eb4-ba25-cc81e1c4379a",
"name": "Human",
"description":
"When you pray for guidance, even for a moment, and ask, \"What here is evil?\" the GM will tell you, honestly.",
"explanation": "",
"classKeys": [
{"key": "paladin", "name": "Paladin", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "c5f7bf31-ea00-4ec7-aa27-5e45e4898539",
"name": "Elf",
"description":
"When you Undertake a Perilous Journey through wilderness whatever job you take you succeed as if you rolled a 10+.",
"explanation": "",
"classKeys": [
{"key": "ranger", "name": "Ranger", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "83f43be2-e604-4930-9e28-12c62294bb32",
"name": "Human",
"description":
"When you Make Camp in a dungeon or city, you don't need to consume a ration.",
"explanation": "",
"classKeys": [
{"key": "ranger", "name": "Ranger", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "3fe3c5de-4f0a-44af-9c79-23955b1b33f4",
"name": "Halfling",
"description": "When you attack with a ranged weapon, deal +2 damage.",
"explanation": "",
"classKeys": [
{"key": "thief", "name": "Thief", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "a4eb5d9c-6f54-4418-bb0b-2368bb826c36",
"name": "Human",
"description":
"You are a professional. When you Spout Lore or Discern Realities about criminal activities, take +1.",
"explanation": "",
"classKeys": [
{"key": "thief", "name": "Thief", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "24d11979-8018-4a69-95a0-346f4bbcad5c",
"name": "Elf",
"description": "Magic is as natural as breath to you. Detect Magic is a cantrip for you.",
"explanation": "",
"classKeys": [
{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "0b0d4852-8757-410e-90ab-e1f29319bc2e",
"name": "Human",
"description": "Choose one Cleric spell. You can cast it as if it was a Wizard spell.",
"explanation": "",
"classKeys": [
{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "b303254e-631d-4b91-aa9b-caa9b7e1e406",
"name": "Salamander",
"description": "Non-magical heat and fire cannot harm you.",
"explanation": "",
"classKeys": [
{"key": "immolator", "name": "Immolator", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "8ac5f523-1005-4b14-a0e0-9436effdb2fc",
"name": "Human",
"description": "When you Make Camp next to a large, open flame, regain all of your HP.",
"explanation": "",
"classKeys": [
{"key": "immolator", "name": "Immolator", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Race.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "1a9183de-262a-4d17-8c15-c8b95f30834b",
"name": "Outsider",
"description":
"You may be elf, dwarf, halfling, or human, but you and your people are not from around here. At the beginning of each session, the GM will ask you something about your homeland, why you left, or what you left behind. If you answer them, mark XP.",
"explanation": "",
"classKeys": [{"key": "barbarian", "name": "Barbarian", "type": "CharacterClass"}],
"tags": [],
"dice": []
})
];

881
lib/data/spells.dart Normal file
View File

@@ -0,0 +1,881 @@
import 'package:dungeon_world_data/spell.dart';
List<Spell> getSpellList() => [
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "light",
"name": "Light",
"description":
"An item you touch glows with arcane light, about as bright as a torch. It gives off no heat or sound and requires no fuel, but it is otherwise like a mundane torch. You have complete control of the color of the flame. The spell lasts as long as it is in your presence.",
"explanation": "",
"level": "cantrip",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"},
{"key":
"wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "sanctify",
"name": "Sanctify",
"description":
"Food or water you hold in your hands while you cast this spell is consecrated by your deity. In addition to now being holy or unholy, the affected substance is purified of any mundane spoilage.",
"explanation": "",
"level": "rote",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "guidance",
"name": "Guidance",
"description":
"The symbol of your deity appears before you and gestures towards the direction or course of action your deity would have you take then disappears. The message is through gesture only; your communication through this spell is severely limited.",
"explanation": "",
"level": "rote",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "bless",
"name": "Bless",
"description":
"Your deity smiles upon a combatant of your choice. They take +1 ongoing so long as battle continues and they stand and fight. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "1",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "cure_light_wounds",
"name": "Cure Light Wounds",
"description":
"At your touch wounds scab and bones cease to ache. Heal an ally you touch of 1d8 damage.",
"explanation": "",
"level": "1",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": ["1d8"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "detect_alignment",
"name": "Detect Alignment",
"description":
"When you cast this spell choose an alignment: Good, Evil, Lawful, or Chaotic. One of your senses is briefly able to detect that alignment. The GM will tell you what here is of that alignment.",
"explanation": "",
"level": "1",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "cause_fear",
"name": "Cause Fear",
"description":
"Choose a target you can see and a nearby object. The target is afraid of the object so long as you maintain the spell. Their reaction is up to them: flee, panic, beg, fight. While this spell is ongoing you take -1 to cast a spell. You cannot target entities with less than animal intelligence (magical constructs, undead, automatons, and the like).",
"explanation": "",
"level": "1",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "magic_weapon",
"name": "Magic Weapon",
"description":
"The weapon you hold while casting does +1d4 damage until you dismiss this spell. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "1",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": ["1d4"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "sanctuary",
"name": "Sanctuary",
"description":
"As you cast this spell, you walk the perimeter of an area, consecrating it to your deity. As long as you stay within that area you are alerted whenever someone acts with malice within the sanctuary (including entering with harmful intent). Anyone who receives healing within a sanctuary heals +1d4 HP.",
"explanation": "",
"level": "1",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": ["1d4"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "speak_with_dead",
"name": "Speak With Dead",
"description":
"A corpse converses with you briefly. It will answer any three questions you pose to it to the best of the knowledge it had in life and the knowledge it gained in death.",
"explanation": "",
"level": "1",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "animate_dead",
"name": "Animate Dead",
"description":
"You invoke a hungry spirit to possess a recently-dead body and serve you. This creates a zombie that follows your orders to the best of its limited abilities. Treat the zombie as a character, but with access to only the basic moves. It has a +1 modifier for all stats and 1 HP. The zombie also gets your choice of 1d4 of these traits:\n\n* It's talented. Give one stat a +2 modifier.\n* It's durable. It has +2 HP for each level you have.\n* It has a functioning brain and can complete complex tasks.\n* It does not appear obviously dead, at least for a day or two.\nThe zombie lasts until it is destroyed by taking damage in excess of its HP, or until you end the spell. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "3",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": ["1d4"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "cure_moderate_wounds",
"name": "Cure Moderate Wounds",
"description":
"You staunch bleeding and set bones through magic. Heal an ally you touch of 2d8 damage.",
"explanation": "",
"level": "3",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": ["2d8"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "darkness",
"name": "Darkness",
"description":
"Choose an area you can see: it's filled with supernatural darkness and shadow. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "3",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "resurrection",
"name": "Resurrection",
"description":
"Tell the GM you would like to resurrect a corpse whose soul has not yet fully departed this world. Resurrection is always possible, but the GM will give you one or more (possibly all) of these conditions to fulfill:\n\n* It's going to take days/weeks/months\n* You must get help from ____\n* It will require a lot of money\n* You must sacrifice ____ to do it\nThe GM may, depending on the circumstances, allow you to resurrect the corpse now, with the understanding that the conditions must be met before it's permanent, or require you to meet the conditions before the corpse is resurrected.",
"explanation": "",
"level": "3",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "hold_person",
"name": "Hold Person",
"description":
"Choose a person you can see. Until you cast a spell or leave their presence they cannot act except to speak. This effect ends immediately if the target takes damage from any source.",
"explanation": "",
"level": "3",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "revelation",
"name": "Revelation",
"description":
"Your deity answers your prayers with a moment of perfect understanding. The GM will shed light on the current situation. When acting on the information, you take +1 forward.",
"explanation": "",
"level": "5",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "cure_critical_wounds",
"name": "Cure Critical Wounds",
"description": "Heal an ally you touch of 3d8 damage.",
"explanation": "",
"level": "5",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": ["3d8"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "divination",
"name": "Divination",
"description":
"Name a person, place, or thing you want to learn about. Your deity grants you visions of the target, as clear as if you were there.",
"explanation": "",
"level": "5",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "contagion",
"name": "Contagion",
"description":
"Choose a creature you can see. Until you end this spell, the target suffers from a disease of your choice. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "5",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "words_of_the_unspeaking",
"name": "Words of the Unspeaking",
"description":
"With a touch you speak to the spirits within things. The non-living object you touch answers three questions you pose, as best it can.",
"explanation": "",
"level": "5",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "true_seeing",
"name": "True Seeing",
"description":
"You see all things as they truly are. This effect persists until you tell a lie or dismiss the spell. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "7",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"},
{"key":
"wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Divination", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "trap_soul",
"name": "Trap Soul",
"description":
"You trap the soul of a dying creature within a gem. The trapped creature is aware of its imprisonment but can still be manipulated through spells, parley, and other effects. All moves against the trapped creature are at +1. You can free the soul at any time but it can never be recaptured once freed.",
"explanation": "",
"level": "5",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "word_of_recall",
"name": "Word of Recall",
"description":
"Choose a word. The first time after casting this spell that you speak the chosen word, you and any allies touching you when you cast the spell are immediately returned to the exact spot where you cast the spell. You can only maintain a single location; casting Word of Recall again before speaking the word replaces the earlier spell.",
"explanation": "",
"level": "7",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "heal",
"name": "Heal",
"description":
"Touch an ally and you may heal their damage a number of points up to your maximum HP.",
"explanation": "",
"level": "7",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "harm",
"name": "Harm",
"description":
"Touch an enemy and strike them with divine wrath-deal 2d8 damage to them and 1d6 damage to yourself. This damage ignores armor.",
"explanation": "",
"level": "7",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": ["2d8", "1d6"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "sever",
"name": "Sever",
"description":
"Choose an appendage on the target such as an arm, tentacle, or wing. The appendage is magically severed from their body, causing no damage but considerable pain. Missing an appendage may, for example, keep a winged creature from flying, or a bull from goring you on its horns. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "7",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "mark_of_death",
"name": "Mark of Death",
"description":
"Choose a creature whose true name you know. This spell creates permanent runes on a target surface that will kill that creature, should they read them.",
"explanation": "",
"level": "7",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "control_weather",
"name": "Control Weather",
"description":
"Pray for rain-or sun, wind, or snow. Within a day or so, your god will answer. The weather will change according to your will and last a handful of days.",
"explanation": "",
"level": "7",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "storm_of_vengeance",
"name": "Storm of Vengeance",
"description":
"Your deity brings the unnatural weather of your choice to pass. Rain of blood or acid, clouds of souls, wind that can carry away buildings, or any other weather you can imagine: ask and it shall come.",
"explanation": "",
"level": "9",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "repair",
"name": "Repair",
"description":
"Choose one event in the target's past. All effects of that event, including damage, poison, disease, and magical effects, are ended and repaired. HP and diseases are healed, poisons are neutralized, magical effects are ended.",
"explanation": "",
"level": "9",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "divine_presence",
"name": "Divine Presence",
"description":
"Every creature must ask your leave to enter your presence, and you must give permission aloud for them to enter. Any creature without your leave takes an extra 1d10 damage whenever they take damage in your presence. While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "9",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "consume_unlife",
"name": "Consume Unlife",
"description":
"The mindless undead creature you touch is destroyed and you steal its death energy to heal yourself or the next ally you touch. The amount of damage healed is equal to the HP that the creature had remaining before you destroyed it.",
"explanation": "",
"level": "9",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "plague",
"name": "Plague",
"description":
"Name a city, town, encampment, or other place where people live. As long as this spell is active that place is beset by a plague appropriate to your deity's domains (locusts, death of the first born, etc.) While this spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "9",
"classKeys": [
{"key": "cleric", "name": "Cleric", "type": "CharacterClass"}
],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "prestidigitation",
"name": "Prestidigitation",
"description":
"You perform minor tricks of true magic. If you touch an item as part of the casting you can make cosmetic changes to it: clean it, soil it, cool it, warm it, flavor it, or change its color. If you cast the spell without touching an item you can instead create minor illusions no bigger than yourself. Prestidigitation illusions are crude and clearly illusions-they won't fool anyone, but they might entertain them.",
"explanation": "",
"level": "cantrip",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "unseen_servant",
"name": "Unseen Servant",
"description":
"You conjure a simple invisible construct that can do nothing but carry items. It has Load 3 and carries anything you hand to it. It cannot pick up items on its own and can only carry those you give to it. Items carried by an unseen servant appear to float in the air a few paces behind you. An unseen servant that takes damage or leaves your presence is immediately dispelled, dropping any items it carried. Otherwise the unseen servant serves you until you end the spell.",
"explanation": "",
"level": "cantrip",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "contact_spirits",
"name": "Contact Spirits",
"description":
"Name the spirit you wish to contact (or leave it to the GM). You pull that creature through the planes, just close enough to speak to you. It is bound to answer any one question you ask to the best of its ability.",
"explanation": "",
"level": "1",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Summoning", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "detect_magic",
"name": "Detect Magic",
"description":
"One of your senses is briefly attuned to magic. The GM will tell you what here is magical.",
"explanation": "",
"level": "1",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Divination", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "telepathy",
"name": "Telepathy",
"description":
"You form a telepathic bond with a single person you touch, enabling you to converse with that person through your thoughts. You can only have one telepathic bond at a time.",
"explanation": "",
"level": "1",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Divination", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "charm_person",
"name": "Charm Person",
"description":
"The person (not beast or monster) you touch while casting this spell counts you as a friend until they take damage or you prove otherwise.",
"explanation": "",
"level": "1",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Enchantment", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "invisibility",
"name": "Invisibility",
"description":
"Touch an ally: nobody can see them. They're invisible! The spell persists until the target attacks or you dismiss the effect. While the spell is ongoing you can't cast a spell.",
"explanation": "",
"level": "1",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Illusion", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "magic_missile",
"name": "Magic Missile",
"description":
"Projectiles of pure magic spring from your fingers. Deal 2d4 damage to one target.",
"explanation": "",
"level": "1",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Evocation", "value": null, "description": ""}
],
"dice": ["2d4"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "alarm",
"name": "Alarm",
"description":
"Walk a wide circle as you cast this spell. Until you prepare spells again your magic will alert you if a creature crosses that circle. Even if you are asleep, the spell will shake you from your slumber.",
"explanation": "",
"level": "1",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "dispel_magic",
"name": "Dispel Magic",
"description":
"Choose a spell or magic effect in your presence: this spell rips it apart. Lesser spells are ended, powerful magic is just reduced or dampened so long as you are nearby.",
"explanation": "",
"level": "3",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "visions_through_time",
"name": "Visions Through Time",
"description":
"Cast this spell and gaze into a reflective surface to see into the depths of time. The GM will reveal the details of a grim portent to you-a bleak event that will come to pass without your intervention. Theyll tell you something useful about how you can interfere with the grim portent's dark outcomes. Rare is the portent that claims \"You'll live happily ever after.\" Sorry.",
"explanation": "",
"level": "3",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Divination", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "fireball",
"name": "Fireball",
"description":
"You evoke a mighty ball of flame that envelops your target and everyone nearby, inflicting 2d6 damage which ignores armor.",
"explanation": "",
"level": "3",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Evocation", "value": null, "description": ""}
],
"dice": ["2d6"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "mimic",
"name": "Mimic",
"description":
"You take the form of someone you touch while casting this spell. Your physical characteristics match theirs exactly but your behavior may not. This change persists until you take damage or choose to return to your own form. While this spell is ongoing you lose access to all your wizard moves.",
"explanation": "",
"level": "3",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "mirror_image",
"name": "Mirror Image",
"description":
"You create an illusory image of yourself. When you are attacked, roll a d6.\n* On a 4, 5, or 6 the attack hits the illusion instead, the image then dissipates and the spell ends.",
"explanation": "",
"level": "3",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Illusion", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "sleep",
"name": "Sleep",
"description":
"1d4 enemies you can see of the GM's choice fall asleep. Only creatures capable of sleeping are affected. They awake as normal: loud noises, jolts, pain.",
"explanation": "",
"level": "3",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Enchantment", "value": null, "description": ""}
],
"dice": ["1d4"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "cage",
"name": "Cage",
"description":
"The target is held in a cage of magical force. Nothing can get in or out of the cage. The cage remains until you cast another spell or dismiss it. While the spell is ongoing, the caged creature can hear your thoughts and you cannot leave sight of the cage.",
"explanation": "",
"level": "5",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Evocation", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "contact_other_plane",
"name": "Contact Other Plane",
"description":
"You send a request to another plane. Specify who or what you'd like to contact by location, type of creature, name, or title. You open a two-way communication with that creature. Your communication can be cut off at any time by you or the creature you contacted.",
"explanation": "",
"level": "5",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Divination", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "polymorph",
"name": "Polymorph",
"description":
"Your touch reshapes a creature entirely, they stay in the form you craft until you cast a spell. Describe the new shape you craft, including any stat changes, significant adaptations, or major weaknesses. The GM will then tell you one or more of these:\n* The form will be unstable and temporary\n* The creature's mind will be altered as well\n* The form has an unintended benefit or weakness",
"explanation": "",
"level": "5",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Enchantment", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "summon_monster",
"name": "Summon Monster",
"description":
"A monster appears and aids you as best it can. Treat it as your character, but with access to only the basic moves. It has +1 modifier for all stats, 1 HP, and uses your damage dice. The monster also gets your choice of 1d6 of these traits:\n* It has +2 instead of +1 to one stat\n* It's not reckless\n* It does 1d8 damage\n* Its bond to your plane is strong: +2 HP for each level you have\n* It has some useful adaptation\nThe GM will tell you the type of monster you get based on the traits you select. The creature remains on this plane until it dies or you dismiss it. While the spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "5",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Summoning", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": ["1d6", "1d8"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "dominate",
"name": "Dominate",
"description":
"Your touch pushes your mind into someone else's. You gain 1d4 hold. Spend one hold to make the target take one of these actions:\n* Speak a few words of your choice\n* Give you something they hold\n* Make a concerted attack on a target of your choice\n* Truthfully answer one question\n\nIf you run out of hold the spell ends. If the target takes damage you lose 1 hold. While the spell is ongoing you cannot cast a spell.",
"explanation": "",
"level": "7",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Enchantment", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": ["1d4"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "shadow_walk",
"name": "Shadow Walk",
"description":
"The shadows you target with this spell become a portal for you and your allies. Name a location, describing it with a number of words up to your level. Stepping through the portal deposits you and any allies present when you cast the spell at the location you described. The portal may only be used once by each ally.",
"explanation": "",
"level": "7",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Illusion", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "contingency",
"name": "Contingency",
"description":
"Choose a 5th level or lower spell you know. Describe a trigger condition using a number of words equal to your level. The chosen spell is held until you choose to unleash it or the trigger condition is met, whichever happens first. You don't have to roll for the held spell, it just takes effect. You may only have a single contingent spell held at a time; if you cast Contingency while you have a held spell, the new held spell replaces the old one.",
"explanation": "",
"level": "7",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Evocation", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "cloudkill",
"name": "Cloudkill",
"description":
"A cloud of fog drifts into this realm from beyond the Black Gates of Death, filling the immediate area. Whenever a creature in the area takes damage it takes an additional, separate 1d6 damage which ignores armor. This spell persists so long as you can see the affected area, or until you dismiss it.",
"explanation": "",
"level": "7",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Summoning", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": ["1d6"]
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "antipathy",
"name": "Antipathy",
"description":
"Choose a target and describe a type of creature or an alignment. Creatures of the specified type or alignment cannot come within sight of the target. If a creature of the specified type does find itself within sight of the target, it immediately flees. This effect continues until you leave the target's presence or you dismiss the spell. While the spell is ongoing you take -1 to cast a spell.",
"explanation": "",
"level": "9",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Enchantment", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "alert",
"name": "Alert",
"description":
"Describe an event. The GM will tell you when that event occurs, no matter where you are or how far away the event is. If you choose, you can view the location of the event as though you were there in person. You can only have one Alert active at a time.",
"explanation": "",
"level": "9",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Divination", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "soul_gem",
"name": "Soul Gem",
"description":
"You trap the soul of a dying creature within a gem. The trapped creature is aware of its imprisonment but can still be manipulated through spells, parley, and other effects. All moves against the trapped creature are at +1. You can free the soul at any time but it can never be recaptured once freed.",
"explanation": "",
"level": "9",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "shelter",
"name": "Shelter",
"description":
"You create a structure out of pure magical power. It can be as large as a castle or as small as a hut, but is impervious to all non-magical damage. The structure endures until you leave it or you end the spell.",
"explanation": "",
"level": "9",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Evocation", "value": null, "description": ""},
{"name": "Ongoing", "value": null, "description": ""}
],
"dice": []
}),
Spell.fromJson({
"_meta": {"language": "EN", "createdBy": "__repo__"},
"key": "perfect_summons",
"name": "Perfect Summons",
"description":
"You teleport a creature to your presence. Name a creature or give a short description of a type of creature. If you named a creature, that creature appears before you. If you described a type of creature, a creature of that type appears before you.",
"explanation": "",
"level": "9",
"classKeys": [{"key": "wizard", "name": "Wizard", "type": "CharacterClass"}],
"tags": [
{"name": "Summoning", "value": null, "description": ""}
],
"dice": []
})
];

182
lib/data/tags.dart Normal file
View File

@@ -0,0 +1,182 @@
import 'package:dungeon_world_data/tag.dart';
List<Tag> getTagList() => [
Tag.fromJson({
"name": "Applied",
"value": null,
"description":
"It's only useful when carefully applied to a person or to something they eat or drink."
}),
Tag.fromJson({
"name": "Awkward",
"value": null,
"description": "It's unwieldy and tough to use."
}),
Tag.fromJson({
"name": "Bonus",
"value": null,
"description":
"It modifies your effectiveness in a specified situation. It might be \"+1 forward to spout lore\" or \"-1 ongoing to hack and slash.\""
}),
Tag.fromJson({
"name": "Coins",
"value": null,
"description":
"How much it costs to buy, normally. If the cost includes \"-Charisma\" a little negotiation subtracts the haggler's Charisma score (not modifier) from the price."
}),
Tag.fromJson({
"name": "Dangerous",
"value": null,
"description":
"It's easy to get in trouble with it. If you interact with it without proper precautions the GM may freely invoke the consequences of your foolish actions."
}),
Tag.fromJson({
"name": "Ration",
"value": null,
"description": "It's edible, more or less."
}),
Tag.fromJson({
"name": "Requires",
"value": null,
"description":
"It's only useful to certain people. If you don't meet the requirements it works poorly, if at all."
}),
Tag.fromJson({
"name": "Slow",
"value": null,
"description": "It takes minutes or more to use."
}),
Tag.fromJson({
"name": "Touch",
"value": null,
"description": "It's used by touching it to the target's skin."
}),
Tag.fromJson({
"name": "Two handed",
"value": null,
"description": "It takes two hands to use it effectively."
}),
Tag.fromJson({
"name": "Weight",
"value": null,
"description":
"Count the listed amount against your load. Something with no listed weight isn't designed to be carried. 100 coins in standard denominations is 1 weight. The same value in gems or fine art may be lighter or heavier."
}),
Tag.fromJson({
"name": "Worn",
"value": null,
"description": "To use it, you have to be wearing it."
}),
Tag.fromJson({
"name": "Uses",
"value": null,
"description": "It can only be used n times."
}),
Tag.fromJson({
"name": "Ammo",
"value": null,
"description":
"It counts as ammunition for appropriate ranged weapons. The number indicated does not represent individual arrows or sling stones, but represents what you have left on hand."
}),
Tag.fromJson({
"name": "Forceful",
"value": null,
"description":
"It can knock someone back a pace, maybe even off their feet."
}),
Tag.fromJson({
"name": "Damage",
"value": null,
"description":
"It is particularly harmful to your enemies. When you deal damage, you add n to it."
}),
Tag.fromJson({
"name": "Ignores armor",
"value": null,
"description": "Don't subtract armor from the damage taken."
}),
Tag.fromJson({
"name": "Messy",
"value": null,
"description":
"It does damage in a particularly destructive way, ripping people and things apart."
}),
Tag.fromJson({
"name": "Piercing",
"value": null,
"description":
"It goes right through armor. When you deal damage with n piercing, you subtract n from the enemy's armor for that attack."
}),
Tag.fromJson({
"name": "Precise",
"value": null,
"description":
"It rewards careful strikes. You use DEX to Hack & Slash with this weapon, not STR."
}),
Tag.fromJson({
"name": "Reload",
"value": null,
"description":
"After you attack with it, it takes more than a moment to reset for another attack."
}),
Tag.fromJson({
"name": "Stun",
"value": null,
"description":
"When you attack with it, it does stun damage instead of normal damage."
}),
Tag.fromJson({
"name": "Thrown",
"value": null,
"description":
"Throw it at someone to hurt them. If you Volley with this weapon, you can't choose to mark off ammo on a 7-9; once you throw it, it's gone until you can recover it."
}),
Tag.fromJson({
"name": "Hand",
"value": null,
"description":
"It's useful for attacking something within your reach, no further."
}),
Tag.fromJson({
"name": "Close",
"value": null,
"description":
"It's useful for attacking something at arm's reach plus a foot or two."
}),
Tag.fromJson({
"name": "Reach",
"value": null,
"description":
"It's useful for attacking something that's several feet away-maybe as far as ten."
}),
Tag.fromJson({
"name": "Near",
"value": null,
"description":
"It's useful for attacking if you can see the whites of their eyes."
}),
Tag.fromJson({
"name": "Far",
"value": null,
"description":
"It's useful for attacking something in shouting distance."
}),
Tag.fromJson({
"name": "Armor",
"value": null,
"description":
"It protects you and stacks with other armor. Add its value to your total armor."
}),
Tag.fromJson({
"name": "Clumsy",
"value": null,
"description":
"It's tough to move around with. -1 ongoing while using it. This penalty is cumulative."
}),
Tag.fromJson({
"name": "Magical",
"value": null,
"description":
"When making your own magic items keep in mind that these items are magical. Simple modifiers, like+1 damage, are the realm of the mundane-magic items should provide more interesting bonuses."
})
];

View File

@@ -1,183 +1,193 @@
import 'dart:convert';
import 'dart:math';
import 'package:quiver/core.dart';
/// Dice can have sides, an amount and modifiers.
///
/// You may also use one of the rolling convenience methods.
/// For more information about rolls see DiceRoll class
class Dice {
// Amount of dice in set
num amount;
Dice({
required this.amount,
required this.sides,
this.modifierValue,
this.modifierStat,
String? modifierSign,
}) : modifierSign = modifierSign ??
(modifierValue != null
? modifierValue >= 0
? "+"
: "-"
: "+");
// No. of sides for the die
num sides;
final int amount;
final int sides;
final int? modifierValue;
final String? modifierStat;
final String modifierSign;
// Modifier value for dice, can be added to `DiceResult` value
num modifier;
// Last result rolled by this die
DiceResult lastResult;
/// Simple dice, with sides, die count and modifier.
/// You can multiply, add or subtract Dice objects to change the amount of rolls (notice dice must
/// be with the same amount of sides).
///
/// To roll multiple dice at once, you may use the static `Dice.roll(<Dice>[...])`.
Dice(this.sides, [this.amount = 1, this.modifier = 0]);
static Dice d4 = Dice(4);
static Dice d6 = Dice(6);
static Dice d8 = Dice(8);
static Dice d10 = Dice(10);
static Dice d12 = Dice(12);
static Dice d20 = Dice(20);
Dice copyWith({int sides, int amount, int modifier}) => Dice(
sides ?? this.sides,
amount ?? this.amount,
modifier ?? this.modifier,
Dice copyWith({
int? amount,
int? sides,
int? modifierValue,
String? modifierSign,
String? modifierStat,
}) =>
Dice(
amount: amount ?? this.amount,
sides: sides ?? this.sides,
modifierSign: modifierSign ?? this.modifierSign,
modifierValue: modifierValue ?? this.modifierValue,
modifierStat: modifierStat ?? this.modifierStat,
);
/// Parse strings such as `d6`, or `2d20` into a `Dice` object.
static Dice parse(String str) {
List segs = str.split(RegExp('d', caseSensitive: true));
if (segs[0] != '') {
return Dice(num.parse(segs[1]), num.parse(segs[0]));
factory Dice.fromRawJson(String str) => Dice.fromJson(json.decode(str));
static Dice d4 = Dice(amount: 1, sides: 4);
static Dice d6 = Dice(amount: 1, sides: 6);
static Dice d8 = Dice(amount: 1, sides: 8);
static Dice d10 = Dice(amount: 1, sides: 10);
static Dice d12 = Dice(amount: 1, sides: 12);
static Dice d20 = Dice(amount: 1, sides: 20);
static Dice d60 = Dice(amount: 1, sides: 60);
static Dice d100 = Dice(amount: 1, sides: 100);
static final _dicePattern = RegExp(r'(\d+)d([0-9]+)(([+-])([0-9a-z]+))?', caseSensitive: false);
String toRawJson() => toJson();
factory Dice.fromJson(String json) {
final matches = _diceMatches(json);
final amount = int.tryParse(matches[0]!);
final sides = int.tryParse(matches[1]!);
final modifierSign = matches[2];
final modifierValue = matches[3] != null ? int.tryParse(matches[3]!) : null;
final modifierStat =
matches[3] != null && modifierValue == null ? matches[3]!.toUpperCase() : null;
if (sides == null || amount == null) {
throw Exception("Dice parsing failed");
}
return Dice(num.parse(segs[1]));
return Dice(
amount: amount,
sides: sides,
modifierValue: modifierSign == '-' ? -(modifierValue ?? 0) : modifierValue,
modifierSign: modifierSign,
modifierStat: modifierStat,
);
}
Dice copyWithModifierValue(int statValue) => copyWith(
amount: amount,
sides: sides,
modifierValue: statValue,
modifierSign: !statValue.isNegative ? '+' : '-',
);
@override
String toString() => "${amount}d$sides$modifierWithSign";
String toJson() => toString();
String get modifierWithSign =>
hasModifier ? "$modifierSign${modifierValue?.abs() ?? modifierStat}" : "";
bool get hasModifier => ((modifierValue != null && modifierValue != 0) || modifierStat != null);
String get modifier =>
hasModifier ? modifierStat ?? (modifierValue != 0 ? modifierValue : '')!.toString() : "";
DiceRoll roll() {
if (needsModifier) {
throw Exception("Dice is being rolled without an actual modifier."
"Use `copyWithModifierValue`.\n"
"Expected modifier: $modifierWithSign");
}
final arr = <int>[];
for (var i = 0; i < amount; i++) {
arr.add(Random().nextInt(sides) + 1);
}
return DiceRoll(dice: this, results: arr);
}
bool get needsModifier => modifierStat != null && modifierValue == null;
operator +(int amount) => copyWith(amount: this.amount + amount);
operator -(int amount) => copyWith(amount: this.amount - amount);
operator *(int amount) => copyWith(amount: this.amount * amount);
operator /(int amount) => copyWith(amount: this.amount ~/ amount);
static List<Dice> flatten(List<Dice> dice) =>
dice.fold([], (all, cur) => [...all, ...List.filled(cur.amount, cur / cur.amount)]);
static List<String?> _diceMatches(String json) {
_assertDicePattern(json);
final m = _dicePattern.firstMatch(json)!;
return m.groups([1, 2, 4, 5]);
}
static void _assertDicePattern(String dice) {
if (!_dicePattern.hasMatch(dice)) {
throw Exception("Dice format is invalid, must be {amount}d{sides}([+-]{modifier})"
"(e.g. 1d20, 2d6+DEX, 1d8-3)\n"
"Received: $dice");
}
}
static List<Dice> guessFromString(String str) {
final basicRollPattern = RegExp(r'\broll([+-][a-z]+)\b', caseSensitive: false);
final dicePattern = RegExp(r'\b\dd\d+\b', caseSensitive: false);
final found = <Dice>[];
final basicRollMatches = basicRollPattern.allMatches(str);
for (final match in basicRollMatches) {
found.add(Dice.fromJson('2d6' + match.group(1)!.toUpperCase()));
}
final diceMatches = dicePattern.allMatches(str);
for (final match in diceMatches) {
found.add(Dice.fromJson(match.input.substring(match.start, match.end)));
}
return found;
}
@override
String toString() =>
'${amount}d${sides}${modifier != null && modifier != 0 ? modRepr : ''}';
Dice operator *(obj) {
if (obj is Dice) {
if (obj.sides != sides) {
throw ("Can't multiply different sided die!");
}
return Dice(sides, obj.amount * amount, modifier);
}
if (obj is num) {
return Dice(sides, (amount * obj).toInt());
}
return this;
}
Dice operator /(obj) {
if (obj is Dice) {
if (obj.sides != sides) {
throw ("Can't divide different sided die!");
}
return Dice(sides, obj.amount / amount, modifier);
}
if (obj is num) {
return Dice(sides, amount ~/ obj, modifier);
}
return this;
}
Dice operator +(obj) {
if (obj is Dice) {
if (obj.sides != sides) {
throw ("Can't add different sided die!");
}
return Dice(sides, obj.amount + amount, modifier);
}
if (obj is num) {
return Dice(sides, amount + obj.toInt(), modifier);
}
return this;
}
Dice operator -(obj) {
if (obj is Dice) {
if (obj.sides != sides) {
throw ("Can't subtract different sided die!");
}
return Dice(sides, obj.amount - amount, modifier);
}
if (obj is num) {
return Dice(sides, amount - obj.toInt(), modifier);
}
return this;
}
bool operator ==(Object? other) =>
identical(this, other) ||
other is Dice &&
runtimeType == other.runtimeType &&
amount == other.amount &&
sides == other.sides &&
modifierValue == other.modifierValue &&
modifierSign == other.modifierSign &&
modifierStat == other.modifierStat;
@override
bool operator ==(obj) {
if (obj is Dice) {
return amount == obj.amount &&
sides == obj.sides &&
modifier == obj.modifier;
}
int get hashCode => Object.hashAll([amount, sides, modifierValue, modifierSign, modifierStat]);
return obj.toString() == toString();
}
@override
int get hashCode => hash3(amount, sides, modifier);
/// Rolls the dice and returns the `DiceResult`.
DiceResult getRoll() {
var results = <num>[];
for (num i = 0; i < amount; i++) {
results.add(Random().nextInt(sides) + 1);
}
return lastResult = DiceResult(this, results);
}
Dice get single => this / amount;
Dice multiple(int amount) => single * amount;
/// Roll arbitrary amount of (possibly) different sided dice.
static List<DiceResult> roll(List<Dice> dice) {
var results = <DiceResult>[];
dice.forEach((die) {
results.add(die.getRoll());
});
return results;
}
String get modRepr => (modifier > 0 ? '+' : '') + modifier.toString();
String get debugProperties =>
'amount: $amount, sides: $sides, modifierValue: $modifierValue, modifierSign: $modifierSign, modifierStat: $modifierStat';
}
class DiceResult {
/// The corresponding dice.
Dice dice;
class DiceRoll {
final Dice dice;
final List<int> results;
/// List of results, by order of dice rolled.
List<num> results;
/// Represents a result of a die roll
DiceResult(this.dice, this.results);
@override
String toString() => '$dice${didHitNaturalMax ? '*' : ''} => $total';
// More detailed version of `toString`.
String get toDetailedString =>
'$dice${didHitNaturalMax ? '*' : ''} => $total\n $mappedResults\n ${didHitNaturalMax ? "Die no. ${indexOfNaturalMax} hit 20" : "Didn\'t hit 20"}';
// All results layed out with their respective die.
String get mappedResults {
var out = <String>[];
for (num i = 0; i < results.length; i++) {
out.add('${i + 1}: ${results[i]}');
}
return out.toString() + (dice.modifier != 0 ? ' (${dice.modRepr})' : '');
DiceRoll({required this.dice, required this.results}) {
assertDiceModifier();
}
/// Total (accumulated) value of result, including modifiers.
num get total => results.reduce((tot, cur) => tot + cur) + dice.modifier;
static List<DiceRoll> rollMany(List<Dice> dice) => dice.map((d) => roll(d)).toList();
// Boolean that represents whether any of the rolled dice hit their natural max value (e.g. 20 for d20)
bool get didHitNaturalMax => results.any((r) => r == dice.sides);
static DiceRoll roll(Dice dice) => dice.roll();
// Get first index of the die that hit natural max value.
num get indexOfNaturalMax => results.indexOf(dice.sides);
int get total => results.reduce((all, cur) => all + cur) + (dice.modifierValue ?? 0);
bool get didHitNaturalMax => indexOfNaturalMax >= 0;
int get indexOfNaturalMax => results.indexOf(dice.sides);
void assertDiceModifier() {
if (dice.needsModifier) {
throw Exception("Dice is being rolled without an actual modifier."
"Use `dice.copyWithModifierValue(int modifierValue)`.\n"
"Expected modifier: ${dice.modifierWithSign}");
}
}
}

View File

@@ -0,0 +1,17 @@
export 'alignment.dart';
export 'character_class.dart';
export 'dice.dart';
export 'entity_reference.dart';
export 'gear_choice.dart';
export 'gear_selection.dart';
export 'item.dart';
export 'monster.dart';
export 'move.dart';
export 'race.dart';
export 'session_mark.dart';
export 'spell.dart';
export 'tag.dart';
import 'repository.dart';
final dungeonWorldData = loadRepository();

View File

@@ -1,14 +0,0 @@
import 'src/_dungeon_world_data.dart';
export 'src/_dungeon_world_data.dart';
export 'alignment.dart';
export 'dice.dart';
export 'equipment.dart';
export 'gear_choice.dart';
export 'mappers.dart';
export 'monster.dart';
export 'move.dart';
export 'player_class.dart';
export 'spell.dart';
export 'tag.dart';
final dungeonWorld = DungeonWorldData();

View File

@@ -1 +0,0 @@
export 'dw_data.dart' show DWEntity;

59
lib/entity_reference.dart Normal file
View File

@@ -0,0 +1,59 @@
import 'dart:convert';
class EntityReference {
final String key;
final String name;
final String type;
EntityReference({
required this.key,
required this.name,
required this.type,
});
factory EntityReference.fromRawJson(String str) => EntityReference.fromJson(json.decode(str));
factory EntityReference.fromJson(Map<String, dynamic> json) => EntityReference(
key: json['key'],
name: json['name'],
type: json['type'],
);
Map<String, dynamic> toJson() => {
'key': key,
'name': name,
'type': type,
};
String toRawJson() => json.encode(toJson());
EntityReference copyWith({
String? key,
String? name,
String? type,
}) =>
EntityReference(
key: key ?? this.key,
name: name ?? this.name,
type: type ?? this.type,
);
@override
bool operator ==(Object? other) =>
other is EntityReference &&
other.key == key &&
// other.name == name &&
other.type == type;
@override
int get hashCode => Object.hashAll([
key,
// name,
type
]);
String get debugProperties => 'key: $key, name: $name, type: $type';
@override
String toString() => 'EntityReference(key: $key, name: $name, type: $type)';
}

View File

@@ -1,56 +0,0 @@
import 'package:meta/meta.dart';
import 'dw_entity.dart';
import 'tag.dart';
import 'mappers.dart';
class Equipment extends DWEntity {
/// Equipment name
String name;
/// Equipment name, in plural
String pluralName;
/// Item description
String description;
/// Equipment tags
List<Tag> tags;
Equipment({
String key,
@required this.name,
String pluralName,
@required this.description,
@required this.tags,
}) : pluralName = pluralName ?? _calcPluralName(name),
super(key: key ?? DWEntity.generateKey(name));
@override
String toString() => '$name: $description (tags: $tags)';
factory Equipment.fromJSON(Map map) => Equipment(
key: map['key'],
name: map['name'],
pluralName: map['plural_name'],
description: map['description'],
tags: listMapper(map['tags'], (i) => Tag.fromJSON(i)),
);
@override
Map toJSON() {
return {
'name': name,
'tags': listMapper<Tag, dynamic, Tag>(tags, (t) => t.toJSON()),
'description': description,
'plural_name': pluralName,
'key': key,
};
}
@override
Equipment copy() {
return Equipment.fromJSON(toJSON());
}
static String _calcPluralName(String name) => '${name}s';
}

View File

@@ -1,78 +1,96 @@
import 'package:dungeon_world_data/tag.dart';
import 'package:meta/meta.dart';
import 'dw_entity.dart';
import 'mappers.dart';
import 'dart:convert';
class GearChoice extends DWEntity {
/// Description of choices, possibly with other benefits to the stats.
String label;
/// The list of options to choose from. `label` should specify how many to choose.
List<GearOption> gearOptions;
import 'base.dart';
import 'gear_selection.dart';
/// This is the top level choice - provides one or more options to choose from.
///
/// For example, "choose a gift from your parents" which gives either "your father's sword" or
/// "your mother's mace"
class GearChoice with KeyMixin {
GearChoice({
String key,
@required this.label,
@required this.gearOptions,
}) : super(key: key);
required this.key,
required this.description,
required this.selections,
this.preselect = const [],
this.maxSelections,
});
factory GearChoice.fromJSON(Map map) => GearChoice(
key: map['key'],
label: map['label'],
gearOptions: gearOptionMapper(map['list']),
@override
final String key;
final String description;
final List<GearSelection> selections;
final List<int> preselect;
final int? maxSelections;
GearChoice copyWith({
String? key,
String? description,
List<GearSelection>? selections,
List<int>? preselect,
int? maxSelections,
}) =>
GearChoice(
key: key ?? this.key,
description: description ?? this.description,
selections: selections ?? this.selections,
preselect: preselect ?? this.preselect,
maxSelections: maxSelections ?? this.maxSelections,
);
@override
Map toJSON() => {
'key': key,
'label': label,
'list': listMapper<GearOption, dynamic, GearOption>(
gearOptions, (i) => i.toJSON()),
};
factory GearChoice.fromRawJson(String str) => GearChoice.fromJson(json.decode(str));
@override
GearChoice copy() => GearChoice.fromJSON(toJSON());
}
String toRawJson() => json.encode(toJson());
class GearOption extends DWEntity {
// String key;
String name;
List<Tag> tags;
GearOption({
String key,
@required this.name,
@required this.tags,
}) : super(key: key ?? DWEntity.generateKey(name));
factory GearOption.fromJSON(Map map) => GearOption(
key: map['key'],
name: map['name'],
tags: listMapper(map['tags'], (t) => Tag.fromJSON(t)),
factory GearChoice.fromJson(Map<String, dynamic> json) => GearChoice(
key: json["key"],
description: json["description"],
selections:
List<GearSelection>.from(json["selections"].map((x) => GearSelection.fromJson(x))),
preselect: List<int>.from(json['preselect']),
maxSelections: json['maxSelections'],
);
factory GearOption.parse(dynamic str) {
if (str is Map) {
return GearOption.fromJSON(str);
}
final num openParen = str.indexOf('(');
final num closeParen = str.indexOf(')');
final name = str.substring(0, openParen > -1 ? openParen : null);
final rawTags = openParen > -1 && closeParen > -1
? str.substring(openParen + 1, closeParen)
: '';
final tags = rawTags.isNotEmpty
? rawTags.split(',').map((tag) => Tag.fromJSON(tag.trim())).toList()
: [];
return GearOption(name: name, tags: tags);
}
@override
dynamic toJSON() => {
'name': name,
'tags': listMapper<Tag, dynamic, Tag>(tags, (t) => t.toJSON()),
Map<String, dynamic> toJson() => {
"key": key,
"description": description,
"selections": List<dynamic>.from(selections.map((x) => x.toJson())),
"preselect": preselect,
"maxSelections": maxSelections,
};
List<GearSelection> get preselectedGearSelections => preselect.isEmpty
? []
: preselect.first == -1
? selections
: selections.sublist(preselect.first, preselect.last);
@override
GearOption copy() => GearOption.fromJSON(toJSON());
String get displayName => description;
@override
bool operator ==(Object? other) =>
identical(this, other) ||
other is GearChoice &&
runtimeType == other.runtimeType &&
key == other.key &&
description == other.description &&
selections == other.selections &&
preselect == other.preselect &&
maxSelections == other.maxSelections;
@override
int get hashCode => Object.hashAll([
key,
description,
selections,
preselect,
maxSelections,
]);
String get debugProperties =>
'key: $key, description: $description, selections: $selections, preselect: $preselect, maxSelections: $maxSelections';
@override
String toString() => 'GearChoice($debugProperties)';
}

65
lib/gear_option.dart Normal file
View File

@@ -0,0 +1,65 @@
import 'dart:convert';
import 'base.dart';
import 'item.dart';
/// Contains an item and amount
class GearOption with KeyMixin {
GearOption({
required this.key,
required this.item,
required this.amount,
});
@override
final String key;
final Item item;
final double amount;
GearOption copyWith({
String? key,
Item? item,
double? amount,
}) =>
GearOption(
key: key ?? this.key,
item: item ?? this.item,
amount: amount ?? this.amount,
);
factory GearOption.fromRawJson(String str) => GearOption.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GearOption.fromJson(Map<String, dynamic> json) => GearOption(
key: json["key"],
item: Item.fromJson(json["item"]),
amount: json["amount"]?.toDouble() ?? 1.0,
);
Map<String, dynamic> toJson() => {
"key": key,
"item": item.toJson(),
"amount": amount,
};
@override
String get displayName => '${item.name} x$amount';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GearOption &&
runtimeType == other.runtimeType &&
key == other.key &&
item == other.item &&
amount == other.amount;
@override
int get hashCode => Object.hashAll([key, item, amount]);
String get debugProperties => 'key: $key, item: ${item.debugProperties}, amount: $amount';
@override
String toString() => 'GearOption($debugProperties)';
}

77
lib/gear_selection.dart Normal file
View File

@@ -0,0 +1,77 @@
import 'dart:convert';
import 'base.dart';
import 'gear_option.dart';
/// One possible selection of many, which is a "bundle" of items and coins. Contains multiple
/// options, each of which is an item and amount, and total coins.
///
/// All of these should be added at once when making a selection. This is the final level of
/// selection, the inner options are all given to the player.
class GearSelection with KeyMixin {
GearSelection({
required this.key,
required this.description,
required this.options,
required this.coins,
});
@override
final String key;
final String description;
final List<GearOption> options;
final double coins;
GearSelection copyWith({
String? key,
String? description,
List<GearOption>? options,
double? coins,
}) =>
GearSelection(
key: key ?? this.key,
description: description ?? this.description,
options: options ?? this.options,
coins: coins ?? this.coins,
);
factory GearSelection.fromRawJson(String str) => GearSelection.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GearSelection.fromJson(Map<String, dynamic> json) => GearSelection(
key: json["key"],
description: json["description"],
options: List<GearOption>.from(json["options"].map((x) => GearOption.fromJson(x))),
coins: json["coins"]?.toDouble(),
);
Map<String, dynamic> toJson() => {
"key": key,
"description": description,
"options": List<dynamic>.from(options.map((x) => x.toJson())),
"coins": coins,
};
@override
String get displayName => description;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GearSelection &&
runtimeType == other.runtimeType &&
key == other.key &&
description == other.description &&
options == other.options &&
coins == other.coins;
@override
int get hashCode => Object.hashAll([key, description, options, coins]);
String get debugProperties =>
'key: $key, description: $description, options: $options, coins: $coins';
@override
String toString() => 'GearSelection($debugProperties)';
}

79
lib/item.dart Normal file
View File

@@ -0,0 +1,79 @@
import 'dart:convert';
import 'base.dart';
import 'tag.dart';
class Item with KeyMixin {
Item({
required this.meta,
required this.key,
required this.description,
required this.name,
required this.tags,
});
final dynamic meta;
@override
final String key;
final String name;
final String description;
final List<Tag> tags;
Item copyWith({
dynamic meta,
String? key,
String? name,
String? description,
List<Tag>? tags,
}) =>
Item(
meta: meta ?? this.meta,
key: key ?? this.key,
name: name ?? this.name,
description: description ?? this.description,
tags: tags ?? this.tags,
);
factory Item.fromRawJson(String str) => Item.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Item.fromJson(Map<String, dynamic> json) => Item(
meta: json["_meta"],
key: json["key"],
name: json["name"],
description: json["description"],
tags: List<Tag>.from(json["tags"].map((x) => Tag.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"_meta": meta,
"key": key,
"name": name,
"description": description,
"tags": List<dynamic>.from(tags.map((x) => x.toJson())),
};
@override
String get displayName => name;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Item &&
runtimeType == other.runtimeType &&
key == other.key &&
name == other.name &&
description == other.description &&
tags == other.tags;
@override
int get hashCode => Object.hashAll([key, name, description, tags]);
String get debugProperties =>
'meta: $meta, key: $key, name: $name, description: $description, tags: $tags';
@override
String toString() => 'Item($debugProperties)';
}

View File

@@ -1,76 +0,0 @@
import 'package:dungeon_world_data/spell.dart';
import 'alignment.dart';
import 'equipment.dart';
import 'gear_choice.dart';
import 'move.dart';
import 'monster.dart';
import 'player_class.dart';
import 'tag.dart';
List<R> listMapper<T, R, A>(List<T> lst, R Function(A obj) mapper) =>
lst != null && lst.isNotEmpty
? List.from(lst).map<R>((obj) => obj is A ? mapper(obj) : null).toList()
: <R>[];
Map<K, V> mapMapper<K, V>(
Map map, MapEntry<K, V> Function(dynamic key, dynamic obj) mapper) =>
map != null ? Map.from(map).map<K, V>(mapper) : {};
List<Move> moveListMapper(List lst) => lst == null || lst.isEmpty
? <Move>[]
: listMapper<Map, Move, dynamic>(
List<Map>.from(lst), (v) => Move.fromJSON(v));
Map<String, Move> moveMapMapper(Map map) => mapMapper<String, Move>(
map, (k, v) => MapEntry<String, Move>(k.toString(), Move.fromJSON(v)));
Map<String, PlayerClass> classMapper(Map map) => mapMapper(
map,
(k, v) =>
MapEntry<String, PlayerClass>(k.toString(), PlayerClass.fromJSON(v)));
Map<String, List<String>> nameMapper(Map map) =>
mapMapper<String, List<String>>(
map,
(k, v) => MapEntry<String, List<String>>(k.toString(),
listMapper<dynamic, String, dynamic>(v, (j) => j.toString())));
List<List<String>> looksMapper(List lst) =>
listMapper(lst, (i) => listMapper(i, (j) => j.toString()));
Map<String, Alignment> alignmentsMapper(Map map) => mapMapper(map,
(k, v) => MapEntry<String, Alignment>(k.toString(), Alignment.fromJSON(v)));
List<GearChoice> gearChoiceMapper(List lst) =>
listMapper(lst, (v) => GearChoice.fromJSON(v));
List<GearOption> gearOptionMapper(List lst) =>
listMapper(lst, (v) => GearOption.parse(v));
Map<String, Equipment> equipmentMapper(Map map) => mapMapper(map,
(k, v) => MapEntry<String, Equipment>(k.toString(), Equipment.fromJSON(v)));
List<Spell> spellsMapper(List lst) {
if (lst == null) {
return [];
}
return listMapper(lst, (v) => Spell.fromJSON(v));
}
Map<String, Monster> monsterMapper(Map map) => mapMapper(map,
(k, v) => MapEntry<String, Monster>(k.toString(), Monster.fromJSON(v)));
Map<String, Tag> tagMapper(Map map) => mapMapper(
map, (k, v) => MapEntry<String, Tag>(k.toString(), Tag.fromJSON(v)));
Map<String, Tag> tagInfoMapper(Map map) => mapMapper(map, (k, v) {
var cleanName = v['name']
.toString()
.replaceAll(RegExp(r'[^a-z]', caseSensitive: false), ' ')
.trim()
.replaceFirst(RegExp(r'^n\s'), '')
.trim();
return MapEntry<String, Tag>(
k.toString(), Tag(cleanName, null, v['description']));
});

View File

@@ -1,57 +1,94 @@
import 'package:meta/meta.dart';
import 'dw_entity.dart';
import 'dart:convert';
import 'base.dart';
import 'tag.dart';
import 'mappers.dart';
class Monster extends DWEntity {
/// Monster name
String name;
/// Monster description
String description;
/// Monster instinct
String instinct;
/// Monster tags
List<Tag> tags;
/// Monster moves
List<String> moves;
class Monster with KeyMixin {
Monster({
String key,
@required this.name,
@required this.description,
@required this.instinct,
@required this.tags,
@required this.moves,
}) : super(key: key ?? DWEntity.generateKey(name));
required this.meta,
required this.key,
required this.name,
required this.description,
required this.instinct,
required this.tags,
required this.moves,
});
final dynamic meta;
@override
String toString() =>
'$name\n tags: $tags,\n moves: ${moves}\n instinct: $instinct';
final String key;
final String name;
final String description;
final String instinct;
final List<Tag> tags;
final List<String> moves;
static Monster fromJSON(Map map) => Monster(
key: map['key'],
name: map['name'],
description: map['description'],
instinct: map['instinct'],
tags: listMapper(map['tags'], (i) => Tag.fromJSON(i)),
moves: listMapper(map['moves'], (i) => i.toString()));
Monster copyWith({
dynamic meta,
String? key,
String? name,
String? description,
String? instinct,
List<Tag>? tags,
List<String>? moves,
}) =>
Monster(
meta: meta ?? this.meta,
key: key ?? this.key,
name: name ?? this.name,
description: description ?? this.description,
instinct: instinct ?? this.instinct,
tags: tags ?? this.tags,
moves: moves ?? this.moves,
);
factory Monster.fromRawJson(String str) => Monster.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Monster.fromJson(Map<String, dynamic> json) => Monster(
meta: json["_meta"],
key: json["key"],
name: json["name"],
description: json["description"],
instinct: json["instinct"],
tags: List<Tag>.from(json["tags"].map((x) => Tag.fromJson(x))).toList(),
moves: json["moves"] ?? [],
);
Map<String, dynamic> toJson() => {
"_meta": meta,
"key": key,
"name": name,
"description": description,
"instinct": instinct,
"tags": List<dynamic>.from(tags.map((x) => x.toJson())),
"moves": moves,
};
@override
Map toJSON() {
return {
'key': key,
'name': name,
'description': description,
'instinct': instinct,
'tags': listMapper<Tag, dynamic, Tag>(tags, (tag) => tag.toJSON()),
'moves': moves,
};
}
String get displayName => name;
@override
Monster copy() => Monster.fromJSON(toJSON());
bool operator ==(Object? other) =>
identical(this, other) ||
other is Monster &&
runtimeType == other.runtimeType &&
meta == other.meta &&
key == other.key &&
name == other.name &&
description == other.description &&
instinct == other.instinct &&
tags == other.tags &&
moves == other.moves;
@override
int get hashCode => Object.hashAll([meta, key, name, description, instinct, tags, moves]);
String get debugProperties =>
'meta: $meta, key: $key, name: $name, description: $description, instinct: $instinct, tags: $tags, moves: $moves';
@override
String toString() => 'Monster($debugProperties)';
}

View File

@@ -1,48 +1,121 @@
import 'package:meta/meta.dart';
import 'dw_entity.dart';
import 'mappers.dart';
import 'dart:convert';
class Move extends DWEntity {
/// Move name
String name;
import 'base.dart';
import 'dice.dart';
import 'entity_reference.dart';
import 'tag.dart';
/// Move description
String description;
/// Move explanation
String explanation;
/// Classes that can use this move.
/// The keys correspond to the `PlayerClass` key.
List<String> classes;
enum MoveCategory {
starting,
basic,
special,
advanced1,
advanced2,
other,
}
class Move with KeyMixin {
Move({
String key,
@required this.name,
@required this.description,
@required this.explanation,
@required this.classes,
}) : super(key: key ?? DWEntity.generateKey(name));
required this.meta,
required this.key,
required this.name,
required this.description,
required this.explanation,
required this.dice,
required this.classKeys,
required this.tags,
required this.category,
});
factory Move.fromJSON(Map map) => Move(
key: map['key'],
name: map['name'],
classes: map['classes'] != null
? listMapper(map['classes'], (j) => j.toString())
: [],
description: map['description'],
explanation: map['explanation'],
);
final dynamic meta;
@override
Map toJSON() => {
'key': key,
'name': name,
'classes': classes,
'description': description,
'explanation': explanation,
final String key;
final String name;
final String description;
final String explanation;
final List<Dice> dice;
final List<EntityReference> classKeys;
final List<Tag> tags;
final MoveCategory category;
Move copyWith({
dynamic meta,
String? key,
String? name,
String? description,
String? explanation,
List<Dice>? dice,
List<EntityReference>? classKeys,
List<Tag>? tags,
MoveCategory? category,
}) =>
Move(
meta: meta ?? this.meta,
key: key ?? this.key,
name: name ?? this.name,
description: description ?? this.description,
explanation: explanation ?? this.explanation,
dice: dice ?? this.dice,
classKeys: classKeys ?? this.classKeys,
tags: tags ?? this.tags,
category: category ?? this.category,
);
factory Move.fromRawJson(String str) => Move.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Move.fromJson(Map<String, dynamic> json) => Move(
meta: json["_meta"],
key: json["key"],
name: json["name"],
description: json["description"],
explanation: json["explanation"],
dice: List<Dice>.from(json["dice"].map((x) => Dice.fromJson(x))),
classKeys:
List<EntityReference>.from(json["classKeys"].map((x) => EntityReference.fromJson(x))),
tags: List<Tag>.from(json["tags"].map((x) => Tag.fromJson(x))),
category: MoveCategory.values.firstWhere((element) => element.name == json["category"]),
);
Map<String, dynamic> toJson() => {
"_meta": meta,
"key": key,
"name": name,
"description": description,
"explanation": explanation,
"dice": List<String>.from(dice.map((x) => x.toJson())),
"classKeys": List<dynamic>.from(classKeys.map((x) => x.toJson())),
"tags": List<dynamic>.from(tags.map((x) => x.toJson())),
"category": category.name,
};
@override
Move copy() => Move.fromJSON(toJSON());
String get displayName => name;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Move &&
runtimeType == other.runtimeType &&
meta == other.meta &&
key == other.key &&
name == other.name &&
description == other.description &&
explanation == other.explanation &&
dice == other.dice &&
classKeys == other.classKeys &&
tags == other.tags &&
category == other.category;
@override
int get hashCode =>
Object.hashAll([meta, key, name, description, explanation, dice, classKeys, tags, category]);
String get debugProperties =>
'name: $name, description: $description, explanation: $explanation, dice: $dice, classKeys: $classKeys, tags: $tags, category: $category';
@override
String toString() => 'Move($debugProperties)';
}

View File

@@ -1,134 +0,0 @@
import 'package:meta/meta.dart';
import 'alignment.dart';
import 'dice.dart';
import 'dw_entity.dart';
import 'mappers.dart';
import 'move.dart';
import 'spell.dart';
import 'gear_choice.dart';
class PlayerClass extends DWEntity {
/// Class name
String name;
/// Class description
String description;
/// Base max. weight load, without the +STR.
num load;
/// Base HP
num baseHP;
/// Hit die
Dice damage;
/// Character name options, mapped by race
Map<String, List<String>> names;
/// Class Bonds
List<String> bonds;
/// Character look options
List<List<String>> looks;
/// Character alignment options. Map of `Alignment.key` => `Alignment`
Map<String, Alignment> alignments;
/// Race moves
List<Move> raceMoves;
/// Starting moves
List<Move> startingMoves;
/// Starting moves
List<Move> advancedMoves1;
/// Starting moves
List<Move> advancedMoves2;
/// Spells
List<Spell> spells;
/// Gear choices
List<GearChoice> gearChoices;
PlayerClass({
String key,
@required this.name,
@required this.description,
@required this.load,
@required this.baseHP,
@required this.damage,
@required this.names,
@required this.bonds,
@required this.looks,
@required this.alignments,
@required this.raceMoves,
@required this.startingMoves,
@required this.advancedMoves1,
@required this.advancedMoves2,
@required this.spells,
@required this.gearChoices,
}) : super(key: key ?? DWEntity.generateKey(name));
/// Combined list of `advancedMoves1` and `advancedMoves2`
List<Move> get advancedMoves => advancedMoves1 + advancedMoves2;
@override
String toString() {
return 'Class: $name, baseHP: $baseHP';
}
factory PlayerClass.fromJSON(Map map) => PlayerClass(
key: map['key'] ??
map['name']
.toString()
.toLowerCase()
.replaceAll(RegExp('[^a-z]'), '_'),
name: map['name'],
description: map['description'],
load: map['load'],
baseHP: map['base_hp'],
damage: Dice.parse(map['damage']),
names: nameMapper(map['names']),
bonds: listMapper(map['bonds'], (j) => j.toString()),
looks: looksMapper(map['looks']),
alignments: alignmentsMapper(map['alignments']),
raceMoves: moveListMapper(map['race_moves']),
startingMoves: moveListMapper(map['starting_moves']),
advancedMoves1: moveListMapper(map['advanced_moves_1']),
advancedMoves2: moveListMapper(map['advanced_moves_2']),
spells: spellsMapper(map['spells']),
gearChoices: gearChoiceMapper(map['gear_choices']),
);
@override
Map toJSON() => {
'key': key,
'name': name,
'description': description,
'load': load,
'base_hp': baseHP,
'damage': damage.toString(),
'names': names,
'bonds': bonds,
'looks': looks,
'alignments': mapMapper<String, Map>(
alignments, (k, v) => MapEntry(k, v.toJSON())),
'race_moves':
listMapper<Move, Map, Move>(raceMoves, (move) => move.toJSON()),
'starting_moves':
listMapper<Move, Map, Move>(startingMoves, (move) => move.toJSON()),
'advanced_moves_1': listMapper<Move, Map, Move>(
advancedMoves1, (move) => move.toJSON()),
'advanced_moves_2': listMapper<Move, Map, Move>(
advancedMoves2, (move) => move.toJSON()),
'spells': listMapper(spells, (v) => v.toJSON()),
'gear_choices': listMapper<GearChoice, Map, GearChoice>(
gearChoices, (choice) => choice.toJSON()),
};
@override
PlayerClass copy() => PlayerClass.fromJSON(toJSON());
}

105
lib/race.dart Normal file
View File

@@ -0,0 +1,105 @@
import 'dart:convert';
import 'base.dart';
import 'dice.dart';
import 'entity_reference.dart';
import 'tag.dart';
class Race with KeyMixin {
Race({
required this.meta,
required this.key,
required this.name,
required this.description,
required this.explanation,
required this.dice,
required this.classKeys,
required this.tags,
});
final dynamic meta;
@override
final String key;
final String name;
final String description;
final String explanation;
final List<EntityReference> classKeys;
final List<Tag> tags;
final List<Dice> dice;
Race copyWith({
dynamic meta,
String? key,
String? name,
String? description,
String? explanation,
List<EntityReference>? classKeys,
List<Tag>? tags,
List<Dice>? dice,
}) =>
Race(
meta: meta ?? this.meta,
key: key ?? this.key,
name: name ?? this.name,
description: description ?? this.description,
explanation: explanation ?? this.explanation,
classKeys: classKeys ?? this.classKeys,
tags: tags ?? this.tags,
dice: dice ?? this.dice,
);
factory Race.fromRawJson(String str) => Race.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Race.fromJson(Map<String, dynamic> json) => Race(
meta: json["_meta"],
key: json["key"],
name: json["name"],
description: json["description"],
explanation: json["explanation"],
classKeys:
List<EntityReference>.from(json["classKeys"].map((x) => EntityReference.fromJson(x))),
tags: List<Tag>.from(json["tags"].map((x) => Tag.fromJson(x))),
dice: List<Dice>.from(json["dice"].map((x) => Dice.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"_meta": meta,
"key": key,
"name": name,
"description": description,
"explanation": explanation,
"classKeys": List<dynamic>.from(classKeys.map((x) => x.toJson())),
"tags": List<dynamic>.from(tags.map((x) => x.toJson())),
"dice": List<dynamic>.from(dice.map((x) => x.toJson()))
};
@override
String get displayName => name;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Race &&
runtimeType == other.runtimeType &&
meta == other.meta &&
key == other.key &&
name == other.name &&
description == other.description &&
explanation == other.explanation &&
classKeys == other.classKeys &&
tags == other.tags &&
dice == other.dice;
@override
int get hashCode =>
Object.hashAll([meta, key, name, description, explanation, classKeys, tags, dice]);
String get debugProperties =>
'meta: $meta, key: $key, name: $name, description: $description, explanation: $explanation, classKeys: $classKeys, tags: $tags, dice: $dice';
@override
String toString() => 'Race($debugProperties)';
}

40
lib/repository.dart Normal file
View File

@@ -0,0 +1,40 @@
import '_repository.dart' as _r;
import 'data/classes.dart';
import 'data/items.dart';
import 'data/monsters.dart';
import 'data/moves.dart';
import 'data/races.dart';
import 'data/spells.dart';
import 'data/tags.dart';
final _repo = _r.DungeonWorldRepository();
_r.DungeonWorldRepository loadRepository() {
_repo.initLocale('en-US');
_repo.changeLocale('en-US');
for (final cls in getCharacterClassList()) {
_repo.characterClasses[cls.key] = cls;
}
for (final item in getItemList()) {
_repo.items[item.key] = item;
}
for (final monster in getMonsterList()) {
_repo.monsters[monster.key] = monster;
}
for (final move in getMoveList()) {
_repo.moves[move.key] = move;
}
for (final race in getRaceList()) {
_repo.races[race.key] = race;
}
for (final spell in getSpellList()) {
_repo.spells[spell.key] = spell;
}
for (final tag in getTagList()) {
_repo.tags[tag.name] = tag;
}
return _repo;
}

73
lib/session_mark.dart Normal file
View File

@@ -0,0 +1,73 @@
import 'dart:convert';
import 'base.dart';
enum SessionMarkType { bond, flag, endOfSession, other }
class SessionMark with KeyMixin {
SessionMark({
required this.key,
required this.description,
required this.completed,
required this.type,
});
@override
final String key;
final String description;
final bool completed;
final SessionMarkType type;
SessionMark copyWith({
String? key,
String? description,
bool? completed,
SessionMarkType? type,
}) =>
SessionMark(
key: key ?? this.key,
description: description ?? this.description,
completed: completed ?? this.completed,
type: type ?? this.type,
);
factory SessionMark.fromRawJson(String str) => SessionMark.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory SessionMark.fromJson(Map<String, dynamic> json) => SessionMark(
key: json["key"],
completed: json["completed"],
description: json["description"],
type: SessionMarkType.values.firstWhere((e) => e.name == json['type']),
);
Map<String, dynamic> toJson() => {
"key": key,
"description": description,
"completed": completed,
"type": type.name,
};
@override
String get displayName => description;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SessionMark &&
runtimeType == other.runtimeType &&
key == other.key &&
description == other.description &&
completed == other.completed &&
type == other.type;
@override
int get hashCode => Object.hashAll([key, description, completed, type]);
String get debugProperties =>
'key: $key, description: $description, completed: $completed, type: $type';
@override
String toString() => 'SessionMark($debugProperties)';
}

View File

@@ -1,51 +1,112 @@
import 'package:meta/meta.dart';
import 'dw_entity.dart';
import 'mappers.dart';
import 'dart:convert';
import 'base.dart';
import 'dice.dart';
import 'entity_reference.dart';
import 'tag.dart';
class Spell extends DWEntity {
/// Spell name
String name;
/// Spell description
String description;
/// Spell level
String level;
/// Spell tags
List<Tag> tags;
class Spell with KeyMixin {
Spell({
String key,
@required this.name,
@required this.description,
@required this.level,
@required this.tags,
}) : super(key: key ?? DWEntity.generateKey(name));
required this.meta,
required this.key,
required this.name,
required this.description,
required this.explanation,
required this.level,
required this.classKeys,
required this.dice,
required this.tags,
});
static Spell fromJSON(Map map) => Spell(
key: map['key'],
name: map['name'],
description: map['description'],
level: map['level'].toString(),
tags: listMapper(map['tags'], (i) => Tag.fromJSON(i)),
final dynamic meta;
@override
final String key;
final String name;
final String description;
final String explanation;
final String level;
final List<EntityReference> classKeys;
final List<Dice> dice;
final List<Tag> tags;
Spell copyWith({
dynamic meta,
String? key,
String? name,
String? description,
String? explanation,
String? level,
List<EntityReference>? classKeys,
List<Dice>? dice,
List<Tag>? tags,
}) =>
Spell(
meta: meta ?? this.meta,
key: key ?? this.key,
name: name ?? this.name,
description: description ?? this.description,
explanation: explanation ?? this.explanation,
level: level ?? this.level,
classKeys: classKeys ?? this.classKeys,
tags: tags ?? this.tags,
dice: dice ?? this.dice,
);
@override
String toString() => '$name ($level)';
factory Spell.fromRawJson(String str) => Spell.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Spell.fromJson(Map<String, dynamic> json) => Spell(
meta: json["_meta"],
key: json["key"],
name: json["name"] ?? '',
description: json["description"] ?? '',
explanation: json["explanation"] ?? '',
level: json["level"] ?? '',
classKeys:
List<EntityReference>.from(json["classKeys"].map((x) => EntityReference.fromJson(x))),
tags: List<Tag>.from(json["tags"].map((x) => Tag.fromJson(x))),
dice: List<Dice>.from(json["dice"].map((x) => Dice.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"_meta": meta,
"key": key,
"name": name,
"description": description,
"explanation": explanation,
"level": level,
"classKeys": List<dynamic>.from(classKeys.map((x) => x.toJson())),
"tags": List<dynamic>.from(tags.map((x) => x.toJson())),
"dice": List<dynamic>.from(dice.map((x) => x.toJson())),
};
@override
Map toJSON() {
return {
'key': key,
'name': name,
'description': description,
'level': level,
'tags': listMapper<Tag, dynamic, Tag>(tags, (tag) => tag.toJSON()),
};
}
String get displayName => name;
@override
Spell copy() => Spell.fromJSON(toJSON());
bool operator ==(Object? other) =>
identical(this, other) ||
other is Spell &&
runtimeType == other.runtimeType &&
meta == other.meta &&
key == other.key &&
name == other.name &&
description == other.description &&
explanation == other.explanation &&
level == other.level &&
classKeys == other.classKeys &&
dice == other.dice &&
tags == other.tags;
@override
int get hashCode =>
Object.hashAll([meta, key, name, description, explanation, level, classKeys, dice, tags]);
String get debugProperties =>
'$meta, $key, $name, $description, $explanation, $level, $classKeys, $dice, $tags';
@override
String toString() => 'Spell($debugProperties)';
}

View File

@@ -1,46 +0,0 @@
part of '_dungeon_world_data.dart';
abstract class DWEntity {
String key;
DWEntity({String key}) {
this.key = key ?? DWEntity.generateKey(null);
}
/// Generates unique key for identification.
///
/// If a non-empty String identifier is supplied, it is cleaned up of non-alphabetic characters
/// and turned to lowercase.
///
/// Otherwise, a `Uuid().v4()` is generated for it.
static String generateKey(String identifier) =>
identifier != null && identifier.trim().isNotEmpty
? identifier
.trim()
.replaceAll(RegExp(r'[^a-z]+', caseSensitive: false), ' ')
.trim()
.replaceAll(RegExp('\\s+'), '_')
.toLowerCase()
: Uuid().v4();
@override
int get hashCode => hash2(runtimeType, key);
@override
bool operator ==(obj) => obj.hashCode == hashCode;
/// Returns a JSON representation of this object.
dynamic toJSON();
/// Creates a new instance of this identity, using the existing previous values.
DWEntity copy();
/// Helper method for getting any `DWEntity` by its key.
static T getByKey<T extends DWEntity>(String key, Iterable<T> list) =>
list.firstWhere(
(element) => element.key == key,
orElse: () => null,
);
}
// typedef Key = String Function(String);

View File

@@ -1,9 +0,0 @@
part of '_dungeon_world_data.dart';
List<Tag> tagList = [];
List<Equipment> equipmentList = [];
List<PlayerClass> playerClassList = [];
List<Monster> monsterList = [];
List<Move> basicMovesList = [];
List<Spell> spellList = [];
List<Move> specialMovesList = [];

File diff suppressed because it is too large Load Diff

View File

@@ -1,96 +0,0 @@
import 'package:quiver/core.dart';
import 'package:uuid/uuid.dart';
import '../tag.dart';
import '../alignment.dart';
import '../equipment.dart';
import '../mappers.dart';
import '../monster.dart';
import '../move.dart';
import '../player_class.dart';
import '../spell.dart';
import '../gear_choice.dart';
import '../dice.dart';
part '_data.dart';
part '_cache.dart';
part '_base.dart';
part '_homebrew.dart';
const String VERSION = '2.0.0';
class DungeonWorldData {
Map<String, dynamic> _raw;
/// Raw data
Map<String, dynamic> get raw {
_raw ??= toJSON();
return _raw;
}
/// Basic moves
List<Move> basicMoves;
/// Special moves
List<Move> specialMoves;
/// Classes
List<PlayerClass> classes;
/// Equipment
List<Equipment> equipment;
/// Spells
List<Spell> spells;
/// Monsters
List<Monster> monsters;
/// Tags
List<Tag> tags;
/// Current version of data, corresponds to same version of https://www.npmjs.com/package/dungeonworld-data
final String version = VERSION;
DungeonWorldData() {
_initFromData();
}
void _initFromData() {
initData();
initHomebrew();
tags = tagList;
basicMoves = basicMovesList;
specialMoves = specialMovesList;
classes = playerClassList;
equipment = equipmentList;
monsters = monsterList;
spells = spellList;
}
Map<String, dynamic> toJSON() => {
'tags': listMapper<Tag, dynamic, Tag>(tags, (t) => t.toJSON()),
'basic_moves':
listMapper<Move, dynamic, Move>(basicMoves, (t) => t.toJSON()),
'special_moves':
listMapper<Move, dynamic, Move>(specialMoves, (t) => t.toJSON()),
'classes': listMapper<PlayerClass, dynamic, PlayerClass>(
classes, (t) => t.toJSON()),
'equipment': listMapper<Equipment, dynamic, Equipment>(
equipment, (t) => t.toJSON()),
'monsters':
listMapper<Monster, dynamic, Monster>(monsters, (t) => t.toJSON()),
'spells': listMapper<Spell, dynamic, Spell>(spells, (t) => t.toJSON()),
};
static Map<String, T> listToMap<T extends DWEntity>(
Iterable<T> list, {
String Function(T) key = entryKey,
}) =>
Map<String, T>.fromEntries(
list.map((v) => MapEntry<String, T>(key(v), v)),
);
static String entryKey(DWEntity item) => item.key;
}

View File

@@ -1,592 +0,0 @@
part of '_dungeon_world_data.dart';
void initHomebrew() {
playerClassList.add(PlayerClass(
key: 'immolator',
name: 'Immolator',
description: '',
load: 9,
baseHP: 4,
damage: Dice.parse('1d8'),
names: {
'human': [
'Solomon',
'Timothy',
'Kalil',
'Omen',
'Yohn',
'Hiko',
'Agasha',
'Elizabeth',
'Harald',
'Fatia',
'Khalwa',
'Adur',
'Ignis',
'Yajna',
'Umlilo',
],
'salamander': [
'Sulfurheart',
'Emberlash',
'Flamewalker',
'Cinderclaw',
'Charfiend',
'Bittertallow',
'Barrowblaze',
'Singescale',
'Candlewick',
'Coalfang',
]
},
bonds: [
'__________ has felt the hellish touch of fire, now they know my strength.',
'I will teach __________ the true meaning of sacrifice.',
'I cast something into the fire for __________ and still owe them their due.'
],
looks: [
['Smoldering eyes', 'Warm eyes', 'Searing eyes'],
['Strange brands', 'Ritual scars', 'Perfect skin'],
['Imperious bearing', 'Manic attitude', 'Barely-hidden rage'],
['Crackling voice', 'Whispering voice', 'Roaring voice']
],
alignments: {
'evil': Alignment(
key: 'evil',
name: 'Evil',
description: 'Sacrifice an unwilling victim to the flames.',
),
'chaotic': Alignment(
key: 'chaotic',
name: 'Chaotic',
description: 'Spread a dangerous new idea',
),
'neutral': Alignment(
key: 'neutral',
name: 'Neutral',
description:
'Exchange a sacrifice, freely given, for a service rendered.',
)
},
raceMoves: [
Move(
key: 'salamander',
name: 'Salamander',
description: 'Non-magical heat and fire cannot harm you.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'human',
name: 'Human',
description:
'When you Make Camp next to a large, open flame, regain all of your HP.',
explanation: null,
classes: ['immolator'],
)
],
startingMoves: [
Move(
key: 'burning_brand',
name: 'Burning Brand',
description:
'When you conjure a weapon of pure flame, roll+CON. On a 10+ choose two of the following tags, on a 7-9 choose one. You may treat your INT as your STR or DEX in regards to making attacks with this weapon. The weapon always begins with the fiery, touch, dangerous, and 3 uses tags. Each attack with the weapon consumes one use.\n\n\n\n- hand\n\n- thrown, near\n\n- +1 damage\n\n- remove the dangerous tag',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'fighting_fire_with_fire',
name: 'Fighting Fire With Fire',
description:
'When you take damage, and that damage is odd (after armor) the flames within you come to your aid. Roll 1d4 and either add that many uses to your burning brand (if active), take that result forward to summon your burning brand, or reduce the damage by that amount, your choice.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'zuko_style',
name: 'Zuko Style',
description:
'When you bend a flame to your will, roll+WIS. On a 10+ it does as you command, taking the shape and movement you desire for as long as it has fuel on which to burn. On a 7-9 the effect is short-lived, lasting only a moment.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'give_me_fuel_give_me_fire',
name: 'Give Me Fuel Give Me Fire',
description:
'When you **gaze intensely into someone eyes**, you may ask their player “what fuels the flames of your desire?” theyll answer with the truth, even if the character does not know or would otherwise keep this hidden.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'hand_crafted',
name: 'Hand Crafted',
description:
'You may use your hands in place of tools and fire to craft metal objects. Mundane weapons, armor and metal jewelry can all be formed from their raw components. You may unmake these things, as well, but to do so without time and safety might require that you Defy Danger first.',
explanation: null,
classes: ['immolator'],
)
],
advancedMoves1: [
Move(
key: 'lore_of_flame',
name: 'Lore of Flame',
description:
'When you **stare into a source of fire, looking for answers**, roll+WIS On a hit, the GM will tell you something new and interesting about the current situation. On a 10+, the GM will give you good detail. On a 79, the GM will give you an impression. If you already know all there is to know, the GM will tell you that.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'burns_twice_as_bright',
name: 'Burns Twice As Bright',
description:
'When you **channel the flames of fate**, you may treat a missed roll as a 7-9 or a 7-9 result as a 10+. This may be a roll you or another character has made. Tell the GM something youve lost; an emotion, a memory or some innate piece of your being. You may not use this move again until youve used Burns Half As Long.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'burns_half_as_long',
name: 'Burns Half As Long',
description:
'You gain this move when you gain Burns Twice as Bright.\n\n\n\nWhen you sacrifice a victory to the flames of fate, treat any roll of 10+ as a miss.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'this_killing_fire',
name: 'This Killing Fire',
description:
'Add the following tags to your options for Burning Brand: **messy, forceful, reach, near, far**',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'firebrand',
name: 'Firebrand',
description:
'When you **introduce a new idea to an NPC**, roll+CHA. On a 10+ They believe the idea to be their own and take to it with fervor On a 7-9, Their passion fades after a day or two. On a miss, they respond negatively, speaking out against the idea.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'ogdru_jahad',
name: 'Ogdru Jahad',
description:
'Gain the Wizard move Ritual. The GM will always tell you what you have to sacrifice to gain the effect you desire.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'moth_to_the_flame',
name: 'Moth To The Flame',
description:
'When you tempt a weak mind with your inner fire, roll+WIS. On a 10+ their will is suppressed, theyll follow you and do as you desire, so long as nothing startles or surprises them. On a 7-9, the effect is only strong enough to distract or confuse them. On a miss, they become agitated and upset, your fire having sparked their hidden desires.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'burning_bridges',
name: 'Burning Bridges',
description:
'When you would take your last breath, dont. Instead, you may erase one of your Bonds. This is permanent and lowers your total available Bonds forever. You are alive and have 1d6 hp. If you have no more Bonds, take your last breath as normal.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'the_enkindler',
name: 'The Enkindler',
description:
'When you bolster the courage of others roll+CHA. On a 10+ they shake off all fear and doubt, becoming brave in an instant. On a 7-9, this effect is fleeting, they realize its superficiality and resort to cowardice after a moment or two. On a miss, theyre cowed or terrified by your presence.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'sick_burn',
name: 'Sick Burn',
description:
'When you insult an NPC, roll + CHA. On a 10+ you leave them no room to react, they bear your insult and the scorn of all who hear it. On a 7-9 you cross a line, they will have their revenge, someday. On a miss youve gone too far, they blow up here and now.',
explanation: null,
classes: ['immolator'],
),
],
advancedMoves2: [
Move(
key: 'from_hell_s_heart',
name: "From Hell's Heart",
description:
'Whenever you summon fire with any of your moves, you can replace it with the black fires of hell itself. This fire does not burn with heat and ignores armor, scorching the soul itself. Those creatures without souls cannot be harmed by this type of flame.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'burning_ring_of_fire',
name: 'Burning Ring Of Fire',
description:
'When you **fuse a willing persons soul to yours**, roll+CHA. On a hit you are bound together, able to sense each other at any distance, as well as sharing your emotional state. On a 7-9, the connection is unstable and dangerous, when you take a debility, so do they (and vice versa). On a miss, the branding is rejected and you both erase any Bonds you have to each other. You may write new Bonds with them at the End of Session as usual. This fusion, once performed, cannot be undone by mortal means.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'fanning_the_flame',
name: 'Fanning The Flame',
description:
'You may apply the effects of your Firebrand move to a group of people a dozen or so all at once.',
explanation: null,
classes: ['immolator'],
),
Move(
key: 'watch_the_world_burn',
name: 'Watch the World Burn',
description:
'When you **open a channel to the burning planes and call a firestorm**, tell the GM what youre sacrificing and roll+WIS. The sky opens up and fire pours like rain from it within an area about equal to a small village. Everyone and everything in the area takes damage as appropriate. On a 10+ you can extinguish the storm with a little effort. On a 7-9 the fires rage out of your control, spreading and gusting where they are carried by wind and weather. On a miss, something cruel, intelligent and hungry comes with the storm.',
explanation: null,
classes: ['immolator'],
)
],
spells: [],
gearChoices: [
GearChoice(
key: '76316345-ccad-43e2-b0f4-fc0e51fbf50b',
label:
'You carry no weapon and need no armor but the flames the burn within you. Choose two:',
gearOptions: [
GearOption(
key: 'dungeon_rations',
name: 'Dungeon Rations',
tags: [Tag.fromJSON('{uses: 5}'), Tag.fromJSON('{weight: 1}')],
),
GearOption(
key: 'healing_potion',
name: '1 Healing Potion',
tags: [Tag.fromJSON('{weight: 0}')],
),
GearOption(
key: 'coins',
name: '10 coins',
tags: [],
)
],
)
],
));
playerClassList.add(PlayerClass(
key: 'barbarian',
name: 'Barbarian',
description: '',
load: 8,
baseHP: 8,
damage: Dice.parse('1d10'),
names: {
'outsider': [
'Gorm',
'Si-Yi',
'Priscilla',
'Sen',
'Xia',
'Anneira',
'Haepha',
'Lur',
'Shar',
'Korrin',
'Nkosi',
'Fafnir',
'Qua',
'Sacer',
'Vercingeto',
'Barbozar',
'Clovis',
'Frael',
'Thra raxes',
'Sillius',
'Sha Sheena',
'Khamisi',
]
},
bonds: [
'______________________ is puny and foolish, but amusing to me.',
"______________________ 's ways are strange and confusing.",
'______________________ is always getting into trouble - I must protect them from themselves.',
'______________________ shares my hunger for glory; the earth will tremble at our passing!'
],
looks: [
['Tormented eyes', 'Haunted eyes', 'Wild eyes', 'Shrouded eyes'],
['Mighty thews', 'Long shanks', 'Scrawny body', 'Supple body'],
['Strange tattoos', 'Unusual jewelry', 'Unmarred by decoration'],
['Scraps', 'Silks', "Scavenger's outfit", 'Weather inappropriate clothes']
],
alignments: {
'chaotic': Alignment(
key: 'chaotic',
name: 'Chaotic',
description: 'You eschew a convention of the civilized world.',
),
'neutral': Alignment(
key: 'neutral',
name: 'Neutral',
description: 'Teach someone the ways of your people.',
)
},
raceMoves: [
Move(
key: 'outsider',
name: 'Outsider',
description:
'You may be elf, dwarf, halfling, or human, but you and your people are not from around here. At the beginning of each session, the GM will ask you something about your homeland, why you left, or what you left behind. If you answer them, mark XP.',
explanation: null,
classes: ['barbarian'],
)
],
startingMoves: [
Move(
key: 'herculean_appetites',
name: 'Herculean Appetites',
description:
'Others may content themselves with just a taste of wine, or dominion over a servant or two, but you want more. Choose two appetites. While pursuing one of your appetites if you would roll for a move, instead of rolling 2d6 you roll 1d6+1d8. If the d6 is the higher die of the pair, the GM will also introduce a complication or danger that comes about due to your heedless pursuits.\n\n* Pure destruction\n* Power over others\n* Mortal pleasures\n* Conquest\n* Riches and property\n* Fame and glory',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'the_upper_hand',
name: 'The Upper Hand',
description:
'You take +1 ongoing to last breath rolls. When you take your last breath, on a 79 you make an offer to Death in return for your life. If Death accepts he will return you to life. If not, you die.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'musclebound',
name: 'Musclebound',
description:
'While you wield a weapon it gains the forceful and messy tags.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'what_are_you_waiting_for',
name: 'What Are You Waiting For?',
description:
'When you cry out a challenge to your enemies, roll+Con.\n\n* On a 10+ they treat you as the most obvious threat to be dealt with and ignore your companions, take +2 damage ongoing against them.\n* On a 79 only a few (the weakest or most foolhardy among them) fall prey to your taunting.',
explanation: null,
classes: ['barbarian'],
)
],
advancedMoves1: [
Move(
key: 'full_plate_and_packing_steel',
name: 'Full Plate and Packing Steel',
description: 'You ignore the clumsy tag on armor you wear.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'unencumbered_unharmed',
name: 'Unencumbered, Unharmed',
description:
'So long as you are below your Load and neither wear armor nor carry a shield, take +1 armor.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'still_hungry',
name: 'Still Hungry',
description: 'Choose an additional appetite.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'appetite_for_destruction',
name: 'Appetite for Destruction',
description:
'Take a move from the fighter, bard or thief class list. You may not take multiclass moves from those classes.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'my_love_for_you_is_like_a_truck',
name: 'My Love For You Is Like a Truck',
description:
'When you perform a feat of strength, name someone present whom you have impressed and take +1 forward to parley with them.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'what_is_best_in_life',
name: 'What Is Best In Life',
description:
'At the end of a session, if during this session you have crushed your enemies, seen them driven before you, or have heard the lamentations of their kinfolk mark XP.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'wide_wanderer',
name: 'Wide Wanderer',
description:
'Youve traveled the wide world over. When you arrive someplace ask the GM about any important traditions, rituals, and so on, theyll tell you what you need to know.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'usurper',
name: 'Usurper',
description:
'When you prove yourself superior to a person in power, take +1 forward with their followers, underlings, and hangers on.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'khan_of_khans',
name: 'Khan Of Khans',
description:
'Your hirelings always accept the gratuitous fulfillment of one of your appetites as payment.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'samson',
name: 'Samson',
description:
'You may take a debility to immediately break free of any physical or mental restraint.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'smash',
name: 'Smash!',
description:
'When you hack and slash, on a 12+ deal your damage and choose something physical your target has (a weapon, their position, a limb): they lose it.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'indestructible_hunger',
name: 'Indestructible Hunger',
description:
'When you take damage you can choose to take 1 ongoing until you sate one of your appetites instead of taking the damage. If you already have this penalty you cannot choose this option.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'eye_for_weakness',
name: 'Eye For Weakness',
description:
'When you discern realities add “What here is weak or vulnerable?” to the list of questions you can ask.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'on_the_move',
name: 'On The Move',
description:
'When you defy a danger caused by movement (maybe falling off a narrow bridge or rushing past an armed guard) take +1.',
explanation: null,
classes: ['barbarian'],
),
],
advancedMoves2: [
Move(
key: 'a_good_day_to_die',
name: 'A Good Day To Die',
description:
'As long as you have less than your Con in current HP (or 1, whichever is higher) take +1 ongoing.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'kill_em_all',
name: "Kill 'Em All",
description:
'**Requires:** Appetite for Destruction\n\nTake another move from the fighter, bard or thief class list. You may not take multiclass moves from those classes.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'war_cry',
name: 'War Cry',
description:
'When you enter battle with a show of force (a shout, a rallying cry, a battle dance) roll+Cha.\n\n* On a 10+ both,\n * On a 79 one or the other.\n * Your allies are rallied and take +1 forward\n * Your enemies feel fear and act accordingly (avoiding you, hiding, attacking with fear driven abandon)',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'mark_of_might',
name: 'Mark Of Might',
description:
'When you take this move and spend some uninterrupted time reflecting on your past glories you may mark yourself with a symbol of your power (a long braid tied with bells, ritual scars or tattoos, etc.) Any intelligent mortal creature who sees this symbol knows instinctively that you are a force to be reckoned with and treats you appropriately.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'more_always_more',
name: 'More! Always More!',
description:
'When you satisfy an appetite to the extreme (destroying something unique and significant, gaining enormous fame, riches, power, etc.) you may choose to resolve it. Cross it off the list and mark XP. While you may pursue that appetite again, you no longer feel the burning desire you once did. In its place, choose a new appetite from the list or write your own.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'the_one_who_knocks',
name: 'The One Who Knocks',
description:
'When you defy danger, on a 12+ you turn the danger back on itself, the GM will describe how.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'healthy_distrust',
name: 'Healthy Distrust',
description:
'Whenever the unclean magic wielded by mortal men causes you to defy danger, treat any result of 6 as a 79.',
explanation: null,
classes: ['barbarian'],
),
Move(
key: 'for_the_blood_god',
name: 'For The Blood God',
description:
'You are initiated in the old ways, the ways of sacrifice. Choose something your gods (or the ancestor spirits, or your totem, etc) value—gold, blood, bones or the like. When you sacrifice those things as per your rites and rituals, roll+Wis.\n\n* On a 10+ the GM will grant you insight into your current trouble or a boon to help you.\n* On a 79 the sacrifice is not enough and your gods take of your flesh as well, but still grant you some insight or boon.\n* On a miss, you earn the ire of the fickle spirits.',
explanation: null,
classes: ['barbarian'],
),
],
spells: [],
gearChoices: [
GearChoice(
key: 'dbb50c21-6f20-44d5-8c4f-68bc77e7bc77',
label:
'Your Load is 8+Str. You carry dungeon rations (5 uses, 1 weight), a dagger (hand, 1 weight) some token of where youve traveled or where youre from, and your choice of weapon:',
gearOptions: [
GearOption(
key: 'axe',
name: 'Axe',
tags: [Tag.fromJSON('close'), Tag.fromJSON('{weight:1}')],
),
GearOption(
key: 'two_handed_sword',
name: 'Two handed sword',
tags: [
Tag.fromJSON('close'),
Tag.fromJSON('{weight:2}'),
Tag.fromJSON('{damage:1}')
],
)
],
),
GearChoice(
key: 'fe326cb4-fa3f-4e16-830a-216280ef039d',
label: 'Choose one:',
gearOptions: [
GearOption(
key: 'adventuring_gear_and_dungeon_rations',
name: 'Adventuring gear and dungeon rations',
tags: [Tag.fromJSON('{weight:2}'), Tag.fromJSON('{uses:5}')],
),
GearOption(
key: 'chainmail',
name: 'Chainmail',
tags: [Tag.fromJSON('{weight:1}'), Tag.fromJSON('{armor:1}')],
)
],
)
],
));
}

View File

@@ -1,71 +1,65 @@
import 'dw_entity.dart';
import 'dart:convert';
class Tag<T> extends DWEntity {
static Map<String, String> tagInfoCache = {};
import 'base.dart';
/// Tag or feature name
String name;
class Tag with KeyMixin {
Tag({
required this.name,
required this.value,
this.description = "",
});
/// Value, if applicable
T value;
String description;
Tag(this.name, [this.value, this.description]) : super(key: name) {
if (description != null &&
description.isNotEmpty &&
!tagInfoCache.containsKey(key)) {
tagInfoCache[key] = description;
} else if (description == null ||
description.isEmpty && tagInfoCache.containsKey(key)) {
description = tagInfoCache[key];
}
}
final String name;
final dynamic value;
final String description;
@override
String toString() => hasValue ? '$name: $value' : name;
String get key => name;
/// Returns whether this tag has a corresponding value or not
bool get hasValue => value != null;
Tag copyWith({
String? name,
dynamic value,
String? description,
}) =>
Tag(
name: name ?? this.name,
value: value ?? this.value,
description: description ?? this.description,
);
factory Tag.fromRawJson(String str) => Tag.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Tag.fromJson(Map<String, dynamic> json) => Tag(
name: json["name"],
value: json["value"],
description: json["description"] ?? "",
);
Map<String, dynamic> toJson() => {
"name": name,
"value": value,
"description": description,
};
@override
String get key => DWEntity.generateKey(name);
factory Tag.fromJSON(obj) {
if (obj is String) {
if (obj == '') {
return null;
}
var amountThenName = RegExp('([0-9]+)\\s(.*)');
if (amountThenName.hasMatch(obj)) {
var match = amountThenName.allMatches(obj).toList().first;
var name = match.group(2);
var value = int.tryParse(match.group(1));
return Tag(name, value as dynamic);
}
var mapLike = RegExp('\\{(.*):\\s?([+-]?[0-9]+)\\}');
if (mapLike.hasMatch(obj)) {
var match = mapLike.allMatches(obj).toList().first;
var name = match.group(1);
var value = int.tryParse(match.group(2));
return Tag(name, value as dynamic);
}
}
if (obj is Map) {
var key = obj.keys.first.toString();
return Tag(key, obj[key]);
}
return Tag(obj);
}
String get displayName => name;
@override
dynamic toJSON() {
return hasValue ? {name: value} : name;
}
bool operator ==(Object other) =>
identical(this, other) ||
other is Tag &&
runtimeType == other.runtimeType &&
name == other.name &&
value == other.value &&
description == other.description;
@override
Tag copy() => Tag.fromJSON(toJSON());
int get hashCode => Object.hashAll([name, value, description]);
String get debugProperties => 'name: $name, value: $value, description: $description';
@override
String toString() => 'Tag($debugProperties)';
}

17
npm_package/CHANGELOG.md Normal file
View File

@@ -0,0 +1,17 @@
# CHANGELOG
## 10.0.15
- Bugfixes
## 10.0.14
- Fix cleric move "Deity" description
## 10.0.13
- Reverted: Unified "Cast A Spell" from separate Cleric/Wizard moves to one move
## 10.0.12
- Unified "Cast A Spell" from separate Cleric/Wizard moves to one move

View File

@@ -1,6 +1,6 @@
{
"name": "dw-data",
"version": "1.0.12",
"version": "1.0.15",
"main": "raw_data.js",
"repository": "https://github.com/chenasraf/dungeon_world_data",
"author": "Chen Asraf <inbox@casraf.com>",
@@ -10,4 +10,4 @@
"build": "cd ../.. && dart web/scripts/build_npm.dart",
"postbuild": "node -e 'require(\"../../npm_package/raw_data.js\")'"
}
}
}

5785
npm_package/raw_data.js Normal file

File diff suppressed because it is too large Load Diff

165
pubspec.lock Normal file → Executable file
View File

@@ -7,377 +7,356 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
version: "22.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.10"
version: "1.7.2"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
version: "2.2.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
version: "2.8.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
collection:
version: "1.3.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
collection:
dependency: "direct main"
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
version: "1.15.0"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "3.0.1"
coverage:
dependency: transitive
description:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0"
version: "0.15.2"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
version: "3.0.1"
dart_style:
dependency: "direct dev"
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.6"
version: "2.0.3"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.1"
version: "2.0.1"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "3.0.1"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
version: "4.0.0"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
version: "1.0.3"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2"
version: "0.6.3"
lints:
dependency: "direct dev"
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
version: "1.0.1"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
version: "0.12.10"
meta:
dependency: "direct main"
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6+3"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
version: "1.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.10"
version: "1.4.13"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
version: "2.0.0"
path:
dependency: "direct dev"
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
pedantic:
dependency: "direct dev"
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
version: "1.11.1"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
version: "1.5.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
version: "2.0.0"
quiver:
dependency: "direct main"
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
version: "3.0.1"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.5"
version: "1.2.0"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.1"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.8"
version: "0.2.9+2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
version: "0.2.4+1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.9"
version: "0.10.10"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.7"
version: "1.16.5"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.16"
version: "0.2.19"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.7"
version: "0.3.15"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
version: "1.3.0"
uuid:
dependency: "direct main"
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "3.0.4"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.4"
version: "6.2.0"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
version: "1.0.0"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.3"
version: "1.0.0"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "3.1.0"
sdks:
dart: ">=2.7.0 <3.0.0"
dart: ">=2.17.0-266.5.beta <3.0.0"

20
pubspec.yaml Normal file → Executable file
View File

@@ -1,23 +1,19 @@
name: dungeon_world_data
homepage: https://github.com/DungeonPaper/dungeon_world_data
description: Data dump of Dungeon World classes, moves, equipment, and more. Also mirrored as NPM package.
version: 2.0.5+8
version: 3.0.0
environment:
sdk: ">=2.3.0 <3.0.0"
sdk: ">=2.17.0-266.5.beta <3.0.0"
dependencies:
quiver: ^2.0.1
collection: ^1.15.0-nullsafety.4
meta: ^1.1.0
uuid: ^2.0.4
quiver: ^3.0.1
uuid: ^3.0.4
dev_dependencies:
pedantic: ^1.9.0
test: ^1.6.4
dart_style: ^1.3.3
lints: ^1.0.1
dart_style: ^2.0.3
path: ^1.6.4
# js: ^0.6.1+1
# build_runner: ^1.9.0
# node_interop: ^1.0.3
# build_node_compilers: ^0.2.4
# build_web_compilers: ^2.9.0
test: ^1.6.4

View File

@@ -1,62 +0,0 @@
import 'dart:io';
import 'package:dart_style/dart_style.dart';
import 'package:dungeon_world_data/dw_data.dart';
import 'package:path/path.dart';
import 'package:pedantic/pedantic.dart';
import 'dart_parsers.dart';
void main() async {
print('Parsing');
var arrays = [
ParseDef<Tag>('Tag', dungeonWorld.tags, parseInfoTags),
ParseDef<Equipment>('Equipment', dungeonWorld.equipment, parseEquipment),
ParseDef<PlayerClass>(
'PlayerClass', dungeonWorld.classes, parsePlayerClass),
ParseDef<Monster>('Monster', dungeonWorld.monsters, parseMonster),
ParseDef<Move>('Move', dungeonWorld.basicMoves, parseMove,
arrayName: 'basicMovesList'),
ParseDef<Spell>('Spell', dungeonWorld.spells, parseSpell),
ParseDef<Move>('Move', dungeonWorld.specialMoves, parseMove,
arrayName: 'specialMovesList'),
];
var arraysImports = ({"part of '_dungeon_world_data.dart';"}..addAll(Set.from(
arrays.map((a) => "import 'package:dungeon_world_data/${a.file}';"))))
.join('\n');
// String arraysDeclares =
// arrays.map((a) => "List<${a.name}> ${a.arrayName} = [];").join('\n');
var arrayFills = arrays
.map((arr) => arr.list
.map((item) => '${arr.arrayName}.add(${arr.parse(item)});')
.join('\n'))
.join('\n');
print('Writing string');
var str = '''
${arraysImports}
void initData() {
${arrayFills}
}
''';
var file = File(join(Directory.current.path, 'output.dart'));
var beforeFormatFile =
File(join(Directory.current.path, 'before-format.dart'));
print('Formatting');
try {
var formatter = DartFormatter();
var formatted = formatter.format(str);
print('Writing to file');
unawaited(file.writeAsString(formatted));
if (beforeFormatFile.existsSync()) {
print('Deleting before-format file');
unawaited(beforeFormatFile.delete());
}
} on FormatterException catch (e) {
print(e);
print('Writing before-format file');
unawaited(beforeFormatFile.writeAsString(str));
} catch (e) {
print(e);
}
}

View File

@@ -1,176 +0,0 @@
import 'package:dungeon_world_data/dw_data.dart';
String dumpClass<T extends DWEntity>(
String className, T obj, String Function(T) parser,
{bool includeKey = true}) {
return """
${className}(${includeKey && obj.key != null ? "key: ${parseValue(obj.key)}, " : ""}${parser(obj).trim()})
""";
}
String Function(T) createClassParser<T extends DWEntity>(
String name, String Function(T obj) parser,
{bool includeKey = true}) {
return (T obj) => dumpClass(name, obj, parser, includeKey: includeKey);
}
String Function(T) createAnyParser<T extends DWEntity>(
String Function(T obj) parser) {
return (T obj) => parser(obj);
}
String parseArray<T>(
String className, Iterable<T> list, String Function(T) parser) {
return "[${list.map(parser).join(', ')}${list.length > 5 ? ',' : ''}]";
}
String parseTag(Tag tag) {
return '''Tag.fromJSON(${parseValue(tag.toString())})''';
}
String parseTagsArray(List<Tag> list) {
return parseArray('Tag', list.where((t) => t != null), parseTag);
}
String parseDice(Dice dice) {
return '''Dice.parse(${parseValue(dice.toString())})''';
}
String parseValue<T>(T value) {
if (value is String) {
String val = value;
if (val.contains('\n')) {
val = val.replaceAll(RegExp('\r?\n'), '\\n');
}
if (val.contains("'")) {
return '"${val.replaceAll('"', "\\\"")}"';
} else if (val.contains('"')) {
return "'${val.replaceAll("'", "\\\'")}'";
}
return "'$val'";
}
return '$value';
}
String Function(Tag) parseTags = createAnyParser<Tag>(parseTag);
String Function(Tag) parseInfoTags = createClassParser<Tag>(
'Tag',
(i) => '''
${parseValue(i.name)},
${parseValue(i.value)},
${parseValue(i.description)},
''',
includeKey: false,
);
String Function(Alignment) parseAlignment = createClassParser<Alignment>(
'Alignment',
(i) => '''
name: ${parseValue(i.name)},
description: ${parseValue(i.description)},
''');
String Function(Equipment) parseEquipment = createClassParser<Equipment>(
'Equipment',
(i) => '''
name: ${parseValue(i.name)},
pluralName: ${parseValue(i.pluralName)},
description: ${parseValue(i.description)},
tags: ${parseTagsArray(i.tags)},
''');
String Function(PlayerClass) parsePlayerClass = createClassParser<PlayerClass>(
'PlayerClass',
(i) => """
name: ${parseValue(i.name)},
description: ${parseValue(i.description)},
load: ${parseValue(i.load)},
baseHP: ${parseValue(i.baseHP)},
damage: ${parseDice(i.damage)},
names: ${i.names.map((r, s) => MapEntry(parseValue(r), parseArray('String', s, parseValue)))},
bonds: ${parseArray('String', i.bonds, parseValue)},
looks: ${parseArray('List<String>', i.looks, (s) => parseArray('String', s, parseValue))},
alignments: ${i.alignments.map((k, v) => MapEntry(parseValue(v.name), parseAlignment(v)))},
raceMoves: ${parseArray('Move', i.raceMoves, parseMove)},
startingMoves: ${parseArray('Move', i.startingMoves, parseMove)},
advancedMoves1: ${parseArray('Move', i.advancedMoves1, parseMove)},
advancedMoves2: ${parseArray('Move', i.advancedMoves2, parseMove)},
spells: ${parseArray('Spell', i.spells, parseSpell)},
gearChoices: ${parseArray('GearChoice', i.gearChoices, parseGearChoice)},
""");
String Function(Move) parseMove = createClassParser<Move>(
'Move',
(i) => """
name: ${parseValue(i.name)},
description: ${parseValue(i.description)},
explanation: ${parseValue(i.explanation)},
classes: ${parseArray('String', i.classes, parseValue)},
""");
String Function(Spell) parseSpell = createClassParser<Spell>(
'Spell',
(i) => '''
name: ${parseValue(i.name)},
description: ${parseValue(i.description)},
level: ${parseValue(i.level)},
tags: ${parseTagsArray(i.tags)},
''');
String Function(GearChoice) parseGearChoice = createClassParser<GearChoice>(
'GearChoice',
(i) => """
label: ${parseValue(i.label)},
gearOptions: ${parseArray('GearOption', i.gearOptions, parseGearOption)},
""");
String Function(GearOption) parseGearOption = createClassParser<GearOption>(
'GearOption',
(i) => '''
name: ${parseValue(i.name)},
tags: ${parseTagsArray(i.tags)},
''');
String Function(Monster) parseMonster = createClassParser<Monster>(
'Monster',
(i) => """
name: ${parseValue(i.name)},
description: ${parseValue(i.description)},
instinct: ${parseValue(i.instinct)},
tags: ${parseTagsArray(i.tags)},
moves: ${parseArray('String', i.moves, parseValue)},
""");
class ParseDef<T> {
final String name;
final Iterable<T> list;
final String Function(T) parser;
final String _file;
final String _arrayName;
String get file => _file;
String get arrayName => _arrayName;
ParseDef(this.name, this.list, this.parser, {String file, String arrayName})
: _file = file ?? _fileFromName(name),
_arrayName = arrayName ?? _arrayNameFromName(name);
String parse(dynamic obj) {
if (obj is T) {
return parser(obj);
}
return '// TYPE MISMATCH: $name';
}
static String _fileFromName(String name) =>
name
.splitMapJoin(RegExp('([A-Z])'),
onMatch: (match) => '_' + match.group(1).toLowerCase(),
onNonMatch: (i) => i)
.substring(1) +
'.dart';
static String _arrayNameFromName(String name) =>
'${name[0].toLowerCase()}${name.substring(1)}List';
}

2
scripts/parsers/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dumps/
dart_dumps/

View File

@@ -0,0 +1,23 @@
import 'dart:convert';
import 'dart:io';
import 'package:dungeon_world_data/dungeon_world_data.dart';
import 'package:path/path.dart' as path;
final _jsonOut = path.join(path.dirname(Platform.script.path), 'dumps', 'All.json');
final _jsonOutCopy =
path.join(path.dirname(Platform.script.path), '..', '..', 'web', 'src', 'lib', 'dw_data.json');
main() async {
final contents = dungeonWorldData.toJson();
await File(_jsonOut).writeAsString(json.encode(contents));
await File(_jsonOutCopy).writeAsString(json.encode(contents));
for (final e in contents.entries) {
final filePath = path.join(path.dirname(_jsonOut), e.key + ".json");
print("Writing $filePath...");
await File(filePath).writeAsString(json.encode(e.value));
}
print("Done");
}

View File

@@ -0,0 +1,57 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
final _jsonIn = path.join(
path.dirname(Platform.script.path),
'dumps',
'all.json',
);
final _dartOut = path.join(
path.dirname(Platform.script.path),
'..',
'..',
'lib',
'data',
);
final clsNameMap = {
'Moves': 'Move',
'Races': 'Race',
'Classes': 'CharacterClass',
'Spells': 'Spell',
'Items': 'Item',
'Monsters': 'Monster',
'Tags': 'Tag',
};
final clsImportMap = {
'Moves': 'move',
'Races': 'race',
'Classes': 'character_class',
'Spells': 'spell',
'Items': 'item',
'Monsters': 'monster',
'Tags': 'tag',
};
main() async {
final Map<String, dynamic> _json = json.decode(await File(_jsonIn).readAsString());
for (final e in _json.entries) {
if (clsNameMap[e.key] == null) {
continue;
}
final _path = path.join(_dartOut, e.key.toLowerCase() + ".dart");
print('Writing to $_path');
final list = e.value.map((r) => '${clsNameMap[e.key]}.fromJson(${json.encode(r)})');
await File(_path).writeAsString('''
import 'package:dungeon_world_data/${clsImportMap[e.key]}.dart';
List<${clsNameMap[e.key]}> get${clsNameMap[e.key]}List() => [
${list.join(',\n\t')}
];
''');
await Process.run('dart', ['format', _path]);
}
}

View File

@@ -1,75 +1,71 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:dungeon_world_data/dice.dart';
import 'package:test/test.dart';
void main() {
group('Dice', () {
test('Sides and amount', () {
var expected = Dice(6, 2);
expect(expected.sides, equals(6));
expect(expected.amount, equals(2));
group("Parse JSON", () {
test("No modifier", () {
var str = "1d6";
var dice = Dice.fromJson(str);
expect(dice.amount, equals(1));
expect(dice.sides, equals(6));
expect(dice.modifierValue, equals(null));
expect(dice.modifierStat, equals(null));
expect(dice.modifierSign, equals('+'));
});
test("With positive modifier", () {
var str = "3d8+3";
var dice = Dice.fromJson(str);
expect(dice.amount, equals(3));
expect(dice.sides, equals(8));
expect(dice.modifierValue, equals(3));
expect(dice.modifierStat, equals(null));
expect(dice.modifierSign, equals('+'));
});
test("With negative modifier", () {
var str = "2d20-4";
var dice = Dice.fromJson(str);
expect(dice.amount, equals(2));
expect(dice.sides, equals(20));
expect(dice.modifierValue, equals(-4));
expect(dice.modifierStat, equals(null));
expect(dice.modifierSign, equals('-'));
});
test("With stat modifier", () {
var str = "1d6+DEX";
var dice = Dice.fromJson(str);
expect(dice.amount, equals(1));
expect(dice.sides, equals(6));
expect(dice.modifierValue, equals(null));
expect(dice.modifierStat, equals("DEX"));
expect(dice.modifierSign, equals('+'));
expect(dice.needsModifier, equals(true));
expect(() => dice.roll(), throwsException);
});
});
test('String representation', () {
var dice1 = Dice(10, 4);
var dice2 = Dice(6, 2, 3);
var dice3 = Dice(20, 2, -4);
expect(dice1.toString(), equals('4d10'));
expect(dice2.toString(), equals('2d6+3'));
expect(dice3.toString(), equals('2d20-4'));
});
test('Multiplication', () {
num amt = 10;
var dice = Dice.d12 * amt;
expect(dice.amount, amt);
});
test('Equality', () {
var compare = {
Dice(4): Dice.d4,
Dice(6): Dice.d6,
Dice(8): Dice.d8,
Dice(10): Dice.d10,
Dice(12): Dice.d12,
Dice(20): Dice.d20,
};
for (var d in compare.keys) {
expect(d, equals(compare[d]));
expect(compare.values.where((el) => el == d).length, equals(1));
}
});
test('Roll', () {
var d1 = Dice.d6;
var d2 = Dice.d12 * 2;
var d3 = Dice.d8 * 3;
var roll1 = d1.getRoll();
var roll2 = d2.getRoll();
var roll3 = d3.getRoll();
expect(roll1.results[0], greaterThanOrEqualTo(1));
expect(roll1.results[0], lessThanOrEqualTo(6));
expect(roll2.results.length, equals(d2.amount));
expect(roll2.results[0], greaterThanOrEqualTo(1));
expect(roll2.results[0], lessThanOrEqualTo(12));
expect(roll2.results[1], greaterThanOrEqualTo(1));
expect(roll2.results[1], lessThanOrEqualTo(12));
expect(roll3.results.length, equals(d3.amount));
expect(roll3.results[0], greaterThanOrEqualTo(1));
expect(roll3.results[0], lessThanOrEqualTo(8));
expect(roll3.results[1], greaterThanOrEqualTo(1));
expect(roll3.results[1], lessThanOrEqualTo(8));
expect(roll3.results[2], greaterThanOrEqualTo(1));
expect(roll3.results[2], lessThanOrEqualTo(8));
});
test('Roll', () {
var dice = Dice.d6;
var roll = dice.getRoll();
expect(roll.results[0], greaterThanOrEqualTo(1));
expect(roll.results[0], lessThanOrEqualTo(6));
group("Dump JSON", () {
test("No modifier", () {
var str = "1d6";
var dice = Dice(amount: 1, sides: 6);
expect(dice.toJson(), equals(str));
});
test("With positive modifier", () {
var str = "3d8+3";
var dice = Dice(amount: 3, sides: 8, modifierValue: 3);
expect(dice.toJson(), equals(str));
});
test("With negative modifier", () {
var str = "2d20-4";
var dice = Dice(amount: 2, sides: 20, modifierValue: -4);
expect(dice.toJson(), equals(str));
});
test("With stat modifier", () {
var str = "2d20-DEX";
var dice = Dice(amount: 2, sides: 20, modifierStat: "DEX", modifierSign: "-");
expect(dice.toJson(), equals(str));
});
});
});
}

View File

@@ -1,42 +0,0 @@
import 'package:meta/meta.dart';
import 'package:test/test.dart';
import 'package:dungeon_world_data/dw_data.dart';
class Test1 extends DWEntity {
final int value;
Test1({
String key,
@required this.value,
}) : super(key: key);
@override
DWEntity copy() => Test1.fromJSON(toJSON());
@override
dynamic toJSON() => 1;
factory Test1.fromJSON(int value) => Test1(value: value);
}
void main() {
group('DWEntity', () {
test('hashCode', () {
var t1 = Test1(key: '1', value: 1);
var t2 = Test1(key: '1', value: 1);
expect(t1, equals(t2));
});
group('Helpers', () {
test('getByKey', () {
var classes = dungeonWorld.classes;
var wizard = dungeonWorld.classes.firstWhere((k) => k.key == 'wizard');
var found = DWEntity.getByKey<PlayerClass>('wizard', classes);
expect(wizard, equals(found));
var notFound = DWEntity.getByKey<PlayerClass>("doesn't exist", classes);
expect(notFound, equals(null));
});
});
});
}

View File

@@ -1,11 +0,0 @@
import 'package:test/test.dart';
import 'package:dungeon_world_data/dw_data.dart';
void main() {
group('Equipment', () {
test('Key', () {
var equipment = DWEntity.getByKey('tricksy_rope', dungeonWorld.equipment);
expect(equipment.key, equals('tricksy_rope'));
});
});
}

View File

@@ -1,11 +0,0 @@
import 'package:test/test.dart';
import 'package:dungeon_world_data/dw_data.dart';
void main() {
group('PlayerClass', () {
test('Key', () {
var paladin = DWEntity.getByKey('paladin', dungeonWorld.classes);
expect(paladin.key, equals('paladin'));
});
});
}

View File

@@ -1,25 +0,0 @@
import 'package:test/test.dart';
import 'package:dungeon_world_data/dw_data.dart';
void main() {
group('Spells', () {
test('Key', () {
var equipment = DWEntity.getByKey('plague', dungeonWorld.spells);
expect(equipment.key, equals('plague'));
});
test('toJSON', () {
var equipment = DWEntity.getByKey('plague', dungeonWorld.spells);
expect(equipment.toJSON()['tags'], contains('ongoing'));
var json = equipment.toJSON();
expect(json['key'], equals('plague'));
expect(json['name'], equals('Plague'));
expect(
json['description'],
equals(
'Name a city, town, encampment, or other place where people live. As long as this spell is active that place is beset by a plague appropriate to your deitys domains (locusts, death of the first born, etc.) While this spell is ongoing you take -1 to cast a spell.'));
expect(json['level'], equals('9'));
expect(json['tags'], contains('ongoing'));
});
});
}

View File

@@ -1,52 +0,0 @@
import 'package:dungeon_world_data/dw_data.dart';
import 'package:test/test.dart';
void main() {
group('Tags', () {
test('key', () {
var tag = Tag.fromJSON('two-handed');
expect(tag.hasValue, equals(false));
expect(tag.name, equals('two-handed'));
expect(tag.key, equals('two_handed'));
});
test('Parse string', () {
var tag1 = Tag.fromJSON('close');
var tag2 = Tag.fromJSON('1 coins');
var tag3 = Tag.fromJSON('{weight:1}');
expect(tag1.hasValue, equals(false));
expect(tag1.name, equals('close'));
expect(tag2.hasValue, equals(true));
expect(tag2.value, equals(1));
expect(tag2.name, equals('coins'));
expect(tag3.hasValue, equals(true));
expect(tag3.value, equals(1));
expect(tag3.name, equals('weight'));
});
test('Parse map', () {
var tag = Tag.fromJSON({'weight': 1});
expect(tag.name, equals('weight'));
expect(tag.hasValue, equals(true));
expect(tag.value, equals(1));
});
test('toString with value', () {
var tag = Tag.fromJSON({'coins': 10});
expect(tag.toString(), equals('coins: 10'));
});
test('toString without value', () {
var tag = Tag.fromJSON('near');
expect(tag.toString(), equals('near'));
});
test('toJSON with value', () {
var tag = Tag.fromJSON({'weight': 10});
var json = tag.toJSON();
expect(json, equals({'weight': 10}));
});
test('toJSON without value', () {
var tag = Tag.fromJSON('two-handed');
expect(tag.toJSON(), equals('two-handed'));
});
});
}

View File

@@ -1,2 +0,0 @@
## 10.0.12
* Unified "Cast A Spell" from separate Cleric/Wizard moves to one move

View File

@@ -1,67 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:dungeon_world_data/dw_data.dart';
import 'package:path/path.dart';
import 'package:quiver/pattern.dart';
void main() async {
final _scr = Platform.script.path;
final _dir = join(dirname(_scr), '..', '..');
Directory.current = Directory(join(dirname(_scr), '..'));
print('Node: Building output file');
final before = {
'tags': dungeonWorld.tags.map((el) => el.toJSON()).toList(),
'basicMoves': dungeonWorld.basicMoves.map((el) => el.toJSON()).toList(),
'specialMoves': dungeonWorld.specialMoves.map((el) => el.toJSON()).toList(),
'classes': dungeonWorld.classes.map((el) => el.toJSON()).toList(),
'equipment': dungeonWorld.equipment.map((el) => el.toJSON()).toList(),
'monsters': dungeonWorld.monsters.map((el) => el.toJSON()).toList(),
'spells': dungeonWorld.spells.map((el) => el.toJSON()).toList(),
};
var rawDataJsonString = jsonEncode(before);
// .replaceAll('"', '\\"')
// .replaceAll("'", "\\'")
// .replaceAll('\\n', '\\\\n');
print('Wrapping JSON string...');
rawDataJsonString = '''
final _dw =
$rawDataJsonString;
_dw.default = _dw;
module.exports = _dw;
'''
.replaceAll(' ' * 6, '');
print('Clearing build directory...');
final outputDir = Directory(join(_dir, 'npm_package'));
final inputDir = Directory(join(_dir, 'web', 'public'));
await outputDir.delete(recursive: true);
await outputDir.create();
final blacklist = <String>[];
await for (final file in inputDir.list(recursive: true)) {
final relativePath = relative(file.path, from: inputDir.path);
final outputPath = join(outputDir.path, relativePath);
print('Found file: ${file.path}, relative: $relativePath');
final inPattern =
blacklist.any((l) => Glob(join(inputDir.path, l)).hasMatch(file.path));
if (!inPattern && (await file.stat()).type == FileSystemEntityType.file) {
final contents = await File(file.path).readAsString();
print('Writing to file: $outputPath');
if (!await Directory(dirname(outputPath)).exists()) {
await Directory(dirname(outputPath)).create(recursive: true);
}
await File(outputPath).writeAsString(contents);
}
}
final rawDataOutputPath = join(outputDir.path, 'raw_data.js');
print('Writing file $rawDataOutputPath...');
await File(rawDataOutputPath).writeAsString(rawDataJsonString);
print('Done.');
}

130
web/src/.gitignore vendored Normal file
View File

@@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

6
web/src/CHANGELOG.md Executable file
View File

@@ -0,0 +1,6 @@
# CHANGELOG
## 3.0.0
- [BREAKING]: Complete overhaul of the data, and all types.
- Version number will stay up to date with the Dart package version from now on.

42
web/src/README.md Normal file
View File

@@ -0,0 +1,42 @@
# Dungeon World Data
This NPM package contains data for Dungeon World, such as classes, moves, spells, equipment, etc.
Homebew classes included:
- Immolator
- Barbarian.
## How to use
Import the default to access the entire data structure.
```javascript
import dungeonWorld from "dw-data";
// or
const dungeonWorld = require("dw-data");
```
## Available data
| Name | Type | Description |
| ------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| basicMoves | `Array<Move>` | Dungeon World's basic moves, such as Hack & Slash, Defy Danger, etc. |
| specialMoves | `Array<Move>` | Dungeon World's special moves, such as Make Camp, Take Watch, etc. |
| classes | `Array<PlayerClass>` | All of Dungeon World's classes, plus some homebrews. See `PlayerClass` class for a full description of the usable properties. |
| equipment | `Array<Equipment>` | Dungeon World's main list of items. |
| spells | `Array<Spell>` | Dungeon World's main spellbook list. Each class can have its own spells list, see `PlayerClass` in the docs for more information. |
| monsters | `Array<Monster>` | Dungeon World's main monster list. |
| tags | `Array<Tag>` | List of all basic tags, along with descriptions. |
## Credits
Credits to ~vindexus who created https://www.npmjs.com/package/dungeonworld-data.
The original data is from there and this is a direct export from the [Dart](https://pub.dev/packages/dungeon_world_data) variation, with a modified data set.
## Contributing
1. Make your changes
1. Make a PR, explain what your changes do, what could break.
1. ???
1. Profit!

1
web/src/lib/dw_data.json Normal file

File diff suppressed because one or more lines are too long

9
web/src/lib/index.ts Normal file
View File

@@ -0,0 +1,9 @@
import _data from "./dw_data.json"
export * from "./types"
import { DungeonWorldRepository } from "./types"
const dungeonWorldData: DungeonWorldRepository = _data as any
export default dungeonWorldData
export { dungeonWorldData }

123
web/src/lib/types.ts Normal file
View File

@@ -0,0 +1,123 @@
export interface DungeonWorldRepository {
CharacterClasses: Record<string, CharacterClass>
Items: Record<string, Item>
Monsters: Record<string, Monster>
Moves: Record<string, Move>
Races: Record<string, Race>
Spells: Record<string, Spell>
Tags: Record<string, Tag>
}
export interface CharacterClass {
_meta: Meta
name: string
key: string
description: string
damageDice: string
load: number
hp: number
alignments: Alignments
bonds: string[]
flags: string[]
gearChoices: GearChoice[]
isSpellcaster: boolean
}
export interface Meta {
language: string
createdBy: string
version: string
sharing: MetaSharing | null
}
interface MetaSharing {
sourceKey: string
sourceOwner: string
sourceVersion: string
}
export interface Alignments {
_meta: Meta
good: string
evil: string
lawful: string
neutral: string
chaotic: string
}
export interface GearChoice {
key: string
description: string
selections: GearSelection[]
preselect: number[]
maxSelections: number | null
}
export interface GearSelection {
key: string
description: string
options: GearOption[]
coins: number
}
export interface GearOption {
key: string
item: Item
amount: number
}
export interface Item {
_meta: Meta
key: string
name: string
description: string
tags: Tag[]
}
export interface Tag {
name: string
value: string | number | null
description: string
}
export interface Monster {
_meta: Meta
key: string
name: string
description: string
instinct: string
tags: Tag[]
moves: string[]
}
export type MoveLike<FaveKey extends string> = {
[key in FaveKey]: boolean
} & {
_meta: Meta
key: string
name: string
description: string
explanation: string
classKeys: EntityReference[]
tags: Tag[]
dice: string[]
}
export interface Move extends MoveLike<"favorite"> {
category: MoveCategory
}
export interface Spell extends MoveLike<"prepared"> {
level: string
}
export interface Race extends MoveLike<"favorite"> {
//
}
export interface EntityReference {
key: string
name: string
type: string
}
export type MoveCategory = "basic" | "special" | "starting" | "advanced1" | "advanced2"

17
web/src/package.json Executable file
View File

@@ -0,0 +1,17 @@
{
"name": "dw-data",
"version": "3.0.0-pre.24",
"main": "index.js",
"repository": "https://github.com/chenasraf/dungeon_world_data",
"author": "Chen Asraf <contact@casraf.blog>",
"license": "MIT",
"types": "./index.d.ts",
"scripts": {
"prebuild": "cd ../.. && dart scripts/parsers/dart_to_json.dart",
"build": "tsc",
"postbuild": "cp package.json ../build/"
},
"devDependencies": {
"typescript": "^4.7.4"
}
}

100
web/src/tsconfig.json Normal file
View File

@@ -0,0 +1,100 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
"resolveJsonModule": true, // /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "../build", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"../build",
"node_modules",
"example"
]
}

8
web/src/yarn.lock Normal file
View File

@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
typescript@^4.7.4:
version "4.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==