Initial commit

This commit is contained in:
shahondin1624
2025-10-25 11:22:27 +02:00
commit cd027b9f9b
116 changed files with 2648 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4c0,2.21 1.79,4 4,4c2.21,0 4,-1.79 4,-4C16,9.79 14.21,8 12,8zM12,14c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,13.1 13.1,14 12,14z" />
<path
android:fillColor="#000000"
android:pathData="M22,7.47V5.35C20.05,4.77 16.56,4 12,4C9.85,4 7.89,4.86 6.46,6.24C6.59,5.39 6.86,3.84 7.47,2H5.35C4.77,3.95 4,7.44 4,12c0,2.15 0.86,4.11 2.24,5.54c-0.85,-0.14 -2.4,-0.4 -4.24,-1.01v2.12C3.95,19.23 7.44,20 12,20c2.15,0 4.11,-0.86 5.54,-2.24c-0.14,0.85 -0.4,2.4 -1.01,4.24h2.12C19.23,20.05 20,16.56 20,12c0,-2.15 -0.86,-4.11 -2.24,-5.54C18.61,6.59 20.16,6.86 22,7.47zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6s6,2.69 6,6S15.31,18 12,18z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S14.76,7 12,7L12,7zM2,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S1.45,13 2,13zM20,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S19.45,13 20,13zM11,2v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V2c0,-0.55 -0.45,-1 -1,-1S11,1.45 11,2zM11,20v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1C11.45,19 11,19.45 11,20zM5.99,4.58c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0s0.39,-1.03 0,-1.41L5.99,4.58zM18.36,16.95c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0c0.39,-0.39 0.39,-1.03 0,-1.41L18.36,16.95zM19.42,5.99c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L19.42,5.99zM7.05,18.36c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L7.05,18.36z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z" />
</vector>

View File

@@ -0,0 +1,7 @@
<resources>
<string name="cyclone">Cyclone</string>
<string name="open_github">Open github</string>
<string name="run">Run</string>
<string name="stop">Stop</string>
<string name="theme">Theme</string>
</resources>

View File

@@ -0,0 +1,119 @@
package org.shahondin1624
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
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 kotlinx.coroutines.launch
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage
import org.shahondin1624.model.EXAMPLE_CHARACTER
import org.shahondin1624.theme.AppTheme
import org.shahondin1624.theme.LocalThemeIsDark
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun App(
onThemeChanged: @Composable (isDark: Boolean) -> Unit = {}
) = AppTheme(onThemeChanged) {
val character = EXAMPLE_CHARACTER
var isDark by LocalThemeIsDark.current
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Spacer(Modifier.height(16.dp))
Text(
"Menu",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleLarge
)
HorizontalDivider()
Spacer(Modifier.height(8.dp))
NavigationDrawerItem(
icon = { Icon(Icons.Default.Person, contentDescription = null) },
label = { Text("Character Sheet") },
selected = true,
onClick = {
scope.launch { drawerState.close() }
},
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
)
NavigationDrawerItem(
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
label = { Text("Settings") },
selected = false,
onClick = {
scope.launch { drawerState.close() }
// TODO: Navigate to settings
},
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
}
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Shadowrun Character Sheet") },
navigationIcon = {
IconButton(onClick = {
scope.launch {
if (drawerState.isClosed) {
drawerState.open()
} else {
drawerState.close()
}
}
}) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
},
actions = {
IconButton(onClick = { isDark = !isDark }) {
Icon(
imageVector = if (isDark) {
Icons.Default.LightMode
} else {
Icons.Default.DarkMode
},
contentDescription = "Toggle ${if (isDark) "Light" else "Dark"} mode"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AttributesPage(character)
}
}
}
}

View File

@@ -0,0 +1,7 @@
package org.shahondin1624.lib.components
import androidx.compose.ui.unit.dp
object UiConstants {
val SMALL_PADDING = 6.dp
}

View File

@@ -0,0 +1,2 @@
package org.shahondin1624.lib.components.charactermodel

View File

@@ -0,0 +1,80 @@
package org.shahondin1624.lib.components.charactermodel.attributespage
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.painterResource
import org.shahondin1624.lib.components.UiConstants.SMALL_PADDING
import org.shahondin1624.lib.functions.DiceRoll
import org.shahondin1624.model.attributes.Attribute
import org.shahondin1624.theme.LocalThemeIsDark
import shadowruncharsheet.sharedui.generated.resources.Res
import shadowruncharsheet.sharedui.generated.resources.dice
@Composable
fun Attribute(
attribute: Attribute,
onRoll: (DiceRoll) -> Unit = {
println("Result for $attribute: ${it.result}")
},
) {
var isInDarkMode by LocalThemeIsDark.current
val textColor = if (isInDarkMode) Color.Black else Color.White
Card {
Row(
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Row(modifier = Modifier
.padding(horizontal = SMALL_PADDING)
.background(attribute.type.color, shape = androidx.compose.foundation.shape.RoundedCornerShape(4.dp))
.clip(androidx.compose.foundation.shape.RoundedCornerShape(4.dp)),
horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = attribute.type.name,
modifier = Modifier
.width(90.dp)
.padding(start = SMALL_PADDING),
textAlign = TextAlign.Center,
color = textColor
)
Text(
text = attribute.value.toString(),
modifier = Modifier
.padding(end = SMALL_PADDING),
textAlign = TextAlign.Center,
color = textColor
)
}
IconButton(
onClick = {
val result = attribute.test()
onRoll(result)
},
modifier = Modifier.padding(end = SMALL_PADDING)
) {
Image(
painter = painterResource(Res.drawable.dice),
contentDescription = "Roll dice",
colorFilter = ColorFilter.tint(
color = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.size(24.dp)
)
}
}
}
}

View File

@@ -0,0 +1,57 @@
package org.shahondin1624.lib.components.charactermodel.attributespage
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
@Composable
fun ColumnScope.AttributesPage(character: ShadowrunCharacter) {
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
val gridState = rememberLazyGridState()
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 75.dp),
state = gridState,
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = character.attributes.getAllAttributes(),
span = {
GridItemSpan(2)
}) { attribute ->
Attribute(attribute)
}
item(span = { GridItemSpan(maxLineSpan) }) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 16.dp),
thickness = 2.dp
)
}
items(
items = character.talents.talents,
span = {
GridItemSpan(3)
}
) { talent ->
Talent(talent, character.attributes)
}
}
}
}

View File

@@ -0,0 +1,100 @@
package org.shahondin1624.lib.components.charactermodel.attributespage
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.painterResource
import org.shahondin1624.lib.components.UiConstants.SMALL_PADDING
import org.shahondin1624.lib.functions.DiceRoll
import org.shahondin1624.model.attributes.Attributes
import org.shahondin1624.model.talents.TalentDefinition
import org.shahondin1624.theme.LocalThemeIsDark
import shadowruncharsheet.sharedui.generated.resources.Res
import shadowruncharsheet.sharedui.generated.resources.dice
@Composable
fun Talent(
talent: TalentDefinition,
attributes: Attributes,
onRoll: (DiceRoll) -> Unit = {
println("Result for $talent: ${it.result}")
},
) {
var isInDarkMode by LocalThemeIsDark.current
val textColor = if (isInDarkMode) Color.Black else Color.White
Card {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Row(
modifier = Modifier
.padding(horizontal = SMALL_PADDING)
.background(
talent.attribute.color,
shape = RoundedCornerShape(4.dp)
)
.clip(androidx.compose.foundation.shape.RoundedCornerShape(4.dp)),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = talent.name,
modifier = Modifier
.width(150.dp)
.padding(start = SMALL_PADDING),
textAlign = TextAlign.Center,
color = textColor
)
Text(
text = attributes.getAttributeByType(talent.attribute).value.toString(),
modifier = Modifier
.padding(end = SMALL_PADDING),
textAlign = TextAlign.Center,
color = textColor
)
Text(
text = talent.value.toString(),
modifier = Modifier
.padding(end = SMALL_PADDING),
textAlign = TextAlign.Center,
color = textColor
)
}
IconButton(
onClick = {
val result = talent.test(modifiers = emptyList(), attributes = attributes)
onRoll(result)
},
modifier = Modifier.padding(end = SMALL_PADDING)
) {
Image(
painter = painterResource(Res.drawable.dice),
contentDescription = "Roll dice",
colorFilter = ColorFilter.tint(
color = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.size(24.dp)
)
}
}
}
}

View File

@@ -0,0 +1,2 @@
package org.shahondin1624.lib.components.settings

View File

@@ -0,0 +1,28 @@
package org.shahondin1624.lib.functions
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
/**
* Platform-agnostic serialization helpers for ShadowrunCharacter.
*
* File I/O (reading/writing from disk) should be implemented per-platform
* (androidMain, jvmMain, iosMain, webMain). Here we only convert to/from JSON strings.
*/
object DataLoader {
// Shared Json configuration used across the app
internal val json: Json = Json {
prettyPrint = true
ignoreUnknownKeys = true
// Default classDiscriminator = "type" works with our sealed TalentDefinition hierarchy
encodeDefaults = true
}
fun serialize(character: ShadowrunCharacter): String =
json.encodeToString(character)
fun deserialize(jsonString: String): ShadowrunCharacter =
json.decodeFromString(jsonString)
}

View File

@@ -0,0 +1,22 @@
package org.shahondin1624.lib.functions
import kotlin.random.Random
fun rollDice(sides: Int) = Random.nextInt(sides) + 1
fun rollXDice(sides: Int = 6, numberOfDice: Int) = (0 until numberOfDice).map { rollDice(sides) }
fun List<Int>.countSuccesses(numberForSuccessOrHigher: Int = 5) = this.count { it >= numberForSuccessOrHigher }
data class DiceRoll(
val numberOfDice: Int,
val numberOfSides: Int = 6,
val numberForSuccessOrHigher: Int = 5,
val result: List<Int> = listOf(),
val numberOfSuccesses: Int = -1
) {
fun roll(numberForSuccessOrHigher: Int = 5): DiceRoll = this.copy(
result = rollXDice(sides = this.numberOfSides, numberOfDice = this.numberOfDice),
numberOfSuccesses = result.countSuccesses(numberForSuccessOrHigher)
)
}

View File

@@ -0,0 +1,43 @@
package org.shahondin1624.model
import org.shahondin1624.model.attributes.Attribute
import org.shahondin1624.model.attributes.AttributeType
import org.shahondin1624.model.attributes.Attributes
import org.shahondin1624.model.characterdata.CharacterData
import org.shahondin1624.model.characterdata.Metatype
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
import org.shahondin1624.model.charactermodel.createDamageMonitor
import org.shahondin1624.model.talents.Talents
import org.shahondin1624.model.talents.createAllProvidedTalents
val EXAMPLE_ATTRIBUTES = Attributes(
body = Attribute(AttributeType.Body, 3),
agility = Attribute(AttributeType.Agility, 4),
reaction = Attribute(AttributeType.Reaction, 3),
strength = Attribute(AttributeType.Strength, 2),
willpower = Attribute(AttributeType.Willpower, 3),
logic = Attribute(AttributeType.Logic, 4),
intuition = Attribute(AttributeType.Intuition, 3),
charisma = Attribute(AttributeType.Charisma, 2),
edge = 2
)
val EXAMPLE_CHARACTER: ShadowrunCharacter = ShadowrunCharacter(
attributes = EXAMPLE_ATTRIBUTES,
talents = Talents(createAllProvidedTalents()),
characterData = CharacterData(
concept = "Example",
nuyen = 10500,
essence = 6.0f,
name = "Max Mustermann",
metatype = Metatype.Human,
age = 27,
gender = "male",
streetCred = 0,
notoriety = 0,
publicAwareness = 0,
totalKarma = 55,
currentKarma = 20
),
damageMonitor = createDamageMonitor(EXAMPLE_ATTRIBUTES),
)

View File

@@ -0,0 +1,5 @@
package org.shahondin1624.model
interface Versionable {
val version: String
}

View File

@@ -0,0 +1,13 @@
package org.shahondin1624.model.attributes
import kotlinx.serialization.Serializable
import org.shahondin1624.lib.functions.DiceRoll
import org.shahondin1624.model.modifier.SRModifier
import org.shahondin1624.model.modifier.accumulateModifiers
@Serializable
data class Attribute(val type: AttributeType, val value: Int) {
fun test(modifiers: List<SRModifier<*>> = emptyList()): DiceRoll {
return modifiers.accumulateModifiers(DiceRoll(numberOfDice = value)).roll()
}
}

View File

@@ -0,0 +1,18 @@
package org.shahondin1624.model.attributes
import androidx.compose.ui.graphics.Color
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
enum class AttributeType(@Transient val color: Color) {
Body(Color(251, 84, 43)),
Agility(Color(239, 229, 138)),
Reaction(Color(89, 170, 56)),
Strength(Color.Red),
Willpower(Color.Blue),
Logic(Color(147, 104, 31)),
Intuition(Color.Gray),
Charisma(Color(225, 172, 46))
;
}

View File

@@ -0,0 +1,124 @@
package org.shahondin1624.model.attributes
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.shahondin1624.model.Versionable
import org.shahondin1624.model.modifier.AttributeModifier
import org.shahondin1624.model.modifier.ModifierCache
@Serializable
data class Attributes(
private val body: Attribute,
private val agility: Attribute,
private val reaction: Attribute,
private val strength: Attribute,
private val willpower: Attribute,
private val logic: Attribute,
private val intuition: Attribute,
private val charisma: Attribute,
val edge: Int,
@Transient
private val cache: ModifierCache = ModifierCache(),
override val version: String = "v0.1"
): Versionable {
private fun applyModifiers(modifiers: List<AttributeModifier>): Attributes {
return cache.applyModifiers(modifiers, this)
}
fun body(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).body.value
}
fun agility(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).agility.value
}
fun reaction(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).reaction.value
}
fun strength(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).strength.value
}
fun willpower(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).willpower.value
}
fun logic(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).logic.value
}
fun intuition(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).intuition.value
}
fun charisma(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).charisma.value
}
fun initiative(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).reaction.value + applyModifiers(modifiers).intuition.value
}
fun matrixInitiative(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).logic.value
}
fun astralInitiative(modifiers: List<AttributeModifier> = emptyList()): Int {
return 0
}
fun composure(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).body.value + applyModifiers(modifiers).willpower.value
}
fun judgeIntent(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).intuition.value + applyModifiers(modifiers).charisma.value
}
fun memory(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).logic.value + applyModifiers(modifiers).willpower.value
}
fun carry(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).strength.value * 2
}
fun run(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).agility.value + applyModifiers(modifiers).body.value
}
fun physicalLimit(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).body.value + applyModifiers(modifiers).strength.value
}
fun mentalLimit(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).willpower.value + applyModifiers(modifiers).logic.value
}
fun socialLimit(modifiers: List<AttributeModifier> = emptyList()): Int {
return applyModifiers(modifiers).charisma.value + applyModifiers(modifiers).body.value
}
fun mageResistance(modifiers: List<AttributeModifier> = emptyList()): Int {
return 0
}
fun getAttributeByType(type: AttributeType): Attribute {
return when (type) {
AttributeType.Body -> body
AttributeType.Agility -> agility
AttributeType.Reaction -> reaction
AttributeType.Strength -> strength
AttributeType.Willpower -> willpower
AttributeType.Logic -> logic
AttributeType.Intuition -> intuition
AttributeType.Charisma -> charisma
}
}
fun getAllAttributes(): List<Attribute> {
return listOf(body, agility, reaction, strength, willpower, logic, intuition, charisma)
}
}

View File

@@ -0,0 +1,21 @@
package org.shahondin1624.model.characterdata
import kotlinx.serialization.Serializable
import org.shahondin1624.model.Versionable
@Serializable
data class CharacterData(
val concept: String,
val nuyen: Int,
val essence: Float,
val name: String,
val metatype: Metatype,
val age: Int,
val gender: String,
val streetCred: Int,
val notoriety: Int,
val publicAwareness: Int,
val totalKarma: Int,
val currentKarma: Int,
override val version: String = "v0.1"
): Versionable

View File

@@ -0,0 +1,8 @@
package org.shahondin1624.model.characterdata
import kotlinx.serialization.Serializable
@Serializable
enum class Metatype {
Human, Elf, Dwarf, Ork, Troll
}

View File

@@ -0,0 +1,88 @@
package org.shahondin1624.model.charactermodel
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.shahondin1624.model.Versionable
import org.shahondin1624.model.attributes.Attribute
import org.shahondin1624.model.attributes.AttributeType
import org.shahondin1624.model.attributes.Attributes
import org.shahondin1624.model.modifier.AttributeModifier
import org.shahondin1624.model.modifier.ModifierCache
private val EMPTY_ATTRIBUTES = Attributes(
body = Attribute(AttributeType.Body, 0),
agility = Attribute(AttributeType.Agility, 0),
reaction = Attribute(AttributeType.Reaction, 0),
strength = Attribute(AttributeType.Strength, 0),
willpower = Attribute(AttributeType.Willpower, 0),
logic = Attribute(AttributeType.Logic, 0),
intuition = Attribute(AttributeType.Intuition, 0),
charisma = Attribute(AttributeType.Charisma, 0),
edge = 0
)
@Serializable
data class DamageMonitor(
@Transient
private var attributes: Attributes = EMPTY_ATTRIBUTES,
private val stunCurrent: Int = 0,
private val physicalCurrent: Int = 0,
private val overflowCurrent: Int = 0,
@Transient
private val cache: ModifierCache = ModifierCache(),
override val version: String = "v0.1"
): Versionable {
init {
require(stunCurrent >= 0) { "Stun damage cannot be negative" }
require(stunCurrent <= stunMax()) { "Stun damage cannot exceed maximum stun damage of ${stunMax()}" }
require(physicalCurrent >= 0) { "Physical damage cannot be negative" }
require(physicalCurrent <= physicalMax()) { "Physical damage cannot exceed maximum physical damage of ${physicalMax()}" }
require(overflowCurrent >= 0) { "Overflow damage cannot be negative" }
require(overflowCurrent <= overflowMax()) { "Overflow damage cannot exceed maximum overflow damage of ${overflowMax()}" }
}
private fun applyModifiers(modifiers: List<AttributeModifier>): Attributes {
return cache.applyModifiers(modifiers, attributes)
}
fun stunMax(modifiers: List<AttributeModifier> = emptyList()): Int {
return 7 + (applyModifiers(modifiers).willpower() / 2.0).toInt()
}
fun stunCurrent(modifiers: List<AttributeModifier> = emptyList()): Int {
return stunCurrent
}
fun physicalMax(modifiers: List<AttributeModifier> = emptyList()): Int {
return 7 + (applyModifiers(modifiers).body() / 2.0).toInt()
}
fun physicalCurrent(modifiers: List<AttributeModifier> = emptyList()): Int {
return physicalCurrent
}
fun overflowMax(modifiers: List<AttributeModifier> = emptyList()): Int {
return (applyModifiers(modifiers).body() / 2.0).toInt()
}
fun overflowCurrent(modifiers: List<AttributeModifier> = emptyList()): Int {
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
}
}
fun setAttributes(attributes: Attributes) {
this.attributes = attributes
}
}
fun createDamageMonitor(attributes: Attributes): DamageMonitor {
val monitor = DamageMonitor(attributes)
return monitor.copy(stunCurrent = monitor.stunMax(), physicalCurrent = monitor.physicalMax(), overflowCurrent = monitor.overflowMax())
}

View File

@@ -0,0 +1,20 @@
package org.shahondin1624.model.charactermodel
import kotlinx.serialization.Serializable
import org.shahondin1624.model.Versionable
import org.shahondin1624.model.attributes.Attributes
import org.shahondin1624.model.characterdata.CharacterData
import org.shahondin1624.model.talents.Talents
@Serializable
data class ShadowrunCharacter(
val characterData: CharacterData,
val attributes: Attributes,
val talents: Talents,
val damageMonitor: DamageMonitor,
override val version: String = "v0.1"
): Versionable {
init {
damageMonitor.setAttributes(attributes)
}
}

View File

@@ -0,0 +1,5 @@
package org.shahondin1624.model.modifier
import org.shahondin1624.model.attributes.Attributes
interface AttributeModifier: SRModifier<Attributes>

View File

@@ -0,0 +1,22 @@
package org.shahondin1624.model.modifier
import org.shahondin1624.model.attributes.Attributes
data class ModifierCache(
private val cache: LinkedHashMap<List<AttributeModifier>, Attributes> = linkedMapOf()
) {
fun applyModifiers(modifiers: List<AttributeModifier> = emptyList(), attributes: Attributes): Attributes {
if (cache.containsKey(modifiers)) {
return cache[modifiers]!!
}
val modified = modifiers.fold(attributes) { acc, modifier -> modifier.apply(acc) }
cache[modifiers] = modified
while (cache.size > 5) {
val oldestKey = cache.keys.firstOrNull()
oldestKey?.let {
cache.remove(oldestKey)
}
}
return modified
}
}

View File

@@ -0,0 +1,12 @@
package org.shahondin1624.model.modifier
import org.shahondin1624.lib.functions.DiceRoll
interface SRModifier<T> {
fun apply(value: T): T
fun getDiceRollModifier(diceRoll: DiceRoll): SRModifier<DiceRoll>
}
fun List<SRModifier<*>>.accumulateModifiers(diceRoll: DiceRoll): DiceRoll = this.fold(diceRoll) { acc, modifier ->
modifier.getDiceRollModifier(acc).apply(acc)
}

View File

@@ -0,0 +1,5 @@
package org.shahondin1624.model.modifier
import org.shahondin1624.model.talents.TalentDefinition
interface TalentModifier: SRModifier<TalentDefinition>

View File

@@ -0,0 +1,84 @@
package org.shahondin1624.model.talents
import kotlinx.serialization.Serializable
import org.shahondin1624.model.attributes.AttributeType
import org.shahondin1624.model.attributes.AttributeType.*
@Serializable
enum class ProvidedTalentName(val attribute: AttributeType) {
Aeronautics_Mechanic(Logic),
Alchemy(Logic),
Animal_Handling(Charisma),
Arcana(Intuition),
Archery(Agility),
Armorer(Logic),
Artificing(Logic),
Artisan(Agility),
Assensing(Intuition),
Astral_Combat(Willpower),
Automatics(Agility),
Automotive_Mechanic(Logic),
Banishing(Willpower),
Binding(Willpower),
Biotechnology(Logic),
Blades(Strength),
Chemistry(Logic),
Clubs(Strength),
//TODO Logic
Computer(Logic),
Sligth_of_hand(Charisma),
Counterspelling(Willpower),
Cybercombat(Logic),
Cybertechnology(Logic),
//TODO Logic
Demolitions(Intuition),
Disenchanting(Willpower),
Disguise(Intuition),
Diving(Body),
Electronic_Warfare(Logic),
Escape_Artist(Agility),
Etiquette(Charisma),
//TODO Exotics
First_Aid(Intuition),
Forgery(Logic),
Free_Fall(Agility),
Gunnery(Agility),
Gymnastics(Agility),
Hacking(Logic),
Hardware(Logic),
Heavy_Weapons(Strength),
Impersonation(Charisma),
Industrial_Mechanic(Logic),
Instruction(Charisma),
Intimidation(Charisma),
Leadership(Charisma),
Locksmith(Intuition),
Longarms(Agility),
Medicine(Logic),
Nautical_Mechanic(Logic),
Navigation(Intuition),
Negotiation(Charisma),
Palming(Agility),
Perception(Intuition),
Performance(Charisma),
Pilot_Aerospace(Reaction),
Pilot_Aircraft(Reaction),
//TODO Pilot Exotic
Pilot_Groundcraft(Reaction),
Pilot_Walker(Reaction),
Pilot_Watercraft(Reaction),
Pistols(Agility),
//TODO, Logic
Ritual_Spellcasting(Willpower),
Running(Body),
Sneaking(Agility),
Software(Logic),
Spellcasting(Willpower),
Summoning(Willpower),
Survival(Intuition),
Swimming(Body),
Throwing_Weapons(Strength),
Tracking(Intuition),
Unarmed_Combat(Strength)
;
}

View File

@@ -0,0 +1,37 @@
package org.shahondin1624.model.talents
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.shahondin1624.lib.functions.DiceRoll
import org.shahondin1624.model.attributes.AttributeType
import org.shahondin1624.model.attributes.Attributes
import org.shahondin1624.model.modifier.SRModifier
import org.shahondin1624.model.modifier.accumulateModifiers
@Serializable
@SerialName("Talent")
data class Talent(
override val name: String,
override val attribute: AttributeType,
override val value: Int,
override val custom: Boolean,
): TalentDefinition {
override fun test(modifiers: List<SRModifier<*>>, attributes: Attributes): DiceRoll {
val attributeValue = attributes.getAttributeByType(attribute).value
val combined = value + attributeValue
return modifiers.accumulateModifiers(DiceRoll(numberOfDice = combined)).roll()
}
}
private fun createProvidedTalent(provided: ProvidedTalentName, value: Int = 0) = Talent(
name = provided.name.replace("_", " "),
attribute = provided.attribute,
value = value,
custom = false
)
fun createAllProvidedTalents() = ProvidedTalentName.entries.map {
createProvidedTalent(it)
}
fun createCustomTalent(name: String, attribute: AttributeType, value: Int = 0) = Talent(name, attribute, value, true)

View File

@@ -0,0 +1,16 @@
package org.shahondin1624.model.talents
import kotlinx.serialization.Serializable
import org.shahondin1624.lib.functions.DiceRoll
import org.shahondin1624.model.attributes.AttributeType
import org.shahondin1624.model.attributes.Attributes
import org.shahondin1624.model.modifier.SRModifier
@Serializable
sealed interface TalentDefinition {
val name: String
val attribute: AttributeType
val value: Int
val custom: Boolean
fun test(modifiers: List<SRModifier<*>>, attributes: Attributes): DiceRoll
}

View File

@@ -0,0 +1,10 @@
package org.shahondin1624.model.talents
import kotlinx.serialization.Serializable
import org.shahondin1624.model.Versionable
@Serializable
data class Talents(
val talents: List<TalentDefinition> = createAllProvidedTalents(),
override val version: String = "v0.1"
) : Versionable

View File

@@ -0,0 +1,80 @@
package org.shahondin1624.theme
import androidx.compose.ui.graphics.Color
//generated by https://materialkolor.com
//Color palette was taken here: https://coolors.co/palette/e63946-f1faee-a8dadc-457b9d-1d3557
internal val Seed = Color(0xFF1D3557)
internal val PrimaryLight = Color(0xFF485F84)
internal val OnPrimaryLight = Color(0xFFFFFFFF)
internal val PrimaryContainerLight = Color(0xFFD5E3FF)
internal val OnPrimaryContainerLight = Color(0xFF30476A)
internal val SecondaryLight = Color(0xFF2B6485)
internal val OnSecondaryLight = Color(0xFFFFFFFF)
internal val SecondaryContainerLight = Color(0xFFC7E7FF)
internal val OnSecondaryContainerLight = Color(0xFF064C6B)
internal val TertiaryLight = Color(0xFF356668)
internal val OnTertiaryLight = Color(0xFFFFFFFF)
internal val TertiaryContainerLight = Color(0xFFB9ECEE)
internal val OnTertiaryContainerLight = Color(0xFF1A4E50)
internal val ErrorLight = Color(0xFFBB152C)
internal val OnErrorLight = Color(0xFFFFFFFF)
internal val ErrorContainerLight = Color(0xFFFFDAD8)
internal val OnErrorContainerLight = Color(0xFF410007)
internal val BackgroundLight = Color(0xFFF9F9F9)
internal val OnBackgroundLight = Color(0xFF1A1C1C)
internal val SurfaceLight = Color(0xFFF9F9F9)
internal val OnSurfaceLight = Color(0xFF1A1C1C)
internal val SurfaceVariantLight = Color(0xFFDCE5D9)
internal val OnSurfaceVariantLight = Color(0xFF404941)
internal val OutlineLight = Color(0xFF717970)
internal val OutlineVariantLight = Color(0xFFC0C9BE)
internal val ScrimLight = Color(0xFF000000)
internal val InverseSurfaceLight = Color(0xFF2F3131)
internal val InverseOnSurfaceLight = Color(0xFFF0F1F1)
internal val InversePrimaryLight = Color(0xFFB0C7F1)
internal val SurfaceDimLight = Color(0xFFDADADA)
internal val SurfaceBrightLight = Color(0xFFF9F9F9)
internal val SurfaceContainerLowestLight = Color(0xFFFFFFFF)
internal val SurfaceContainerLowLight = Color(0xFFF3F3F4)
internal val SurfaceContainerLight = Color(0xFFEEEEEE)
internal val SurfaceContainerHighLight = Color(0xFFE8E8E8)
internal val SurfaceContainerHighestLight = Color(0xFFE2E2E2)
internal val PrimaryDark = Color(0xFFB0C7F1)
internal val OnPrimaryDark = Color(0xFF183153)
internal val PrimaryContainerDark = Color(0xFF30476A)
internal val OnPrimaryContainerDark = Color(0xFFD5E3FF)
internal val SecondaryDark = Color(0xFF98CDF2)
internal val OnSecondaryDark = Color(0xFF00344C)
internal val SecondaryContainerDark = Color(0xFF064C6B)
internal val OnSecondaryContainerDark = Color(0xFFC7E7FF)
internal val TertiaryDark = Color(0xFF9ECFD1)
internal val OnTertiaryDark = Color(0xFF003739)
internal val TertiaryContainerDark = Color(0xFF1A4E50)
internal val OnTertiaryContainerDark = Color(0xFFB9ECEE)
internal val ErrorDark = Color(0xFFFFB3B1)
internal val OnErrorDark = Color(0xFF680011)
internal val ErrorContainerDark = Color(0xFF92001C)
internal val OnErrorContainerDark = Color(0xFFFFDAD8)
internal val BackgroundDark = Color(0xFF121414)
internal val OnBackgroundDark = Color(0xFFE2E2E2)
internal val SurfaceDark = Color(0xFF121414)
internal val OnSurfaceDark = Color(0xFFE2E2E2)
internal val SurfaceVariantDark = Color(0xFF404941)
internal val OnSurfaceVariantDark = Color(0xFFC0C9BE)
internal val OutlineDark = Color(0xFF8A9389)
internal val OutlineVariantDark = Color(0xFF404941)
internal val ScrimDark = Color(0xFF000000)
internal val InverseSurfaceDark = Color(0xFFE2E2E2)
internal val InverseOnSurfaceDark = Color(0xFF2F3131)
internal val InversePrimaryDark = Color(0xFF485F84)
internal val SurfaceDimDark = Color(0xFF121414)
internal val SurfaceBrightDark = Color(0xFF37393A)
internal val SurfaceContainerLowestDark = Color(0xFF0C0F0F)
internal val SurfaceContainerLowDark = Color(0xFF1A1C1C)
internal val SurfaceContainerDark = Color(0xFF1E2020)
internal val SurfaceContainerHighDark = Color(0xFF282A2B)
internal val SurfaceContainerHighestDark = Color(0xFF333535)

View File

@@ -0,0 +1,105 @@
package org.shahondin1624.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.*
private val LightColorScheme = lightColorScheme(
primary = PrimaryLight,
onPrimary = OnPrimaryLight,
primaryContainer = PrimaryContainerLight,
onPrimaryContainer = OnPrimaryContainerLight,
secondary = SecondaryLight,
onSecondary = OnSecondaryLight,
secondaryContainer = SecondaryContainerLight,
onSecondaryContainer = OnSecondaryContainerLight,
tertiary = TertiaryLight,
onTertiary = OnTertiaryLight,
tertiaryContainer = TertiaryContainerLight,
onTertiaryContainer = OnTertiaryContainerLight,
error = ErrorLight,
onError = OnErrorLight,
errorContainer = ErrorContainerLight,
onErrorContainer = OnErrorContainerLight,
background = BackgroundLight,
onBackground = OnBackgroundLight,
surface = SurfaceLight,
onSurface = OnSurfaceLight,
surfaceVariant = SurfaceVariantLight,
onSurfaceVariant = OnSurfaceVariantLight,
outline = OutlineLight,
outlineVariant = OutlineVariantLight,
scrim = ScrimLight,
inverseSurface = InverseSurfaceLight,
inverseOnSurface = InverseOnSurfaceLight,
inversePrimary = InversePrimaryLight,
surfaceDim = SurfaceDimLight,
surfaceBright = SurfaceBrightLight,
surfaceContainerLowest = SurfaceContainerLowestLight,
surfaceContainerLow = SurfaceContainerLowLight,
surfaceContainer = SurfaceContainerLight,
surfaceContainerHigh = SurfaceContainerHighLight,
surfaceContainerHighest = SurfaceContainerHighestLight,
)
private val DarkColorScheme = darkColorScheme(
primary = PrimaryDark,
onPrimary = OnPrimaryDark,
primaryContainer = PrimaryContainerDark,
onPrimaryContainer = OnPrimaryContainerDark,
secondary = SecondaryDark,
onSecondary = OnSecondaryDark,
secondaryContainer = SecondaryContainerDark,
onSecondaryContainer = OnSecondaryContainerDark,
tertiary = TertiaryDark,
onTertiary = OnTertiaryDark,
tertiaryContainer = TertiaryContainerDark,
onTertiaryContainer = OnTertiaryContainerDark,
error = ErrorDark,
onError = OnErrorDark,
errorContainer = ErrorContainerDark,
onErrorContainer = OnErrorContainerDark,
background = BackgroundDark,
onBackground = OnBackgroundDark,
surface = SurfaceDark,
onSurface = OnSurfaceDark,
surfaceVariant = SurfaceVariantDark,
onSurfaceVariant = OnSurfaceVariantDark,
outline = OutlineDark,
outlineVariant = OutlineVariantDark,
scrim = ScrimDark,
inverseSurface = InverseSurfaceDark,
inverseOnSurface = InverseOnSurfaceDark,
inversePrimary = InversePrimaryDark,
surfaceDim = SurfaceDimDark,
surfaceBright = SurfaceBrightDark,
surfaceContainerLowest = SurfaceContainerLowestDark,
surfaceContainerLow = SurfaceContainerLowDark,
surfaceContainer = SurfaceContainerDark,
surfaceContainerHigh = SurfaceContainerHighDark,
surfaceContainerHighest = SurfaceContainerHighestDark,
)
internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) }
@Composable
internal fun AppTheme(
onThemeChanged: @Composable (isDark: Boolean) -> Unit,
content: @Composable () -> Unit
) {
val systemIsDark = isSystemInDarkTheme()
val isDarkState = remember(systemIsDark) { mutableStateOf(systemIsDark) }
CompositionLocalProvider(
LocalThemeIsDark provides isDarkState
) {
val isDark by isDarkState
onThemeChanged(!isDark)
MaterialTheme(
colorScheme = if (isDark) DarkColorScheme else LightColorScheme,
content = { Surface(content = content) }
)
}
}