feat(rust): named NPC markers, bigger bosses, and an event's progress lines (runicnpc stage 8) #54
@@ -88,6 +88,24 @@ data class EventOccurrenceDto(
|
||||
val scope: String? = null,
|
||||
val phase: String? = null,
|
||||
val resultsPublishedAt: String? = null,
|
||||
/**
|
||||
* On the live occurrence only: how its steps stand, in the words their modules
|
||||
* chose (MODULE_API 1.12.0; runicnpc D304). Core interprets none of it, and nor
|
||||
* does the app: each line is a label with a count or a percent.
|
||||
*/
|
||||
val progress: List<EventProgressDto> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* One progress line: a [label] with [left] of [of] ("Bandits: 3 of 8 left"), or a
|
||||
* [percent] ("The Juggernaut: 62%"). Never both.
|
||||
*/
|
||||
@Serializable
|
||||
data class EventProgressDto(
|
||||
val label: String = "",
|
||||
val left: Int? = null,
|
||||
val of: Int? = null,
|
||||
val percent: Int? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -122,7 +122,10 @@ data class RustMapLiveDto(
|
||||
val mates: List<RustMapPlayerDto>? = null,
|
||||
)
|
||||
|
||||
/** A world event: `cargo`, `heli`, `chinook`, `bradley`, `supply` or `crate`. */
|
||||
/**
|
||||
* A world event: `cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`, or a RunicNPC
|
||||
* `boss` placed outside any event (runicnpc D302), with its [name].
|
||||
*/
|
||||
@Serializable
|
||||
data class RustMapWorldDto(
|
||||
val kind: String = "",
|
||||
@@ -131,6 +134,7 @@ data class RustMapWorldDto(
|
||||
/** A locked crate being hacked: seconds left. */
|
||||
val hackLeftSec: Int? = null,
|
||||
val hacked: Boolean = false,
|
||||
val name: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -146,8 +150,13 @@ data class RustMapEventDto(
|
||||
val x: Double = 0.0,
|
||||
val z: Double = 0.0,
|
||||
val radius: Double? = null,
|
||||
/** A zone's name, or a RunicNPC NPC's (runicnpc stage 8). */
|
||||
val name: String? = null,
|
||||
val prefab: String? = null,
|
||||
/** A RunicNPC boss: drawn bigger (D303). Its health is not sent. */
|
||||
val boss: Boolean = false,
|
||||
/** One of a boss's adds, placed by the boss rather than by a step. */
|
||||
val add: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.runicgateway.app.R
|
||||
import com.runicgateway.app.data.api.dto.EventOccurrenceDto
|
||||
import com.runicgateway.app.data.api.dto.EventParticipantDto
|
||||
import com.runicgateway.app.data.api.dto.EventProgressDto
|
||||
import com.runicgateway.app.data.api.dto.PublicEventDto
|
||||
import com.runicgateway.app.ui.PollWhileResumed
|
||||
import com.runicgateway.app.ui.UiState
|
||||
import com.runicgateway.app.ui.components.ErrorView
|
||||
import com.runicgateway.app.ui.components.HtmlText
|
||||
@@ -62,9 +64,29 @@ fun EventScreen(
|
||||
when (val s = state) {
|
||||
is UiState.Error -> ErrorView(s.kind, onRetry = viewModel::load, modifier = modifier)
|
||||
is UiState.Loading -> LoadingView(modifier)
|
||||
is UiState.Success -> EventBody(s.data, onOpenSeries, onOpenRun, modifier)
|
||||
is UiState.Success -> {
|
||||
// A live event's progress lines move (MODULE_API 1.12.0, runicnpc D304),
|
||||
// so the page is re-read while it is live and on screen.
|
||||
if (s.data.live) PollWhileResumed(intervalMs = PROGRESS_REFRESH_MS) { viewModel.refresh() }
|
||||
EventBody(s.data, onOpenSeries, onOpenRun, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** How often a live event's page is re-read, as the web page does. */
|
||||
private const val PROGRESS_REFRESH_MS = 15_000L
|
||||
|
||||
/**
|
||||
* One progress line as text: "Bandits: 3 of 8 left", or "The Juggernaut: 62%".
|
||||
* Anything else renders nothing. Internal for the test.
|
||||
*/
|
||||
internal fun progressText(line: EventProgressDto, count: (String, Int, Int) -> String, percent: (String, Int) -> String): String? {
|
||||
if (line.label.isBlank()) return null
|
||||
val left = line.left
|
||||
val of = line.of
|
||||
if (left != null && of != null) return count(line.label, left, of)
|
||||
return line.percent?.let { percent(line.label, it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EventBody(
|
||||
@@ -196,6 +218,22 @@ private fun Headline(event: PublicEventDto) {
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
// How it stands, in the words the steps' modules chose.
|
||||
val countWords = stringResource(R.string.events_progress_count)
|
||||
val percentWords = stringResource(R.string.events_progress_percent)
|
||||
current.progress.forEach { line ->
|
||||
progressText(
|
||||
line,
|
||||
count = { label, left, of -> countWords.format(label, left, of) },
|
||||
percent = { label, p -> percentWords.format(label, p) },
|
||||
)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
event.next != null -> {
|
||||
Text(
|
||||
|
||||
@@ -56,4 +56,16 @@ class EventViewModel @Inject constructor(
|
||||
_state.value = repository.event(slug, run).toUiState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read while the event is live, for its progress lines (MODULE_API 1.12.0,
|
||||
* runicnpc D304). Quiet: the page stays on screen, and a failed re-read keeps
|
||||
* what is shown rather than replacing the page with an error.
|
||||
*/
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
val fresh = repository.event(slug, run).toUiState()
|
||||
if (fresh is UiState.Success) _state.value = fresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,18 @@ class EventRunResolver(
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private var slugs: Map<String, String> = emptyMap()
|
||||
private var titles: Map<String, String> = emptyMap()
|
||||
private var readAt: Long? = null
|
||||
|
||||
/**
|
||||
* The event title for each of [runIds] the last read found (runicnpc D303: an NPC's
|
||||
* card names its event). Never reads by itself: [resolve] decides when the calendar
|
||||
* is asked, and this answers from what it found.
|
||||
*/
|
||||
suspend fun titles(runIds: Set<String>): Map<String, String> = mutex.withLock {
|
||||
runIds.mapNotNull { id -> titles[id]?.let { id to it } }.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* The slug for each of [runIds] the public calendar lists. Ids it does not
|
||||
* list are absent from the answer.
|
||||
@@ -57,9 +67,9 @@ class EventRunResolver(
|
||||
if (result is ApiResult.Ok) {
|
||||
// Core sends the id as a number and the plugin as a string, so they
|
||||
// meet as text.
|
||||
slugs = result.data.entries
|
||||
.filter { it.kind == "run" && it.runId != null && it.slug.isNotBlank() }
|
||||
.associate { it.runId.toString() to it.slug }
|
||||
val runs = result.data.entries.filter { it.kind == "run" && it.runId != null && it.slug.isNotBlank() }
|
||||
slugs = runs.associate { it.runId.toString() to it.slug }
|
||||
titles = runs.filter { it.title.isNotBlank() }.associate { it.runId.toString() to it.title }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ data class MapMarker(
|
||||
val online: Boolean = false,
|
||||
val sleeping: Boolean = false,
|
||||
val self: Boolean = false,
|
||||
/** A RunicNPC boss, in an event or the world layer: drawn bigger (runicnpc D303). */
|
||||
val boss: Boolean = false,
|
||||
/** One of a boss's adds. */
|
||||
val add: Boolean = false,
|
||||
) {
|
||||
/** A zone is ground, not a point: it is drawn and tapped as an area. */
|
||||
val isZone: Boolean get() = layer == MapLayer.EVENTS && kind == "zone"
|
||||
@@ -60,7 +64,18 @@ fun mapMarkers(map: RustMapDto, live: RustMapLiveDto?, shown: Set<MapLayer>): Li
|
||||
add(MapMarker(MapLayer.WORLD, "monument", it.x, it.z, name = it.label, grid = it.grid))
|
||||
}
|
||||
live?.world.orEmpty().forEach {
|
||||
add(MapMarker(MapLayer.WORLD, it.kind, it.x, it.z, hackLeftSec = it.hackLeftSec, hacked = it.hacked))
|
||||
add(
|
||||
MapMarker(
|
||||
MapLayer.WORLD,
|
||||
it.kind,
|
||||
it.x,
|
||||
it.z,
|
||||
name = it.name,
|
||||
hackLeftSec = it.hackLeftSec,
|
||||
hacked = it.hacked,
|
||||
boss = it.kind == "boss",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (MapLayer.EVENTS in shown) {
|
||||
@@ -74,6 +89,8 @@ fun mapMarkers(map: RustMapDto, live: RustMapLiveDto?, shown: Set<MapLayer>): Li
|
||||
name = it.name,
|
||||
runId = it.runId?.takeIf { id -> id.isNotBlank() },
|
||||
radiusMetres = it.radius,
|
||||
boss = it.boss,
|
||||
add = it.add,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ private fun MapContent(
|
||||
marker = marker,
|
||||
grid = marker.grid ?: frame.gridLabel(marker.x, marker.z),
|
||||
eventSlug = marker.runId?.let { ui.eventSlugs[it] },
|
||||
eventTitle = marker.runId?.let { ui.eventTitles[it] },
|
||||
onOpenEvent = onOpenEvent,
|
||||
onClose = { onSelect(null) },
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(12.dp),
|
||||
@@ -404,9 +405,12 @@ private fun markerStyle(m: MapMarker): MarkerStyle = when (m.layer) {
|
||||
MapLayer.WORLD -> when (m.kind) {
|
||||
"monument" -> MarkerStyle(MONUMENT, 4)
|
||||
"cargo" -> MarkerStyle(WORLD_COLOURS.getValue("cargo"), 7)
|
||||
// A RunicNPC boss outside any event (runicnpc D302): as big as a ship.
|
||||
"boss" -> MarkerStyle(BOSS, 7, 2)
|
||||
else -> MarkerStyle(WORLD_COLOURS[m.kind] ?: WORLD_COLOURS.getValue("crate"), 5)
|
||||
}
|
||||
MapLayer.EVENTS -> MarkerStyle(EVENT, 5)
|
||||
// An event's boss is bigger (runicnpc D303).
|
||||
MapLayer.EVENTS -> if (m.boss) MarkerStyle(BOSS, 8, 2) else MarkerStyle(EVENT, 5)
|
||||
MapLayer.PLAYERS -> if (m.online) MarkerStyle(ONLINE, 5) else MarkerStyle(SLEEPING, 4)
|
||||
MapLayer.BASES -> MarkerStyle(if (m.kind == "vending") VENDING else TC, 4)
|
||||
MapLayer.MATES -> if (m.self) MarkerStyle(SELF, 8, 2) else MarkerStyle(MATE, 6, 2)
|
||||
@@ -422,6 +426,7 @@ private fun MarkerCard(
|
||||
marker: MapMarker,
|
||||
grid: String?,
|
||||
eventSlug: String?,
|
||||
eventTitle: String?,
|
||||
onOpenEvent: (String, String) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -429,7 +434,7 @@ private fun MarkerCard(
|
||||
ShardCard(modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(start = 16.dp, end = 8.dp, top = 12.dp, bottom = 4.dp)) {
|
||||
Text(markerTitle(marker), style = MaterialTheme.typography.titleSmall)
|
||||
markerDetail(marker)?.let {
|
||||
markerDetail(marker, eventTitle)?.let {
|
||||
Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
grid?.let {
|
||||
@@ -465,11 +470,21 @@ private fun markerTitle(m: MapMarker): String = when (m.layer) {
|
||||
"bradley" -> stringResource(R.string.rust_map_bradley)
|
||||
"supply" -> stringResource(R.string.rust_map_supply)
|
||||
"crate" -> stringResource(R.string.rust_map_crate)
|
||||
"boss" -> stringResource(R.string.rust_map_boss, m.name?.takeIf { it.isNotBlank() } ?: stringResource(R.string.rust_map_boss_unnamed))
|
||||
else -> prefabName(m.kind)
|
||||
}
|
||||
MapLayer.EVENTS -> when (m.kind) {
|
||||
"zone" -> m.name?.takeIf { it.isNotBlank() } ?: stringResource(R.string.rust_map_event_zone)
|
||||
"npc" -> stringResource(R.string.rust_map_event_npc)
|
||||
// A RunicNPC NPC says its name, and whether it is a boss or a boss's add (D303).
|
||||
"npc" -> {
|
||||
val name = m.name?.takeIf { it.isNotBlank() }
|
||||
when {
|
||||
name == null -> stringResource(R.string.rust_map_event_npc)
|
||||
m.boss -> stringResource(R.string.rust_map_boss, name)
|
||||
m.add -> stringResource(R.string.rust_map_boss_add, name)
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
else -> stringResource(R.string.rust_map_event_crate)
|
||||
}
|
||||
MapLayer.PLAYERS -> m.name ?: ""
|
||||
@@ -479,7 +494,7 @@ private fun markerTitle(m: MapMarker): String = when (m.layer) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun markerDetail(m: MapMarker): String? = when (m.layer) {
|
||||
private fun markerDetail(m: MapMarker, eventTitle: String? = null): String? = when (m.layer) {
|
||||
MapLayer.WORLD -> when {
|
||||
m.kind != "crate" -> null
|
||||
m.hacked -> stringResource(R.string.rust_map_crate_hacked)
|
||||
@@ -487,8 +502,10 @@ private fun markerDetail(m: MapMarker): String? = when (m.layer) {
|
||||
else -> null
|
||||
}
|
||||
// Every site-event marker says so, linked or not (§31.2): a run the public
|
||||
// calendar does not list gets this line and no button.
|
||||
MapLayer.EVENTS -> stringResource(R.string.rust_map_site_event)
|
||||
// calendar does not list gets this line and no button. One it lists names its
|
||||
// event (runicnpc D303).
|
||||
MapLayer.EVENTS -> eventTitle?.let { stringResource(R.string.rust_map_site_event_named, it) }
|
||||
?: stringResource(R.string.rust_map_site_event)
|
||||
MapLayer.PLAYERS, MapLayer.MATES -> when {
|
||||
!m.online -> stringResource(R.string.rust_map_player_offline)
|
||||
m.sleeping -> stringResource(R.string.rust_map_player_sleeping)
|
||||
@@ -649,6 +666,7 @@ private val GRID_LINE = Color.White.copy(alpha = 0.18f)
|
||||
private val GRID_LABEL = Color.White.copy(alpha = 0.55f)
|
||||
private val MONUMENT = Color(0xFFE8D9A8)
|
||||
private val EVENT = Color(0xFFCE93D8)
|
||||
private val BOSS = Color(0xFFD500F9)
|
||||
private val ONLINE = Color(0xFFFFFFFF)
|
||||
private val SLEEPING = Color(0xFF9E9E9E)
|
||||
private val TC = Color(0xFFFF7043)
|
||||
|
||||
@@ -40,6 +40,8 @@ data class RustMapUi(
|
||||
val selected: MapMarker? = null,
|
||||
/** Run id → event slug, for the runs core's public calendar lists. */
|
||||
val eventSlugs: Map<String, String> = emptyMap(),
|
||||
/** Run id → event title, from the same read: an NPC's card names its event (runicnpc D303). */
|
||||
val eventTitles: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -176,8 +178,9 @@ class RustMapViewModel @Inject constructor(
|
||||
val ids = eventRunIds(answer)
|
||||
if (ids.isEmpty()) return
|
||||
val slugs = resolver.resolve(ids)
|
||||
val titles = resolver.titles(ids)
|
||||
if (asked != generation) return
|
||||
_state.update { it.copy(eventSlugs = slugs) }
|
||||
_state.update { it.copy(eventSlugs = slugs, eventTitles = titles) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -550,6 +550,8 @@
|
||||
<string name="events_part_of">Part of %1$s</string>
|
||||
<string name="events_next">Next</string>
|
||||
<string name="events_under_way">Under way</string>
|
||||
<string name="events_progress_count">%1$s: %2$d of %3$d left</string>
|
||||
<string name="events_progress_percent">%1$s: %2$d%%</string>
|
||||
<string name="events_nothing_scheduled">Nothing scheduled at the moment.</string>
|
||||
<string name="events_never_scheduled">This event has not been scheduled yet.</string>
|
||||
<string name="events_coming_up">Coming up</string>
|
||||
@@ -628,6 +630,10 @@
|
||||
<string name="rust_map_event_crate">Event crate</string>
|
||||
<string name="rust_map_event_npc">Event NPC</string>
|
||||
<string name="rust_map_site_event">Site event</string>
|
||||
<string name="rust_map_site_event_named">Site event · %1$s</string>
|
||||
<string name="rust_map_boss">%1$s (boss)</string>
|
||||
<string name="rust_map_boss_unnamed">Boss</string>
|
||||
<string name="rust_map_boss_add">%1$s (a boss\'s add)</string>
|
||||
<string name="rust_map_open_event">Open event</string>
|
||||
<string name="rust_map_tc">Tool cupboard</string>
|
||||
<string name="rust_map_vending">Vending machine</string>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.runicgateway.app.ui.events
|
||||
|
||||
import com.runicgateway.app.data.api.dto.EventProgressDto
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* A live event's progress lines (MODULE_API 1.12.0, runicnpc D304): a count or a
|
||||
* percent with a label, and nothing else renders.
|
||||
*/
|
||||
class EventProgressTest {
|
||||
|
||||
private fun text(line: EventProgressDto) = progressText(
|
||||
line,
|
||||
count = { label, left, of -> "$label: $left of $of left" },
|
||||
percent = { label, p -> "$label: $p%" },
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `a count and a percent read as the web reads them`() {
|
||||
assertEquals("Bandits: 3 of 8 left", text(EventProgressDto("Bandits", left = 3, of = 8)))
|
||||
assertEquals("The Juggernaut: 62%", text(EventProgressDto("The Juggernaut", percent = 62)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no label, or neither shape, renders nothing`() {
|
||||
assertNull(text(EventProgressDto("", left = 1, of = 2)))
|
||||
assertNull(text(EventProgressDto("Odd")))
|
||||
assertNull(text(EventProgressDto("Half", left = 1)))
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,22 @@ class EventRunResolverTest {
|
||||
assertEquals(2, api.calendarCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the same read names each run's event, and titles never asks the calendar itself`() = runTest {
|
||||
api.calendar = EventCalendarDto(
|
||||
listOf(
|
||||
EventCalendarEntryDto(kind = "run", runId = 41, slug = "harbour-raid", title = "Harbour Raid"),
|
||||
EventCalendarEntryDto(kind = "run", runId = 42, slug = "untitled", title = ""),
|
||||
),
|
||||
)
|
||||
assertEquals(emptyMap<String, String>(), resolver.titles(setOf("41")))
|
||||
assertEquals(0, api.calendarCalls)
|
||||
|
||||
resolver.resolve(setOf("41", "42"))
|
||||
assertEquals(mapOf("41" to "Harbour Raid"), resolver.titles(setOf("41", "42", "99")))
|
||||
assertEquals(1, api.calendarCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a failed read keeps what the last one found, and still counts as a read`() = runTest {
|
||||
api.calendar = EventCalendarDto(listOf(run(41, "harbor-brawl")))
|
||||
|
||||
@@ -138,4 +138,25 @@ class MapMarkersTest {
|
||||
assertEquals(setOf("41", "42"), eventRunIds(live))
|
||||
assertEquals(emptySet<String>(), eventRunIds(RustMapLiveDto()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `runicnpc D302 and D303 - an NPC's name, a boss and an add, and a boss outside any event`() {
|
||||
val live = RustMapLiveDto(
|
||||
live = true,
|
||||
world = listOf(RustMapWorldDto(kind = "boss", x = 5.0, z = 5.0, name = "The Warden")),
|
||||
events = listOf(
|
||||
RustMapEventDto(kind = "npc", runId = "41", name = "The Juggernaut", boss = true),
|
||||
RustMapEventDto(kind = "npc", runId = "41", name = "Bandit", add = true),
|
||||
RustMapEventDto(kind = "npc", runId = "41", name = "Bandit"),
|
||||
),
|
||||
)
|
||||
val markers = mapMarkers(RustMapDto(geometry = geometry), live, all)
|
||||
val world = markers.single { it.layer == MapLayer.WORLD }
|
||||
assertEquals("The Warden", world.name)
|
||||
assertTrue(world.boss)
|
||||
val npcs = markers.filter { it.layer == MapLayer.EVENTS }
|
||||
assertEquals(listOf(true, false, false), npcs.map { it.boss })
|
||||
assertEquals(listOf(false, true, false), npcs.map { it.add })
|
||||
assertEquals(listOf("The Juggernaut", "Bandit", "Bandit"), npcs.map { it.name })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package com.runicgateway.app.ui.rust
|
||||
|
||||
import com.runicgateway.app.data.api.dto.EventCalendarDto
|
||||
import com.runicgateway.app.data.api.dto.EventOccurrenceDto
|
||||
import com.runicgateway.app.data.api.dto.RustMapDto
|
||||
import com.runicgateway.app.data.api.dto.RustMapLiveDto
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -103,4 +104,33 @@ class RustMapDtoTest {
|
||||
)
|
||||
assertEquals(listOf(41L, null, null), calendar.entries.map { it.runId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `runicnpc stage 8 - NPC rows and a world boss decode, and an older row without them still does`() {
|
||||
val live = json.decodeFromString<RustMapLiveDto>(
|
||||
"""{"live":true,
|
||||
"world":[{"kind":"boss","x":1,"z":2,"name":"The Warden"},{"kind":"cargo","x":3,"z":4}],
|
||||
"events":[{"kind":"npc","runId":"41","x":0,"z":0,"prefab":"profile:jugg","key":"k1","name":"The Juggernaut","boss":true},
|
||||
{"kind":"npc","runId":"41","x":0,"z":0,"name":"Bandit","add":true},
|
||||
{"kind":"zone","runId":"41","x":0,"z":0,"radius":50}]}""",
|
||||
)
|
||||
assertEquals("The Warden", live.world!![0].name)
|
||||
assertNull(live.world!![1].name)
|
||||
val events = live.events!!
|
||||
assertTrue(events[0].boss)
|
||||
assertFalse(events[0].add)
|
||||
assertTrue(events[1].add)
|
||||
assertFalse(events[2].boss)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an occurrence's progress decodes, and an older answer without it is an empty list`() {
|
||||
val withLines = json.decodeFromString<EventOccurrenceDto>(
|
||||
"""{"runId":41,"live":true,"progress":[{"label":"Bandits","left":3,"of":8},{"label":"The Juggernaut","percent":62}]}""",
|
||||
)
|
||||
assertEquals(listOf("Bandits", "The Juggernaut"), withLines.progress.map { it.label })
|
||||
assertEquals(3, withLines.progress[0].left)
|
||||
assertEquals(62, withLines.progress[1].percent)
|
||||
assertEquals(emptyList<Any>(), json.decodeFromString<EventOccurrenceDto>("""{"runId":41}""").progress)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user