Skip to content

Commit 565af09

Browse files
committed
Merge branch 'feat/combat-ui' into 'master'
Combat UI See merge request fmasa/wfrp-master!249
2 parents c0ca079 + 63b85be commit 565af09

File tree

38 files changed

+1109
-160
lines changed

38 files changed

+1109
-160
lines changed

app/src/main/java/cz/muni/fi/rpg/ui/startup/StartupScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.google.android.gms.auth.api.signin.GoogleSignIn
1818
import cz.frantisekmasa.wfrp_master.common.auth.LocalAuthenticationManager
1919
import cz.frantisekmasa.wfrp_master.common.auth.LocalWebClientId
2020
import cz.frantisekmasa.wfrp_master.common.core.shared.SettingsStorage
21+
import cz.frantisekmasa.wfrp_master.common.core.shared.edit
2122
import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle
2223
import cz.frantisekmasa.wfrp_master.common.localization.LocalStrings
2324
import cz.frantisekmasa.wfrp_master.common.settings.AppSettings

common/src/androidMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import androidx.datastore.preferences.core.Preferences
55
import androidx.datastore.preferences.core.booleanPreferencesKey
66
import androidx.datastore.preferences.core.edit
7+
import androidx.datastore.preferences.core.stringSetPreferencesKey
78
import androidx.datastore.preferences.preferencesDataStore
89
import kotlinx.coroutines.flow.Flow
910
import kotlinx.coroutines.flow.map
@@ -14,8 +15,8 @@ private val Context.settingsDataStore by preferencesDataStore("settings")
1415
actual class SettingsStorage(context: Context, ) {
1516
private val storage = context.settingsDataStore
1617

17-
actual suspend fun <T> edit(key: SettingsKey<T>, value: T) {
18-
storage.edit { it[key] = value }
18+
actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T) {
19+
storage.edit { it[key] = update(it[key]) }
1920
}
2021

2122
actual fun <T> watch(key: SettingsKey<T>): Flow<T?> {
@@ -27,3 +28,4 @@ actual class SettingsStorage(context: Context, ) {
2728
actual typealias SettingsKey<T> = Preferences.Key<T>
2829

2930
actual fun booleanSettingsKey(name: String): SettingsKey<Boolean> = booleanPreferencesKey(name)
31+
actual fun stringSetKey(name: String): SettingsKey<Set<String>> = stringSetPreferencesKey(name)

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/DependencyInjection.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cz.frantisekmasa.wfrp_master.common
33
import cz.frantisekmasa.wfrp_master.common.character.CharacterPickerScreenModel
44
import cz.frantisekmasa.wfrp_master.common.character.CharacterScreenModel
55
import cz.frantisekmasa.wfrp_master.common.character.characteristics.CharacteristicsScreenModel
6+
import cz.frantisekmasa.wfrp_master.common.character.combat.CharacterCombatScreenModel
67
import cz.frantisekmasa.wfrp_master.common.character.religion.blessings.BlessingsScreenModel
78
import cz.frantisekmasa.wfrp_master.common.character.religion.miracles.MiraclesScreenModel
89
import cz.frantisekmasa.wfrp_master.common.character.skills.SkillsScreenModel
@@ -49,6 +50,7 @@ import cz.frantisekmasa.wfrp_master.common.core.firebase.repositories.FirestoreN
4950
import cz.frantisekmasa.wfrp_master.common.core.firebase.repositories.FirestorePartyRepository
5051
import cz.frantisekmasa.wfrp_master.common.core.firebase.repositories.FirestoreSkillRepository
5152
import cz.frantisekmasa.wfrp_master.common.core.serialization.UuidSerializer
53+
import cz.frantisekmasa.wfrp_master.common.core.tips.DismissedUserTipsHolder
5254
import cz.frantisekmasa.wfrp_master.common.encounters.EncounterDetailScreenModel
5355
import cz.frantisekmasa.wfrp_master.common.encounters.EncountersScreenModel
5456
import cz.frantisekmasa.wfrp_master.common.encounters.domain.EncounterRepository
@@ -107,6 +109,8 @@ val appModule = DI.Module("Common") {
107109
FirestoreCompendium(Schema.Compendium.Miracles, instance(), mapper())
108110
}
109111

112+
bindSingleton { DismissedUserTipsHolder(instance()) }
113+
110114
bindSingleton<InvitationProcessor> { FirestoreInvitationProcessor(instance(), instance()) }
111115
bindSingleton<SkillRepository> { FirestoreSkillRepository(instance(), mapper()) }
112116
bindSingleton<TalentRepository> { characterItemRepository(Schema.Character.Talents) }
@@ -143,6 +147,10 @@ val appModule = DI.Module("Common") {
143147
/**
144148
* ViewModels
145149
*/
150+
151+
bindFactory { characterId: CharacterId ->
152+
CharacterCombatScreenModel(characterId, instance(), instance(), instance())
153+
}
146154
bindFactory { characterId: CharacterId -> CharacteristicsScreenModel(characterId, instance()) }
147155
bindFactory { characterId: CharacterId ->
148156
CharacterScreenModel(

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/character/CharacterDetailScreen.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import cafe.adriel.voyager.core.screen.Screen
3131
import cafe.adriel.voyager.navigator.LocalNavigator
3232
import cafe.adriel.voyager.navigator.currentOrThrow
3333
import cz.frantisekmasa.wfrp_master.common.character.characteristics.CharacteristicsScreen
34+
import cz.frantisekmasa.wfrp_master.common.character.combat.CharacterCombatScreen
3435
import cz.frantisekmasa.wfrp_master.common.character.conditions.ConditionsScreen
3536
import cz.frantisekmasa.wfrp_master.common.character.religion.ReligionScreen
3637
import cz.frantisekmasa.wfrp_master.common.character.skills.SkillsScreen
@@ -299,6 +300,13 @@ data class CharacterDetailScreen(
299300
party = party,
300301
)
301302
}
303+
CharacterTab.COMBAT -> {
304+
CharacterCombatScreen(
305+
screenModel = rememberScreenModel(arg = characterId),
306+
trappingsScreenModel = rememberScreenModel(arg = characterId),
307+
modifier = modifier,
308+
)
309+
}
302310
CharacterTab.CONDITIONS -> {
303311
ConditionsScreen(
304312
character = character,
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package cz.frantisekmasa.wfrp_master.common.character.combat
2+
3+
import androidx.compose.animation.animateContentSize
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material.Icon
11+
import androidx.compose.material.ListItem
12+
import androidx.compose.material.MaterialTheme
13+
import androidx.compose.material.Text
14+
import androidx.compose.material.icons.Icons
15+
import androidx.compose.material.icons.rounded.ExpandLess
16+
import androidx.compose.material.icons.rounded.ExpandMore
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.Stable
19+
import androidx.compose.runtime.getValue
20+
import androidx.compose.runtime.key
21+
import androidx.compose.runtime.mutableStateOf
22+
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.setValue
24+
import androidx.compose.ui.Alignment
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.text.font.FontWeight
27+
import androidx.compose.ui.unit.dp
28+
import cz.frantisekmasa.wfrp_master.common.character.combat.CharacterCombatScreenModel.WornArmourPiece
29+
import cz.frantisekmasa.wfrp_master.common.core.domain.HitLocation
30+
import cz.frantisekmasa.wfrp_master.common.core.domain.localizedName
31+
import cz.frantisekmasa.wfrp_master.common.core.domain.trappings.Armour
32+
import cz.frantisekmasa.wfrp_master.common.core.domain.trappings.ArmourPoints
33+
import cz.frantisekmasa.wfrp_master.common.core.domain.trappings.InventoryItem
34+
import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardContainer
35+
import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardTitle
36+
import cz.frantisekmasa.wfrp_master.common.core.ui.forms.Chip
37+
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.Spacing
38+
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.UserTip
39+
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.UserTipCard
40+
import cz.frantisekmasa.wfrp_master.common.localization.LocalStrings
41+
42+
@Composable
43+
fun ArmourCard(
44+
armour: Armour,
45+
armourPieces: Map<HitLocation, List<WornArmourPiece>>,
46+
toughnessBonus: UInt,
47+
onTrappingClick: (InventoryItem) -> Unit,
48+
) {
49+
UserTipCard(UserTip.ARMOUR_TRAPPINGS, Modifier.padding(horizontal = 8.dp))
50+
51+
CardContainer(
52+
Modifier
53+
.padding(horizontal = 8.dp)
54+
.padding(bottom = 8.dp)
55+
) {
56+
CardTitle(LocalStrings.current.armour.title)
57+
58+
Row(
59+
modifier = Modifier.padding(top = Spacing.large),
60+
horizontalArrangement = Arrangement.spacedBy(
61+
Spacing.large,
62+
Alignment.CenterHorizontally
63+
),
64+
) {
65+
Row(
66+
horizontalArrangement = Arrangement.End,
67+
verticalAlignment = Alignment.CenterVertically,
68+
modifier = Modifier.weight(1f),
69+
) {
70+
Text(
71+
LocalStrings.current.characteristics.toughnessBonusShortcut,
72+
Modifier.padding(end = Spacing.medium),
73+
)
74+
Points(toughnessBonus.toInt())
75+
}
76+
77+
Row(
78+
verticalAlignment = Alignment.CenterVertically,
79+
modifier = Modifier.weight(1f),
80+
) {
81+
Points(armour.shield)
82+
Text(
83+
LocalStrings.current.armour.shield,
84+
Modifier.padding(start = Spacing.medium),
85+
)
86+
}
87+
}
88+
89+
90+
val locations = remember { HitLocation.values().sortedBy { it.rollRange.first } }
91+
92+
locations.forEach { location ->
93+
key(location) {
94+
Location(
95+
location = location,
96+
points = armour.armourPoints(location),
97+
trappings = armourPieces[location] ?: emptyList(),
98+
onTrappingClick = onTrappingClick,
99+
)
100+
}
101+
}
102+
}
103+
}
104+
105+
@Composable
106+
@Stable
107+
private fun formatRoll(roll: Int): String {
108+
if (roll == 100) {
109+
return "00"
110+
}
111+
112+
return roll.toString().padStart(2, '0')
113+
}
114+
115+
@Composable
116+
private fun Points(value: Int) {
117+
Chip(padding = Spacing.tiny) {
118+
Text(value.toString())
119+
}
120+
}
121+
122+
@Composable
123+
private fun Location(
124+
location: HitLocation,
125+
points: ArmourPoints,
126+
trappings: List<WornArmourPiece>,
127+
onTrappingClick: (InventoryItem) -> Unit,
128+
) {
129+
val rollRange = location.rollRange
130+
var expanded by remember { mutableStateOf(false) }
131+
132+
Row(
133+
verticalAlignment = Alignment.CenterVertically,
134+
modifier = Modifier
135+
.fillMaxWidth()
136+
.padding(start = Spacing.large, top = Spacing.large)
137+
.let {
138+
if (trappings.isNotEmpty())
139+
it.clickable { expanded = !expanded }
140+
else it
141+
}
142+
) {
143+
Row(Modifier.weight(1f)) {
144+
Row {
145+
Text(
146+
"${formatRoll(rollRange.first)}-${formatRoll(rollRange.last)}",
147+
fontWeight = FontWeight.Bold,
148+
modifier = Modifier.padding(end = Spacing.medium),
149+
)
150+
151+
Text(location.localizedName)
152+
}
153+
154+
if (trappings.isNotEmpty()) {
155+
Icon(
156+
if (expanded)
157+
Icons.Rounded.ExpandLess
158+
else Icons.Rounded.ExpandMore,
159+
null,
160+
)
161+
}
162+
}
163+
164+
Points(points.value)
165+
}
166+
167+
Column(Modifier.animateContentSize()) {
168+
if (!expanded) {
169+
return@Column
170+
}
171+
172+
trappings.forEach { item ->
173+
key(item.trapping.id) {
174+
val armour = item.armour
175+
176+
ListItem(
177+
modifier = Modifier.clickable { onTrappingClick(item.trapping) },
178+
text = { Text(item.trapping.name) },
179+
secondaryText = if (armour.qualities.isNotEmpty() || armour.flaws.isNotEmpty())
180+
({
181+
TrappingFeatureList(
182+
armour.qualities,
183+
armour.flaws,
184+
Modifier.fillMaxWidth()
185+
)
186+
})
187+
else null,
188+
trailing = {
189+
Text(
190+
text = armour.points.value.toString(),
191+
style = MaterialTheme.typography.body2,
192+
)
193+
}
194+
)
195+
}
196+
}
197+
}
198+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cz.frantisekmasa.wfrp_master.common.character.combat
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.foundation.rememberScrollState
7+
import androidx.compose.foundation.verticalScroll
8+
import androidx.compose.material.MaterialTheme
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.getValue
11+
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.runtime.remember
13+
import androidx.compose.runtime.setValue
14+
import androidx.compose.ui.Modifier
15+
import cz.frantisekmasa.wfrp_master.common.character.trappings.InventoryItemDialog
16+
import cz.frantisekmasa.wfrp_master.common.character.trappings.TrappingsScreenModel
17+
import cz.frantisekmasa.wfrp_master.common.core.domain.trappings.InventoryItem
18+
import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle
19+
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.FullScreenProgress
20+
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.Spacing
21+
22+
@Composable
23+
fun CharacterCombatScreen(
24+
screenModel: CharacterCombatScreenModel,
25+
trappingsScreenModel: TrappingsScreenModel,
26+
modifier: Modifier,
27+
) {
28+
val armour = screenModel.armour.collectWithLifecycle(null).value
29+
val armourPieces = screenModel.armourPieces.collectWithLifecycle(null).value
30+
val weapons = screenModel.equippedWeapons.collectWithLifecycle(null).value
31+
val toughnessBonus = screenModel.toughnessBonus.collectWithLifecycle(null).value
32+
33+
if (armour == null || armourPieces == null || weapons == null || toughnessBonus == null) {
34+
FullScreenProgress()
35+
return
36+
}
37+
38+
var openedTrapping: InventoryItem? by remember { mutableStateOf(null) }
39+
40+
if (openedTrapping != null) {
41+
InventoryItemDialog(
42+
trappingsScreenModel,
43+
openedTrapping,
44+
onDismissRequest = { openedTrapping = null },
45+
)
46+
}
47+
48+
Column(
49+
modifier
50+
.background(MaterialTheme.colors.background)
51+
.verticalScroll(rememberScrollState())
52+
.padding(top = Spacing.small),
53+
) {
54+
WeaponsCard(weapons, onTrappingClick = { openedTrapping = it })
55+
ArmourCard(armour, armourPieces, toughnessBonus, onTrappingClick = { openedTrapping = it })
56+
}
57+
}

0 commit comments

Comments
 (0)