Skip to content

Commit a89e34d

Browse files
authored
Merge pull request #94 from fmasa/npc-copy
Let GMs copy NPCs
2 parents 485e6a3 + afc5543 commit a89e34d

File tree

6 files changed

+117
-8
lines changed

6 files changed

+117
-8
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,20 @@ val appModule = DI.Module("Common") {
246246
)
247247
}
248248
bindFactory { partyId: PartyId -> GameMasterScreenModel(partyId, instance(), instance()) }
249-
bindFactory { partyId: PartyId -> NpcsScreenModel(partyId, instance()) }
249+
bindFactory { partyId: PartyId ->
250+
NpcsScreenModel(
251+
partyId,
252+
instance(),
253+
instance(),
254+
instance(),
255+
instance(),
256+
instance(),
257+
instance(),
258+
instance(),
259+
instance(),
260+
instance(),
261+
)
262+
}
250263
bindFactory { partyId: PartyId ->
251264
SkillTestScreenModel(partyId, instance(), instance(), instance())
252265
}

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/character/Character.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cz.frantisekmasa.wfrp_master.common.core.domain.character
22

33
import androidx.compose.runtime.Immutable
44
import com.benasher44.uuid.Uuid
5+
import com.benasher44.uuid.uuid4
56
import cz.frantisekmasa.wfrp_master.common.core.common.requireMaxLength
67
import cz.frantisekmasa.wfrp_master.common.core.domain.Ambitions
78
import cz.frantisekmasa.wfrp_master.common.core.domain.Money
@@ -10,6 +11,7 @@ import cz.frantisekmasa.wfrp_master.common.core.domain.Stats
1011
import cz.frantisekmasa.wfrp_master.common.core.domain.trappings.Encumbrance
1112
import cz.frantisekmasa.wfrp_master.common.core.shared.Parcelable
1213
import cz.frantisekmasa.wfrp_master.common.core.shared.Parcelize
14+
import cz.frantisekmasa.wfrp_master.common.core.utils.duplicateName
1315
import cz.frantisekmasa.wfrp_master.common.encounters.domain.Wounds
1416
import kotlinx.serialization.Contextual
1517
import kotlinx.serialization.SerialName
@@ -233,6 +235,11 @@ data class Character(
233235

234236
fun updateConditions(newConditions: CurrentConditions) = copy(conditions = newConditions)
235237

238+
fun duplicate() = copy(
239+
id = uuid4().toString(),
240+
name = duplicateName(name),
241+
)
242+
236243
fun archive() = copy(
237244
isArchived = true,
238245
userId = null,

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/firebase/repositories/FirestoreCharacterRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class FirestoreCharacterRepository(
103103

104104
override fun inParty(partyId: PartyId, types: Set<CharacterType>): Flow<List<Character>> {
105105
return characters(partyId)
106+
.orderBy("name")
106107
.documents(mapper)
107108
.map { characters -> characters.filter { it.type in types && !it.isArchived } }
108109
}

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/localization/Strings.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,7 @@ data class NpcMessages(
952952
val noNpcsSubtext: String = "There are no NPCs yet. Add first one.",
953953
val noNpcsSearched: String = "No NPCs found",
954954
val noNpcsSearchedSubtext: String = "Consider changing the search phrase",
955+
val removalConfirmation: String = "Do you really want to permanently remove this NPC?",
955956
)
956957

957958
@Immutable

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/npcs/NpcsScreen.kt

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.material.FloatingActionButton
66
import androidx.compose.material.Icon
77
import androidx.compose.material.ListItem
88
import androidx.compose.material.Text
9+
import androidx.compose.material.TextButton
910
import androidx.compose.material.icons.Icons
1011
import androidx.compose.material.icons.rounded.Add
1112
import androidx.compose.runtime.Composable
@@ -20,13 +21,14 @@ import cafe.adriel.voyager.navigator.LocalNavigator
2021
import cafe.adriel.voyager.navigator.currentOrThrow
2122
import cz.frantisekmasa.wfrp_master.common.character.CharacterDetailScreen
2223
import cz.frantisekmasa.wfrp_master.common.characterCreation.CharacterCreationScreen
24+
import cz.frantisekmasa.wfrp_master.common.core.domain.character.Character
2325
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterType
2426
import cz.frantisekmasa.wfrp_master.common.core.domain.identifiers.CharacterId
2527
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
26-
import cz.frantisekmasa.wfrp_master.common.core.shared.IO
2728
import cz.frantisekmasa.wfrp_master.common.core.shared.Resources
2829
import cz.frantisekmasa.wfrp_master.common.core.ui.CharacterAvatar
2930
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.HamburgerButton
31+
import cz.frantisekmasa.wfrp_master.common.core.ui.dialogs.AlertDialog
3032
import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle
3133
import cz.frantisekmasa.wfrp_master.common.core.ui.menu.WithContextMenu
3234
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.ContextMenu
@@ -44,14 +46,15 @@ class NpcsScreen(
4446
@Composable
4547
override fun Content() {
4648
val navigator = LocalNavigator.currentOrThrow
47-
var removing by remember { mutableStateOf(false) }
49+
var processing by remember { mutableStateOf(false) }
4850

4951
val strings = LocalStrings.current
5052
val screenModel: NpcsScreenModel = rememberScreenModel(arg = partyId)
5153
val npcs by screenModel.npcs.collectWithLifecycle(null)
54+
val (npcToRemove, setNpcToRemove) = remember { mutableStateOf<Character?>(null) }
5255

5356
val data by derivedStateOf {
54-
if (removing) {
57+
if (processing) {
5558
return@derivedStateOf SearchableList.Data.Loading
5659
}
5760

@@ -61,6 +64,32 @@ class NpcsScreen(
6164

6265
val coroutineScope = rememberCoroutineScope()
6366

67+
if (npcToRemove != null) {
68+
AlertDialog(
69+
onDismissRequest = { setNpcToRemove(null) },
70+
text = { Text(LocalStrings.current.npcs.messages.removalConfirmation) },
71+
confirmButton = {
72+
TextButton(
73+
onClick = {
74+
coroutineScope.launch(Dispatchers.IO) {
75+
processing = true
76+
setNpcToRemove(null)
77+
screenModel.archiveNpc(npcToRemove)
78+
processing = false
79+
}
80+
}
81+
) {
82+
Text(LocalStrings.current.commonUi.buttonRemove)
83+
}
84+
},
85+
dismissButton = {
86+
TextButton(onClick = { setNpcToRemove(null) }) {
87+
Text(LocalStrings.current.commonUi.buttonCancel)
88+
}
89+
}
90+
)
91+
}
92+
6493
SearchableList(
6594
data = data,
6695
navigationIcon = { HamburgerButton() },
@@ -93,12 +122,18 @@ class NpcsScreen(
93122
navigator.push(CharacterDetailScreen(CharacterId(partyId, npc.id)))
94123
},
95124
items = listOf(
96-
ContextMenu.Item(LocalStrings.current.commonUi.buttonRemove) {
125+
ContextMenu.Item(LocalStrings.current.commonUi.buttonDuplicate) {
97126
coroutineScope.launch(Dispatchers.IO) {
98-
removing = true
99-
screenModel.archiveNpc(npc)
100-
removing = false
127+
processing = true
128+
try {
129+
screenModel.duplicate(npc)
130+
} finally {
131+
processing = false
132+
}
101133
}
134+
},
135+
ContextMenu.Item(LocalStrings.current.commonUi.buttonRemove) {
136+
setNpcToRemove(npc)
102137
}
103138
)
104139
) {

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/npcs/NpcsScreenModel.kt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,71 @@ package cz.frantisekmasa.wfrp_master.common.npcs
22

33
import cafe.adriel.voyager.core.model.ScreenModel
44
import cz.frantisekmasa.wfrp_master.common.core.domain.character.Character
5+
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterItem
6+
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterItemRepository
57
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterRepository
68
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterType
9+
import cz.frantisekmasa.wfrp_master.common.core.domain.identifiers.CharacterId
710
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
11+
import cz.frantisekmasa.wfrp_master.common.core.domain.religion.BlessingRepository
12+
import cz.frantisekmasa.wfrp_master.common.core.domain.religion.MiracleRepository
13+
import cz.frantisekmasa.wfrp_master.common.core.domain.skills.SkillRepository
14+
import cz.frantisekmasa.wfrp_master.common.core.domain.spells.SpellRepository
15+
import cz.frantisekmasa.wfrp_master.common.core.domain.talents.TalentRepository
16+
import cz.frantisekmasa.wfrp_master.common.core.domain.traits.TraitRepository
17+
import cz.frantisekmasa.wfrp_master.common.core.domain.trappings.InventoryItemRepository
18+
import cz.frantisekmasa.wfrp_master.common.firebase.firestore.Firestore
19+
import cz.frantisekmasa.wfrp_master.common.firebase.firestore.Transaction
820
import kotlinx.coroutines.flow.Flow
21+
import kotlinx.coroutines.flow.first
922

1023
class NpcsScreenModel(
1124
private val partyId: PartyId,
1225
private val characters: CharacterRepository,
26+
private val skills: SkillRepository,
27+
private val talents: TalentRepository,
28+
private val traits: TraitRepository,
29+
private val spells: SpellRepository,
30+
private val blessings: BlessingRepository,
31+
private val miracles: MiracleRepository,
32+
private val trappings: InventoryItemRepository,
33+
private val firestore: Firestore,
1334
) : ScreenModel {
1435

1536
val npcs: Flow<List<Character>> = characters.inParty(partyId, CharacterType.NPC)
1637

1738
suspend fun archiveNpc(npc: Character) {
1839
characters.save(partyId, npc.archive())
1940
}
41+
42+
suspend fun duplicate(npc: Character) {
43+
firestore.runTransaction { transaction ->
44+
val newNpc = npc.duplicate()
45+
val existingCharacterId = CharacterId(partyId, npc.id)
46+
val newCharacterId = CharacterId(partyId, newNpc.id)
47+
48+
copyItems(transaction, skills, existingCharacterId, newCharacterId)
49+
copyItems(transaction, talents, existingCharacterId, newCharacterId)
50+
copyItems(transaction, traits, existingCharacterId, newCharacterId)
51+
copyItems(transaction, spells, existingCharacterId, newCharacterId)
52+
copyItems(transaction, blessings, existingCharacterId, newCharacterId)
53+
copyItems(transaction, miracles, existingCharacterId, newCharacterId)
54+
copyItems(transaction, trappings, existingCharacterId, newCharacterId)
55+
56+
characters.save(transaction, partyId, newNpc)
57+
}
58+
}
59+
60+
private suspend fun <T : CharacterItem<T, *>> copyItems(
61+
transaction: Transaction,
62+
repository: CharacterItemRepository<T>,
63+
existingCharacterId: CharacterId,
64+
newCharacterId: CharacterId,
65+
) {
66+
repository.findAllForCharacter(existingCharacterId)
67+
.first()
68+
.forEach { item ->
69+
repository.save(transaction, newCharacterId, item)
70+
}
71+
}
2072
}

0 commit comments

Comments
 (0)