package com.craigvg.lichun_android.network.dto import com.craigvg.lichun_android.domain.models.AnswerOption import com.craigvg.lichun_android.domain.models.ActiveLifeGoal import com.craigvg.lichun_android.domain.models.CompletedLifeGoal import com.craigvg.lichun_android.domain.models.JustCompletedLifeGoal import com.craigvg.lichun_android.domain.models.LifeSummary import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** * `@Serializable` DTOs for inbound server -> client WebSocket message payloads. * * These mirror the EventV2 envelope approach: instead of hand-rolled * `jsonObject["x"]?.jsonPrimitive?.intOrNull` extraction, message bodies are * decoded with `Json.decodeFromJsonElement(...)` against these classes. * * DESIGN RULES (to preserve EXACT pre-refactor behavior): * - Every field that the old hand-rolled parser tolerated being absent has a * default here, so decoding never throws on a missing field. Defaults match * the old `?: ` values exactly (`""`, `0`, `0.0`, `false`, `null`). * - Fields that map to enums with a try/catch-uppercase fallback in the old * code (achievement / quest categories, relationship event types) are kept * as raw `String` here and resolved by the manager with the SAME fallback, * because kotlinx.serialization would otherwise THROW on an unknown value. * - Decoding uses a lenient Json (ignoreUnknownKeys = true, isLenient = true, * coerceInputValues = true), so unmodeled server fields are ignored. */ // --------------------------------------------------------------------------- // messageEvent // --------------------------------------------------------------------------- @Serializable data class MessageEventDto( val id: String? = null, val message: String = "", val type: String = "messageEvent", val date: String? = null, val hour: Int = 0, val energyCost: Int? = null, val diamondCost: Int? = null, val moneyCost: Double? = null, val affinityChange: Int? = null, val title: String? = null, val image: String? = null, val category: String? = null, val status: String? = null ) // --------------------------------------------------------------------------- // questionEvent (answers reuse the existing AnswerOption model directly) // --------------------------------------------------------------------------- @Serializable data class QuestionEventDto( val id: String = "", val message: String = "", val image: String? = null, val answers: List = emptyList() ) // --------------------------------------------------------------------------- // conversationEvent // --------------------------------------------------------------------------- @Serializable data class ConversationMessageDto( val id: String = "", val message: String = "", val sentiment: String = "", val answerOptions: List? = null, val sender: String? = null, val datetime: String = "", val date: String = "", val time: String = "" ) @Serializable data class ConversationEventDto( val id: String? = null, val character: String? = null, val cType: String? = null, val conversation: List = emptyList(), val question: Int = 0 ) // --------------------------------------------------------------------------- // extraCurriculars // --------------------------------------------------------------------------- @Serializable data class ExtracurricularItemDto( val id: String = "", val title: String = "", val image: String = "", val type: String = "", val description: String = "" ) @Serializable data class ExtracurricularsDto( val extraCurriculars: List = emptyList() ) // --------------------------------------------------------------------------- // relationshipEvent (eventType resolved to enum with fallback by the manager) // --------------------------------------------------------------------------- @Serializable data class RelationshipEventChoiceDto( val id: String = "", val text: String = "", val affinityChange: Int = 0, val diamondCost: Int = 0, val moneyCost: Double = 0.0, val energyCost: Int = 0 ) @Serializable data class RelationshipEventDto( val id: String? = null, val eventType: String = "ARGUMENT", val title: String = "", val description: String = "", val partnerName: String = "", val partnerId: String = "", val choices: List = emptyList() ) // --------------------------------------------------------------------------- // error // --------------------------------------------------------------------------- @Serializable data class ErrorMessageDto( val message: String? = null, @SerialName("error_code") val errorCode: String? = null, val required: Int = 0, val available: Int = 0 ) // --------------------------------------------------------------------------- // accountDeletionScheduled / accountDeletionCancelled // --------------------------------------------------------------------------- @Serializable data class AccountDeletionScheduledResultDto( val success: Boolean = false, val scheduledAt: String? = null ) @Serializable data class AccountDeletionScheduledDto( val result: AccountDeletionScheduledResultDto? = null, val gracePeriodDays: Int? = null ) @Serializable data class AccountDeletionCancelledDto( val success: Boolean = false, val message: String? = null ) // --------------------------------------------------------------------------- // energyRefillTiers (tiers is a map keyed by tier type) // --------------------------------------------------------------------------- @Serializable data class EnergyRefillTierValueDto( val energy: Int = 0, val diamonds: Int = 0 ) @Serializable data class EnergyRefillTiersDto( val tiers: Map = emptyMap() ) // --------------------------------------------------------------------------- // timeSkipTiers // --------------------------------------------------------------------------- @Serializable data class TimeSkipTierValueDto( val durationSeconds: Double? = null, val diamonds: Int = 0 ) @Serializable data class TimeSkipTiersDto( val tiers: Map = emptyMap() ) // --------------------------------------------------------------------------- // purchaseComplete // --------------------------------------------------------------------------- @Serializable data class PurchaseBalanceDto( val diamonds: Int? = null, val energy: Int? = null, val money: Double? = null, val unlimitedUntil: String? = null ) @Serializable data class PurchaseCompleteDto( val itemId: String? = null, val success: Boolean? = null, val newBalance: PurchaseBalanceDto? = null, val diamonds: Int? = null, val energy: Int? = null, val money: Double? = null, val unlimitedEnergyUntil: String? = null ) // --------------------------------------------------------------------------- // timeSkipComplete // --------------------------------------------------------------------------- @Serializable data class TimeSkipEventDto( val type: String = "", val description: String = "", @SerialName("money_earned") val moneyEarned: Double? = null, @SerialName("smarts_gained") val smartsGained: Int? = null ) @Serializable data class TimeSkipStatChangesDto( val money: Double = 0.0, val energy: Int = 0, val health: Int = 0, val happiness: Int = 0 ) @Serializable data class TimeSkipSummaryDto( val diamonds: Int = 0, val newTime: String = "", val durationHours: Double = 0.0, val events: List = emptyList(), val statChanges: TimeSkipStatChangesDto? = null ) // --------------------------------------------------------------------------- // achievementsList / achievementUnlocked (category resolved with fallback) // --------------------------------------------------------------------------- @Serializable data class AchievementDto( val id: String = "", val name: String = "", val description: String = "", val category: String = "CAREER", val reward: Int = 0, val requirement: String = "", val unlocked: Boolean = false, val unlockedAt: Long? = null, val progress: Int? = null, val progressRequired: Int? = null, val acknowledged: Boolean = false ) @Serializable data class AchievementsListDto( val achievements: List = emptyList(), /** * Wave 2: per-category collection summary (locked + unlocked entries, hints, * completion %) rides alongside the flat achievements list. The domain model * [com.craigvg.lichun_android.domain.models.AchievementSummary] is itself * @Serializable with defaults, so it decodes directly. */ val summary: com.craigvg.lichun_android.domain.models.AchievementSummary? = null ) // --------------------------------------------------------------------------- // dailyRewardStatus // --------------------------------------------------------------------------- @Serializable data class DayRewardDto( val id: Int? = null, val day: Int? = null, val diamonds: Int = 0, val energy: Int? = null, val money: Double? = null, val bonusItem: String? = null, val claimed: Boolean = false ) @Serializable data class DailyRewardStatusDto( val currentStreak: Int = 0, val lastLoginDate: String = "", val nextResetDate: String = "", val canClaim: Boolean = false, val todaysClaimed: Boolean = false, val rewards: List = emptyList() ) // --------------------------------------------------------------------------- // dailyRewardClaimed // --------------------------------------------------------------------------- @Serializable data class RewardDeltaDto( val diamonds: Int? = null, val energy: Int? = null, val money: Double? = null ) @Serializable data class DailyRewardClaimedDto( val success: Boolean = false, val day: Int? = null, val diamonds: Int? = null, val energy: Int? = null, val reward: RewardDeltaDto? = null ) // --------------------------------------------------------------------------- // dailyQuestsStatus (category resolved with fallback) // --------------------------------------------------------------------------- @Serializable data class QuestRewardDto( val diamonds: Int = 0, val energy: Int? = null, val money: Double? = null ) @Serializable data class DailyQuestDto( val id: String = "", val name: String = "", val description: String = "", val category: String = "SOCIAL", val reward: QuestRewardDto? = null, val progress: Int = 0, val target: Int = 1, val completed: Boolean = false, val claimed: Boolean = false ) @Serializable data class DailyQuestsStatusDto( val lastResetDate: String = "", val nextResetDate: String = "", val quests: List = emptyList() ) // --------------------------------------------------------------------------- // questProgress // --------------------------------------------------------------------------- @Serializable data class QuestProgressInnerDto( val id: String? = null, val progress: Int? = null, val completed: Boolean = false, val progressRequired: Int? = null ) @Serializable data class QuestProgressDto( val quest: QuestProgressInnerDto? = null ) // --------------------------------------------------------------------------- // questRewardClaimed // --------------------------------------------------------------------------- @Serializable data class QuestRewardClaimedDto( val success: Boolean = false, val questId: String? = null, val diamonds: Int? = null, val energy: Int? = null, val money: Double? = null, val reward: RewardDeltaDto? = null ) // --------------------------------------------------------------------------- // dateIdeas // --------------------------------------------------------------------------- @Serializable data class DateIdeaDto( val name: String? = null, val energy_cost: Int = 0, val money_cost: Double = 0.0, val image: String = "" ) @Serializable data class DateIdeasDto( val dateIdeas: List = emptyList() ) // --------------------------------------------------------------------------- // playerUpdate // --------------------------------------------------------------------------- @Serializable data class PlayerUpdateDto( val diamonds: Int? = null, val energy: Int? = null, val unlimitedEnergyUntil: String? = null ) // --------------------------------------------------------------------------- // lightweight update ("u") // --------------------------------------------------------------------------- @Serializable data class LightweightUpdateDto( val date: String? = null, val minuteOfHour: Int? = null, val hourOfDay: Int? = null, val gameSpeed: Int? = null, val money: Double? = null, val diamonds: Int? = null, val location: String? = null, val calcEnergy: Int? = null, val status: String? = null, val intraDayMessage: String? = null, val mood: String? = null ) // --------------------------------------------------------------------------- // playerObject (scalar fields; collections decoded separately via decodeList) // --------------------------------------------------------------------------- @Serializable data class PlayerObjectScalarsDto( val date: String = "", val gameSpeed: Int = 0, val status: String = "", val season: String = "", val hourOfDay: Int = 0, val minuteOfHour: Int = 0 ) // --------------------------------------------------------------------------- // action confirmation / debug action complete / inAppPurchaseComplete // --------------------------------------------------------------------------- @Serializable data class ActionConfirmationDto( val success: Boolean = true, val message: String? = null ) @Serializable data class DebugActionCompleteDto( val success: Boolean = true, val message: String? = null, val type: String? = null ) @Serializable data class SuccessFlagDto( val success: Boolean = false ) // --------------------------------------------------------------------------- // tutorial messages // --------------------------------------------------------------------------- @Serializable data class TutorialMessageDto( val type: String? = null, val step: Int? = null, val message: String? = null ) // --------------------------------------------------------------------------- // Tier 2 retention spine (lifeSummaryEvent / offlineDigest / lifeGoalsUpdate) // // The domain models (LifeSummary, OfflineDigest, ActiveLifeGoal, ...) are // themselves @Serializable with a default on every field, so they decode // directly from the lenient JSON. Only the message envelopes need a wrapper. // --------------------------------------------------------------------------- /** `lifeSummaryEvent` envelope: { type, summary: LifeSummary }. */ @Serializable data class LifeSummaryEventDto( val summary: LifeSummary? = null ) /** `offlineDigest` envelope: the digest fields live on the message body itself. */ @Serializable data class OfflineDigestDto( val minutesAway: Int = 0, val moneyDelta: Double = 0.0, val ageYearsDelta: Int = 0, val notableEvents: List = emptyList(), val generatedAt: String = "" ) /** `lifeGoalsUpdate` envelope: the goal collections live on the message body. */ @Serializable data class LifeGoalsUpdateDto( val active: List = emptyList(), val completed: List = emptyList(), val lifeScore: Int = 0, val justCompleted: List = emptyList() )