Add support for special / critical roll action.

This commit is contained in:
Thomas Andres Gomez 2025-03-26 17:12:47 +01:00
parent 9cc6414648
commit 51e63202e3
24 changed files with 537 additions and 193 deletions

View file

@ -29,6 +29,10 @@
<string name="roll_page__dc_normal__label">Normal</string>
<string name="roll_page__dc_hard__label">Difficile</string>
<string name="roll_page__dc_impossible__label">Impossible</string>
<string name="roll_page__criticality__label">Type de jet :</string>
<string name="roll_page__criticality_normal__label">Normal</string>
<string name="roll_page__criticality_special__label">Spécial</string>
<string name="roll_page__criticality_critical__label">Critique</string>
<string name="character_sheet_edit__create__title">Création de personnage</string>
<string name="character_sheet_edit__edit__title">Édition de personnage</string>
@ -117,7 +121,10 @@
<string name="character_sheet_edit__occupation__title">Occupations</string>
<string name="character_sheet_edit__occupation__add_action">Ajouter une occupation</string>
<string name="character_sheet_edit__actions__name_label">Nom</string>
<string name="character_sheet_edit__actions__action_label">Action de lancer</string>
<string name="character_sheet_edit__actions__description_label">Description</string>
<string name="character_sheet_edit__actions__default_action_label">Action normal</string>
<string name="character_sheet_edit__actions__spacial_action_label">Action spécial</string>
<string name="character_sheet_edit__actions__critical_action_label">Action critique</string>
<string name="character_sheet_edit__magic__title">Compétences magiques</string>
<string name="character_sheet_edit__magic__add_action">Ajouter une compétence magique</string>
<string name="character_sheet_edit__add_roll_action">Ajouter une action de lancer</string>

View file

@ -71,7 +71,7 @@ class LwaClientImpl(
override suspend fun updateCharacter(
sheet: CharacterSheetJson,
) = client
.put("$root/character/update") {
.put("$root/character/update/sheet") {
contentType(ContentType.Application.Json)
setBody(sheet)
}

View file

@ -0,0 +1,33 @@
package com.pixelized.desktop.lwa.ui.composable.checkbox
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaCheckboxColors
import kotlinx.coroutines.flow.StateFlow
@Stable
data class LwaCheckBoxUio(
val enable: Boolean = true,
val checked: StateFlow<Boolean>,
val onCheckedChange: (Boolean) -> Unit,
)
@Composable
fun LwaCheckBox(
modifier: Modifier = Modifier,
colors: CheckboxColors = LwaCheckboxColors(),
field: LwaCheckBoxUio,
) {
val checked = field.checked.collectAsState()
Checkbox(
modifier = modifier,
checked = checked.value,
colors = colors,
onCheckedChange = field.onCheckedChange,
)
}

View file

@ -3,7 +3,6 @@ package com.pixelized.desktop.lwa.ui.overlay.roll
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
@ -24,7 +23,7 @@ class RollHostState {
val rollAction: State<RollAction?> get() = currentRollAction
suspend fun showRollOverlay(
roll: RollAction.RollActionUio,
roll: RollAction.Uio,
): RollResult = mutex.withLock {
try {
return suspendCancellableCoroutine { continuation ->
@ -40,7 +39,7 @@ class RollHostState {
@Stable
private class RollActionImpl(
override val roll: RollAction.RollActionUio,
override val roll: RollAction.Uio,
private val continuation: CancellableContinuation<RollResult>,
) : RollAction {
override fun action(result: RollResult) {
@ -61,15 +60,30 @@ enum class RollResult {
@Stable
interface RollAction {
val roll: RollActionUio
val roll: Uio
fun action(result: RollResult)
@Stable
data class RollActionUio(
val characterSheetId: String,
val label: String,
val rollAction: String,
val rollSuccessValue: Int?,
)
sealed interface Uio {
val characterSheetId: String
val label: String
@Stable
data class BoundedRollActionUio(
override val characterSheetId: String,
override val label: String,
val rollAction: String,
val rollSuccessValue: Int,
): Uio
@Stable
data class BoundlessRollActionUio(
override val characterSheetId: String,
override val label: String,
val canBeCritical: Boolean,
val rollDefaultAction: String,
val rollSpecialAction: String?,
val rollCriticalAction: String?,
): Uio
}
}

View file

@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.overlay.roll.CriticalityUio.Criticality
import com.pixelized.desktop.lwa.ui.overlay.roll.DifficultyUio.Difficulty
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.DisableInteractionSource
@ -58,6 +59,10 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__dc_impossible
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_normal__label
import lwacharactersheet.composeapp.generated.resources.roll_page__roll__label
import lwacharactersheet.composeapp.generated.resources.roll_page__roll__success_label
import lwacharactersheet.composeapp.generated.resources.roll_page__criticality__label
import lwacharactersheet.composeapp.generated.resources.roll_page__criticality_normal__label
import lwacharactersheet.composeapp.generated.resources.roll_page__criticality_special__label
import lwacharactersheet.composeapp.generated.resources.roll_page__criticality_critical__label
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@ -83,6 +88,16 @@ data class DifficultyUio(
}
}
@Stable
data class CriticalityUio(
val open: Boolean,
val criticality: Criticality,
) {
enum class Criticality {
NORMAL, SPECIAL, CRITICAL
}
}
@Composable
fun RollPage(
modifier: Modifier = Modifier,
@ -231,6 +246,25 @@ fun RollPage(
)
}
}
viewModel.rollCriticality.value?.let {
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp)
.clickable(
interactionSource = remember { DisableInteractionSource() },
indication = null,
onClick = { },
)
) {
Criticality(
criticality = it,
onToggle = { viewModel.toggleCriticality() },
onCriticality = { viewModel.onCriticality(it) },
)
}
}
}
}
@ -299,19 +333,19 @@ fun Difficulty(
expanded = difficulty.open,
onDismissRequest = { onToggle() }
) {
DifficultyDropDownItem(
LocalDropDownItem(
label = stringResource(Res.string.roll_page__dc_easy__label),
onClick = { onDifficulty(Difficulty.EASY) },
)
DifficultyDropDownItem(
LocalDropDownItem(
label = stringResource(Res.string.roll_page__dc_normal__label),
onClick = { onDifficulty(Difficulty.NORMAL) },
)
DifficultyDropDownItem(
LocalDropDownItem(
label = stringResource(Res.string.roll_page__dc_hard__label),
onClick = { onDifficulty(Difficulty.HARD) },
)
DifficultyDropDownItem(
LocalDropDownItem(
label = stringResource(Res.string.roll_page__dc_impossible__label),
onClick = { onDifficulty(Difficulty.IMPOSSIBLE) },
)
@ -319,8 +353,88 @@ fun Difficulty(
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun DifficultyDropDownItem(
fun Criticality(
modifier: Modifier = Modifier,
criticality: CriticalityUio,
onToggle: () -> Unit,
onCriticality: (Criticality) -> Unit,
) {
ExposedDropdownMenuBox(
modifier = modifier,
expanded = criticality.open,
onExpandedChange = { onToggle() },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 8.dp,
),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
text = stringResource(Res.string.roll_page__criticality__label)
)
AnimatedContent(
targetState = criticality.criticality,
transitionSpec = {
val enter = fadeIn() + slideInVertically { -16 }
val exit = fadeOut() + slideOutVertically { 16 }
enter togetherWith exit using SizeTransform(clip = false)
},
) {
Text(
modifier = Modifier.alignByBaseline().animateContentSize(),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.primary,
text = when (it) {
Criticality.NORMAL -> stringResource(Res.string.roll_page__criticality_normal__label)
Criticality.SPECIAL -> stringResource(Res.string.roll_page__criticality_special__label)
Criticality.CRITICAL -> stringResource(Res.string.roll_page__criticality_critical__label)
}
)
}
}
val rotation = animateFloatAsState(
targetValue = if (criticality.open) -180f else 0f,
)
Icon(
modifier = Modifier
.offset(x = (-12).dp)
.graphicsLayer { rotationZ = rotation.value },
imageVector = Icons.Default.ArrowDropDown,
contentDescription = null
)
}
ExposedDropdownMenu(
expanded = criticality.open,
onDismissRequest = { onToggle() }
) {
LocalDropDownItem(
label = stringResource(Res.string.roll_page__criticality_normal__label),
onClick = { onCriticality(Criticality.NORMAL) },
)
LocalDropDownItem(
label = stringResource(Res.string.roll_page__criticality_special__label),
onClick = { onCriticality(Criticality.SPECIAL) },
)
LocalDropDownItem(
label = stringResource(Res.string.roll_page__criticality_critical__label),
onClick = { onCriticality(Criticality.CRITICAL) },
)
}
}
}
@Composable
private fun LocalDropDownItem(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp),
label: String,

View file

@ -9,8 +9,8 @@ import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.overlay.roll.CriticalityUio.Criticality
import com.pixelized.desktop.lwa.ui.overlay.roll.DifficultyUio.Difficulty
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.shared.lwa.model.AlteredCharacterSheet
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.protocol.websocket.RollEvent
@ -42,7 +42,7 @@ class RollViewModel(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
) : ViewModel() {
private var alteredCharacterSheet: AlteredCharacterSheet? = null
private var rollAction: RollActionUio? = null
private var rollAction: RollAction.Uio? = null
var lastRollResult: RollResult = RollResult.Dismissed
private set
@ -64,6 +64,9 @@ class RollViewModel(
private val _rollDifficulty = mutableStateOf<DifficultyUio?>(null)
val rollDifficulty: State<DifficultyUio?> get() = _rollDifficulty
private val _rollCriticality = mutableStateOf<CriticalityUio?>(null)
val rollCriticality: State<CriticalityUio?> get() = _rollCriticality
private val _shareResult = mutableStateOf(true)
val shareResult: State<Boolean> = _shareResult
@ -88,7 +91,7 @@ class RollViewModel(
* return true if the viewModel is ready to roll, otherwise false.
*/
suspend fun prepareRoll(
roll: RollActionUio,
roll: RollAction.Uio,
) {
rollRotation.snapTo(0f)
rollScale.snapTo(1f)
@ -111,23 +114,39 @@ class RollViewModel(
this.rollAction = roll
val rollStep = roll.rollSuccessValue?.let {
skillStepUseCase.computeSkillStep(skill = it)
}
val rollStep = roll
.let { it as? RollAction.Uio.BoundedRollActionUio }
?.rollSuccessValue
?.let { skillStepUseCase.computeSkillStep(skill = it) }
_shareResult.value = true
_cancellable.value = true
_rollResult.value = null
_rollTitle.value = RollTitleUio(
label = roll.label,
value = rollStep?.success?.last
)
_rollDifficulty.value = roll.rollSuccessValue?.let {
DifficultyUio(
open = false,
difficulty = Difficulty.NORMAL,
)
}
_rollDifficulty.value = roll
.let { it as? RollAction.Uio.BoundedRollActionUio }
?.rollSuccessValue
?.let {
DifficultyUio(
open = false,
difficulty = Difficulty.NORMAL,
)
}
_rollCriticality.value = roll
.let { it as? RollAction.Uio.BoundlessRollActionUio }
?.takeIf { it.canBeCritical }
?.let {
CriticalityUio(
open = false,
criticality = Criticality.NORMAL
)
}
}
suspend fun roll() {
@ -151,22 +170,34 @@ class RollViewModel(
delay(500)
_cancellable.value = false
// compute the skill critical success to critical failure ranges.
val rollStep = rollAction.rollSuccessValue?.let {
skillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2
Difficulty.NORMAL -> it
Difficulty.HARD -> it / 2
Difficulty.IMPOSSIBLE -> it / 4
else -> it
}
)
}
val rollStep = rollAction
.let { it as? RollAction.Uio.BoundedRollActionUio }
?.rollSuccessValue?.let {
skillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2
Difficulty.NORMAL -> it
Difficulty.HARD -> it / 2
Difficulty.IMPOSSIBLE -> it / 4
else -> it
}
)
}
// compute the expression.
val expression = when (rollAction) {
is RollAction.Uio.BoundedRollActionUio -> rollAction.rollAction
is RollAction.Uio.BoundlessRollActionUio -> when (rollCriticality.value?.criticality) {
Criticality.NORMAL -> rollAction.rollDefaultAction
Criticality.SPECIAL -> rollAction.rollSpecialAction
Criticality.CRITICAL -> rollAction.rollCriticalAction
null -> null
}
} ?: ""
// compute the roll (typically use the expression inside the rollAction)
val roll = skillComputation.computeRoll(
sheet = alteredCharacterSheet,
expression = rollAction.rollAction,
expression = expression,
)
// check where the roll fall into the rollSteps.
@ -227,22 +258,38 @@ class RollViewModel(
open = false,
difficulty = difficulty,
)
val rollStep = rollAction?.rollSuccessValue?.let {
skillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2
Difficulty.NORMAL -> it
Difficulty.HARD -> it / 2
Difficulty.IMPOSSIBLE -> it / 4
else -> it
}
)
}
val rollStep = rollAction
.let { it as? RollAction.Uio.BoundedRollActionUio }
?.rollSuccessValue
?.let {
skillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2
Difficulty.NORMAL -> it
Difficulty.HARD -> it / 2
Difficulty.IMPOSSIBLE -> it / 4
else -> it
}
)
}
_rollTitle.value = _rollTitle.value?.copy(
value = rollStep?.success?.last
)
}
fun toggleCriticality() {
_rollCriticality.value = _rollCriticality.value?.copy(
open = _rollCriticality.value?.open?.not() ?: false
)
}
fun onCriticality(criticality: Criticality) {
_rollCriticality.value = CriticalityUio(
open = false,
criticality = criticality,
)
}
private suspend fun diceRotationAnimation() {
rollRotation.animateTo(
targetValue = rollRotation.value.let { it - it % 360 } + 360f * 3,

View file

@ -1,7 +1,8 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetActionUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio
@ -130,7 +131,7 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__str),
description = getString(Res.string.tooltip__characteristics__strength),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__str),
rollAction = "1d100",
@ -144,7 +145,7 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__dex),
description = getString(Res.string.tooltip__characteristics__dexterity),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__dex),
rollAction = "1d100",
@ -158,7 +159,7 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__con),
description = getString(Res.string.tooltip__characteristics__constitution),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__con),
rollAction = "1d100",
@ -172,7 +173,7 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__hei),
description = getString(Res.string.tooltip__characteristics__height),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__hei),
rollAction = "1d100",
@ -186,7 +187,7 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__int),
description = getString(Res.string.tooltip__characteristics__intelligence),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__int),
rollAction = "1d100",
@ -200,7 +201,7 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__pow),
description = getString(Res.string.tooltip__characteristics__power),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__pow),
rollAction = "1d100",
@ -214,7 +215,7 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__cha),
description = getString(Res.string.tooltip__characteristics__charisma),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__cha),
rollAction = "1d100",
@ -241,7 +242,7 @@ class CharacterDetailFactory(
description = it,
)
},
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = skill.label,
rollAction = "1d100",
@ -268,7 +269,7 @@ class CharacterDetailFactory(
description = it,
)
},
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = skill.label,
rollAction = "1d100",
@ -295,7 +296,7 @@ class CharacterDetailFactory(
description = it,
)
},
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = skill.label,
rollAction = "1d100",
@ -313,14 +314,16 @@ class CharacterDetailFactory(
description = it,
)
},
roll = RollActionUio(
roll = RollAction.Uio.BoundlessRollActionUio(
characterSheetId = characterSheetId,
label = action.label,
rollAction = action.roll,
rollSuccessValue = null,
canBeCritical = action.canBeCritical,
rollDefaultAction = action.default,
rollSpecialAction = action.special,
rollCriticalAction = action.critical,
)
)
}
}.sortedWith(compareBy(Collator.getInstance()) { it.label }),
)
}
}

View file

@ -17,7 +17,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.shapes.MasteryShape
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
import org.jetbrains.compose.resources.painterResource
@ -27,7 +27,7 @@ data class CharacterDetailSheetActionUio(
val actionId: String,
val label: String,
val tooltips: TooltipUio?,
val roll: RollActionUio,
val roll: RollAction.Uio,
)
@Composable

View file

@ -15,14 +15,14 @@ import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
@Stable
data class CharacterDetailSheetCharacteristicUio(
val value: String,
val label: String,
val tooltips: TooltipUio,
val roll: RollActionUio,
val roll: RollAction.Uio,
)
@OptIn(ExperimentalFoundationApi::class)

View file

@ -21,7 +21,7 @@ import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.shapes.MasteryShape
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
@Stable
data class CharacterDetailSheetSkillUio(
@ -31,7 +31,7 @@ data class CharacterDetailSheetSkillUio(
val used: Boolean,
val occupation: Boolean,
val tooltips: TooltipUio?,
val roll: RollActionUio,
val roll: RollAction.Uio,
)
@OptIn(ExperimentalFoundationApi::class)

View file

@ -1,7 +1,7 @@
package com.pixelized.desktop.lwa.ui.screen.characterSheet.detail
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Node
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
@ -70,7 +70,7 @@ class CharacterSheetFactory(
title = getString(Res.string.character_sheet__characteristics__str),
description = getString(Res.string.tooltip__characteristics__strength),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__str),
rollAction = "1d100",
@ -86,7 +86,7 @@ class CharacterSheetFactory(
title = getString(Res.string.character_sheet__characteristics__dex),
description = getString(Res.string.tooltip__characteristics__dexterity),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__dex),
rollAction = "1d100",
@ -102,7 +102,7 @@ class CharacterSheetFactory(
title = getString(Res.string.character_sheet__characteristics__con),
description = getString(Res.string.tooltip__characteristics__constitution),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__con),
rollAction = "1d100",
@ -118,7 +118,7 @@ class CharacterSheetFactory(
title = getString(Res.string.character_sheet__characteristics__hei),
description = getString(Res.string.tooltip__characteristics__height),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__hei),
rollAction = "1d100",
@ -134,7 +134,7 @@ class CharacterSheetFactory(
title = getString(Res.string.character_sheet__characteristics__int),
description = getString(Res.string.tooltip__characteristics__intelligence),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__int),
rollAction = "1d100",
@ -150,7 +150,7 @@ class CharacterSheetFactory(
title = getString(Res.string.character_sheet__characteristics__pow),
description = getString(Res.string.tooltip__characteristics__power),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__pow),
rollAction = "1d100",
@ -166,7 +166,7 @@ class CharacterSheetFactory(
title = getString(Res.string.character_sheet__characteristics__cha),
description = getString(Res.string.tooltip__characteristics__charisma),
),
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__characteristics__cha),
rollAction = "1d100",
@ -271,7 +271,7 @@ class CharacterSheetFactory(
description = it,
)
},
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = skill.label,
rollAction = "1d100",
@ -297,7 +297,7 @@ class CharacterSheetFactory(
description = it,
)
},
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = skill.label,
rollAction = "1d100",
@ -323,7 +323,7 @@ class CharacterSheetFactory(
description = it,
)
},
roll = RollActionUio(
roll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = skill.label,
rollAction = "1d100",
@ -331,16 +331,19 @@ class CharacterSheetFactory(
),
)
},
actions = characterSheet.actions.mapNotNull {
if (it.roll.isEmpty()) return@mapNotNull null
actions = characterSheet.actions.mapNotNull { action ->
if (action.default.isEmpty()) return@mapNotNull null
CharacterSheetPageUio.Roll(
label = it.label,
value = it.roll,
roll = RollActionUio(
label = action.label,
value = action.default,
roll = RollAction.Uio.BoundlessRollActionUio(
characterSheetId = characterSheetId,
label = it.label,
rollAction = it.roll,
rollSuccessValue = null,
label = action.label,
canBeCritical = action.canBeCritical,
rollDefaultAction = action.default,
rollSpecialAction = action.special,
rollCriticalAction = action.critical,
)
)
}

View file

@ -64,7 +64,7 @@ import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindow
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.overlay.roll.RollPage
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic
@ -108,7 +108,7 @@ data class CharacterSheetPageUio(
val value: String,
val editable: Boolean,
val tooltips: TooltipUio?,
val roll: RollActionUio?,
val roll: RollAction.Uio?,
)
@Stable
@ -118,14 +118,14 @@ data class CharacterSheetPageUio(
val value: Int,
val used: Boolean,
val tooltips: TooltipUio? = null,
val roll: RollActionUio,
val roll: RollAction.Uio,
)
@Stable
data class Roll(
val label: String,
val value: String,
val roll: RollActionUio,
val roll: RollAction.Uio,
)
}

View file

@ -3,6 +3,8 @@ package com.pixelized.desktop.lwa.ui.screen.characterSheet.edit
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBoxUio
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.occupation
@ -14,9 +16,13 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.textfi
import com.pixelized.desktop.lwa.utils.extention.unAccent
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__critical_action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__default_action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__description_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__name_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__spacial_action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__cha
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__con
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__dex
@ -186,10 +192,12 @@ class CharacterSheetEditFactory(
actions = editedSheet.actions.map {
CharacterSheet.Roll(
id = it.id,
label = it.label.value.value,
description = null, // TODO
canBeCritical = false, // TODO
roll = it.action.value.value,
label = it.label.valueFlow.value,
description = it.description.valueFlow.value,
canBeCritical = it.canBeCritical.checked.value,
default = it.default.valueFlow.value,
special = it.special?.valueFlow?.value,
critical = it.critical?.valueFlow?.value,
)
},
)
@ -456,13 +464,28 @@ class CharacterSheetEditFactory(
actions = sheet?.actions?.map { action ->
ActionFieldUio(
id = action.id,
label = skillFieldFactory.createWrapper(
label = mutableStateOf(getString(Res.string.character_sheet_edit__actions__name_label)),
label = createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__name_label),
value = action.label,
),
action = skillFieldFactory.createWrapper(
label = mutableStateOf(getString(Res.string.character_sheet_edit__actions__action_label)),
value = action.roll,
description = createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__description_label),
value = action.description ?: "",
),
canBeCritical = createLwaBox(
checked = action.canBeCritical,
),
default = createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__default_action_label),
value = action.default,
),
special = createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__spacial_action_label),
value = action.special,
),
critical = createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__critical_action_label),
value = action.critical,
),
option = skillFieldFactory.deleteOption { onDeleteSkill(action.id) },
)
@ -471,6 +494,41 @@ class CharacterSheetEditFactory(
}
}
fun createLwaTextField(
enable: Boolean = true,
isError: Boolean = false,
label: String? = null,
placeholder: String? = null,
value: String? = null,
): LwaTextFieldUio {
val valueFlow = MutableStateFlow(value ?: "")
val labelFlow = MutableStateFlow(label)
val placeholderFlow = MutableStateFlow(placeholder)
val isErrorFlow = MutableStateFlow(isError)
return LwaTextFieldUio(
enable = enable,
isError = isErrorFlow,
labelFlow = labelFlow,
valueFlow = valueFlow,
placeHolderFlow = placeholderFlow,
onValueChange = { valueFlow.value = it },
)
}
fun createLwaBox(
enable: Boolean = true,
checked: Boolean,
): LwaCheckBoxUio {
val checkedFlow = MutableStateFlow(checked)
return LwaCheckBoxUio(
enable = enable,
checked = checkedFlow,
onCheckedChange = { checkedFlow.value = it },
)
}
private suspend fun createLevelUpWrapper(
shouldLevelUp: Boolean,
): LevelUpWrapperUio {

View file

@ -34,7 +34,6 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindow
import com.pixelized.desktop.lwa.ui.screen.characterSheet.copy.CharacterSheetCopyDialog
import com.pixelized.desktop.lwa.ui.screen.characterSheet.delete.CharacterSheetDeleteDialog
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialog
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionField
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.BaseSkillFieldUio
@ -430,8 +429,10 @@ fun CharacterSheetEdit(
) {
form.actions.forEach {
ActionField(
modifier = Modifier.fillMaxWidth().padding(end = (4 + 2).dp),
action = it
modifier = Modifier
.fillMaxWidth()
.padding(end = 6.dp, bottom = 16.dp),
action = it,
)
}
TextButton(

View file

@ -14,8 +14,11 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.Action
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__critical_action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__description_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__default_action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__name_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__spacial_action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__copy__label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__copy__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_title
@ -125,11 +128,28 @@ class CharacterSheetEditViewModel(
val id = UUID.randomUUID().toString()
val field = ActionFieldUio(
id = id,
label = skillFactory.createWrapper(
label = mutableStateOf(getString(Res.string.character_sheet_edit__actions__name_label)),
label = sheetFactory.createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__name_label),
value = "",
),
action = skillFactory.createWrapper(
label = mutableStateOf(getString(Res.string.character_sheet_edit__actions__action_label)),
description = sheetFactory.createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__description_label),
value = "",
),
canBeCritical = sheetFactory.createLwaBox(
checked = false,
),
default = sheetFactory.createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__default_action_label),
value = "",
),
special = sheetFactory.createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__spacial_action_label),
value = "",
),
critical = sheetFactory.createLwaTextField(
label = getString(Res.string.character_sheet_edit__actions__critical_action_label),
value = "",
),
option = skillFactory.deleteOption { deleteSkill(id) },
)
@ -151,7 +171,7 @@ class CharacterSheetEditViewModel(
},
actions = _characterSheet.value.actions.toMutableList().also { actions ->
actions.removeIf { it.id == skillId }
}
},
)
}

View file

@ -1,9 +1,10 @@
package com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.DropdownMenu
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@ -12,45 +13,57 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBox
import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBoxUio
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.option.ActionOption
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.option.DropDownActionMenuItem
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.textfield.TextFieldWrapper
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.textfield.TextFieldWrapperUio
@Stable
data class ActionFieldUio(
val id: String,
val label: TextFieldWrapperUio,
val action: TextFieldWrapperUio,
val label: LwaTextFieldUio,
val description: LwaTextFieldUio,
val canBeCritical: LwaCheckBoxUio,
val default: LwaTextFieldUio,
val special: LwaTextFieldUio?,
val critical: LwaTextFieldUio?,
val option: ActionOption.DeleteOptionUio,
)
@Composable
fun ActionField(
modifier: Modifier = Modifier,
space: Dp = 4.dp,
action: ActionFieldUio,
) {
val showMenu = remember { mutableStateOf(false) }
val canBeCritical = action.canBeCritical.checked.collectAsState()
Row(
Column(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.End),
verticalAlignment = Alignment.CenterVertically,
verticalArrangement = Arrangement.spacedBy(space = space),
) {
TextFieldWrapper(
modifier = Modifier.weight(weight = 1f),
wrapper = action.label,
)
TextFieldWrapper(
modifier = Modifier.width(width = (192+4).dp),
wrapper = action.action,
)
Box {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = space),
verticalAlignment = Alignment.CenterVertically,
) {
LwaTextField(
modifier = Modifier.weight(1f),
field = action.label,
)
LwaCheckBox(
field = action.canBeCritical,
)
IconButton(
onClick = { showMenu.value = showMenu.value.not() },
) {
@ -59,19 +72,47 @@ fun ActionField(
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false }
) {
DropDownActionMenuItem(
wrapper = action.option,
onClick = {
showMenu.value = false
action.option.onOption()
}
)
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false }
) {
DropDownActionMenuItem(
wrapper = action.option,
onClick = {
showMenu.value = false
action.option.onOption()
}
)
}
}
}
Row(
modifier = Modifier.fillMaxWidth().animateContentSize(),
horizontalArrangement = Arrangement.spacedBy(space = space),
) {
LwaTextField(
modifier = Modifier.weight(1f),
field = action.default,
)
if (canBeCritical.value) {
action.special?.let { field ->
LwaTextField(
modifier = Modifier.weight(1f),
field = field,
)
}
action.critical?.let { field ->
LwaTextField(
modifier = Modifier.weight(1f),
field = field,
)
}
}
}
LwaTextField(
modifier = Modifier.fillMaxWidth(),
singleLine = false,
field = action.description,
)
}
}

View file

@ -41,6 +41,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController
@ -153,11 +154,12 @@ private fun GameMasterContent(
.clickable { onGameMaster(gameMaster.value.not()) }
.padding(all = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp)
horizontalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
color = MaterialTheme.lwa.colorScheme.base.primary,
style = MaterialTheme.typography.caption,
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.SemiBold,
text = stringResource(Res.string.game_master__action),
)
Switch(

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.calculateStartPadding
@ -107,47 +106,40 @@ fun GMCharacter(
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.then(other = modifier),
) {
Column {
Row(
modifier = Modifier.padding(start = startPadding),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.padding(start = startPadding),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.weight(weight = 1f),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
text = character.name,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.caption,
text = stringResource(
Res.string.game_master__character_level__label,
character.level,
),
)
}
OverflowActionMenu(
character = character,
onAction = onAction,
)
}
Row(
modifier = Modifier
.padding(paddingValues = padding)
.padding(bottom = 8.dp),
modifier = Modifier.weight(weight = 1f),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
character.tags.forEach { tag ->
GMTag(
elevation = 4.dp,
tag = tag,
)
}
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
text = character.name,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.caption,
text = stringResource(
Res.string.game_master__character_level__label,
character.level,
),
)
}
character.tags.forEach { tag ->
GMTag(
elevation = 4.dp,
tag = tag,
)
}
OverflowActionMenu(
character = character,
onAction = onAction,
)
}
}
}

View file

@ -1,7 +1,8 @@
package com.pixelized.desktop.lwa.ui.screen.levelup
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult
import com.pixelized.desktop.lwa.ui.screen.levelup.items.LevelUpCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.levelup.items.LevelUpSkillUio
@ -312,7 +313,7 @@ class LevelUpFactory(
)
},
roll = when (results[skill.id]) {
null -> RollActionUio(
null -> RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = skill.label,
rollAction = "1d100",

View file

@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.shapes.MasteryShape
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.level_up__skill_level
@ -41,7 +41,7 @@ data class LevelUpSkillUio(
val levelUp: Boolean,
val occupation: Boolean,
val tooltips: TooltipUio?,
val roll: RollActionUio?,
val roll: RollAction.Uio?,
)
@OptIn(ExperimentalFoundationApi::class)