Add the alteration system to the server & link the app on it.
This commit is contained in:
parent
4ed11660c3
commit
29747dcb5c
83 changed files with 1797 additions and 811 deletions
|
|
@ -1,9 +1,17 @@
|
|||
package com.pixelized.shared.lwa
|
||||
|
||||
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
|
||||
import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory
|
||||
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
|
||||
import com.pixelized.shared.lwa.parser.dice.DiceParser
|
||||
import com.pixelized.shared.lwa.parser.expression.ExpressionParser
|
||||
import com.pixelized.shared.lwa.parser.word.WordParser
|
||||
import com.pixelized.shared.lwa.usecase.CampaignUseCase
|
||||
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
|
||||
import com.pixelized.shared.lwa.usecase.ExpressionUseCase
|
||||
import com.pixelized.shared.lwa.usecase.RollUseCase
|
||||
import com.pixelized.shared.lwa.usecase.SkillStepUseCase
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
import org.koin.dsl.module
|
||||
|
|
@ -12,6 +20,7 @@ val sharedModuleDependencies
|
|||
get() = listOf(
|
||||
toolsDependencies,
|
||||
factoryDependencies,
|
||||
parserDependencies,
|
||||
useCaseDependencies,
|
||||
)
|
||||
|
||||
|
|
@ -29,10 +38,23 @@ val factoryDependencies
|
|||
get() = module {
|
||||
factoryOf(::CharacterSheetJsonFactory)
|
||||
factoryOf(::CampaignJsonFactory)
|
||||
factoryOf(::AlteredCharacterSheetFactory)
|
||||
factoryOf(::AlterationJsonFactory)
|
||||
}
|
||||
|
||||
val parserDependencies
|
||||
get() = module {
|
||||
factoryOf(::WordParser)
|
||||
factoryOf(::DiceParser)
|
||||
factoryOf(::ExpressionParser)
|
||||
}
|
||||
|
||||
val useCaseDependencies
|
||||
get() = module {
|
||||
factoryOf(::CharacterSheetUseCase)
|
||||
factoryOf(::CampaignUseCase)
|
||||
factoryOf(::SkillStepUseCase)
|
||||
factoryOf(::RollUseCase)
|
||||
factoryOf(::ExpressionUseCase)
|
||||
factoryOf(::CharacterSheetUseCase)
|
||||
}
|
||||
|
|
@ -43,4 +43,13 @@ fun campaignPath(
|
|||
OperatingSystem.Windows -> "${storePath(os = os)}campaign\\"
|
||||
OperatingSystem.Macintosh -> "${storePath(os = os)}campaign/"
|
||||
}
|
||||
}
|
||||
|
||||
fun alterationsPath(
|
||||
os: OperatingSystem = OperatingSystem.current,
|
||||
): String {
|
||||
return when (os) {
|
||||
OperatingSystem.Windows -> "${storePath(os = os)}alterations\\"
|
||||
OperatingSystem.Macintosh -> "${storePath(os = os)}alterations/"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.pixelized.shared.lwa.model
|
||||
|
||||
import com.pixelized.shared.lwa.model.alteration.FieldAlteration
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.ARMOR
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.CHA
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.CON
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.DEX
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.DMG
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.GHP
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HEI
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HP
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.INT
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.LB
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.LVL
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.MOV
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.PORTRAIT
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.POW
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.PP
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.STR
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.THUMBNAIL
|
||||
import com.pixelized.shared.lwa.parser.expression.Expression
|
||||
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
|
||||
import com.pixelized.shared.lwa.usecase.ExpressionUseCase
|
||||
|
||||
class AlteredCharacterSheetFactory(
|
||||
private val sheetUseCase: CharacterSheetUseCase,
|
||||
private val expressionUseCase: ExpressionUseCase,
|
||||
) {
|
||||
fun sheet(
|
||||
characterSheet: CharacterSheet,
|
||||
alterations: Map<String, List<FieldAlteration>>,
|
||||
): AlteredCharacterSheet {
|
||||
return AlteredCharacterSheet(
|
||||
sheetUseCase = sheetUseCase,
|
||||
expressionUseCase = expressionUseCase,
|
||||
sheet = characterSheet,
|
||||
alterations = alterations,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class AlteredCharacterSheet(
|
||||
private val sheetUseCase: CharacterSheetUseCase,
|
||||
private val expressionUseCase: ExpressionUseCase,
|
||||
private val sheet: CharacterSheet,
|
||||
private val alterations: Map<String, List<FieldAlteration>>,
|
||||
) {
|
||||
val id: String = sheet.id
|
||||
|
||||
val name: String = sheet.name
|
||||
|
||||
val portrait: String?
|
||||
get() = alterations[PORTRAIT]
|
||||
?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url }
|
||||
?: sheet.portrait
|
||||
|
||||
val thumbnail: String?
|
||||
get() = alterations[THUMBNAIL]
|
||||
?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url }
|
||||
?: sheet.thumbnail
|
||||
|
||||
val level: Int
|
||||
get() = sheet.level + alterations[LVL].sum()
|
||||
|
||||
val strength: Int
|
||||
get() = sheet.strength + alterations[STR].sum()
|
||||
|
||||
val dexterity: Int
|
||||
get() = sheet.dexterity + alterations[DEX].sum()
|
||||
|
||||
val constitution: Int
|
||||
get() = sheet.constitution + alterations[CON].sum()
|
||||
|
||||
val height: Int
|
||||
get() = sheet.height + alterations[HEI].sum()
|
||||
|
||||
val intelligence: Int
|
||||
get() = sheet.intelligence + alterations[INT].sum()
|
||||
|
||||
val power: Int
|
||||
get() = sheet.power + alterations[POW].sum()
|
||||
|
||||
val charisma: Int
|
||||
get() = sheet.charisma + alterations[CHA].sum()
|
||||
|
||||
val movement: Int
|
||||
get() = sheetUseCase.movement() + alterations[MOV].sum()
|
||||
|
||||
val armor: Int
|
||||
get() = sheetUseCase.armor() + alterations[ARMOR].sum()
|
||||
|
||||
val maxHp: Int
|
||||
get() = sheetUseCase.maxHp(
|
||||
constitution = constitution,
|
||||
height = height,
|
||||
level = level,
|
||||
) + alterations[HP].sum()
|
||||
|
||||
val maxPp: Int
|
||||
get() = sheetUseCase.maxPp(
|
||||
power = power,
|
||||
) + alterations[PP].sum()
|
||||
|
||||
val damageBonus: String
|
||||
get() {
|
||||
val initial = sheetUseCase.damageBonus(
|
||||
strength = strength,
|
||||
height = height,
|
||||
)
|
||||
return alterations[DMG]
|
||||
?.joinToString(separator = "+") { it.expression.toString() }
|
||||
?.let { "$initial+$it" }
|
||||
?: initial
|
||||
}
|
||||
|
||||
val learning: Int
|
||||
get() = sheetUseCase.learning(
|
||||
intelligence = intelligence
|
||||
) + alterations[LB].sum()
|
||||
|
||||
val hpGrow: Int
|
||||
get() = sheetUseCase.hpGrow(
|
||||
constitution = constitution,
|
||||
) + alterations[GHP].sum()
|
||||
|
||||
// Helper method
|
||||
|
||||
private fun List<FieldAlteration>?.sum() = this?.sumOf {
|
||||
expressionUseCase.computeExpression(
|
||||
sheet = sheet,
|
||||
alterations = alterations,
|
||||
expression = it.expression
|
||||
)
|
||||
} ?: 0
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.pixelized.shared.lwa.model.alteration
|
||||
|
||||
import com.pixelized.shared.lwa.parser.expression.Expression
|
||||
|
||||
data class Alteration(
|
||||
val id: String,
|
||||
val metadata: MetaData,
|
||||
val fields: List<Field>,
|
||||
) {
|
||||
data class MetaData(
|
||||
val name: String,
|
||||
val description: String,
|
||||
)
|
||||
|
||||
data class Field(
|
||||
val fieldId: String, // this id is not the id of the instance but the id of the impacted field (characteristic, skill etc.)
|
||||
val expression: Expression,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.pixelized.shared.lwa.model.alteration
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface AlterationJson
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.pixelized.shared.lwa.model.alteration
|
||||
|
||||
import com.pixelized.shared.lwa.parser.expression.Expression
|
||||
import com.pixelized.shared.lwa.parser.expression.ExpressionParser
|
||||
|
||||
class AlterationJsonFactory(
|
||||
private val expressionParser: ExpressionParser,
|
||||
) {
|
||||
|
||||
fun convertFromJson(
|
||||
json: AlterationJson,
|
||||
): Alteration {
|
||||
return when (json) {
|
||||
is AlterationJsonV1 -> convertFromJsonV1(json = json)
|
||||
}
|
||||
}
|
||||
|
||||
fun convertFromJsonV1(
|
||||
json: AlterationJsonV1,
|
||||
): Alteration {
|
||||
return Alteration(
|
||||
id = json.id,
|
||||
metadata = convertFromJsonV1(json = json.metadata),
|
||||
fields = json.fields.map { convertFromJsonV1(json = it) }
|
||||
)
|
||||
}
|
||||
|
||||
fun convertFromJsonV1(
|
||||
json: AlterationJsonV1.AlterationMetadataJsonV1,
|
||||
): Alteration.MetaData {
|
||||
return Alteration.MetaData(
|
||||
name = json.name,
|
||||
description = json.description,
|
||||
)
|
||||
}
|
||||
|
||||
fun convertFromJsonV1(
|
||||
json: AlterationJsonV1.FieldJsonV1,
|
||||
): Alteration.Field {
|
||||
return Alteration.Field(
|
||||
fieldId = json.fieldId,
|
||||
expression = expressionParser.parse(json.expression) ?: Expression.Flat(0),
|
||||
)
|
||||
}
|
||||
|
||||
fun convertToJson(
|
||||
data: Alteration,
|
||||
): AlterationJson {
|
||||
return AlterationJsonV1(
|
||||
id = data.id,
|
||||
metadata = convertToJson(data = data.metadata),
|
||||
fields = data.fields.map { convertToJson(data = it) },
|
||||
)
|
||||
}
|
||||
|
||||
fun convertToJson(
|
||||
data: Alteration.MetaData,
|
||||
): AlterationJsonV1.AlterationMetadataJsonV1 {
|
||||
return AlterationJsonV1.AlterationMetadataJsonV1(
|
||||
name = data.name,
|
||||
description = data.description,
|
||||
)
|
||||
}
|
||||
|
||||
fun convertToJson(
|
||||
data: Alteration.Field,
|
||||
): AlterationJsonV1.FieldJsonV1 {
|
||||
return AlterationJsonV1.FieldJsonV1(
|
||||
fieldId = data.fieldId,
|
||||
expression = data.expression.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.pixelized.shared.lwa.model.alteration
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AlterationJsonV1(
|
||||
val id: String,
|
||||
val metadata: AlterationMetadataJsonV1,
|
||||
val fields: List<FieldJsonV1>,
|
||||
) : AlterationJson {
|
||||
|
||||
@Serializable
|
||||
data class FieldJsonV1(
|
||||
val fieldId: String, // this id is not the id of the instance but the id of the impacted field (characteristic, skill etc.)
|
||||
val expression: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AlterationMetadataJsonV1(
|
||||
val name: String,
|
||||
val description: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.pixelized.shared.lwa.model.alteration
|
||||
|
||||
import com.pixelized.shared.lwa.parser.expression.Expression
|
||||
|
||||
data class FieldAlteration(
|
||||
val alterationId: String,
|
||||
val metadata: Alteration.MetaData,
|
||||
val expression: Expression,
|
||||
)
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
package com.pixelized.shared.lwa.model.campaign
|
||||
|
||||
data class Campaign(
|
||||
val characters: Map<String, CharacterInstance>,
|
||||
val npcs: Map<String, CharacterInstance>,
|
||||
val characters: Map<CharacterInstance.Id, CharacterInstance>,
|
||||
val npcs: Map<CharacterInstance.Id, CharacterInstance>,
|
||||
) {
|
||||
data class CharacterInstance(
|
||||
val characteristic: Map<Characteristic, Int>,
|
||||
) {
|
||||
data class Id(
|
||||
val characterSheetId: String,
|
||||
val instanceId: Int,
|
||||
)
|
||||
|
||||
enum class Characteristic {
|
||||
Damage,
|
||||
Power,
|
||||
|
|
@ -21,12 +26,18 @@ data class Campaign(
|
|||
}
|
||||
}
|
||||
|
||||
fun Campaign.character(id: String): Campaign.CharacterInstance {
|
||||
fun Campaign.character(id: Campaign.CharacterInstance.Id): Campaign.CharacterInstance {
|
||||
return characters[id] ?: Campaign.CharacterInstance(
|
||||
characteristic = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
||||
fun Campaign.npc(id: Campaign.CharacterInstance.Id): Campaign.CharacterInstance {
|
||||
return npcs[id] ?: Campaign.CharacterInstance(
|
||||
characteristic = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
||||
val Campaign.CharacterInstance.level
|
||||
get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 1
|
||||
|
||||
|
|
|
|||
|
|
@ -6,51 +6,94 @@ class CampaignJsonFactory {
|
|||
json: CampaignJson,
|
||||
): Campaign {
|
||||
return when (json) {
|
||||
is CampaignJsonV1 -> convertFromV1(json = json)
|
||||
is CampaignJsonV1 -> convertFromV1(campaignJson = json)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertFromV1(
|
||||
json: CampaignJsonV1,
|
||||
campaignJson: CampaignJsonV1,
|
||||
): Campaign {
|
||||
return Campaign(
|
||||
characters = json.characters.mapValues { convertFromV1(json = it.value) },
|
||||
npcs = json.npcs.mapValues { convertFromV1(json = it.value) },
|
||||
characters = campaignJson.characters
|
||||
.map {
|
||||
convertFromV1(characterInstanceIdJson = it.key) to convertFromV1(
|
||||
characterInstanceJson = it.value
|
||||
)
|
||||
}
|
||||
.toMap(),
|
||||
npcs = campaignJson.npcs
|
||||
.map {
|
||||
convertFromV1(characterInstanceIdJson = it.key) to convertFromV1(
|
||||
characterInstanceJson = it.value
|
||||
)
|
||||
}
|
||||
.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertFromV1(
|
||||
json: CampaignJsonV1.CharacterInstanceJson,
|
||||
fun convertFromV1(
|
||||
characterInstanceIdJson: String,
|
||||
): Campaign.CharacterInstance.Id {
|
||||
return Campaign.CharacterInstance.Id(
|
||||
characterSheetId = characterInstanceIdJson.drop(4), // drop first 3 number then the -
|
||||
instanceId = characterInstanceIdJson.take(3).toIntOrNull() ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
fun convertFromV1(
|
||||
characterInstanceJson: CampaignJsonV1.CharacterInstanceJson,
|
||||
): Campaign.CharacterInstance {
|
||||
return Campaign.CharacterInstance(
|
||||
characteristic = json.characteristic.map { char ->
|
||||
when (char.key) {
|
||||
CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage -> Campaign.CharacterInstance.Characteristic.Damage
|
||||
CampaignJsonV1.CharacterInstanceJson.Characteristic.Power -> Campaign.CharacterInstance.Characteristic.Power
|
||||
} to char.value
|
||||
}.toMap(),
|
||||
characteristic = characterInstanceJson.characteristic
|
||||
.map { char -> convertFromV1(characteristicJson = char.key) to char.value }
|
||||
.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
fun convertFromV1(
|
||||
characteristicJson: CampaignJsonV1.CharacterInstanceJson.Characteristic,
|
||||
): Campaign.CharacterInstance.Characteristic {
|
||||
return when (characteristicJson) {
|
||||
CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage -> Campaign.CharacterInstance.Characteristic.Damage
|
||||
CampaignJsonV1.CharacterInstanceJson.Characteristic.Power -> Campaign.CharacterInstance.Characteristic.Power
|
||||
}
|
||||
}
|
||||
|
||||
fun convertToJson(
|
||||
data: Campaign,
|
||||
): CampaignJson {
|
||||
return CampaignJsonV1(
|
||||
characters = data.characters.mapValues { convertToJson(data = it.value) },
|
||||
npcs = data.npcs.mapValues { convertToJson(data = it.value) },
|
||||
characters = data.characters
|
||||
.map { convertToJson(id = it.key) to convertToJson(data = it.value) }
|
||||
.toMap(),
|
||||
npcs = data.npcs
|
||||
.map { convertToJson(id = it.key) to convertToJson(data = it.value) }
|
||||
.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertToJson(
|
||||
fun convertToJson(
|
||||
id: Campaign.CharacterInstance.Id,
|
||||
): String {
|
||||
return "${String.format("%03d", id.instanceId)}-${id.characterSheetId}"
|
||||
}
|
||||
|
||||
fun convertToJson(
|
||||
data: Campaign.CharacterInstance,
|
||||
): CampaignJsonV1.CharacterInstanceJson {
|
||||
return CampaignJsonV1.CharacterInstanceJson(
|
||||
characteristic = data.characteristic.map { char ->
|
||||
when (char.key) {
|
||||
Campaign.CharacterInstance.Characteristic.Damage -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage
|
||||
Campaign.CharacterInstance.Characteristic.Power -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Power
|
||||
} to char.value
|
||||
}.toMap(),
|
||||
characteristic = data.characteristic
|
||||
.map { char -> convertToJson(characteristic = char.key) to char.value }
|
||||
.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
fun convertToJson(
|
||||
characteristic: Campaign.CharacterInstance.Characteristic,
|
||||
): CampaignJsonV1.CharacterInstanceJson.Characteristic {
|
||||
return when (characteristic) {
|
||||
Campaign.CharacterInstance.Characteristic.Damage -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage
|
||||
Campaign.CharacterInstance.Characteristic.Power -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Power
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,12 +16,7 @@ data class CharacterSheet(
|
|||
val charisma: Int,
|
||||
// sub characteristics
|
||||
val movement: Int,
|
||||
val hp: Int,
|
||||
val pp: Int,
|
||||
val damageBonus: String,
|
||||
val armor: Int,
|
||||
val learning: Int,
|
||||
val hpGrow: Int,
|
||||
// skills
|
||||
val commonSkills: List<Skill>,
|
||||
val specialSkills: List<Skill>,
|
||||
|
|
|
|||
|
|
@ -31,26 +31,8 @@ class CharacterSheetJsonFactory(
|
|||
intelligence = json.intelligence,
|
||||
power = json.power,
|
||||
charisma = json.charisma,
|
||||
movement = defaultMovement(),
|
||||
hp = defaultMaxHp(
|
||||
constitution = json.constitution,
|
||||
height = json.height,
|
||||
level = json.level
|
||||
),
|
||||
pp = defaultMaxPower(
|
||||
power = json.power,
|
||||
),
|
||||
damageBonus = defaultDamageBonus(
|
||||
strength = json.strength,
|
||||
height = json.height,
|
||||
),
|
||||
armor = defaultArmor(),
|
||||
learning = defaultLearning(
|
||||
intelligence = json.intelligence,
|
||||
),
|
||||
hpGrow = defaultHpGrow(
|
||||
constitution = json.constitution,
|
||||
),
|
||||
movement = movement(),
|
||||
armor = armor(),
|
||||
commonSkills = json.skills.map {
|
||||
CharacterSheet.Skill(
|
||||
id = it.id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.pixelized.shared.lwa.parser.dice
|
||||
|
||||
data class Dice(
|
||||
val modifier: Modifier?,
|
||||
val quantity: Int,
|
||||
val faces: Int,
|
||||
) {
|
||||
enum class Modifier {
|
||||
ADVANTAGE,
|
||||
DISADVANTAGE,
|
||||
EMPHASIS,
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val modifier = when (modifier) {
|
||||
Modifier.ADVANTAGE -> "a"
|
||||
Modifier.DISADVANTAGE -> "d"
|
||||
Modifier.EMPHASIS -> "e"
|
||||
null -> ""
|
||||
}
|
||||
return "${modifier}${quantity}d${faces}"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.pixelized.shared.lwa.parser.dice
|
||||
|
||||
class DiceParser {
|
||||
private val diceParser = Regex(
|
||||
"""^(?<modifier>[ade])?(?<quantity>\d+)[dD](?<faces>\d+)"""
|
||||
)
|
||||
|
||||
fun parse(expression: String): Dice? {
|
||||
return diceParser.find(expression)?.let {
|
||||
val (modifier, quantity, faces) = it.destructured
|
||||
Dice(
|
||||
modifier = parseModifier(value = modifier),
|
||||
quantity = quantity.toInt(),
|
||||
faces = faces.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseModifier(value: String): Dice.Modifier? {
|
||||
return when (value) {
|
||||
"a" -> Dice.Modifier.ADVANTAGE
|
||||
"d" -> Dice.Modifier.DISADVANTAGE
|
||||
"e" -> Dice.Modifier.EMPHASIS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.pixelized.shared.lwa.parser.expression
|
||||
|
||||
import com.pixelized.shared.lwa.parser.dice.Dice
|
||||
import com.pixelized.shared.lwa.parser.word.Word
|
||||
|
||||
sealed interface Expression {
|
||||
|
||||
data class Add(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "($first+$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Minus(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "($first-$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Div(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "($first/$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Prod(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "($first*$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Minimum(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "min($first,$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Maximum(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "max($first,$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Inversion(
|
||||
val expression: Expression,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "-($expression)"
|
||||
}
|
||||
}
|
||||
|
||||
data class UrlExpression(
|
||||
val url: String,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
data class DiceExpression(
|
||||
val dice: Dice,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return dice.toString()
|
||||
}
|
||||
}
|
||||
|
||||
data class WordExpression(
|
||||
val word: Word,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return word.toString()
|
||||
}
|
||||
}
|
||||
|
||||
data class Flat(
|
||||
val value: Int,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "$value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
package com.pixelized.shared.lwa.parser.expression
|
||||
|
||||
import com.pixelized.shared.lwa.parser.dice.DiceParser
|
||||
import com.pixelized.shared.lwa.parser.word.WordParser
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Highly inspired by the following javascript implementation:
|
||||
* https://tiarkrompf.github.io/notes/?/just-write-the-parser/
|
||||
*/
|
||||
class ExpressionParser(
|
||||
private val diceParser: DiceParser,
|
||||
private val wordParser: WordParser,
|
||||
) {
|
||||
companion object {
|
||||
private val tokenBreak = arrayOf(
|
||||
'+', '-', '/', '*', '(', ')', ','
|
||||
)
|
||||
private val tokenBreakException = mapOf(
|
||||
'/' to listOf("http:", "https:"),
|
||||
'.' to listOf("http:", "https:"),
|
||||
'+' to listOf("http:", "https:"),
|
||||
'-' to listOf("http:", "https:"),
|
||||
)
|
||||
private val operators = mapOf(
|
||||
'+' to Operator(
|
||||
evaluations = { first, second -> Expression.Add(first, second) },
|
||||
priority = 100,
|
||||
association = 1,
|
||||
),
|
||||
'-' to Operator(
|
||||
evaluations = { first, second -> Expression.Minus(first, second) },
|
||||
priority = 100,
|
||||
association = 1,
|
||||
),
|
||||
'*' to Operator(
|
||||
evaluations = { first, second -> Expression.Prod(first, second) },
|
||||
priority = 200,
|
||||
association = 1,
|
||||
),
|
||||
'/' to Operator(
|
||||
evaluations = { first, second -> Expression.Div(first, second) },
|
||||
priority = 200,
|
||||
association = 1,
|
||||
),
|
||||
)
|
||||
|
||||
private inline fun guard(condition: Boolean, error: () -> Error) {
|
||||
return if (condition.not()) throw error() else Unit
|
||||
}
|
||||
}
|
||||
|
||||
private val stack = Stack()
|
||||
|
||||
/**
|
||||
* Helper method to determined when a token should end.
|
||||
* Every characters that are not un the [Companion.tokenBreak] list can be part of a token.
|
||||
* @see Companion.tokenBreak
|
||||
*/
|
||||
private fun isToken(
|
||||
currentToken: String? = null,
|
||||
): Boolean = stack.peek().let {
|
||||
it != null && !tokenBreak.contains(it) ||
|
||||
(tokenBreakException[it]?.any { currentToken?.contains(it) ?: false } ?: false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to pull from the stack until we have a complete token.
|
||||
* Use the isToken() method to determined when a token should end.
|
||||
* @see isToken
|
||||
*/
|
||||
private fun token(): String {
|
||||
guard(isToken()) {
|
||||
Error.ExpectedTokenCharacter(
|
||||
actual = stack.peek(),
|
||||
expression = stack.input,
|
||||
)
|
||||
}
|
||||
val token = StringBuilder()
|
||||
do {
|
||||
stack.pull().let(token::append)
|
||||
} while (isToken(token.toString()))
|
||||
return token.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to construct an expression from a token.
|
||||
* This method handle bracket recursion, functions [min(), max(), etc.]
|
||||
* and token expression creation (Word, Dice, Flat, etc.).
|
||||
* @see token
|
||||
*/
|
||||
private fun factor(): Expression? {
|
||||
when (stack.peek()) {
|
||||
'(' -> {
|
||||
stack.moveCursor()
|
||||
val result = evaluate()
|
||||
guard(stack.peek() == ')') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ')',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
stack.moveCursor()
|
||||
return result
|
||||
}
|
||||
|
||||
'-' -> { // this is considered as a sign function for the following expression.
|
||||
stack.moveCursor()
|
||||
val result = evaluate()
|
||||
return result?.let(Expression::Inversion)
|
||||
}
|
||||
|
||||
'+' -> { // this is considered as a sign function for the following expression.
|
||||
stack.moveCursor()
|
||||
val result = evaluate()
|
||||
return result
|
||||
}
|
||||
|
||||
else -> when (val token = token()) {
|
||||
"min" -> {
|
||||
// consume the '(' character
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the first parameter.
|
||||
val first = evaluate()
|
||||
// check that the expression is well formed, need a ,.
|
||||
guard(stack.peek() == ',') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ',',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ',' character of the second parameter.
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the second parameter.
|
||||
val second = evaluate()
|
||||
// check that the expression is well formed, need a ).
|
||||
guard(stack.peek() == ')') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ')',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ')' character
|
||||
stack.moveCursor()
|
||||
// build the final function expression
|
||||
return Expression.Minimum(first, second)
|
||||
}
|
||||
|
||||
"max" -> {
|
||||
// consume the '(' character
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the first parameter.
|
||||
val first = evaluate()
|
||||
// check that the expression is well formed, need a ,.
|
||||
guard(stack.peek() == ',') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ',',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ',' character of the second parameter.
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the second parameter.
|
||||
val second = evaluate()
|
||||
// check that the expression is well formed, need a ).
|
||||
guard(stack.peek() == ')') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ')',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ')' character
|
||||
stack.moveCursor()
|
||||
// build the final function expression
|
||||
return Expression.Maximum(first, second)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = token.toIntOrNull()
|
||||
if (value != null) {
|
||||
return Expression.Flat(value)
|
||||
}
|
||||
|
||||
val word = wordParser.parse(token)
|
||||
if (word != null) {
|
||||
return Expression.WordExpression(word)
|
||||
}
|
||||
|
||||
val dice = diceParser.parse(token)
|
||||
if (dice != null) {
|
||||
return Expression.DiceExpression(dice)
|
||||
}
|
||||
|
||||
val url = try {
|
||||
URI.create(token).toString()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (url != null) {
|
||||
return Expression.UrlExpression(url)
|
||||
}
|
||||
|
||||
throw Error.UnRecognizedToken(actual = token, expression = stack.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle operator priority and associativity.
|
||||
* build with the example https://tiarkrompf.github.io/notes/?/just-write-the-parser/ in mind.
|
||||
* @param minPriority the minimum priority to check.
|
||||
* @return a nullable [Expression]
|
||||
*/
|
||||
private fun evaluate(minPriority: Int): Expression? {
|
||||
var res = factor()
|
||||
while (
|
||||
operators.contains(stack.peek()) &&
|
||||
(operators[stack.peek()]?.priority ?: 0) >= minPriority
|
||||
) {
|
||||
val nextMin = operators[stack.peek()]?.let { it.priority + it.association } ?: 0
|
||||
res = operators[stack.pull()]?.evaluations?.invoke(
|
||||
res,
|
||||
evaluate(nextMin)
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to evaluate a expression that have been initialized into the stack.
|
||||
* This is typically call just after the stack have been initialized but also when a recursion is needed.
|
||||
* @return a nullable [Expression]
|
||||
*/
|
||||
private fun evaluate(): Expression? {
|
||||
return evaluate(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Public parsing method for an expression.
|
||||
* @param input the input expression [String]
|
||||
* @return a nullable [Expression]
|
||||
*/
|
||||
fun parse(input: String?): Expression? {
|
||||
return input.takeIf { it?.isNotBlank() == true }?.let {
|
||||
stack.init(expression = it)
|
||||
evaluate()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Error(
|
||||
message: String,
|
||||
expression: String,
|
||||
) : Exception("Expession:$expression - $message") {
|
||||
|
||||
class ExpectedTokenCharacter(
|
||||
val actual: Char?,
|
||||
val expression: String,
|
||||
) : Error(
|
||||
"Expected a token character (either a letter or a digit), but was: $actual", expression
|
||||
)
|
||||
|
||||
class ExpectedOperator(
|
||||
val expected: Char,
|
||||
val actual: Char?,
|
||||
val expression: String,
|
||||
) : Error("Expected operator: $expected, but was $actual", expression)
|
||||
|
||||
class UnRecognizedToken(
|
||||
val actual: String,
|
||||
val expression: String,
|
||||
) : Error(
|
||||
"Expected a specific token 'word', 'digit' or 'function', but was: $actual",
|
||||
expression
|
||||
)
|
||||
}
|
||||
|
||||
private data class Operator(
|
||||
val evaluations: (Expression?, Expression?) -> Expression?,
|
||||
val priority: Int, // operator priority, higher is priority
|
||||
val association: Int, // 0 : left associativity, 1: right
|
||||
)
|
||||
|
||||
private data class Stack(
|
||||
var input: String = "",
|
||||
private var cursor: Int = 0,
|
||||
private var peek: Char? = null,
|
||||
) {
|
||||
fun init(expression: String) {
|
||||
input = expression
|
||||
cursor = 0
|
||||
peek = null
|
||||
moveCursor()
|
||||
}
|
||||
|
||||
fun peek(): Char? = peek
|
||||
|
||||
fun pull(): Char? = peek().also { moveCursor() }
|
||||
|
||||
fun moveCursor() {
|
||||
peek = input.getOrNull(cursor)
|
||||
cursor = (cursor + 1).takeIf { it < input.length } ?: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pixelized.shared.lwa.parser.word
|
||||
|
||||
data class Word(
|
||||
val type: Type,
|
||||
) {
|
||||
enum class Type {
|
||||
BDC, // Damages bonus for melee
|
||||
BDD, // Damages bonus for range
|
||||
STR, // Strength
|
||||
DEX, // Dexterity
|
||||
CON, // Constitution
|
||||
HEI, // Height
|
||||
INT, // Intelligence
|
||||
POW, // Power
|
||||
CHA, // Charisma
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$type"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.pixelized.shared.lwa.parser.word
|
||||
|
||||
class WordParser {
|
||||
|
||||
fun parse(
|
||||
value: String,
|
||||
): Word? {
|
||||
return try {
|
||||
Word(type = Word.Type.valueOf(value))
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,13 @@ sealed class RestSynchronisation : MessagePayload {
|
|||
val characterId: String,
|
||||
) : RestSynchronisation()
|
||||
|
||||
@Serializable
|
||||
data class ToggleActiveAlteration(
|
||||
val characterId: String,
|
||||
val alterationId: String,
|
||||
val active: Boolean,
|
||||
) : RestSynchronisation()
|
||||
|
||||
@Serializable
|
||||
data object Campaign : RestSynchronisation()
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package com.pixelized.shared.lwa.protocol.websocket.payload
|
||||
|
||||
import com.pixelized.shared.lwa.model.campaign.Campaign
|
||||
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UpdatePlayerCharacteristicMessage(
|
||||
val characterId: String,
|
||||
val characteristic: Campaign.CharacterInstance.Characteristic,
|
||||
val characterInstanceId: String,
|
||||
val characteristic: CampaignJsonV1.CharacterInstanceJson.Characteristic,
|
||||
val value: Int,
|
||||
) : MessagePayload
|
||||
|
|
@ -10,31 +10,31 @@ class CharacterSheetUseCase {
|
|||
return value - value % 5 // (truncate(value.toFloat() / 5f) * 5f).toInt()
|
||||
}
|
||||
|
||||
fun defaultMovement(): Int = 10
|
||||
fun movement(): Int = 10
|
||||
|
||||
fun defaultMaxHp(
|
||||
fun maxHp(
|
||||
constitution: Int,
|
||||
height: Int,
|
||||
level: Int,
|
||||
): Int {
|
||||
val add = max(defaultHpGrow(constitution = constitution) * (level - 1), 0)
|
||||
val add = max(hpGrow(constitution = constitution) * (level - 1), 0)
|
||||
return (ceil((constitution + height) / 2f).toInt()) + add
|
||||
}
|
||||
|
||||
fun defaultMaxPower(
|
||||
fun maxPp(
|
||||
power: Int,
|
||||
): Int {
|
||||
return power
|
||||
}
|
||||
|
||||
fun defaultDamageBonus(
|
||||
fun damageBonus(
|
||||
strength: Int,
|
||||
height: Int,
|
||||
): String {
|
||||
return defaultDamageBonus(sum = strength + height)
|
||||
return damageBonus(sum = strength + height)
|
||||
}
|
||||
|
||||
fun defaultDamageBonus(
|
||||
fun damageBonus(
|
||||
sum: Int,
|
||||
): String {
|
||||
return when {
|
||||
|
|
@ -47,13 +47,13 @@ class CharacterSheetUseCase {
|
|||
}
|
||||
}
|
||||
|
||||
fun defaultArmor(): Int = 0
|
||||
fun armor(): Int = 0
|
||||
|
||||
fun defaultLearning(intelligence: Int): Int {
|
||||
fun learning(intelligence: Int): Int {
|
||||
return max(0, (intelligence - 10) * 2)
|
||||
}
|
||||
|
||||
fun defaultHpGrow(constitution: Int): Int {
|
||||
fun hpGrow(constitution: Int): Int {
|
||||
return (constitution / 3)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
package com.pixelized.shared.lwa.usecase
|
||||
|
||||
|
||||
import com.pixelized.shared.lwa.model.alteration.FieldAlteration
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.CHA
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.CON
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.DEX
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HEI
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.INT
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.POW
|
||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.STR
|
||||
import com.pixelized.shared.lwa.parser.expression.Expression
|
||||
import com.pixelized.shared.lwa.parser.expression.ExpressionParser
|
||||
import com.pixelized.shared.lwa.parser.word.Word
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class ExpressionUseCase(
|
||||
private val expressionParser: ExpressionParser,
|
||||
private val characterSheetUseCase: CharacterSheetUseCase,
|
||||
private val rollUseCase: RollUseCase,
|
||||
) {
|
||||
fun computeSkillValue(
|
||||
sheet: CharacterSheet,
|
||||
alterations: Map<String, List<FieldAlteration>>,
|
||||
skill: CharacterSheet.Skill,
|
||||
): Int {
|
||||
val context = Context(
|
||||
sheet = sheet,
|
||||
skill = skill,
|
||||
alterations = alterations,
|
||||
)
|
||||
|
||||
val base: Int = context.evaluate(
|
||||
expression = skill.base.let(expressionParser::parse),
|
||||
).let {
|
||||
when (skill.occupation) {
|
||||
true -> max(MIN_OCCUPATION_VALUE, it)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
|
||||
val bonus = context.evaluate(
|
||||
expression = skill.bonus?.let(expressionParser::parse),
|
||||
)
|
||||
|
||||
val level = max((skill.level - 1) * 5, 0)
|
||||
|
||||
val alteration = alterations[skill.id]?.sumOf {
|
||||
context.evaluate(it.expression)
|
||||
} ?: 0
|
||||
|
||||
return max(base + bonus + level + alteration, 0)
|
||||
}
|
||||
|
||||
fun computeRoll(
|
||||
sheet: CharacterSheet,
|
||||
alterations: Map<String, List<FieldAlteration>>,
|
||||
expression: String,
|
||||
): Int {
|
||||
return expressionParser.parse(input = expression)?.let {
|
||||
computeExpression(
|
||||
sheet = sheet,
|
||||
alterations = alterations,
|
||||
expression = it,
|
||||
)
|
||||
} ?: 0
|
||||
}
|
||||
|
||||
fun computeExpression(
|
||||
sheet: CharacterSheet,
|
||||
alterations: Map<String, List<FieldAlteration>>,
|
||||
expression: Expression,
|
||||
): Int {
|
||||
val context = Context(
|
||||
sheet = sheet,
|
||||
skill = null,
|
||||
alterations = alterations,
|
||||
)
|
||||
return context.evaluate(
|
||||
expression = expression,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Context.evaluate(expression: Expression?): Int {
|
||||
|
||||
fun List<FieldAlteration>?.sum() = this?.sumOf { evaluate(it.expression) } ?: 0
|
||||
|
||||
return when (expression) {
|
||||
is Expression.Add -> {
|
||||
evaluate(expression.first) + evaluate(expression.second)
|
||||
}
|
||||
|
||||
is Expression.Minus -> {
|
||||
evaluate(expression.first) - evaluate(expression.second)
|
||||
}
|
||||
|
||||
is Expression.Div -> {
|
||||
evaluate(expression.first) / evaluate(expression.second)
|
||||
}
|
||||
|
||||
is Expression.Prod -> {
|
||||
evaluate(expression.first) * evaluate(expression.second)
|
||||
}
|
||||
|
||||
is Expression.Inversion -> {
|
||||
-evaluate(expression.expression)
|
||||
}
|
||||
|
||||
is Expression.Maximum -> {
|
||||
min(evaluate(expression.first), evaluate(expression.second))
|
||||
}
|
||||
|
||||
is Expression.Minimum -> {
|
||||
max(evaluate(expression.first), evaluate(expression.second))
|
||||
}
|
||||
|
||||
is Expression.Flat -> {
|
||||
expression.value
|
||||
}
|
||||
|
||||
is Expression.DiceExpression -> {
|
||||
rollUseCase.roll(expression.dice)
|
||||
}
|
||||
|
||||
is Expression.UrlExpression -> {
|
||||
0 // Ignore this case.
|
||||
}
|
||||
|
||||
is Expression.WordExpression -> when (expression.word.type) {
|
||||
Word.Type.BDC -> evaluate(
|
||||
expression = expressionParser.parse(
|
||||
characterSheetUseCase.damageBonus(
|
||||
strength = sheet.strength + alterations[STR].sum(),
|
||||
height = sheet.height + alterations[HEI].sum(),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Word.Type.BDD -> evaluate(
|
||||
expression = expressionParser.parse(
|
||||
characterSheetUseCase.damageBonus(
|
||||
strength = sheet.strength + alterations[STR].sum(),
|
||||
height = sheet.height + alterations[HEI].sum(),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Word.Type.STR -> sheet.strength + alterations[STR].sum()
|
||||
Word.Type.DEX -> sheet.dexterity + alterations[DEX].sum()
|
||||
Word.Type.CON -> sheet.constitution + alterations[CON].sum()
|
||||
Word.Type.HEI -> sheet.height + alterations[HEI].sum()
|
||||
Word.Type.INT -> sheet.intelligence + alterations[INT].sum()
|
||||
Word.Type.POW -> sheet.power + alterations[POW].sum()
|
||||
Word.Type.CHA -> sheet.charisma + alterations[CHA].sum()
|
||||
}
|
||||
|
||||
null -> 0
|
||||
}
|
||||
}
|
||||
|
||||
data class Context(
|
||||
val sheet: CharacterSheet,
|
||||
val skill: CharacterSheet.Skill?,
|
||||
val alterations: Map<String, List<FieldAlteration>>,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MIN_OCCUPATION_VALUE = 40
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.pixelized.shared.lwa.usecase
|
||||
|
||||
|
||||
import com.pixelized.shared.lwa.parser.dice.Dice
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class RollUseCase {
|
||||
|
||||
fun roll(dice: Dice): Int {
|
||||
return roll(
|
||||
modifier = dice.modifier,
|
||||
quantity = dice.quantity,
|
||||
faces = dice.faces,
|
||||
)
|
||||
}
|
||||
|
||||
fun roll(
|
||||
modifier: Dice.Modifier? = null,
|
||||
quantity: Int = 1,
|
||||
faces: Int,
|
||||
): Int {
|
||||
print("{")
|
||||
return sum(count = quantity) { left ->
|
||||
when (modifier) {
|
||||
Dice.Modifier.ADVANTAGE -> {
|
||||
val roll1 = roll(faces = faces)
|
||||
val roll2 = roll(faces = faces)
|
||||
print("[$roll1,$roll2]")
|
||||
max(roll1, roll2)
|
||||
}
|
||||
|
||||
Dice.Modifier.DISADVANTAGE -> {
|
||||
val roll1 = roll(faces = faces)
|
||||
val roll2 = roll(faces = faces)
|
||||
print("[$roll1,$roll2]")
|
||||
min(roll1, roll2)
|
||||
}
|
||||
|
||||
Dice.Modifier.EMPHASIS -> {
|
||||
val roll1 = roll(faces = faces)
|
||||
val roll2 = roll(faces = faces)
|
||||
print("[$roll1,$roll2]")
|
||||
val half = faces / 2
|
||||
val roll1Abs = abs(half - roll1)
|
||||
val roll2Abs = abs(half - roll2)
|
||||
when {
|
||||
roll1Abs == roll2Abs -> max(roll1, roll2)
|
||||
roll1Abs < roll2Abs -> roll2
|
||||
else -> roll1
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
roll(faces = faces).also { print("$it") }
|
||||
}
|
||||
}.also {
|
||||
if (quantity > 1 && left != 1) print(",")
|
||||
}
|
||||
}.also {
|
||||
print("}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun roll(faces: Int): Int {
|
||||
return (Math.random() * faces.toDouble() + 1.0).toInt()
|
||||
}
|
||||
|
||||
private fun sum(count: Int, block: (Int) -> Int): Int {
|
||||
return if (count > 1) {
|
||||
block(count) + sum(count - 1, block)
|
||||
} else {
|
||||
block(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.pixelized.shared.lwa.usecase
|
||||
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SkillStepUseCase {
|
||||
|
||||
data class SkillStep(
|
||||
val criticalSuccess: IntRange,
|
||||
val specialSuccess: IntRange,
|
||||
val success: IntRange,
|
||||
val failure: IntRange,
|
||||
val criticalFailure: IntRange,
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper method to compute the range in which a roll is a either critical, special, success or failure.
|
||||
*/
|
||||
fun computeSkillStep(skill: Int): SkillStep {
|
||||
val criticalSuccess = 1..min(roundToInt { skill * 0.05f }, 99)
|
||||
val specialSuccess =
|
||||
(roundToInt { skill * 0.05f } + 1)..min(roundToInt { skill * 0.2f }, 99)
|
||||
val success = (roundToInt { skill * 0.2f } + 1)..min(skill, 99)
|
||||
val criticalFailure = 100 - max(4 - criticalSuccess.last, 0)..100
|
||||
val failure = (success.last + 1) until criticalFailure.first
|
||||
|
||||
return SkillStep(
|
||||
criticalSuccess = criticalSuccess.takeIf { it.first <= it.last } ?: NONE,
|
||||
specialSuccess = specialSuccess.takeIf { it.first <= it.last } ?: NONE,
|
||||
success = success.takeIf { it.first <= it.last } ?: NONE,
|
||||
failure = failure.takeIf { it.first <= it.last } ?: NONE,
|
||||
criticalFailure = criticalFailure.takeIf { it.first <= it.last } ?: NONE,
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun roundToInt(block: () -> Float): Int = round(block()).roundToInt()
|
||||
|
||||
fun exportWiki() {
|
||||
fun print(range: IntRange): String = when {
|
||||
range == NONE -> "-"
|
||||
range.first == range.last -> "${range.first}"
|
||||
else -> "${range.first} - ${range.last}"
|
||||
}
|
||||
repeat(100) { skill ->
|
||||
val step = computeSkillStep(skill + 1)
|
||||
println(
|
||||
"|!${skill + 1} " +
|
||||
"|${print(step.criticalSuccess)} " +
|
||||
"|${print(step.specialSuccess)} " +
|
||||
"|${print(step.success)} " +
|
||||
"|${print(step.failure)} " +
|
||||
"|${print(step.criticalFailure)} " +
|
||||
"|"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun exportTest() {
|
||||
println("val expected = hashMapOf(")
|
||||
(1..500).forEach {
|
||||
println(" $it to ${computeSkillStep(it)},")
|
||||
}
|
||||
println(")")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NONE: IntRange = -1..-1
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue