mirror of
https://github.com/DungeonPaper/dungeon_world_data.git
synced 2026-05-17 18:08:01 +00:00
dice with late stat modifier
This commit is contained in:
120
lib/dice.dart
120
lib/dice.dart
@@ -5,22 +5,35 @@ class Dice {
|
||||
Dice({
|
||||
required this.amount,
|
||||
required this.sides,
|
||||
this.modifier,
|
||||
});
|
||||
this.modifierValue,
|
||||
this.modifierStat,
|
||||
String? modifierSign,
|
||||
}) : modifierSign = modifierSign ??
|
||||
(modifierValue != null
|
||||
? modifierValue >= 0
|
||||
? "+"
|
||||
: "-"
|
||||
: "+");
|
||||
|
||||
final int amount;
|
||||
final int sides;
|
||||
final int? modifier;
|
||||
final int? modifierValue;
|
||||
final String? modifierStat;
|
||||
final String modifierSign;
|
||||
|
||||
Dice copyWith({
|
||||
int? amount,
|
||||
int? sides,
|
||||
int? modifier,
|
||||
int? modifierValue,
|
||||
String? modifierSign,
|
||||
String? modifierStat,
|
||||
}) =>
|
||||
Dice(
|
||||
amount: amount ?? this.amount,
|
||||
sides: sides ?? this.sides,
|
||||
modifier: modifier ?? this.modifier,
|
||||
modifierSign: modifierSign ?? this.modifierSign,
|
||||
modifierValue: modifierValue ?? this.modifierValue,
|
||||
modifierStat: modifierStat ?? this.modifierStat,
|
||||
);
|
||||
|
||||
factory Dice.fromRawJson(String str) => Dice.fromJson(json.decode(str));
|
||||
@@ -33,21 +46,18 @@ class Dice {
|
||||
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) {
|
||||
var parts = json.split("d");
|
||||
var amount = int.tryParse(parts[0]);
|
||||
int? sides;
|
||||
int? modifier;
|
||||
if (parts[1].contains(RegExp(r'[-+]'))) {
|
||||
var idx = parts[1].indexOf(RegExp(r'[^0-9]'));
|
||||
sides = int.tryParse(parts[1].substring(0, idx));
|
||||
modifier = int.tryParse(parts[1].substring(idx));
|
||||
} else {
|
||||
sides = int.tryParse(parts[1]);
|
||||
}
|
||||
var matches = _diceMatches(json);
|
||||
var amount = int.tryParse(matches[0]!);
|
||||
var sides = int.tryParse(matches[1]!);
|
||||
var modifierSign = matches[2];
|
||||
var modifierValue = matches[3] != null ? int.tryParse(matches[3]!) : null;
|
||||
var modifierStat =
|
||||
matches[3] != null && modifierValue == null ? matches[3]!.toUpperCase() : null;
|
||||
|
||||
if (sides == null || amount == null) {
|
||||
throw Exception("Dice parsing failed");
|
||||
@@ -56,50 +66,84 @@ class Dice {
|
||||
return Dice(
|
||||
amount: amount,
|
||||
sides: sides,
|
||||
modifier: modifier,
|
||||
modifierValue: modifierSign == '-' ? -(modifierValue ?? 0) : modifierValue,
|
||||
modifierSign: modifierSign,
|
||||
modifierStat: modifierStat,
|
||||
);
|
||||
}
|
||||
|
||||
Dice copyWithModifierValue(int statValue) =>
|
||||
copyWith(amount: amount, sides: sides, modifierValue: statValue);
|
||||
|
||||
@override
|
||||
String toString() => "${amount}d$sides$modifierWithSign";
|
||||
|
||||
String toJson() => toString();
|
||||
|
||||
String get modifierWithSign => modifier == null
|
||||
? ""
|
||||
: modifier! > 0
|
||||
? "+$modifier"
|
||||
: "$modifier";
|
||||
String get modifierWithSign =>
|
||||
hasModifier ? "$modifierSign${modifierValue?.abs() ?? modifierStat}" : "";
|
||||
|
||||
DiceResult roll() => DiceResult.roll(this);
|
||||
bool get hasModifier => (modifierValue != null || modifierStat != null);
|
||||
|
||||
String get modifier => hasModifier ? modifierStat ?? modifierValue!.toString() : "";
|
||||
|
||||
DiceRoll roll() {
|
||||
if (needsModifier) {
|
||||
throw Exception("Dice is being rolled without an actual modifier."
|
||||
"Use `copyWithModifierValue`.\n"
|
||||
"Expected modifier: $modifierWithSign");
|
||||
}
|
||||
var arr = <int>[];
|
||||
for (var i = 0; i < amount; i++) {
|
||||
arr.add(Random().nextInt(sides));
|
||||
}
|
||||
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);
|
||||
|
||||
static List<String?> _diceMatches(String json) {
|
||||
_assertDicePattern(json);
|
||||
var 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DiceResult {
|
||||
class DiceRoll {
|
||||
final Dice dice;
|
||||
final List<int> results;
|
||||
|
||||
DiceResult({required this.dice, required this.results});
|
||||
|
||||
static List<DiceResult> rollMany(List<Dice> dice) {
|
||||
return dice.map((d) {
|
||||
var arr = <int>[];
|
||||
for (var i = 0; i < d.amount; i++) {
|
||||
arr.add(Random().nextInt(d.sides));
|
||||
}
|
||||
return DiceResult(dice: d, results: arr);
|
||||
}).toList();
|
||||
DiceRoll({required this.dice, required this.results}) {
|
||||
assertDiceModifier();
|
||||
}
|
||||
|
||||
int get total =>
|
||||
results.reduce((all, cur) => all + cur) + (dice.modifier ?? 0);
|
||||
static List<DiceRoll> rollMany(List<Dice> dice) => dice.map((d) => roll(d)).toList();
|
||||
|
||||
int get total => results.reduce((all, cur) => all + cur) + (dice.modifierValue ?? 0);
|
||||
|
||||
bool get didHitNaturalMax => indexOfNaturalMax >= 0;
|
||||
int get indexOfNaturalMax => results.indexOf(dice.sides);
|
||||
|
||||
static DiceResult roll(Dice dice) {
|
||||
return rollMany([dice])[0];
|
||||
static DiceRoll roll(Dice dice) {
|
||||
return dice.roll();
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,134 @@
|
||||
class Repository {}
|
||||
import '../character_class.dart';
|
||||
import '../alignment.dart';
|
||||
import '../item.dart';
|
||||
import '../monster.dart';
|
||||
import '../move.dart';
|
||||
import '../race.dart';
|
||||
import '../spell.dart';
|
||||
import '../tag.dart';
|
||||
|
||||
class RepositoryItem<T> {
|
||||
final Map<String, Map<String, T>> _cache = {};
|
||||
String currentLocale;
|
||||
class Repository {
|
||||
String _currentLocale;
|
||||
String get currentLocale => _currentLocale;
|
||||
final Set<String> _locales = {};
|
||||
|
||||
RepositoryItem({
|
||||
required this.currentLocale,
|
||||
});
|
||||
final Map<String, RepositoryItem<AlignmentValue>> _alignments = {};
|
||||
final Map<String, RepositoryItem<CharacterClass>> _classes = {};
|
||||
final Map<String, RepositoryItem<Item>> _items = {};
|
||||
final Map<String, RepositoryItem<Monster>> _monsters = {};
|
||||
final Map<String, RepositoryItem<Race>> _races = {};
|
||||
final Map<String, RepositoryItem<Move>> _moves = {};
|
||||
final Map<String, RepositoryItem<Spell>> _spells = {};
|
||||
final Map<String, RepositoryItem<Tag>> _tags = {};
|
||||
|
||||
List<T> get list {
|
||||
_ensureLocale(currentLocale);
|
||||
return _cache[currentLocale]!.values.toList();
|
||||
}
|
||||
Repository({
|
||||
required String currentLocale,
|
||||
}) : _currentLocale = currentLocale;
|
||||
|
||||
Map<String, T> get map {
|
||||
_ensureLocale(currentLocale);
|
||||
return _cache[currentLocale]!;
|
||||
}
|
||||
RepositoryItem<AlignmentValue> get alignments => _withCurrentLocale(_alignments);
|
||||
RepositoryItem<CharacterClass> get classes => _withCurrentLocale(_classes);
|
||||
RepositoryItem<Item> get items => _withCurrentLocale(_items);
|
||||
RepositoryItem<Monster> get monsters => _withCurrentLocale(_monsters);
|
||||
RepositoryItem<Race> get races => _withCurrentLocale(_races);
|
||||
RepositoryItem<Move> get moves => _withCurrentLocale(_moves);
|
||||
RepositoryItem<Spell> get spells => _withCurrentLocale(_spells);
|
||||
RepositoryItem<Tag> get tags => _withCurrentLocale(_tags);
|
||||
|
||||
void registerLocale(String locale) {
|
||||
if (_cache[locale] != null) {
|
||||
if (localeExists(locale)) {
|
||||
return;
|
||||
}
|
||||
_cache[locale] = {};
|
||||
|
||||
for (var entry in _allRepositories.entries) {
|
||||
if (entry.value[locale] != null) {
|
||||
continue;
|
||||
}
|
||||
entry.value[locale] = RepositoryItem(locale: locale);
|
||||
}
|
||||
|
||||
_locales.add(locale);
|
||||
}
|
||||
|
||||
void addItems(String locale, Map<String, T> items) {
|
||||
void changeLocale(String locale) {
|
||||
_ensureLocale(locale);
|
||||
_cache[locale]!.addAll(items);
|
||||
_currentLocale = locale;
|
||||
}
|
||||
|
||||
void loadItems<T>(String locale, Map<String, T> items) {
|
||||
registerLocale(locale);
|
||||
switch (T) {
|
||||
case AlignmentValue:
|
||||
_alignments[locale]!.addItems(items.cast<String, AlignmentValue>());
|
||||
break;
|
||||
case CharacterClass:
|
||||
_classes[locale]!.addItems(items.cast<String, CharacterClass>());
|
||||
break;
|
||||
case Item:
|
||||
_items[locale]!.addItems(items.cast<String, Item>());
|
||||
break;
|
||||
case Monster:
|
||||
_monsters[locale]!.addItems(items.cast<String, Monster>());
|
||||
break;
|
||||
case Race:
|
||||
_races[locale]!.addItems(items.cast<String, Race>());
|
||||
break;
|
||||
case Move:
|
||||
_moves[locale]!.addItems(items.cast<String, Move>());
|
||||
break;
|
||||
case Spell:
|
||||
_spells[locale]!.addItems(items.cast<String, Spell>());
|
||||
break;
|
||||
case Tag:
|
||||
_tags[locale]!.addItems(items.cast<String, Tag>());
|
||||
break;
|
||||
default:
|
||||
throw Exception("Type $T not supported");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Map<String, RepositoryItem<dynamic>>> get _allRepositories => {
|
||||
'alignments': _alignments,
|
||||
'classes': _classes,
|
||||
'items': _items,
|
||||
'monsters': _monsters,
|
||||
'races': _races,
|
||||
'moves': _moves,
|
||||
'spells': _spells,
|
||||
'tags': _tags,
|
||||
};
|
||||
|
||||
void _ensureLocale(String locale) {
|
||||
if (_cache[locale] == null) {
|
||||
if (!localeExists(locale)) {
|
||||
throw Exception('Locale $locale does not exist.');
|
||||
}
|
||||
}
|
||||
|
||||
bool localeExists(String locale) => _locales.contains(locale);
|
||||
|
||||
T _withCurrentLocale<T>(Map<String, T> iter) {
|
||||
_ensureLocale(currentLocale);
|
||||
if (iter.isEmpty || iter[currentLocale] == null) {
|
||||
throw Exception('Repository Item does not exist for locale $currentLocale');
|
||||
}
|
||||
return iter[currentLocale]!;
|
||||
}
|
||||
}
|
||||
|
||||
class RepositoryItem<T> {
|
||||
final Map<String, T> _cache = {};
|
||||
final String locale;
|
||||
|
||||
RepositoryItem({required this.locale});
|
||||
|
||||
List<T> get list {
|
||||
return _cache.values.toList();
|
||||
}
|
||||
|
||||
Map<String, T> get map {
|
||||
return _cache;
|
||||
}
|
||||
|
||||
void addItems(Map<String, T> items) {
|
||||
_cache!.addAll(items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,7 @@ main() async {
|
||||
// Races
|
||||
print("Adding ${cls.raceMoves.length} races");
|
||||
for (var race in cls.raceMoves) {
|
||||
json['races']!
|
||||
.add(raceMapper(race, cls.key ?? makeKey(cls.name)).toJson());
|
||||
json['races']!.add(raceMapper(race, cls.key ?? makeKey(cls.name)).toJson());
|
||||
}
|
||||
|
||||
// Classes
|
||||
@@ -121,12 +120,12 @@ String makeKey(String str) {
|
||||
}
|
||||
|
||||
Set<Dice> guessDice(String str) {
|
||||
var basicRollPattern = RegExp(r'\broll\+[a-z]{3}\b', caseSensitive: false);
|
||||
var basicRollPattern = RegExp(r'\broll([+-][a-z]+)\b', caseSensitive: false);
|
||||
var dicePattern = RegExp(r'\b\dd\d\b', caseSensitive: false);
|
||||
var found = <Dice>{};
|
||||
var basicRollMatches = basicRollPattern.allMatches(str);
|
||||
for (var match in basicRollMatches) {
|
||||
found.add(Dice.d6 * 2);
|
||||
found.add(Dice.fromJson('2d6' + match.group(1)!.toUpperCase()));
|
||||
}
|
||||
var diceMatches = dicePattern.allMatches(str);
|
||||
for (var match in diceMatches) {
|
||||
@@ -212,10 +211,8 @@ CharacterClass classMapper(old.PlayerClass cls) => CharacterClass(
|
||||
items: [
|
||||
GearOption(
|
||||
amount: o.name.contains(RegExp(r'[0-9]+'))
|
||||
? double.tryParse(RegExp(r'[0-9]+')
|
||||
.firstMatch(o.name)
|
||||
?.group(0) ??
|
||||
"1.0") ??
|
||||
? double.tryParse(
|
||||
RegExp(r'[0-9]+').firstMatch(o.name)?.group(0) ?? "1.0") ??
|
||||
1.0
|
||||
: 1.0,
|
||||
item: Item(
|
||||
|
||||
@@ -9,21 +9,39 @@ void main() {
|
||||
var dice = Dice.fromJson(str);
|
||||
expect(dice.amount, equals(1));
|
||||
expect(dice.sides, equals(6));
|
||||
expect(dice.modifier, equals(null));
|
||||
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.modifier, equals(3));
|
||||
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.modifier, equals(-4));
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,12 +53,17 @@ void main() {
|
||||
});
|
||||
test("With positive modifier", () {
|
||||
var str = "3d8+3";
|
||||
var dice = Dice(amount: 3, sides: 8, modifier: 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, modifier: -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));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user