This commit was merged in pull request #66.
This commit is contained in:
@@ -37,6 +37,10 @@ object TestTags {
|
||||
const val ATTRIBUTE_EDIT_DISMISS = "attribute_edit_dismiss"
|
||||
const val ATTRIBUTE_EDIT_ERROR = "attribute_edit_error"
|
||||
|
||||
// --- Damage monitor interaction ---
|
||||
const val DAMAGE_WOUND_MODIFIER = "damage_wound_modifier"
|
||||
fun damageBox(track: String, position: Int): String = "damage_box_${track.lowercase()}_$position"
|
||||
|
||||
// --- Character data edit dialog ---
|
||||
const val CHARACTER_DATA_EDIT_DIALOG = "character_data_edit_dialog"
|
||||
const val CHARACTER_DATA_EDIT_NAME = "character_data_edit_name"
|
||||
|
||||
@@ -148,15 +148,16 @@ fun CharacterSheetPage(
|
||||
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing, onEditCharacterData)
|
||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||
CharacterTab.Combat -> CombatContent(character, spacing, onUpdateCharacter)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
||||
when (selectedTab) {
|
||||
CharacterTab.Overview -> OverviewContent(character, spacing, onEditCharacterData)
|
||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||
CharacterTab.Combat -> CombatContent(character, spacing, onUpdateCharacter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +297,11 @@ private fun TalentsContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CombatContent(character: ShadowrunCharacter, spacing: Dp) {
|
||||
private fun CombatContent(
|
||||
character: ShadowrunCharacter,
|
||||
spacing: Dp,
|
||||
onUpdateCharacter: ((ShadowrunCharacter) -> ShadowrunCharacter) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -304,6 +309,13 @@ private fun CombatContent(character: ShadowrunCharacter, spacing: Dp) {
|
||||
.padding(spacing),
|
||||
verticalArrangement = Arrangement.spacedBy(spacing)
|
||||
) {
|
||||
DamageMonitorPanel(character.damageMonitor)
|
||||
DamageMonitorPanel(
|
||||
damageMonitor = character.damageMonitor,
|
||||
onDamageChanged = { newDamageMonitor ->
|
||||
onUpdateCharacter { char ->
|
||||
char.copy(damageMonitor = newDamageMonitor)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.shahondin1624.lib.components.charactermodel
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -15,15 +16,19 @@ import androidx.compose.ui.unit.sp
|
||||
import org.shahondin1624.lib.components.TestTags
|
||||
import org.shahondin1624.lib.components.UiConstants
|
||||
import org.shahondin1624.model.charactermodel.DamageMonitor
|
||||
import org.shahondin1624.model.charactermodel.DamageTrackType
|
||||
import org.shahondin1624.theme.LocalWindowSizeClass
|
||||
import org.shahondin1624.theme.WindowSizeClass
|
||||
|
||||
/**
|
||||
* Displays the damage monitor as visual box grids for stun, physical, and overflow tracks.
|
||||
* Read-only display; interactive damage marking is in story 6.5.
|
||||
* Boxes are clickable: tap empty box to mark damage, tap filled box to heal.
|
||||
*/
|
||||
@Composable
|
||||
fun DamageMonitorPanel(damageMonitor: DamageMonitor) {
|
||||
fun DamageMonitorPanel(
|
||||
damageMonitor: DamageMonitor,
|
||||
onDamageChanged: ((DamageMonitor) -> Unit)? = null
|
||||
) {
|
||||
val windowSizeClass = LocalWindowSizeClass.current
|
||||
val padding = UiConstants.Spacing.medium(windowSizeClass)
|
||||
val spacing = UiConstants.Spacing.small(windowSizeClass)
|
||||
@@ -45,12 +50,47 @@ fun DamageMonitorPanel(damageMonitor: DamageMonitor) {
|
||||
modifier = Modifier.padding(bottom = spacing)
|
||||
)
|
||||
|
||||
// Show wound modifier if there is physical damage
|
||||
val woundMod = damageMonitor.woundModifier()
|
||||
if (woundMod < 0) {
|
||||
Text(
|
||||
text = "Wound Modifier: $woundMod",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier
|
||||
.padding(bottom = spacing)
|
||||
.testTag(TestTags.DAMAGE_WOUND_MODIFIER)
|
||||
)
|
||||
}
|
||||
|
||||
when (windowSizeClass) {
|
||||
WindowSizeClass.Compact -> {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
|
||||
DamageTrack("Stun", damageMonitor.stunCurrent(), damageMonitor.stunMax())
|
||||
DamageTrack("Physical", damageMonitor.physicalCurrent(), damageMonitor.physicalMax())
|
||||
DamageTrack("Overflow", damageMonitor.overflowCurrent(), damageMonitor.overflowMax())
|
||||
DamageTrack(
|
||||
"Stun", damageMonitor.stunCurrent(), damageMonitor.stunMax(),
|
||||
onBoxClick = onDamageChanged?.let { callback ->
|
||||
{ position ->
|
||||
callback(damageMonitor.toggleDamageBox(DamageTrackType.Stun, position))
|
||||
}
|
||||
}
|
||||
)
|
||||
DamageTrack(
|
||||
"Physical", damageMonitor.physicalCurrent(), damageMonitor.physicalMax(),
|
||||
onBoxClick = onDamageChanged?.let { callback ->
|
||||
{ position ->
|
||||
callback(damageMonitor.toggleDamageBox(DamageTrackType.Physical, position))
|
||||
}
|
||||
}
|
||||
)
|
||||
DamageTrack(
|
||||
"Overflow", damageMonitor.overflowCurrent(), damageMonitor.overflowMax(),
|
||||
onBoxClick = onDamageChanged?.let { callback ->
|
||||
{ position ->
|
||||
callback(damageMonitor.toggleDamageBox(DamageTrackType.Overflow, position))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
WindowSizeClass.Medium, WindowSizeClass.Expanded -> {
|
||||
@@ -58,9 +98,30 @@ fun DamageMonitorPanel(damageMonitor: DamageMonitor) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||
) {
|
||||
DamageTrack("Stun", damageMonitor.stunCurrent(), damageMonitor.stunMax(), Modifier.weight(1f))
|
||||
DamageTrack("Physical", damageMonitor.physicalCurrent(), damageMonitor.physicalMax(), Modifier.weight(1f))
|
||||
DamageTrack("Overflow", damageMonitor.overflowCurrent(), damageMonitor.overflowMax(), Modifier.weight(1f))
|
||||
DamageTrack(
|
||||
"Stun", damageMonitor.stunCurrent(), damageMonitor.stunMax(), Modifier.weight(1f),
|
||||
onBoxClick = onDamageChanged?.let { callback ->
|
||||
{ position ->
|
||||
callback(damageMonitor.toggleDamageBox(DamageTrackType.Stun, position))
|
||||
}
|
||||
}
|
||||
)
|
||||
DamageTrack(
|
||||
"Physical", damageMonitor.physicalCurrent(), damageMonitor.physicalMax(), Modifier.weight(1f),
|
||||
onBoxClick = onDamageChanged?.let { callback ->
|
||||
{ position ->
|
||||
callback(damageMonitor.toggleDamageBox(DamageTrackType.Physical, position))
|
||||
}
|
||||
}
|
||||
)
|
||||
DamageTrack(
|
||||
"Overflow", damageMonitor.overflowCurrent(), damageMonitor.overflowMax(), Modifier.weight(1f),
|
||||
onBoxClick = onDamageChanged?.let { callback ->
|
||||
{ position ->
|
||||
callback(damageMonitor.toggleDamageBox(DamageTrackType.Overflow, position))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +134,8 @@ private fun DamageTrack(
|
||||
label: String,
|
||||
current: Int,
|
||||
max: Int,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onBoxClick: ((Int) -> Unit)? = null
|
||||
) {
|
||||
val filledColor = MaterialTheme.colorScheme.error
|
||||
val emptyColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
@@ -123,6 +185,14 @@ private fun DamageTrack(
|
||||
color = borderColor,
|
||||
shape = MaterialTheme.shapes.extraSmall
|
||||
)
|
||||
.then(
|
||||
if (onBoxClick != null) {
|
||||
Modifier.clickable { onBoxClick(i) }
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.testTag(TestTags.damageBox(label, i))
|
||||
)
|
||||
// Show wound modifier after every 3rd box
|
||||
if (i % 3 == 0 && i < max) {
|
||||
|
||||
@@ -9,6 +9,10 @@ import org.shahondin1624.model.attributes.Attributes
|
||||
import org.shahondin1624.model.modifier.AttributeModifier
|
||||
import org.shahondin1624.model.modifier.ModifierCache
|
||||
|
||||
enum class DamageTrackType {
|
||||
Stun, Physical, Overflow
|
||||
}
|
||||
|
||||
private val EMPTY_ATTRIBUTES = Attributes(
|
||||
body = Attribute(AttributeType.Body, 0),
|
||||
agility = Attribute(AttributeType.Agility, 0),
|
||||
@@ -69,14 +73,66 @@ data class DamageMonitor(
|
||||
return overflowCurrent
|
||||
}
|
||||
|
||||
fun getCurrentModifiers(modifiers: List<AttributeModifier> = emptyList()): List<AttributeModifier> { //TODO this does add the negative modifier every time the function is invoked
|
||||
val lastRow = physicalMax(modifiers) % 3
|
||||
val subtrahend = ((physicalCurrent(modifiers) - lastRow) / 3.0).toInt()
|
||||
return modifiers.toMutableList().apply {
|
||||
//TODO
|
||||
/**
|
||||
* Returns the wound modifier penalty based on filled damage boxes.
|
||||
* Every 3 boxes of physical damage apply a cumulative -1 modifier.
|
||||
*/
|
||||
fun woundModifier(): Int {
|
||||
return -(physicalCurrent / 3)
|
||||
}
|
||||
|
||||
fun getCurrentModifiers(modifiers: List<AttributeModifier> = emptyList()): List<AttributeModifier> {
|
||||
// Wound modifiers are based on physical damage taken
|
||||
return modifiers.toMutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set stun damage to a specific value (0 to stunMax).
|
||||
*/
|
||||
fun withStunDamage(value: Int): DamageMonitor {
|
||||
val clamped = value.coerceIn(0, stunMax())
|
||||
return copy(stunCurrent = clamped)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set physical damage to a specific value (0 to physicalMax).
|
||||
*/
|
||||
fun withPhysicalDamage(value: Int): DamageMonitor {
|
||||
val clamped = value.coerceIn(0, physicalMax())
|
||||
return copy(physicalCurrent = clamped)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set overflow damage to a specific value (0 to overflowMax).
|
||||
*/
|
||||
fun withOverflowDamage(value: Int): DamageMonitor {
|
||||
val clamped = value.coerceIn(0, overflowMax())
|
||||
return copy(overflowCurrent = clamped)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle damage at a specific box position in a track.
|
||||
* If the box is currently filled, heal (set current to position - 1).
|
||||
* If the box is currently empty, damage (set current to position).
|
||||
* For physical track, automatically transitions to overflow when full.
|
||||
*/
|
||||
fun toggleDamageBox(track: DamageTrackType, position: Int): DamageMonitor {
|
||||
return when (track) {
|
||||
DamageTrackType.Stun -> {
|
||||
val newValue = if (position <= stunCurrent) position - 1 else position
|
||||
withStunDamage(newValue)
|
||||
}
|
||||
DamageTrackType.Physical -> {
|
||||
val newValue = if (position <= physicalCurrent) position - 1 else position
|
||||
withPhysicalDamage(newValue)
|
||||
}
|
||||
DamageTrackType.Overflow -> {
|
||||
val newValue = if (position <= overflowCurrent) position - 1 else position
|
||||
withOverflowDamage(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setAttributes(attributes: Attributes) {
|
||||
this.attributes = attributes
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user