Initial commit

This commit is contained in:
2026-08-23 15:51:15 -04:00
commit 92c14dfc81
62 changed files with 2685 additions and 0 deletions
@@ -0,0 +1,117 @@
package dev.geekback.dopewars.desktop
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
/**
* ApiClient
*
* Client for accessing the Dopewars API server
*/
class ApiClient() {
private var address: String = ""
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(
Json {
allowSpecialFloatingPointValues = true
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}
fun new(address: String) {
this.address = address
}
/**
* login
*
* Create a new game
*/
suspend fun login(baseUrl: String): Player {
println("URL = [${baseUrl}/players]")
val response = client.post(urlString = "$baseUrl/players") {
contentType(ContentType.Application.Json)
}
return if (response.status == HttpStatusCode.Created) {
response.body()
} else {
println("Login fail. -> ${response.status}")
Player(name = "Invalid Login Attempt", id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
}
}
suspend fun market(forPlayer: Player): Market {
val result: Market = client.get("${this.address}/market/${forPlayer.id}") {
contentType(ContentType.Application.Json)
}.body()
return result
}
suspend fun rename(newName: String, player: Player): Player {
val response = client.post("${this.address}/players/${player.id}/rename") {
val newBody = PlayerNameUpdateRequest(newName)
setBody(newBody)
contentType(ContentType.Application.Json)
}
return if (response.status == HttpStatusCode.OK) {
val result: Player = response.body()
println("Rename Success -> $result")
result
} else {
println("Rename Fail -> ${response.status} / ${this.address}/players/${player.id}/rename")
player
}
}
suspend fun refreshPlayer(initialState: Player): Player {
val response = client.get("${this.address}/players/${initialState.id}") {
contentType(ContentType.Application.Json)
}
return if (response.status == HttpStatusCode.OK) {
val result: Player = response.body()
println("Refresh Success -> $result")
result
} else {
println("Refresh Failure")
initialState
}
}
suspend fun travel(baseUrl: String, playerId: String, destination: TravelRequest) {
val response = client.post("$baseUrl/players/${playerId}/travel") {
setBody(destination)
contentType(ContentType.Application.Json)
}
println("Travel result -> $response")
}
// suspend fun travel(destination: Location): TravelResult {
// val response = client.get("${this.address}/players/${this.player}")
// }
}
@@ -0,0 +1,42 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTheme(content: @Composable () -> Unit) {
Scaffold {
MaterialTheme {
content()
}
}
}
@Composable
fun BorderedCard(title: String, content: @Composable () -> Unit) {
Card(modifier = Modifier
.border(BorderStroke(width = 1.dp, color = Color.Black))
.fillMaxWidth()
.padding(16.dp)) {
Column {
Text(
modifier = Modifier.padding(16.dp),
text = title,
style = MaterialTheme.typography.titleMedium)
content()
}
}
}
@@ -0,0 +1,64 @@
package dev.geekback.dopewars.desktop
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.ApiClient
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.PlayerUi
class ClientState(player: Player) {
var player by mutableStateOf(player)
var apiUrl: String = "http://localhost:8080"
private var api: ApiClient = ApiClient()
suspend fun renamePlayer(newName: String) {
println("Renaming player from ${this.player.name} to $newName")
this.player = this.api.rename(newName, this.player)
}
suspend fun login() : Player {
val p = this.api.login("http://localhost:8080")
println("Logged in as ${p.name} / ${p.id}")
return p
}
suspend fun updateMarket() {
println("Updating market")
this.player.lastMarket = this.api.market(this.player)
println("Market updated to this -> ${this.player.lastMarket}")
}
suspend fun refreshPlayer() {
println("Refreshing player")
this.player = this.api.refreshPlayer(this.player)
println("Refreshed player")
}
fun isLoggedIn(): Boolean {
println("Testing if user is logged in - ${this.player.id.isNotEmpty()}")
return this.player.id.isNotEmpty()
}
fun refreshApi(url: String) {
println("[XXX] Refreshing API UrL to $url")
this.api.new(url)
}
suspend fun travel(player: Player, destination: TravelRequest) {
val result = this.api.travel("http://localhost:8080", player.id, destination)
}
companion object {
fun new(): ClientState {
return ClientState(
Player.new()
)
}
}
}
@@ -0,0 +1,181 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo
import dev.geekback.dopewars.shared.Drugs
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Locations
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.MarketRequest
import dev.geekback.dopewars.shared.api.MarketResult
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.shared.api.PlayerNameUpdateResponse
import dev.geekback.dopewars.shared.api.TradeAction
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.DopeWarsUi
import kotlinx.coroutines.launch
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "DopeWars Dev Client"
) {
DemoApp()
}
}
data class ApiData(
var marketRequest: MarketRequest,
var marketResult: MarketResult,
var playerNameUpdateRequest: PlayerNameUpdateRequest,
var playerNameUpadteResponse: PlayerNameUpdateResponse,
var tradeAction: TradeAction,
var tradeRequest: TradeRequest,
var travelRequest: TravelRequest,
var travelResult: TravelResult
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DemoApp() {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
var currentScreen by remember { mutableStateOf(Screens.HOME) }
var drug by remember { mutableStateOf(DrugInfo(drug = Drug.WEED, minValue = 100.0f, maxValue = 1000.0f)) }
var player by remember { mutableStateOf(Player(id = "____", name = "TBD")) }
var location by remember { mutableStateOf(Location(name = "LocationDefault"))}
var market by remember { mutableStateOf(Market(
location = Location(name = "MarketDefault"),
prices = mapOf()
))}
val ui = DopeWarsUi.new()
val travelResultLocation = Locations.random();
var api by remember { mutableStateOf(ApiData(
marketRequest = MarketRequest(
location = Location(name = "MarketRequestLocation")
),
marketResult = MarketResult(
market = Market(location = Location(name = "MarketResultLocation"),
prices = emptyMap()
)
),
playerNameUpdateRequest = PlayerNameUpdateRequest("newPlayerName"),
playerNameUpadteResponse = PlayerNameUpdateResponse(
updatedPlayer = Player(id = "____", name = "updatedPlayerName")
),
tradeAction = TradeAction.BUY,
tradeRequest = TradeRequest(
action = TradeAction.BUY,
drug = Drug.random(),
quantity = 10
),
travelRequest = TravelRequest(
destination = Locations.random()
),
travelResult = TravelResult(
location = travelResultLocation,
day = 1,
market = Market(location = travelResultLocation, mapOf())
)
)) }
MaterialTheme {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Button(onClick = {
currentScreen = Screens.HOME
scope.launch { drawerState.close() }
}) { Text("Home")}
Button( onClick = {
currentScreen = Screens.DRUG
scope.launch { drawerState.close() }
}) { Text("Drug") }
Button( onClick = {
currentScreen = Screens.PLAYER
scope.launch { drawerState.close() }
}) { Text("Player") }
Button(onClick = {
currentScreen = Screens.LOCATION
scope.launch { drawerState.close() }
}) { Text("Location") }
Button( onClick = {
currentScreen = Screens.API
scope.launch { drawerState.close() }
}) { Text("API") }
}
}
) {
// Main content
when (currentScreen) {
Screens.HOME -> {
Column {
ui.player.view(player)
// ui.drug.view(drug)
}
}
Screens.DRUG -> {
// ui.drug.form(drug) {
// drug = it
// }
}
Screens.PLAYER -> {
ui.player.form(player) {
player = it
}
}
Screens.LOCATION -> {
ui.location.form(location) {
location = it
}
}
Screens.MARKET -> {
ui.market.form(market) {
market = it
}
}
Screens.API -> {
Column {
ui.api.marketRequest.form(api.marketRequest) { api.marketRequest = it }
ui.api.marketResponse.form(api.marketResult) { api.marketResult = it }
ui.api.playerNameUpdateRequest.form(api.playerNameUpdateRequest) { api.playerNameUpdateRequest = it }
ui.api.playerNameUpdateResponse.form(api.playerNameUpadteResponse) { api.playerNameUpadteResponse = it }
ui.api.travelRequest.form(api.travelRequest) { api.travelRequest = it }
ui.api.travelResponse.form(api.travelResult) { api.travelResult = it}
ui.api.tradeRequest.form(api.tradeRequest) { api.tradeRequest = it}
}
}
}
}
}
}
@@ -0,0 +1,139 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.composables.icons.materialicons.MaterialIcons
import com.composables.icons.materialicons.filled.Menu
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Player
import kotlinx.coroutines.launch
fun main() {
val state = ClientState(Player(id = "", name = "TestClient"))
// var drug = Drug("TestDrug", minPrice = 10.0f, maxPrice = 100.0f)
var player = Player(id = "default", name = "defaultName")
var location = Location(name = "DefaultLocation")
application {
var currentScreen = remember { Screens.DRUG }
val scope = rememberCoroutineScope()
// state.updateApiUrl("http://localhost:8080")
// val scaffoldState = rememberScaffoldState(drawerState = DrawerState(DrawerValue.Closed))
// var menuVisible by remember { mutableStateOf(false) }
Window(onCloseRequest = ::exitApplication, title = "DopeWars") {
/*
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
Button(onClick = {
currentScreen = Screens.DRUG
scope.launch {
scaffoldState.drawerState.close()
}
}) { Text("Drug")}
Button(onClick = {
currentScreen = Screens.PLAYER
scope.launch {
scaffoldState.drawerState.close()
}
}) { Text("Player") }
},
topBar = {
TopAppBar(
title = {
Text("Title")
},
navigationIcon = {
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(imageVector = MaterialIcons.Filled.Menu , contentDescription = "Menu")
}
})
}
) {
*/
AppTheme {
Text("Test123")
Column(modifier = Modifier.fillMaxSize()) {
/*
when (currentScreen) {
Screens.DRUG -> {
drugForm(drug) {
drug = it
}
drugView(drug)
}
Screens.PLAYER -> {
playerView(player)
}
Screens.LOCATION -> {
locationView(location)
locationForm(location) {
location = it
}
}
Screens.INVENTORY -> {
inventoryView(emptyList())
}
Screens.MARKET -> {
marketView(Market(Location("Unknown Location"), emptyMap()))
}
}
*/
}
}
}
/*
println("Session ID = ${state.player.id}")
Column {
Text("DopeWars Desktop Client")
configPanel(config = state, onUrlChanged = {
state.updateApiUrl(it)
})
PlayerForm(
initialState = state.player,
onRequestLogin = {
scope.launch {
state.login()
}
},
onRequestPlayerRename = { newName ->
scope.launch {
state.renamePlayer(newName)
}
},
onGetMarket = {
scope.launch {
state.updateMarket()
}
},
onRefresh = {
scope.launch {
state.refreshPlayer()
}
}
)
}
*/
}
}
@@ -0,0 +1,124 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.composables.icons.materialicons.MaterialIcons
import com.composables.icons.materialicons.filled.Login
import dev.geekback.dopewars.ui.PlayerUi
import dev.geekback.dopewars.ui.TravelUi
import kotlinx.coroutines.launch
fun main() {
application {
Window(
onCloseRequest = ::exitApplication,
title = "DopeWars"
) {
App()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun App() {
var state by remember { mutableStateOf(ClientState.new() ) }
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
AppTheme {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
if (state.isLoggedIn()) {
// Display the current player state
PlayerUi().view(state.player)
} else {
NavigationDrawerItem(
label = {
Row {
Icon(
modifier = Modifier.padding(16.dp),
imageVector = MaterialIcons.Filled.Login,
contentDescription = "Login Icon"
)
Text(
modifier = Modifier.padding(16.dp),
text = "Login"
)
}
},
selected = false,
onClick = {
scope.launch {
state.player = state.login()
}
}
)
}
}
}
) {
if (state.isLoggedIn()) {
mainWindow(state)
} else {
Column(modifier = Modifier.padding(16.dp)) {
Text("<-- Start a new game")
}
}
}
}
}
@Composable
fun mainWindow(state: ClientState) {
val scope = rememberCoroutineScope();
var showingTravelWizard by remember { mutableStateOf(false) }
PlayerUi().view(state.player)
Row {
Button(onClick = {
println("Transactions")
}) {
Text("Transactions")
}
Button(onClick = {
showingTravelWizard = true
}) {
Text("Travel")
}
if (showingTravelWizard) {
TravelUi().wizard(state.player) {
println("Player wants to go to $it")
showingTravelWizard = false
scope.launch {
state.travel(state.player, it)
}
}
}
}
}
@@ -0,0 +1,10 @@
package dev.geekback.dopewars.desktop
enum class Screens {
HOME,
DRUG,
PLAYER,
LOCATION,
MARKET,
API
}
@@ -0,0 +1,37 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun configPanel(config: ClientState, onUrlChanged: (String) -> Unit) {
var apiUrl by remember { mutableStateOf(config.apiUrl) }
Card {
Column {
Text("Configuration")
Row {
Text("ApiURL = $apiUrl")
Text("Config.apiUrl = ${config.apiUrl}")
TextField(value = apiUrl, onValueChange = { apiUrl = it })
Button({
onUrlChanged(apiUrl)
}) {
Text("Update API Url")
}
}
}
}
}
@@ -0,0 +1,61 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
@Composable
fun MarketDisplay(market: Market?) {
if (market == null) {
Text("No Market Available")
} else {
Card {
Column {
Text("Market for ${market.location}", modifier = Modifier.padding(16.dp))
market.prices.forEach { (name, price) ->
Text("$name = $${price}")
}
}
}
}
}
@Composable
fun PlayerForm(
initialState: Player,
onRequestLogin: () -> Unit,
onRequestPlayerRename: (String) -> Unit,
onGetMarket: () -> Unit,
onRefresh: () -> Unit
) {
var username by remember { mutableStateOf(initialState.name) }
Column {
Text("This is for player $username")
Text("Session ID: [${initialState.id}]")
Card {
Column {
Button({ onRequestLogin() }) { Text("New Session") }
TextField(value = username, onValueChange = { username = it }, label = { Text("Username") })
Button(onClick = { onRequestPlayerRename(username) }) { Text("Update Player Name") }
Button(onClick = { onGetMarket() }) { Text("Get the Market") }
Button(onClick = { onRefresh() }) { Text("Refresh Player") }
MarketDisplay(initialState.lastMarket)
}
}
}
// form fields
}
@@ -0,0 +1,36 @@
package dev.geekback.dopewars.ui
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.shared.DrugInfo
import dev.geekback.dopewars.ui.api.ApiUi
interface DopeWarsModelUi<T> {
@Composable
fun view(model: T);
@Composable
fun form(model: T, onModelUpdate: (T) -> Unit)
}
data class DopeWarsUi(
val drug: DrugInfoUi,
val inventory: InventoryUi,
val inventoryItem: InventoryItemUi,
val location: LocationUi,
val market: MarketUi,
val player: PlayerUi,
val api: ApiUi
) {
companion object {
fun new() : DopeWarsUi {
return DopeWarsUi(
drug = DrugInfoUi(),
inventory = InventoryUi(),
inventoryItem = InventoryItemUi(),
location = LocationUi(),
market = MarketUi(),
player = PlayerUi(),
api = ApiUi.new()
)
}
}
}
@@ -0,0 +1,90 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.onClick
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import com.composables.icons.materialicons.MaterialIcons
import com.composables.icons.materialicons.filled.Menu
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Locations
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.TravelRequest
class TravelUi {
/*
Wizard
Holds the users hand through picking a new location
*/
@Composable
fun wizard(current: Player, onTicketPunched: (TravelRequest) -> Unit) {
var stage by remember { mutableStateOf(0) }
var pickedLocation by remember { mutableStateOf(0) }
val locations = Locations.all
var expanded by remember { mutableStateOf(false) }
Window(onCloseRequest = { onTicketPunched(TravelRequest(destination = current.location)) }, title = "Select New Location") {
Card(modifier = Modifier.padding(16.dp).fillMaxSize()) {
Column {
Text("Wizard")
var index = 0;
when (stage) {
0 -> {
Text(text = locations[pickedLocation].name)
IconButton(onClick = { expanded = !expanded }) {
Icon(MaterialIcons.Filled.Menu, contentDescription = "More options")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
locations.forEach {
DropdownMenuItem(
text = { Text("$it.name / $index")},
onClick = {
expanded = false
pickedLocation = (index - 1)
println("location index to $index")
}
)
index += 1;
}
}
Button(onClick = {
stage += 1
}) {
Text("Next Step")
}
}
1 -> {
Text("Stage 1")
Button(onClick = {
println("Selected Location - $pickedLocation")
onTicketPunched(TravelRequest(locations[pickedLocation]))
}) {
Text("Finish")
}
}
}
}
}
}
}
}
@@ -0,0 +1,26 @@
package dev.geekback.dopewars.ui.api
data class ApiUi(
val marketRequest: MarketRequestUi,
val marketResponse: MarketResponseUi,
val playerNameUpdateRequest: PlayerNameUpdateRequestUi,
val playerNameUpdateResponse: PlayerNameUpdateResponseUi,
val tradeRequest: TradeRequestUi,
// TODO: tradeResponse
val travelRequest: TravelRequestUi,
val travelResponse: TravelResponseUi,
) {
companion object {
fun new(): ApiUi {
return ApiUi(
marketRequest = MarketRequestUi(),
marketResponse = MarketResponseUi(),
playerNameUpdateRequest = PlayerNameUpdateRequestUi(),
playerNameUpdateResponse = PlayerNameUpdateResponseUi(),
tradeRequest = TradeRequestUi(),
travelRequest = TravelRequestUi(),
travelResponse = TravelResponseUi()
)
}
}
}
@@ -0,0 +1,47 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.MarketRequest
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.LocationUi
class MarketRequestUi: DopeWarsModelUi<MarketRequest> {
@Composable
override fun view(model: MarketRequest) {
BorderedCard("Market Request for ${model.location.name}") {
LocationUi().view(model.location)
}
}
@Composable
override fun form(
model: MarketRequest,
onModelUpdate: (MarketRequest) -> Unit
) {
var localModel by remember { mutableStateOf(model) }
BorderedCard("Market Request Form") {
Column {
LocationUi().form(model.location) {
localModel = MarketRequest(location = it)
}
Row {
Button(onClick = { onModelUpdate(localModel) }) {
Text("Apply")
}
Button(onClick = { localModel = model }) {
Text("Cancel")
}
}
}
}
}
}
@@ -0,0 +1,29 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.MarketResult
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.MarketUi
class MarketResponseUi: DopeWarsModelUi<MarketResult> {
@Composable
override fun view(model: MarketResult) {
BorderedCard("Market Result for ${model.market.location.name}") {
MarketUi().view(model.market)
}
}
@Composable
override fun form(
model: MarketResult,
onModelUpdate: (MarketResult) -> Unit
) {
BorderedCard("Market Result Form") {
MarketUi().form(model.market) {
onModelUpdate(model)
}
}
}
}
@@ -0,0 +1,25 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
class PlayerNameUpdateRequestUi(): DopeWarsModelUi<PlayerNameUpdateRequest> {
@Composable
override fun view(model: PlayerNameUpdateRequest) {
BorderedCard("Player Name Update Request for ${model.newName}") {
}
}
@Composable
override fun form(model: PlayerNameUpdateRequest, onModelUpdate: (PlayerNameUpdateRequest) -> Unit) {
Text("Player Name Update Request Form")
Button(onClick = { onModelUpdate(model) }) {
Text("Update with same data")
}
}
}
@@ -0,0 +1,26 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.PlayerUi
import dev.geekback.dopewars.shared.api.PlayerNameUpdateResponse
class PlayerNameUpdateResponseUi: DopeWarsModelUi<PlayerNameUpdateResponse> {
@Composable
override fun view(model: PlayerNameUpdateResponse) {
BorderedCard("Player Name Update Response") {
PlayerUi().view(model.updatedPlayer)
}
}
@Composable
override fun form(
model: PlayerNameUpdateResponse,
onModelUpdate: (PlayerNameUpdateResponse) -> Unit
) {
PlayerUi().form(model.updatedPlayer) {
onModelUpdate(model)
}
}
}
@@ -0,0 +1,50 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.ui.DopeWarsModelUi
class TradeRequestUi: DopeWarsModelUi<TradeRequest> {
@Composable
override fun view(model: TradeRequest) {
BorderedCard("Trade Request for ${model.drug.name} / ${model.action} / ${model.quantity}") {
}
}
@Composable
override fun form(
model: TradeRequest,
onModelUpdate: (TradeRequest) -> Unit
) {
var localModel by remember { mutableStateOf(model) }
var actionIndex by remember { mutableStateOf(0) }
var quantity by remember { mutableStateOf(model.quantity) }
var drug by remember { mutableStateOf(model.drug) }
BorderedCard("Trade Requesst Form") {
Row {
Button(onClick = {
onModelUpdate(localModel)
println("Trade Request Form Apply Clicked")
}) {
Text("Apply")
}
Button(onClick = {
localModel = model
}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,25 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.shared.api.TravelRequest
class TravelRequestUi: DopeWarsModelUi<TravelRequest> {
@Composable
override fun view(model: TravelRequest) {
BorderedCard("Travel Request to ${model.destination.name}") {
}
}
@Composable
override fun form(
model: TravelRequest,
onModelUpdate: (TravelRequest) -> Unit
) {
BorderedCard("Travel Request Form") {
Text("Location Picker goes here.")
}
}
}
@@ -0,0 +1,54 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.LocationUi
import dev.geekback.dopewars.ui.MarketUi
class TravelResponseUi: DopeWarsModelUi<TravelResult> {
@Composable
override fun view(model: TravelResult) {
BorderedCard("Travel Result ${model.location.name}") {
LocationUi().view(model.location)
Text("Day: ${model.day}")
MarketUi().view(model.market)
}
}
@Composable
override fun form(model: TravelResult, onModelUpdate: (TravelResult) -> Unit) {
var day by remember { mutableStateOf(model.day) }
var location by remember { mutableStateOf(model.location) }
var market by remember { mutableStateOf(model.market) }
var localModel by remember { mutableStateOf(model) }
BorderedCard("Travel Result Form") {
LocationUi().form(model.location) {
println("Update location to $it")
}
Row {
Button(onClick = {}) {
Text("Apply")
}
Button(onClick = {}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,94 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.DRUG_MAX_PRICE
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo
class DrugInfoUi: DopeWarsModelUi<DrugInfo> {
@Composable
override fun view(model: DrugInfo) {
BorderedCard(model.drug.name) {
Column {
Text("Price: ${model.minValue} - ${model.maxValue}")
}
}
}
@Composable
override fun form(
model: DrugInfo,
onModelUpdate: (DrugInfo) -> Unit
) {
var drug by remember { mutableStateOf(model) }
var name by remember { mutableStateOf(model.drug.name) }
var minPrice by remember { mutableFloatStateOf(model.minValue) }
var maxPrice by remember { mutableFloatStateOf(model.maxValue) }
BorderedCard("Drug Form") {
Column {
// Name
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
)
Row {
TextField(
value = minPrice.toString(),
onValueChange = { minPrice = it.toFloatOrNull() ?: minPrice },
label = { Text("Min Price") },
)
TextField(
value = maxPrice.toString(),
onValueChange = { maxPrice = it.toFloatOrNull() ?: maxPrice },
label = { Text("Max Price") },
)
}
RangeSlider(
value = (minPrice)..(maxPrice),
onValueChangeFinished = { },
valueRange = 1f..DRUG_MAX_PRICE,
onValueChange = { it ->
minPrice = it.start
maxPrice = it.endInclusive
}
)
Row {
Button(onClick = {
drug = DrugInfo(Drug.WEED , minPrice, maxPrice)
onModelUpdate(drug)
}) {
Text("Apply")
}
Button(onClick = {
name = Drug.WEED.name
minPrice = drug.minValue
maxPrice = drug.maxValue
}) {
Text("Cancel")
}
}
}
}
}
}
@@ -0,0 +1,29 @@
package dev.geekback.dopewars.ui
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.InventoryItem
class InventoryUi: DopeWarsModelUi<List<InventoryItem>> {
@Composable
override fun view(model: List<InventoryItem>) {
BorderedCard("Inventory Items") {
}
}
@Composable
override fun form(
model: List<InventoryItem>,
onModelUpdate: (List<InventoryItem>) -> Unit
) {
BorderedCard("Inventory Items Form") {
Button(onClick = { onModelUpdate(model) }) {
Text("Update")
}
}
}
}
@@ -0,0 +1,19 @@
package dev.geekback.dopewars.ui
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.shared.InventoryItem
class InventoryItemUi: DopeWarsModelUi<InventoryItem> {
@Composable
override fun view(model: InventoryItem) {
}
@Composable
override fun form(
model: InventoryItem,
onModelUpdate: (InventoryItem) -> Unit
) {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,55 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.Location
class LocationUi: DopeWarsModelUi<Location> {
@Composable
override fun view(model: Location) {
BorderedCard("Location - ${model.name}") {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun form(
model: Location,
onModelUpdate: (Location) -> Unit
) {
var name by remember { mutableStateOf(model.name) }
var location by remember { mutableStateOf(model)}
BorderedCard("Location") {
TextField(value = name, label = {
Text("Location Name")
}, onValueChange = {
name = it
})
Row {
Button(onClick = {
location.name = name;
onModelUpdate(location)
}) {
Text("Apply")
}
Button(onClick = {
name = location.name
}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,38 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.Market
class MarketUi: DopeWarsModelUi<Market> {
@Composable
override fun view(model: Market) {
val prices = model.prices.toList()
BorderedCard("Market for ${model.location.name}") {
Column {
LazyColumn {
items(prices.size) { index ->
val item = prices[index];
val name = item.first.name
val price = item.second.toFloat()
Text("Drug $name $${price}")
}
}
}
}
}
@Composable
override fun form(
model: Market,
onModelUpdate: (Market) -> Unit
) {
BorderedCard("Market for ${model.location.name}") {
Text("Market goes here.")
}
}
}
@@ -0,0 +1,47 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import kotlin.time.Instant
class PlayerUi: DopeWarsModelUi<Player> {
@Composable
override fun view(model: Player) {
BorderedCard(model.name) {
Column {
Text("ID: ${model.id}")
Text("Cash: $${model.cash}")
Text("Debt: $${model.debt}")
Text("Health: ${model.health}")
Text("Location: ${model.location.name}")
// TODO: this is an ugly memory hack
InventoryUi().view(model.inventory)
// marketView(model.lastMarket ?: Market(location = Location("Unknown"), prices = mapOf()))
val lastTime = Instant.fromEpochSeconds(model.lastActionTime)
Text("Last Action: $lastTime")
}
}
}
@Composable
override fun form(
model: Player,
onModelUpdate: (Player) -> Unit
) {
BorderedCard("Player ${model.name}") {
Button(onClick = { onModelUpdate(model) }) {
Text("Update")
}
}
}
}
@Composable
fun playerView(player: Player) {
}