From: TheSaminator Date: Tue, 12 Jul 2022 15:51:30 +0000 (-0400) Subject: Preparatory changes X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=cc230169c7c805e59f26c885b689b99f3f157647;p=starship-fights Preparatory changes --- diff --git a/plan/background/galaxy-bg-dark.jpg b/plan/background/galaxy-bg-dark.jpg index a00b3bd..d3867ab 100644 Binary files a/plan/background/galaxy-bg-dark.jpg and b/plan/background/galaxy-bg-dark.jpg differ diff --git a/plan/background/galaxy-bg-dark.pdn b/plan/background/galaxy-bg-dark.pdn index 3ab7890..ffe8d74 100644 --- a/plan/background/galaxy-bg-dark.pdn +++ b/plan/background/galaxy-bg-dark.pdn @@ -1,8 +1,8 @@ -PDN3¸ÿÿÿÿ PPaintDotNet.Data, Version=4.310.8103.32785, Culture=neutral, PublicKeyToken=nullPaintDotNet.Document -isDisposedlayerswidthheight savedWithuserMetadataItemsPaintDotNet.LayerListSystem.VersionæSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][]   PaintDotNet.LayerListparentArrayList+_itemsArrayList+_sizeArrayList+_versionPaintDotNet.Document   System.Version_Major_Minor_Build _Revision6§€äSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]øÿÿÿäSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]keyvalue $exif.tag0[0] -Dõÿÿÿøÿÿÿ $exif.tag1[0] /òÿÿÿøÿÿÿ $exif.tag2[0]7ïÿÿÿøÿÿÿ $exif.tag3[0]7       PPaintDotNet.Core, Version=4.310.8103.32785, Culture=neutral, PublicKeyToken=nullPaintDotNet.BitmapLayer +PDN3pÿÿÿÿ PPaintDotNet.Data, Version=4.311.8179.42221, Culture=neutral, PublicKeyToken=nullPaintDotNet.Document +isDisposedlayerswidthheight savedWithuserMetadataItemsPaintDotNet.LayerListSystem.VersionæSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][]   PaintDotNet.LayerListparentArrayList+_itemsArrayList+_sizeArrayList+_versionPaintDotNet.Document   System.Version_Major_Minor_Build _Revision7óí¤äSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]øÿÿÿäSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]keyvalue $exif.tag0[0] +Dõÿÿÿøÿÿÿ $exif.tag1[0] /òÿÿÿøÿÿÿ $exif.tag2[0]7ïÿÿÿøÿÿÿ $exif.tag3[0]7       PPaintDotNet.Core, Version=4.311.8179.42221, Culture=neutral, PublicKeyToken=nullPaintDotNet.BitmapLayer propertiessurfaceLayer+isDisposed Layer+width Layer+heightLayer+properties-PaintDotNet.BitmapLayer+BitmapLayerPropertiesPaintDotNet.Surface!PaintDotNet.Layer+LayerProperties       ! " # $ % & ' (-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp&PaintDotNet.UserBlendOps+NormalBlendOp )PaintDotNet.Surfacewidthheightstridescan0PaintDotNet.MemoryBlock @ *!PaintDotNet.Layer+LayerPropertiesnameuserMetadataItemsvisible isBackgroundopacity blendModeæSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][]PaintDotNet.LayerBlendMode+ -Background ,ÿÓÿÿÿPaintDotNet.LayerBlendModevalue__-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp)PaintDotNet.UserBlendOps+ColorBurnBlendOp . @ /0Layer 2 ,ÿÎÿÿÿÓÿÿÿ -PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp*PaintDotNet.UserBlendOps+ColorDodgeBlendOp 3! @ 4"5Layer 3 ,ÿÉÿÿÿÓÿÿÿ#-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp(PaintDotNet.UserBlendOps+AdditiveBlendOp 8$ @ 9%:Layer 4 ,ÿÄÿÿÿÓÿÿÿ&-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp'PaintDotNet.UserBlendOps+OverlayBlendOp =' @ >(?Layer 5 ,ÿ¿ÿÿÿÓÿÿÿ)&PaintDotNet.UserBlendOps+NormalBlendOp*PaintDotNet.MemoryBlocklength64 hasParentdeferred @,äSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]].)PaintDotNet.UserBlendOps+ColorBurnBlendOp/*@3*PaintDotNet.UserBlendOps+ColorDodgeBlendOp4*@8(PaintDotNet.UserBlendOps+AdditiveBlendOp9*@='PaintDotNet.UserBlendOps+OverlayBlendOp>*@ ‹ +Background ,ÿÓÿÿÿPaintDotNet.LayerBlendModevalue__-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp)PaintDotNet.UserBlendOps+ColorBurnBlendOp . @ /0Layer 2 ,ÿÎÿÿÿÓÿÿÿ -PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp*PaintDotNet.UserBlendOps+ColorDodgeBlendOp 3! @ 4"5Layer 3 ,ÿÉÿÿÿÓÿÿÿ#-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp&PaintDotNet.UserBlendOps+ScreenBlendOp 8$ @ 9%:Layer 4 ,ÿÄÿÿÿÓÿÿÿ &-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp'PaintDotNet.UserBlendOps+OverlayBlendOp =' @ >(?Layer 5 ,ÿ¿ÿÿÿÓÿÿÿ)&PaintDotNet.UserBlendOps+NormalBlendOp*PaintDotNet.MemoryBlocklength64 hasParentdeferred @,äSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]].)PaintDotNet.UserBlendOps+ColorBurnBlendOp/*@3*PaintDotNet.UserBlendOps+ColorDodgeBlendOp4*@8&PaintDotNet.UserBlendOps+ScreenBlendOp9*@='PaintDotNet.UserBlendOps+OverlayBlendOp>*@ ‹ íÒA „0ü›¾%ÓÑH«%`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€yà 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0Àyà 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0Àyà 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0Àyà 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0À 0ÀóÀ`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€ú(Pò!¢‹ íÝÙnã¸ÐÌLÿÿ/÷…/`@ ¸T‘Ôbç<ÔCÇ—â!%«Åäççç`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`€`à¯È 0À 0À 0À 0À |‚þù'«çCDsúï¿ÿþ?jç?ÿ¤x÷嘏×Ïÿûï¿¿þüÙ¯òʼëÚYß«¬rœZãsìsk|?alËñËøŸ©o”—2÷¥­ãÕÚ3:¦5‡mx—ñ.§Õ®Ú8Ï-c§‘Ѹ´Æ©ÖžÚ9£u=ӞU7g–=g´̎åŽ9•©«7®åÏgú°k\w­73õg™5ZŽGÍÐû˜]ciÏ(×Ù¼EÍd,Dú2ëq”“ëAÖ{ÄÈÙëÇo‹Z^[×ókµ²Ï¸—-çLíßǾϛicyßT»¶dçú®ï™w›y8k×òXÖ×:¦µÞDÇeרñ<ã·?ÃrÀO4Y×w<ËþÔØqý›-[È 0À 0À 0À 0À 0À 0À 0À 0À 0ÀÏ0pÖ»ßü®eïóÚ¾‰Zܽ÷$²w$Òöì~üã>¡™úËãjû¶wô·÷yæ¼'ÆÛn¶/3Ž#ÇÖæRd ¬ô¡vüÌzÑ29³œ•ëíY[½zWۓ­£UOÄì®q¼jm™ÍùÙ¹Þåz÷¹Ç2²×úVݑcvE¤ìÕqËÌåcWÎäze>Ìäq÷z¹ÛµØ;g2c»3÷½{à–›Þ¹µÏ#÷õeý»ÖüÕµ;r þ­ñÎGæÚ6ó, úl`åÂ쳉o}~!䀸.g>w¿ûÿ#„0À 0À 0À 0À 0ð#rÀ 0À 0À 0À 0ÀÀ/1põ;£óî~?rç;eYwï×XÙ󲒳wß{ë<¯2"^j}*ï“ñxw®ÏôU+”ÇwÇ­e"Û·Z_WsUþ]ÞÒì®ñyëÕWέQÛʲ˾öΩåzengú9ڋqT–ék«¿µs³Fjn3nZ¹[-GW¯GÇvD}Ϝ™ åù™¶gÇ7ú{ej¼vTŽ_+7-»gŒkÖl­=™µ®5?G÷ ½u¤×¶ÕûÑ^³óæW½¼EÖ£È9»ó±³üÕsf֘̚–mçîuº57WïÍ3ÑÚÿFÙ³Q»ÆÞñu—Ç«WÖYæz5¢ý‰ÖikfþeêrÀ 0ðÍv^ÿ…0À 0À 0À 0À 0À 0À 0À 0À 0À |£+ßÕ»û½Âý‹sž§î©ßÚ»–ÙûÿŠ™<÷ú•Ýçsf®W˹Êñ®ZÝ­¿ )/Ó·•y}lgËl«¯e{VûÖ;~fN¶Ž­õuWßzs¶•ב›ìxFìeړÉm䜕ùVÇÈXžÝ·L™½|dÆxµþH_#õ¾>+¯­¹0º¶Í˜ŒžY_Z׽՜gƳwÜL®#íiµ!šãg­û’™\ÖÎYY[3¶GkJvœ#ùXñ±ºVí*çŠ9²»Þ–¯Ñ*{´ß¾ŒQùgìÿ¯ý€³\1þ‹wÕY[ƒ£ÏzÏ2mË<ÃÈ´³wÜ·<3rÀ 00càîÿrÀ 0À 0À 0À 0ÀÀÈ 0À 0À 0À 0ÀbàŠ÷ô>å}ÐQ®Ø“q羏cÏØ´s¬vô5Zv´þ¬³H?®ÌÕ;jîËq,çõÈLmOé­WöhŸNä¸Q;[õ×òÙړù¤5¯·ž¯²µüï˜ÿµ²WûÙWÝúÙYómvþEçoonçY¯¼r>FÖ¤Ù} wυ2/Ùµ-³ó!»–ÖŒ·Æº,ÿ¸ÖîèGoÜ{íŠØŽæöØè؎l·æÞì}ãì<ªÕ7²P[3º¾ž97G})?_¹ݵf·~֚ µuxT¤Ý£ŸÑu­7¶ÇszFw¬YwÇÕãÓ«/»^ÍΕÝuî*ûhìªg.w=ËÙ9×[׍Ö1W¶çÌ6|Ê8µÚZûüÎ6 9` ¿¶ü–üœyrÀ 0À 0À 0À 0À 0À 0À 0À 0À 0ð©v¿—÷-ï^½/⬽‘(ËzíGÙõ÷3Ïhßî\ôö~DÛs·å]¹¬õ?š÷è^àÖy;öùdê‹î³¬å!º§0šë®í7~V«³Öß]û[ýoµ#Ú·™ú²ù\YƒZý­9ªµ#â&Û¾Vþ39Ȍ{«œÚzmÎõnuÍ8c¿g6·Ù¨¿»oÙödÇ [nïœÑ1™>¯åZS~ÖZûwŒuëÚ2šG­sZëFk~÷ÖöZ[[õ×¼öŽ[±9º>ôbtÞ(g‘5¿ÌS¶¾Qî3Qۑ£ŒÛžÓ•{¦–×£‘•ï>»¾/£q¼+®êÿLŽÎŠÞuàŽg:;ú±rÌÙm:« ½rŸ0>³îîn§ž¹†ÜµV 9`€`€`€`€`àGä€`€`€`€`€7°ëºo|O0š‡ò³÷¿³{®ÞŸPëCv_ÃqßËν*½q:sFvÇû¸ÈžÌوŒ[6?ïrÊãkûKwÌóZùgíÿŒ”Ӛǽrwz‹æ:3[egö×êÏZØÑžh¾[îG}›»Ì<رF•u•ÿÎÌÙZÛg½góѪ;3þÑúŽ}iÍ¿ÖÚÚs$>'-¯Ÿ0¶½ëFt­Y©;²ÞEֆèµ0³nd׌r-ånÇxÍÞ'µîÅ{ë6zã0»¶Ï¬­Kå1«ýÛe2;Ÿ/§»~Ï@kº³Ÿ2»úºò¼§•·»Ÿïdž½œYÿ•uŽúùMÏ×îÈßÝcù„~?)z®?¡ý¿->åy¦‘gøB`€`€`€`€`€`€`€`€`€žh`×ûuO|pµ?+¹œy?{~vÄ(Çzg÷e÷þ—õ÷âʜdrwlßqOL¤ý™þ÷Ê«e´þò˜È>ºÙ˜ÝSÍŨŒÒy/µ2WÚÉɊÙl¾gÆ®×®;Ç”ÿÑx·ö ÔúUӚs³ó}ÔçZQGµ6Glì\3³&¢ki´Ü]6³skgùbϘ|Ò¸\Ùþìuaåü]qwW®‹‘è­ÿ£ëF«¿;ړYï˟׮½½zjŸµ®‡­vÎösW>®ŒÚ=Bk/ÿ(z¿à)Qs±ó^åIÑï賊™²"õì0{,«W¶¼H;Ï|~&®]ûjãô­ã÷IV£kËÝíc_OËQ´g^„0À 0À 0À 0À 0ð#rÀ 0À 0À 0À 0ÀÀFÑ÷Ùfށ»û½¿ï diff --git a/plan/background/galaxy-bg.jpg b/plan/background/galaxy-bg.jpg index ce319ab..6ff8ae2 100644 Binary files a/plan/background/galaxy-bg.jpg and b/plan/background/galaxy-bg.jpg differ diff --git a/plan/campaign/star-clusters.md b/plan/campaign/star-clusters.md new file mode 100644 index 0000000..f92eca7 --- /dev/null +++ b/plan/campaign/star-clusters.md @@ -0,0 +1,41 @@ +# Star Cluster Programming Plan + +## Data + +* **StarCluster** + * (unique) host -> Admiral +* **Admiral** + * inCluster -> StarCluster? + * invitedTo -> StarCluster[] + * owner -> User + * (unique) inCluster, owner + * No User may have multiple Admiral records in a single StarCluster + +## Actions + +*The following code is written in somewhat-pretentious SQL-looking pseudocode* + +* `Admiral.create(): StarCluster` + * Prereq: `THIS.inCluster IS null` + * Result: `CREATE StarCluster cluster(host: THIS); SET THIS.inCluster TO cluster` +* `Admiral.invite(other: Admiral)` + * Prereq: `THIS.inCluster ISN'T NULL && THIS.inCluster.host IS THIS && other.inCluster IS NULL` + * Result: `INTO other.invitedTo INSERT THIS.inCluster` +* `Admiral.acceptInvitation(cluster: StarCluster)` + * Prereq: `THIS.inCluster IS NULL && THIS.invitedTo HAS cluster` + * Result: `SET THIS.inCluster TO cluster && CLEAR THIS.invitedTo` +* `Admiral.rejectInvitation(cluster: StarCluster)` + * Prereq: `THIS.invitedTo HAS cluster` + * Result: `FROM THIS.invitedTo REMOVE cluster` +* `Admiral.kick(other: Admiral)` + * Prereq: `THIS.inCluster ISN'T NULL && THIS.inCluster.host IS THIS && other ISN'T THIS && other.inCluster IS THIS.inCluster` + * Result: `SET other.inCluster TO NULL` +* `Admiral.makeHost(other: Admiral)` + * Prereq: `THIS.inCluster ISN'T NULL && THIS.inCluster.host IS THIS && other ISN'T THIS && other.inCluster IS THIS.inCluster` + * Result: `SET THIS.inCluster.host TO other` +* `Admiral.quit()` + * Prereq: `THIS.inCluster ISN'T NULL && THIS.inCluster.host ISN'T THIS` + * Result: `SET THIS.inCluster TO NULL` +* `Admiral.deleteCluster(cluster: StarCluster)` + * Prereq: `NO Admiral a EXISTS WHERE (a ISN'T THIS && a.inCluster IS cluster)` + * Result: `OBLITERATE cluster` diff --git a/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt b/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt index 33fae42..e7559c9 100644 --- a/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt +++ b/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt @@ -1,6 +1,7 @@ package net.starshipfights.campaign import kotlinx.serialization.Serializable +import net.starshipfights.game.Faction import net.starshipfights.game.FactionFlavor import kotlin.jvm.JvmInline import kotlin.math.ceil @@ -60,16 +61,27 @@ enum class ClusterFactionMode { @Serializable value class ClusterFactions private constructor(private val factions: Map) { init { - require(factions.values.any { it != ClusterFactionMode.EXCLUDE }) { "Excluding all factions is a bad idea!" } + require(FactionFlavor.values().any { factions[it] != ClusterFactionMode.EXCLUDE }) { "Must not exclude all factions when creating star cluster" } + } + + fun getModesForFaction(faction: Faction) = faction.allegiences.map { this[it] }.toSet() + + fun canFitInto(size: ClusterSize) = factions.count { (_, mode) -> mode == ClusterFactionMode.REQUIRE } < size.maxStars * 2 / 5 + + fun > forceFitInto(size: ClusterSize, precedence: (FactionFlavor) -> T): ClusterFactions { + val canDemote = factions.filterValues { it == ClusterFactionMode.REQUIRE }.keys.sortedBy(precedence) + val mustDemote = (canDemote.size - size.maxStars * 2 / 5).coerceAtLeast(0) + return this + canDemote.take(mustDemote).associateWith { ClusterFactionMode.ALLOW } } operator fun get(factionFlavor: FactionFlavor) = factions[factionFlavor] ?: ClusterFactionMode.ALLOW operator fun plus(other: ClusterFactions) = ClusterFactions(factions + other.factions) + private operator fun plus(otherFactions: Map) = ClusterFactions(factions + otherFactions) fun asGenerationSequence() = sequence { - val required = factions.filterValues { it == ClusterFactionMode.REQUIRE }.keys - val included = factions.filterValues { it != ClusterFactionMode.EXCLUDE }.keys + val required = FactionFlavor.values().filter { this@ClusterFactions[it] == ClusterFactionMode.REQUIRE }.toSet() + val included = FactionFlavor.values().filter { this@ClusterFactions[it] != ClusterFactionMode.EXCLUDE }.toSet() // first, start with the required flavors yieldAll(required.shuffled()) @@ -88,7 +100,7 @@ value class ClusterFactions private constructor(private val factions: Map) = Default + ClusterFactions(factions) + fun of(factions: Map) = Default + factions } } diff --git a/src/commonMain/kotlin/net/starshipfights/game/game_state.kt b/src/commonMain/kotlin/net/starshipfights/game/game_state.kt index 7240f4c..44dc501 100644 --- a/src/commonMain/kotlin/net/starshipfights/game/game_state.kt +++ b/src/commonMain/kotlin/net/starshipfights/game/game_state.kt @@ -240,8 +240,8 @@ enum class GlobalSide { @Serializable data class GlobalShipController(val side: GlobalSide, val disambiguation: String) { companion object { - val Player1Disambiguation = "PLAYER 1" - val Player2Disambiguation = "PLAYER 2" + const val Player1Disambiguation = "PLAYER 1" + const val Player2Disambiguation = "PLAYER 2" } } diff --git a/src/jsMain/kotlin/externals/threejs/Material.module_three.kt b/src/jsMain/kotlin/externals/threejs/Material.module_three.kt index 6c8b4eb..bfb3575 100644 --- a/src/jsMain/kotlin/externals/threejs/Material.module_three.kt +++ b/src/jsMain/kotlin/externals/threejs/Material.module_three.kt @@ -126,7 +126,7 @@ external interface MaterialParameters { var stencilZPass: StencilOp? get() = definedExternally set(value) = definedExternally - var userData: Any? + var userData: dynamic get() = definedExternally set(value) = definedExternally } @@ -177,7 +177,7 @@ external open class Material : EventTarget { open var uuid: String open var vertexColors: Boolean open var visible: Boolean - open var userData: Any + open var userData: dynamic open var version: Number open fun clone(): Material /* this */ open fun copy(material: Material): Material /* this */ diff --git a/src/jsMain/kotlin/net/starshipfights/game/popup.kt b/src/jsMain/kotlin/net/starshipfights/game/popup.kt index a9564e4..c9993d2 100644 --- a/src/jsMain/kotlin/net/starshipfights/game/popup.kt +++ b/src/jsMain/kotlin/net/starshipfights/game/popup.kt @@ -60,6 +60,7 @@ sealed class Popup { } } + // Game matchmaking popups class ChooseAdmiralScreen(private val admirals: List) : Popup() { override fun TagConsumer<*>.render(context: CoroutineContext, callback: (InGameAdmiral) -> Unit) { if (admirals.isEmpty()) { @@ -595,6 +596,34 @@ sealed class Popup { } } + // Battle popups + class GameOver(private val winner: GlobalSide?, private val outcome: String, private val subplotStatuses: Map, private val finalState: GameState) : Popup() { + override fun TagConsumer<*>.render(context: CoroutineContext, callback: (Nothing) -> Unit) { + p { + style = "text-align:center" + + strong(classes = "heading") { + +"${victoryTitle(mySide, winner, subplotStatuses)}!" + } + } + p { + style = "text-align:center" + + +outcome + } + p { + style = "text-align:center" + + val admiralId = finalState.admiralInfo(mySide).id + + a(href = "/admiral/${admiralId}") { + +"Exit Battle" + } + } + } + } + + // Utility popups class LoadingScreen(private val loadingText: String, private val loadAction: suspend () -> T) : Popup() { override fun TagConsumer<*>.render(context: CoroutineContext, callback: (T) -> Unit) { p { @@ -644,30 +673,4 @@ sealed class Popup { } } } - - class GameOver(private val winner: GlobalSide?, private val outcome: String, private val subplotStatuses: Map, private val finalState: GameState) : Popup() { - override fun TagConsumer<*>.render(context: CoroutineContext, callback: (Nothing) -> Unit) { - p { - style = "text-align:center" - - strong(classes = "heading") { - +"${victoryTitle(mySide, winner, subplotStatuses)}!" - } - } - p { - style = "text-align:center" - - +outcome - } - p { - style = "text-align:center" - - val admiralId = finalState.admiralInfo(mySide).id - - a(href = "/admiral/${admiralId}") { - +"Exit Battle" - } - } - } - } } diff --git a/src/jvmMain/kotlin/net/starshipfights/auth/providers.kt b/src/jvmMain/kotlin/net/starshipfights/auth/providers.kt index 67d65af..6d8af3c 100644 --- a/src/jvmMain/kotlin/net/starshipfights/auth/providers.kt +++ b/src/jvmMain/kotlin/net/starshipfights/auth/providers.kt @@ -314,7 +314,7 @@ interface AuthProvider { call.getUserSession()?.let { sess -> launch { - val newTime = Instant.now().minusMillis(100) + val newTime = Instant.now() UserSession.update(UserSession::id eq sess.id, setValue(UserSession::expiration, newTime)) } } @@ -329,7 +329,7 @@ interface AuthProvider { val id = Id(call.parameters.getOrFail("id")) call.getUserSession()?.let { sess -> launch { - val newTime = Instant.now().minusMillis(100) + val newTime = Instant.now() UserSession.update(and(UserSession::id eq id, UserSession::user eq sess.user), setValue(UserSession::expiration, newTime)) } } @@ -342,7 +342,7 @@ interface AuthProvider { call.getUserSession()?.let { sess -> launch { - val newTime = Instant.now().minusMillis(100) + val newTime = Instant.now() UserSession.update(and(UserSession::user eq sess.user, UserSession::id ne sess.id), setValue(UserSession::expiration, newTime)) } } diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt index 15f1d58..bf6c575 100644 --- a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt +++ b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt @@ -17,7 +17,7 @@ class ClusterGenerator(val settings: ClusterGenerationSettings) { var throttle: suspend () -> Unit = ::`yield` suspend fun generateCluster(): StarClusterView { - return withTimeoutOrNull(10_000L) { + return coroutineScope { val positionsAsync = async { val rp = fixPositions(generatePositions().take(settings.size.maxStars).toList()) val p = indexPositions(rp) @@ -49,7 +49,7 @@ class ClusterGenerator(val settings: ClusterGenerationSettings) { systems = generateFleets(assignFactions(systems, warpLanes)), lanes = warpLanes, ) - } ?: generateCluster() + } } private fun generatePositions() = flow { diff --git a/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admiral_names.kt b/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admiral_names.kt index 6fd1dc4..89f6eef 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admiral_names.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admiral_names.kt @@ -6,7 +6,7 @@ import kotlin.random.Random enum class AdmiralNameFlavor { MECHYRDIA, TYLA, CALIBOR, OLYMPIA, // Mechyrdia-aligned - DUTCH, // NdRC-aliged + DUTCH, NORSE, // NdRC-aliged NORTHERN_DIADOCHI, SOUTHERN_DIADOCHI, // Masra Draetsen-aligned FULKREYKK, // Isarnareykk-aligned AMERICAN, HISPANIC_AMERICAN; // Vestigium-aligned @@ -18,6 +18,7 @@ enum class AdmiralNameFlavor { CALIBOR -> "Caliborese" OLYMPIA -> "Olympian" DUTCH -> "Dutch" + NORSE -> "Norse" NORTHERN_DIADOCHI -> "Northern Diadochi" SOUTHERN_DIADOCHI -> "Southern Diadochi" FULKREYKK -> "Thedish" @@ -39,11 +40,11 @@ enum class AdmiralNameFlavor { FactionFlavor.MECHYRDIA -> setOf(MECHYRDIA, TYLA, DUTCH) FactionFlavor.TYLA -> setOf(TYLA) FactionFlavor.OLYMPIA -> setOf(OLYMPIA) - FactionFlavor.TEXANDRIA -> setOf(MECHYRDIA, TYLA, DUTCH) + FactionFlavor.TEXANDRIA -> setOf(DUTCH, NORSE) FactionFlavor.NDRC -> setOf(DUTCH) - FactionFlavor.CCC -> setOf(MECHYRDIA, TYLA, DUTCH) - FactionFlavor.MJOLNIR_ENERGY -> setOf(MECHYRDIA, TYLA, DUTCH) + FactionFlavor.CCC -> setOf(MECHYRDIA, TYLA, DUTCH, NORSE) + FactionFlavor.MJOLNIR_ENERGY -> setOf(DUTCH, NORSE) FactionFlavor.MASRA_DRAETSEN -> setOf(CALIBOR, NORTHERN_DIADOCHI, SOUTHERN_DIADOCHI) FactionFlavor.AEDON_CULTISTS -> setOf(NORTHERN_DIADOCHI, SOUTHERN_DIADOCHI) @@ -544,6 +545,61 @@ object AdmiralNames { private fun randomDutchName(isFemale: Boolean) = (if (isFemale) dutchFemaleNames else dutchMaleNames).random() + " van " + dutchMerchantHouses.random() + private val norseMaleNames = listOf( + "Arni" to "Arna", + "BiÇ«rn" to "Biarnar", + "Bragi" to "Braga", + "Egill" to "Egils", + "Eileifr" to "Eileifs", + "Eiríkr" to "Eiríks", + "Finnr" to "Finns", + "Fridthiófr" to "Fridthiófs", + "Fródi" to "Fróda", + "Geirr" to "Geirs", + "Gudbrandr" to "Gudbrands", + "Haraldr" to "Haralds", + "Hrólfr" to "Hrólfs", + "Hákon" to "Hákonar", + "Iátvardr" to "Iátvardar", + "Knútr" to "Knúts", + "Magnús" to "Magnúss", + "Ríkvidr" to "Ríkvidar", + "Sigurdr" to "Sigurdar", + "Sindri" to "Sindra", + "Sveinn" to "Sveins", + "VidbiÇ«rn" to "Vidbiarnar", + "Óláfr" to "Óláfs", + "Thorsteinn" to "Thorsteins", + "Thórir" to "Thóris", + ) + + private val norseFemaleNames = listOf( + "Borghildr" to "Borghildar", + "Dagný" to "Dagnýiar", + "Grimhildr" to "Grimhildar", + "Gunnr" to "Gunnar", + "Gudrún" to "Gudrúnar", + "Helga" to "Helgu", + "Hreidunn" to "Hreidunnar", + "Inga" to "Ingu", + "Iórunn" to "Iórunnar", + "Ragnfridr" to "Ragnfridra", + "Ragnhildr" to "Ragnhildar", + "Signý" to "Signýiar", + "Áslaug" to "Áslaugar", + "Ástrídr" to "Ástrídra", + ) + + private fun norseFirstName(isFemale: Boolean) = (if (isFemale) norseFemaleNames else norseMaleNames).random().first + + private fun norseLastName(isFemale: Boolean) = if (isFemale) + (norseMaleNames + norseFemaleNames).random().second + "dóttir" + else if (Random.nextDouble() < 0.01) + "HÇ«ldahamarr" + else norseMaleNames.random().second + "sonr" + + private fun randomNorseName(isFemale: Boolean) = "${norseFirstName(isFemale)} ${norseLastName(isFemale)}" + private val diadochiMaleNames = listOf( "Oqatai", "Amogus", @@ -895,6 +951,7 @@ object AdmiralNames { AdmiralNameFlavor.CALIBOR -> randomCaliboreseName(isFemale) AdmiralNameFlavor.OLYMPIA -> randomLatinName(isFemale) AdmiralNameFlavor.DUTCH -> randomDutchName(isFemale) + AdmiralNameFlavor.NORSE -> randomNorseName(isFemale) AdmiralNameFlavor.NORTHERN_DIADOCHI -> randomNorthernDiadochiName(isFemale) AdmiralNameFlavor.SOUTHERN_DIADOCHI -> randomSouthernDiadochiName(isFemale) AdmiralNameFlavor.FULKREYKK -> randomThedishName(isFemale) diff --git a/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admirals.kt b/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admirals.kt index 07963a5..b05d878 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admirals.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/admiralty/admirals.kt @@ -1,10 +1,17 @@ package net.starshipfights.data.admiralty +import com.github.jershell.kbson.NonEncodeNull +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.toList import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.starshipfights.campaign.CampaignMenuAdmiral +import net.starshipfights.campaign.CampaignMenuAdmiralStatus +import net.starshipfights.campaign.StarClusterMenuData import net.starshipfights.data.DataDocument import net.starshipfights.data.DocumentTable import net.starshipfights.data.Id @@ -31,9 +38,11 @@ data class Admiral( val acumen: Int, val money: Int, + val isOnline: Boolean = false, + + @NonEncodeNull val inCluster: Id? = null, val invitedToClusters: Set> = emptySet(), - val requestedClusters: Set> = emptySet(), ) : DataDocument { val rank: AdmiralRank get() = AdmiralRank.fromAcumen(acumen) @@ -45,10 +54,22 @@ data class Admiral( index(Admiral::owningUser) index(Admiral::inCluster) index(Admiral::invitedToClusters) - index(Admiral::requestedClusters) + uniqueIf(Admiral::inCluster.exists(), Admiral::owningUser, Admiral::inCluster) }) } +suspend fun lockAdmiral(admiralId: Id): Boolean { + val admiral = Admiral.get(admiralId) ?: return false + if (admiral.isOnline) return false + + Admiral.set(admiralId, setValue(Admiral::isOnline, true)) + return true +} + +suspend fun unlockAdmiral(admiralId: Id) { + Admiral.set(admiralId, setValue(Admiral::isOnline, false)) +} + fun generateAIName(faction: Faction, isFemale: Boolean) = AdmiralNames.randomName(AdmiralNameFlavor.forFaction(faction).random(), isFemale) fun generateAIAdmiral(faction: Faction, forBattleSize: BattleSize): Admiral { @@ -115,7 +136,7 @@ data class ShipMemorial( }) } -suspend fun getAllInGameAdmirals(user: User) = Admiral.filter(Admiral::owningUser eq user.id).map { admiral -> +suspend fun getAllInGameAdmiralsForBattle(user: User) = Admiral.filter(and(Admiral::owningUser eq user.id, Admiral::inCluster eq null, Admiral::isOnline eq false)).map { admiral -> InGameAdmiral( admiral.id.reinterpret(), InGameUser(user.id.reinterpret(), user.profileName), @@ -141,6 +162,83 @@ suspend fun getInGameAdmiral(admiralId: Id) = Admiral.get(admiral getInGameAdmiral(admiral) } +suspend fun getInGameAdmiralsInCluster(clusterId: Id) = coroutineScope { + Admiral.filter(Admiral::inCluster eq clusterId).map { admiral -> + async { + User.get(admiral.owningUser)?.let { user -> + InGameAdmiral( + admiral.id.reinterpret(), + InGameUser(user.id.reinterpret(), user.profileName), + admiral.name, + admiral.isFemale, + admiral.faction, + admiral.rank + ) + } + } + }.mapNotNull { it.await() }.toList() +} + +suspend fun getInGameAdmiralsInvitedToCluster(clusterId: Id) = coroutineScope { + Admiral.filter(Admiral::invitedToClusters contains clusterId).map { admiral -> + async { + User.get(admiral.owningUser)?.let { user -> + InGameAdmiral( + admiral.id.reinterpret(), + InGameUser(user.id.reinterpret(), user.profileName), + admiral.name, + admiral.isFemale, + admiral.faction, + admiral.rank + ) + } + } + }.mapNotNull { it.await() }.toList() +} + +suspend fun getAllInGameAdmiralsForCampaignInvite(user: User) = Admiral.filter(and(Admiral::owningUser eq user.id, Admiral::inCluster eq null)).map { admiral -> + InGameAdmiral( + admiral.id.reinterpret(), + InGameUser(user.id.reinterpret(), user.profileName), + admiral.name, + admiral.isFemale, + admiral.faction, + admiral.rank + ) +}.toList() + +suspend fun getClusterMenuData(clusterId: Id) = StarCluster.get(clusterId)?.let { cluster -> + StarClusterMenuData( + cluster.id.reinterpret(), + getInGameAdmiralsInCluster(cluster.id), + cluster.host.reinterpret(), + getInGameAdmiralsInvitedToCluster(cluster.id) + ) +} + +suspend fun getCampaignMenuAdmirals(user: User) = Admiral.filter(and(Admiral::owningUser eq user.id, Admiral::isOnline eq false)).map { admiral -> + CampaignMenuAdmiral( + InGameAdmiral( + admiral.id.reinterpret(), + InGameUser(user.id.reinterpret(), user.profileName), + admiral.name, + admiral.isFemale, + admiral.faction, + admiral.rank + ), + admiral.inCluster?.let { getClusterMenuData(it) }?.let { clusterMenuData -> + CampaignMenuAdmiralStatus.InCluster(clusterMenuData) + } ?: CampaignMenuAdmiralStatus.NotInCluster( + coroutineScope { + admiral.invitedToClusters + .map { async { getClusterMenuData(it) } } + .mapNotNull { it.await() } + .toSet() + } + ) + ) +}.toList() + suspend fun getAdmiralsShips(admiralId: Id): Map, Ship> { val now = Instant.now() @@ -161,7 +259,7 @@ fun generateFleet(admiral: Admiral, flavor: FactionFlavor = FactionFlavor.defaul }.orEmpty() } .let { shipTypes -> - val now = Instant.now().minusMillis(100L) + val now = Instant.now() val shipNames = mutableSetOf() shipTypes.mapNotNull { st -> diff --git a/src/jvmMain/kotlin/net/starshipfights/data/auth/user_sessions.kt b/src/jvmMain/kotlin/net/starshipfights/data/auth/user_sessions.kt index 4f1c991..6231e27 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/auth/user_sessions.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/auth/user_sessions.kt @@ -31,8 +31,6 @@ data class User( val showUserStatus: Boolean, val logIpAddresses: Boolean, - - val status: UserStatus = UserStatus.AVAILABLE, ) : DataDocument { val discordAvatarUrl: String get() = discordAvatar?.takeIf { showDiscordName }?.let { diff --git a/src/jvmMain/kotlin/net/starshipfights/data/data_documents.kt b/src/jvmMain/kotlin/net/starshipfights/data/data_documents.kt index e423380..166bce5 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/data_documents.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/data_documents.kt @@ -41,8 +41,10 @@ object DocumentIdController : IdController { interface DocumentTable> { fun initialize() - suspend fun index(property: KProperty1) - suspend fun unique(property: KProperty1) + suspend fun index(vararg properties: KProperty1) + suspend fun unique(vararg properties: KProperty1) + suspend fun indexIf(condition: Bson, vararg properties: KProperty1) + suspend fun uniqueIf(condition: Bson, vararg properties: KProperty1) suspend fun put(doc: T) suspend fun put(docs: Iterable) @@ -87,12 +89,20 @@ private class DocumentTableImpl>(val kclass: KClass, priv initFunc(this) } - override suspend fun index(property: KProperty1) { - collection().ensureIndex(property) + override suspend fun index(vararg properties: KProperty1) { + collection().ensureIndex(*properties) } - override suspend fun unique(property: KProperty1) { - collection().ensureUniqueIndex(property, indexOptions = IndexOptions().partialFilterExpression(property.exists())) + override suspend fun unique(vararg properties: KProperty1) { + collection().ensureUniqueIndex(*properties) + } + + override suspend fun indexIf(condition: Bson, vararg properties: KProperty1) { + collection().ensureIndex(*properties, indexOptions = IndexOptions().partialFilterExpression(condition)) + } + + override suspend fun uniqueIf(condition: Bson, vararg properties: KProperty1) { + collection().ensureUniqueIndex(*properties, indexOptions = IndexOptions().partialFilterExpression(condition)) } override suspend fun put(doc: T) { diff --git a/src/jvmMain/kotlin/net/starshipfights/data/space/star_clusters.kt b/src/jvmMain/kotlin/net/starshipfights/data/space/star_clusters.kt index 25976aa..363513a 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/space/star_clusters.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/space/star_clusters.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -18,8 +19,8 @@ import net.starshipfights.data.invoke import net.starshipfights.game.FactionFlavor import net.starshipfights.game.Position import net.starshipfights.game.Ship -import org.litote.kmongo.eq -import org.litote.kmongo.setValue +import org.litote.kmongo.* +import java.time.Instant @Serializable data class StarCluster( @@ -29,7 +30,9 @@ data class StarCluster( val background: StarClusterBackground, val lanes: Set ) : DataDocument { - companion object Table : DocumentTable by DocumentTable.create() + companion object Table : DocumentTable by DocumentTable.create({ + unique(StarCluster::host) + }) } @Serializable @@ -69,6 +72,9 @@ data class ClusterFleetPresence( ) : DataDocument { companion object Table : DocumentTable by DocumentTable.create({ index(ClusterFleetPresence::starSystemId) + + val admiralIdProp = ClusterFleetPresence::fleetPresence / FleetPresenceData.Player::admiralId + uniqueIf(admiralIdProp.exists(), admiralIdProp) }) } @@ -87,27 +93,14 @@ sealed class FleetPresenceData { ) : FleetPresenceData() } -suspend fun getCampaignStatus(admiral: Admiral, clusterId: Id): CampaignAdmiralStatus? { - val cluster = StarCluster.get(clusterId) ?: return null - - admiral.inCluster?.let { inCluster -> - if (inCluster == clusterId) { - return if (cluster.host == admiral.id) - CampaignAdmiralStatus.HOST - else - CampaignAdmiralStatus.MEMBER - } else if (cluster.host == admiral.id) { - Admiral.set(admiral.id, setValue(Admiral::inCluster, clusterId)) - return CampaignAdmiralStatus.HOST - } - } - - return if (clusterId in admiral.invitedToClusters) - CampaignAdmiralStatus.INVITED - else null +suspend fun getFleetPresenceOf(admiralId: Id): ClusterFleetPresence? { + return ClusterFleetPresence.filter( + (ClusterFleetPresence::fleetPresence / FleetPresenceData.Player::admiralId) eq admiralId + ).singleOrNull() } -suspend fun FleetPresenceData.resolve(inCluster: Id): FleetPresence? { +suspend fun FleetPresenceData.resolve(): FleetPresence? { + val now = Instant.now() return when (this) { is FleetPresenceData.NPC -> FleetPresence( name = name, @@ -117,29 +110,31 @@ suspend fun FleetPresenceData.resolve(inCluster: Id): FleetPresence is FleetPresenceData.Player -> { val (admiral, ships) = coroutineScope { val admiralAsync = async { Admiral.get(admiralId) } - val shipsAsync = async { ShipInDrydock.filter(ShipInDrydock::owningAdmiral eq admiralId).toList() } + val shipsAsync = async { + ShipInDrydock.filter( + and(ShipInDrydock::owningAdmiral eq admiralId, ShipInDrydock::readyAt lte now) + ).toList() + } admiralAsync.await() to shipsAsync.await() } admiral ?: return null - val inGameAdmiral = getInGameAdmiral(admiral) ?: return null - val campaignStatus = getCampaignStatus(admiral, inCluster) ?: return null FleetPresence( name = "Fleet of ${admiral.fullName}", ships = ships.associate { inDrydock -> inDrydock.id.reinterpret() to inDrydock.shipData }, - admiral = FleetPresenceAdmiral.Player( - CampaignAdmiral(inGameAdmiral, campaignStatus) - ) + admiral = FleetPresenceAdmiral.Player(admiral.id.reinterpret()) ) } } } -suspend fun deleteCluster(clusterId: Id) { +suspend fun deleteCluster(clusterViewId: Id) { + val clusterId = clusterViewId.reinterpret() + coroutineScope { launch { StarCluster.del(clusterId) } launch { @@ -155,13 +150,19 @@ suspend fun deleteCluster(clusterId: Id) { } } } + launch { + Admiral.update(Admiral::inCluster eq clusterId, unset(Admiral::inCluster)) + } + launch { + Admiral.update(Admiral::invitedToClusters contains clusterId, pull(Admiral::invitedToClusters, clusterId)) + } } } -suspend fun createCluster(clusterView: StarClusterView, forAdmiral: Id): Id { +suspend fun createCluster(clusterView: StarClusterView, forHost: Id): Id { val cluster = StarCluster( id = Id(), - host = forAdmiral, + host = forHost, background = clusterView.background, lanes = clusterView.lanes ) @@ -204,7 +205,7 @@ suspend fun createCluster(clusterView: StarClusterView, forAdmiral: Id) admiral = admiral ) is FleetPresenceAdmiral.Player -> FleetPresenceData.Player( - admiral.admiral.admiral.id.reinterpret() + admiral.id.reinterpret() ) } ) @@ -215,10 +216,12 @@ suspend fun createCluster(clusterView: StarClusterView, forAdmiral: Id) } } - return cluster.id + return cluster.id.reinterpret() } -suspend fun viewCluster(clusterId: Id): StarClusterView? { +suspend fun viewCluster(clusterViewId: Id): StarClusterView? { + val clusterId = clusterViewId.reinterpret() + return coroutineScope { val clusterAsync = async { StarCluster.get(clusterId) } val systemsAsync = async { @@ -234,7 +237,7 @@ suspend fun viewCluster(clusterId: Id): StarClusterView? { val fleetsAsync = async { ClusterFleetPresence.filter(ClusterFleetPresence::starSystemId eq cSystem.id).map { fleet -> async { - fleet.fleetPresence.resolve(clusterId)?.let { fleet.id.reinterpret() to it } + fleet.fleetPresence.resolve()?.let { fleet.id.reinterpret() to it } } }.mapNotNull { it.await() }.toList().toMap() } @@ -261,3 +264,37 @@ suspend fun viewCluster(clusterId: Id): StarClusterView? { ) } } + +suspend fun viewAdmiralsInCluster(clusterViewId: Id): List { + val cluster = StarCluster.get(clusterViewId.reinterpret()) ?: return emptyList() + + val (host, members, invitees) = coroutineScope { + val hostAsync = async { + listOfNotNull(getInGameAdmiral(cluster.host.reinterpret())) + } + + val membersAsync = async { + Admiral.filter(and(Admiral::inCluster eq clusterViewId.reinterpret(), Admiral::id ne cluster.host)) + .map { async { getInGameAdmiral(it) } } + .mapNotNull { it.await() } + .toList() + } + + val inviteesAsync = async { + Admiral.filter(Admiral::invitedToClusters contains clusterViewId.reinterpret()) + .map { async { getInGameAdmiral(it) } } + .mapNotNull { it.await() } + .toList() + } + + Triple(hostAsync.await(), membersAsync.await(), inviteesAsync.await()) + } + + return host.map { + CampaignAdmiral(it, CampaignAdmiralStatus.HOST) + } + members.map { + CampaignAdmiral(it, CampaignAdmiralStatus.MEMBER) + } + invitees.map { + CampaignAdmiral(it, CampaignAdmiralStatus.INVITED) + } +} diff --git a/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt b/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt index f177084..fe7ff5a 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt @@ -274,7 +274,7 @@ private val constellationBayerNames = listOf( "Dalet", "Heh", "Waw", - "Zayin", + "Dzayin", "H'et", "T'et", "Yod", @@ -282,10 +282,10 @@ private val constellationBayerNames = listOf( "Lamed", "Mem", "Nun", - "Samek", + "Tsamek", "Ayin", "Peh", - "S'adeh", + "Ts'adeh", "Qop", "Resh", "Shin", diff --git a/src/jvmMain/kotlin/net/starshipfights/game/endpoints_game.kt b/src/jvmMain/kotlin/net/starshipfights/game/endpoints_game.kt index 1106eb6..421b6ad 100644 --- a/src/jvmMain/kotlin/net/starshipfights/game/endpoints_game.kt +++ b/src/jvmMain/kotlin/net/starshipfights/game/endpoints_game.kt @@ -6,23 +6,15 @@ import io.ktor.http.* import io.ktor.routing.* import io.ktor.websocket.* import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import net.starshipfights.auth.getUser -import net.starshipfights.data.DocumentTable -import net.starshipfights.data.admiralty.getAllInGameAdmirals -import net.starshipfights.data.auth.User -import net.starshipfights.data.auth.UserStatus +import net.starshipfights.data.admiralty.getAllInGameAdmiralsForBattle import net.starshipfights.redirect -import org.litote.kmongo.setValue fun Routing.installGame() { get("/lobby") { val user = call.getUser() ?: redirect("/login") - val clientMode = if (user.status == UserStatus.AVAILABLE) - ClientMode.MatchmakingMenu(getAllInGameAdmirals(user)) - else - ClientMode.Error("You cannot play in multiple battles at the same time") + val clientMode = ClientMode.MatchmakingMenu(getAllInGameAdmiralsForBattle(user)) call.respondHtml(HttpStatusCode.OK, clientMode.view()) } @@ -30,14 +22,7 @@ fun Routing.installGame() { post("/play") { delay(750L) // nasty hack - val user = call.getUser() ?: redirect("/login") - - val clientMode = when (user.status) { - UserStatus.AVAILABLE -> ClientMode.Error("You must use the matchmaking interface to enter a game") - UserStatus.IN_MATCHMAKING -> ClientMode.Error("You must start a game in the matchmaking interface") - UserStatus.READY_FOR_BATTLE -> call.getGameClientMode() - UserStatus.IN_BATTLE -> ClientMode.Error("You cannot play in multiple battles at the same time") - } + val clientMode = call.getGameClientMode() call.respondHtml(HttpStatusCode.OK, clientMode.view()) } @@ -49,45 +34,15 @@ fun Routing.installGame() { } webSocket("/matchmaking") { - val oldUser = call.getUser() ?: closeAndReturn("You must be logged in to play") { return@webSocket } - if (oldUser.status != UserStatus.AVAILABLE) - closeAndReturn("You cannot play in multiple battles at the same time") { return@webSocket } - - val user = oldUser.copy(status = UserStatus.IN_MATCHMAKING) - User.put(user) - - closeReason.invokeOnCompletion { - DocumentTable.launch { - delay(150L) - if (User.get(user.id)?.status == UserStatus.IN_MATCHMAKING) - User.set(user.id, setValue(User::status, UserStatus.AVAILABLE)) - } - } + val user = call.getUser() ?: closeAndReturn("You must be logged in to play") { return@webSocket } - if (matchmakingEndpoint(user)) - User.set(user.id, setValue(User::status, UserStatus.READY_FOR_BATTLE)) + matchmakingEndpoint(user) } webSocket("/game/{token}") { val token = call.parameters["token"] ?: closeAndReturn("Invalid or missing battle token") { return@webSocket } - val oldUser = call.getUser() ?: closeAndReturn("You must be logged in to play") { return@webSocket } - - if (oldUser.status == UserStatus.IN_BATTLE) - closeAndReturn("You cannot play in multiple battles at the same time") { return@webSocket } - if (oldUser.status == UserStatus.IN_MATCHMAKING) - closeAndReturn("You must start a game in the matchmaking interface") { return@webSocket } - if (oldUser.status == UserStatus.AVAILABLE) - closeAndReturn("You must use the matchmaking interface to enter a game") { return@webSocket } - - val user = oldUser.copy(status = UserStatus.IN_BATTLE) - User.put(user) - - closeReason.invokeOnCompletion { - DocumentTable.launch { - User.set(user.id, setValue(User::status, UserStatus.AVAILABLE)) - } - } + val user = call.getUser() ?: closeAndReturn("You must be logged in to play") { return@webSocket } gameEndpoint(user, token) } diff --git a/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt b/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt index b03bbf1..695a4e6 100644 --- a/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt +++ b/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt @@ -11,10 +11,7 @@ import kotlinx.coroutines.sync.withLock import net.starshipfights.admin.announcements import net.starshipfights.data.DocumentTable import net.starshipfights.data.Id -import net.starshipfights.data.admiralty.Admiral -import net.starshipfights.data.admiralty.BattleRecord -import net.starshipfights.data.admiralty.ShipInDrydock -import net.starshipfights.data.admiralty.ShipMemorial +import net.starshipfights.data.admiralty.* import net.starshipfights.data.auth.User import net.starshipfights.data.createToken import net.starshipfights.game.ai.AISession @@ -46,6 +43,9 @@ object GameManager { val endedAt = Instant.now() on1v1GameEnd(session.state.value, end, startedAt, endedAt) + + unlockAdmiral(hostInfo.id.reinterpret()) + unlockAdmiral(guestInfo.id.reinterpret()) } val hostId = createToken() @@ -100,6 +100,9 @@ object GameManager { aiJob.cancel() on2v1GameEnd(session.state.value, end, startedAt, endedAt) + + unlockAdmiral(hostInfo.id.reinterpret()) + unlockAdmiral(guestInfo.id.reinterpret()) } val hostId = createToken() diff --git a/src/jvmMain/kotlin/net/starshipfights/game/server_matchmaking.kt b/src/jvmMain/kotlin/net/starshipfights/game/server_matchmaking.kt index bc836f3..f77578c 100644 --- a/src/jvmMain/kotlin/net/starshipfights/game/server_matchmaking.kt +++ b/src/jvmMain/kotlin/net/starshipfights/game/server_matchmaking.kt @@ -8,6 +8,8 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedSendChannelException import kotlinx.coroutines.launch import net.starshipfights.data.admiralty.getInGameAdmiral +import net.starshipfights.data.admiralty.lockAdmiral +import net.starshipfights.data.admiralty.unlockAdmiral import net.starshipfights.data.auth.User private val open1v1Sessions = ConcurrentCurator(mutableListOf()) @@ -36,15 +38,18 @@ class Join2v1Invitation(val joinRequest: JoinRequest, val responseHandler: Compl val gameIdHandler = CompletableDeferred() } -suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boolean { - val playerLogin = receiveObject(PlayerLogin.serializer()) { closeAndReturn { return false } } +suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User) { + val playerLogin = receiveObject(PlayerLogin.serializer()) { closeAndReturn { return } } val admiralId = playerLogin.admiral - val inGameAdmiral = getInGameAdmiral(admiralId) ?: closeAndReturn("That admiral does not exist") { return false } - if (inGameAdmiral.user.id != user.id) closeAndReturn("You do not own that admiral") { return false } + val inGameAdmiral = getInGameAdmiral(admiralId) ?: closeAndReturn("That admiral does not exist") { return } + if (inGameAdmiral.user.id != user.id.reinterpret()) closeAndReturn("You do not own that admiral") { return } + + if (!lockAdmiral(admiralId.reinterpret())) + closeAndReturn("That admiral is not available") { return } when (val loginMode = playerLogin.login) { is LoginMode.Train -> { - closeAndReturn("Invalid input: LoginMode.Train should redirect you directly to training endpoint") { return false } + closeAndReturn("Invalid input: LoginMode.Train should redirect you directly to training endpoint") { return unlockAdmiral(admiralId.reinterpret()) } } is LoginMode.Host1v1 -> { val battleInfo = loginMode.battleInfo @@ -57,6 +62,8 @@ suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boole @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch { + unlockAdmiral(admiralId.reinterpret()) + open1v1Sessions.use { it.remove(hostInvitation) } @@ -68,7 +75,7 @@ suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boole val joinResponse = receiveObject(JoinResponse.serializer()) { closeAndReturn { joinInvitation.responseHandler.complete(JoinResponse(false)) - return false + return unlockAdmiral(admiralId.reinterpret()) } } @@ -107,13 +114,18 @@ suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boole val joinListing = JoinListing(openGames.mapValues { (_, invitation) -> invitation.joinable }) sendObject(JoinListing.serializer(), joinListing) - val joinSelection = receiveObject(JoinSelection.serializer()) { closeAndReturn { return false } } + val joinSelection = receiveObject(JoinSelection.serializer()) { closeAndReturn { return unlockAdmiral(admiralId.reinterpret()) } } val hostInvitation = openGames.getValue(joinSelection.selectedId) val joinResponseHandler = CompletableDeferred() val joinInvitation = Join1v1Invitation(joinRequest, joinResponseHandler) closeReason.invokeOnCompletion { joinResponseHandler.cancel() + + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch { + unlockAdmiral(admiralId.reinterpret()) + } } try { @@ -144,6 +156,8 @@ suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boole @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch { + unlockAdmiral(admiralId.reinterpret()) + open2v1Sessions.use { it.remove(hostInvitation) } @@ -155,7 +169,7 @@ suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boole val joinResponse = receiveObject(JoinResponse.serializer()) { closeAndReturn { joinInvitation.responseHandler.complete(JoinResponse(false)) - return false + return unlockAdmiral(admiralId.reinterpret()) } } @@ -197,13 +211,18 @@ suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boole val joinListing = JoinListing(openGames.mapValues { (_, invitation) -> invitation.joinable }) sendObject(JoinListing.serializer(), joinListing) - val joinSelection = receiveObject(JoinSelection.serializer()) { closeAndReturn { return false } } + val joinSelection = receiveObject(JoinSelection.serializer()) { closeAndReturn { return unlockAdmiral(admiralId.reinterpret()) } } val hostInvitation = openGames.getValue(joinSelection.selectedId) val joinResponseHandler = CompletableDeferred() val joinInvitation = Join2v1Invitation(joinRequest, joinResponseHandler) closeReason.invokeOnCompletion { joinResponseHandler.cancel() + + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch { + unlockAdmiral(admiralId.reinterpret()) + } } try { @@ -224,6 +243,4 @@ suspend fun DefaultWebSocketServerSession.matchmakingEndpoint(user: User): Boole } } } - - return true } diff --git a/src/jvmMain/kotlin/net/starshipfights/info/view_bar.kt b/src/jvmMain/kotlin/net/starshipfights/info/view_bar.kt index d660058..d25971c 100644 --- a/src/jvmMain/kotlin/net/starshipfights/info/view_bar.kt +++ b/src/jvmMain/kotlin/net/starshipfights/info/view_bar.kt @@ -2,7 +2,6 @@ package net.starshipfights.info import kotlinx.html.* import net.starshipfights.data.auth.User -import net.starshipfights.data.auth.UserStatus import net.starshipfights.data.auth.getTrophies import net.starshipfights.data.auth.renderTrophy import net.starshipfights.game.ShipType @@ -66,12 +65,7 @@ data class UserProfileSidebar(val user: User, val isCurrentUser: Boolean, val ha if (user.showUserStatus) { p { style = "text-align:center" - +when (user.status) { - UserStatus.IN_BATTLE -> "In Battle" - UserStatus.READY_FOR_BATTLE -> "In Battle" - UserStatus.IN_MATCHMAKING -> "In Matchmaking" - UserStatus.AVAILABLE -> if (hasOpenSessions) "Online" else "Offline" - } + +if (hasOpenSessions) "Online" else "Offline" } p { style = "text-align:center" diff --git a/src/jvmMain/resources/static/images/background-dark.jpg b/src/jvmMain/resources/static/images/background-dark.jpg index cc4c5be..c8ee92e 100644 Binary files a/src/jvmMain/resources/static/images/background-dark.jpg and b/src/jvmMain/resources/static/images/background-dark.jpg differ diff --git a/src/jvmMain/resources/static/images/background.jpg b/src/jvmMain/resources/static/images/background.jpg index 22d6692..c638d26 100644 Binary files a/src/jvmMain/resources/static/images/background.jpg and b/src/jvmMain/resources/static/images/background.jpg differ diff --git a/src/jvmMain/resources/static/init.js b/src/jvmMain/resources/static/init.js index f7cb6d6..648b355 100644 --- a/src/jvmMain/resources/static/init.js +++ b/src/jvmMain/resources/static/init.js @@ -256,8 +256,9 @@ const chosenClass = setSomeButton.getAttribute("data-enable-class"); const factionChoices = document.getElementsByClassName("faction-choice"); for (const factionChoice of factionChoices) { - if (factionChoice.classList.contains(filterClass)) + if (factionChoice.classList.contains(filterClass)) { factionChoice.checked = factionChoice.classList.contains(chosenClass); + } } }; }