Add fleet rendering in star cluster view
authorTheSaminator <thesaminator@users.noreply.github.com>
Sun, 3 Jul 2022 17:39:34 +0000 (13:39 -0400)
committerTheSaminator <thesaminator@users.noreply.github.com>
Sun, 3 Jul 2022 17:39:34 +0000 (13:39 -0400)
16 files changed:
src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt
src/commonMain/kotlin/net/starshipfights/campaign/fleet_presence.kt
src/commonMain/kotlin/net/starshipfights/campaign/star_systems.kt
src/commonMain/kotlin/net/starshipfights/game/ship_faction_flavors.kt
src/jsMain/kotlin/net/starshipfights/campaign/campaign_main.kt
src/jsMain/kotlin/net/starshipfights/campaign/campaign_selection.kt
src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui.kt
src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui_impl.kt
src/jsMain/kotlin/net/starshipfights/campaign/space_fleet_render.kt
src/jsMain/kotlin/net/starshipfights/campaign/space_render.kt
src/jsMain/kotlin/net/starshipfights/game/client_game.kt
src/jsMain/kotlin/net/starshipfights/game/game_ui.kt
src/jsMain/kotlin/net/starshipfights/game/ship_faction_flavors_js.kt
src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt
src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt [new file with mode: 0644]
src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt

index 42b6ae4bb5f5fadf6739e236d667f8ae1737c97f..489f4128c9e7407253658e5171fea0138d21cd2e 100644 (file)
@@ -13,8 +13,8 @@ enum class ClusterSize(val maxStars: Int, val maxHyperlaneDistanceFactor: Double
                get() = name.lowercase().replaceFirstChar { it.uppercase() }
 }
 
-enum class ClusterLaneDensity(val chanceToRemove: Double, val numToAdd: Int) {
-       SPARSE(0.72, 1), MEDIUM(0.36, 2), DENSE(0.12, 3);
+enum class ClusterLaneDensity(val chanceToRemove: Double) {
+       SPARSE(0.72), MEDIUM(0.36), DENSE(0.12);
        
        val displayName: String
                get() = name.lowercase().replaceFirstChar { it.uppercase() }
index e1211fdb24c7723c0640114a7d459ec1b295491d..dc0308c10888f9cd27b547b1584b6bb06239f800 100644 (file)
@@ -2,37 +2,9 @@ package net.starshipfights.campaign
 
 import kotlinx.serialization.Serializable
 import net.starshipfights.data.Id
-import net.starshipfights.game.*
-
-val FactionFlavor.getMapColor: IntColor
-       get() = when (this) {
-               FactionFlavor.MECHYRDIA -> IntColor(255, 204, 51)
-               FactionFlavor.TYLA -> IntColor(255, 204, 51)
-               FactionFlavor.OLYMPIA -> IntColor(255, 204, 51)
-               FactionFlavor.TEXANDRIA -> IntColor(255, 204, 51)
-               
-               FactionFlavor.NDRC -> IntColor(255, 204, 51)
-               FactionFlavor.CCC -> IntColor(255, 204, 51)
-               FactionFlavor.MJOLNIR_ENERGY -> IntColor(255, 204, 51)
-               
-               FactionFlavor.MASRA_DRAETSEN -> IntColor(204, 34, 34)
-               FactionFlavor.AEDON_CULTISTS -> IntColor(204, 34, 34)
-               FactionFlavor.FERTHLON_EXILES -> IntColor(204, 34, 34)
-               
-               FactionFlavor.RES_NOSTRA -> IntColor(204, 102, 153)
-               FactionFlavor.CORSAIRS -> IntColor(204, 102, 153)
-               FactionFlavor.FELINAE_FELICES -> IntColor(204, 102, 153)
-               
-               FactionFlavor.ISARNAREYKK -> IntColor(34, 221, 34)
-               FactionFlavor.SWARTAREYKK -> IntColor(34, 221, 34)
-               FactionFlavor.THEUDAREYKK -> IntColor(255, 204, 51) // Mechyrdia
-               FactionFlavor.STAHLAREYKK -> IntColor(255, 204, 51) // Also Mechyrdia
-               FactionFlavor.LYUDAREYKK -> IntColor(34, 221, 34)
-               FactionFlavor.NEUIA_FULKREYKK -> IntColor(34, 221, 34)
-               
-               FactionFlavor.CORVUS_CLUSTER_VESTIGIUM -> IntColor(108, 96, 153)
-               FactionFlavor.COLEMAN_SF_BASE_VESTIGIUM -> IntColor(108, 96, 153)
-       }
+import net.starshipfights.game.Faction
+import net.starshipfights.game.FactionFlavor
+import net.starshipfights.game.Ship
 
 val Faction.allegiences: Set<FactionFlavor>
        get() = FactionFlavor.values().filter { this in it.loyalties }.toSet()
@@ -67,36 +39,6 @@ val FactionFlavor.loyalties: List<Faction>
                FactionFlavor.COLEMAN_SF_BASE_VESTIGIUM -> listOf(Faction.VESTIGIUM)
        }
 
-val FactionFlavor.mapCounterShipClass: ShipType
-       get() = when (this) {
-               FactionFlavor.MECHYRDIA -> ShipType.VENSCA
-               FactionFlavor.TYLA -> ShipType.VENSCA
-               FactionFlavor.OLYMPIA -> ShipType.VENSCA
-               FactionFlavor.TEXANDRIA -> ShipType.VENSCA
-               
-               FactionFlavor.NDRC -> ShipType.VOORHOEDE
-               FactionFlavor.CCC -> ShipType.VOORHOEDE
-               FactionFlavor.MJOLNIR_ENERGY -> ShipType.VOORHOEDE
-               
-               FactionFlavor.MASRA_DRAETSEN -> ShipType.MORGOTH
-               FactionFlavor.AEDON_CULTISTS -> ShipType.MORGOTH
-               FactionFlavor.FERTHLON_EXILES -> ShipType.MORGOTH
-               
-               FactionFlavor.RES_NOSTRA -> ShipType.BOBCAT
-               FactionFlavor.CORSAIRS -> ShipType.BOBCAT
-               FactionFlavor.FELINAE_FELICES -> ShipType.BOBCAT
-               
-               FactionFlavor.ISARNAREYKK -> ShipType.TEFRAN
-               FactionFlavor.SWARTAREYKK -> ShipType.TEFRAN
-               FactionFlavor.THEUDAREYKK -> ShipType.TEFRAN
-               FactionFlavor.STAHLAREYKK -> ShipType.TEFRAN
-               FactionFlavor.LYUDAREYKK -> ShipType.TEFRAN
-               FactionFlavor.NEUIA_FULKREYKK -> ShipType.TEFRAN
-               
-               FactionFlavor.CORVUS_CLUSTER_VESTIGIUM -> ShipType.LEXINGTON
-               FactionFlavor.COLEMAN_SF_BASE_VESTIGIUM -> ShipType.LEXINGTON
-       }
-
 @Serializable
 data class FleetPresence(
        val name: String,
index dbfc65273359a895fdf3ac07e6d5dab76f6d01eb..c237444dd88d847d71d848ea14eecb3eeb5574e7 100644 (file)
@@ -13,7 +13,7 @@ enum class StarClusterBackground {
 }
 
 @Serializable
-class StarClusterView(
+data class StarClusterView(
        val background: StarClusterBackground,
        val systems: Map<Id<StarSystem>, StarSystem>,
        val lanes: Set<WarpLane>
index 96bd473b013035eac1cefe3ef0e72372ad803c0a..b862097c7e887166df7b2b2b7db4095faea4c28c 100644 (file)
@@ -19,6 +19,15 @@ data class IntColor(val red: Int, val green: Int, val blue: Int) {
        }
 }
 
+val IntColor.shadow: IntColor
+       get() = let { (r, g, b) ->
+               IntColor(
+                       r * 2 / 3,
+                       g * 2 / 3,
+                       b * 2 / 3,
+               )
+       }
+
 val IntColor.highlight: IntColor
        get() = let { (r, g, b) ->
                IntColor(
index 71c650358fea1b85f7fc58f8ad63531b1448488e..6e7bbc2055d2ccf987ebb1e771c25f942ae92dde 100644 (file)
@@ -13,11 +13,15 @@ import kotlinx.coroutines.launch
 import net.starshipfights.data.Id
 import net.starshipfights.game.*
 
-suspend fun campaignMain(playingAs: Id<InGameAdmiral>, otherAdmirals: Map<Id<InGameAdmiral>, CampaignAdmiral>, clusterToken: Id<StarClusterView>, clusterView: StarClusterView) {
+var mySide: CampaignAdmiral? = null
+
+suspend fun campaignMain(playingAs: Id<InGameAdmiral>, admirals: Map<Id<InGameAdmiral>, CampaignAdmiral>, clusterToken: Id<StarClusterView>, clusterView: StarClusterView) {
        Popup.LoadingScreen("Loading resources...") {
                CampaignResources.load()
        }.display()
        
+       mySide = admirals[playingAs]
+       
        val updateLoop = Popup.LoadingScreen<FlowCollector<Double>>("Rendering cluster...") {
                delay(500L)
                
@@ -61,7 +65,7 @@ suspend fun campaignMain(playingAs: Id<InGameAdmiral>, otherAdmirals: Map<Id<InG
                
                renderer.render(scene, camera)
                
-               CampaignUI.initCampaignUI(uiResponder(clusterView))
+               CampaignUI.initCampaignUI(uiResponder(clusterView, scene))
                
                addSelectionHandler(clusterView, camera, scene)
                
@@ -71,6 +75,7 @@ suspend fun campaignMain(playingAs: Id<InGameAdmiral>, otherAdmirals: Map<Id<InG
                        cameraControls.update(dt)
                        renderer.render(scene, camera)
                        CampaignUI.renderCampaignUI(cameraControls)
+                       CampaignUI.fitLabels()
                        
                        scene.traverse { obj3d ->
                                obj3d.celestialObjectRenderImmediate?.let { ptr ->
index 5c7b91c3586fb3a924672ecbe64f161d2ffa44d1..c23df36dd8c2def7d47b5410312d1d922e39c25b 100644 (file)
@@ -17,6 +17,7 @@ sealed class Selection {
        object None : Selection()
        data class System(val id: Id<StarSystem>) : Selection()
        data class CelestialObject(val pointer: CelestialObjectPointer) : Selection()
+       data class FleetPresence(val pointer: FleetPresencePointer) : Selection()
 }
 
 private val selectionMutable = MutableStateFlow<Selection>(Selection.None)
@@ -45,6 +46,16 @@ fun addSelectionHandler(starClusterView: StarClusterView, camera: PerspectiveCam
                                y = 1 - (me.clientY.toDouble() / window.innerHeight) * 2
                        }, camera)
                        
+                       val fleetCounters = scene.children
+                               .single { it.isStarCluster }.children
+                               .single { it.isStarClusterFleets }
+                       
+                       val fleetPresence = raycaster.intersectObject(fleetCounters, recursive = true).firstOrNull()?.`object`?.fleetPresenceRender
+                       if (fleetPresence != null) {
+                               selectionMutable.value = Selection.FleetPresence(fleetPresence)
+                               return
+                       }
+                       
                        val position = raycaster.intersectXZPlane()
                        if (position == null) {
                                selectionMutable.value = Selection.None
index 76b764a59f230b0ef77106ab29e999ca6e2cba7d..6160c3db35e19d21a7d6e880301aa0c8bd5fc696 100644 (file)
@@ -19,6 +19,7 @@ import kotlin.math.PI
 
 interface CampaignUIResponder {
        fun getStarCluster(): StarClusterView
+       fun getRenderScene(): Scene
 }
 
 object CampaignUI {
@@ -39,6 +40,9 @@ object CampaignUI {
        private val selectedObjectIndicators = mutableMapOf<CelestialObjectPointer, CSS3DSprite>()
        private val visibleSelectedObjectIndicators = mutableSetOf<CelestialObjectPointer>()
        
+       private val selectedFleetIndicators = mutableMapOf<FleetPresencePointer, CSS3DSprite>()
+       private val visibleSelectedFleetIndicators = mutableSetOf<FleetPresencePointer>()
+       
        private lateinit var errorMessages: HTMLParagraphElement
        private lateinit var helpMessages: HTMLParagraphElement
        
@@ -81,6 +85,10 @@ object CampaignUI {
                        systemsOverlayRenderer.setSize(window.innerWidth, window.innerHeight)
                })
                
+               val fleetRenders = responder.getRenderScene()
+                       .children.single { it.isStarCluster }
+                       .children.single { it.isStarClusterFleets }
+               
                for ((systemId, system) in responder.getStarCluster().systems) {
                        val systemWithId = StarSystemWithId(systemId, system)
                        systemsOverlayScene.add(CSS3DSprite(document.create.div {
@@ -91,7 +99,7 @@ object CampaignUI {
                                element.style.asDynamic().pointerEvents = "none"
                                
                                position.copy(CampaignScaling.toWorldPosition(system.position))
-                               position.y = 50
+                               position.y = 64
                        })
                        
                        selectedSystemIndicators[systemId] = CSS3DObject(document.create.img(src = "/static/game/images/crosshair-round.svg")).apply {
@@ -118,9 +126,33 @@ object CampaignUI {
                                        visible = false
                                }.also { systemsOverlayScene.add(it) }
                        }
+                       
+                       val fleetRender = fleetRenders
+                               .children.single { it.isStarSystemFleets(systemId) }
+                       
+                       for (fleetId in system.fleets.keys) {
+                               val fleetPtr = FleetPresencePointer(systemId, fleetId)
+                               val currFleetRender = fleetRender
+                                       .children.single { it.fleetPresenceRender == fleetPtr }
+                               
+                               selectedFleetIndicators[fleetPtr] = CSS3DSprite(document.create.img(src = "/static/game/images/crosshair.svg")).apply {
+                                       scale.setScalar(0.004)
+                                       
+                                       element.style.asDynamic().pointerEvents = "none"
+                                       
+                                       position.copy(currFleetRender.position)
+                                       
+                                       visible = false
+                               }.also { systemsOverlayScene.add(it) }
+                       }
                }
-               
+       }
+       
+       private var labelsFit = false
+       fun fitLabels() {
+               if (labelsFit) return
                textFit(document.getElementsByClassName("system-label"))
+               labelsFit = true
        }
        
        fun renderCampaignUI(controls: CampaignCameraControls) {
@@ -146,6 +178,11 @@ object CampaignUI {
                }
                visibleSelectedObjectIndicators.clear()
                
+               for (ptr in visibleSelectedFleetIndicators) {
+                       selectedFleetIndicators[ptr]?.visible = false
+               }
+               visibleSelectedFleetIndicators.clear()
+               
                topRightBar.clear()
                topRightBar.append {
                        when (selection) {
@@ -168,6 +205,12 @@ object CampaignUI {
                                        p {
                                                style = "text-align:center"
                                                +(system.holder?.loyalties?.first()?.getDefiniteShortName()?.let { "Controlled by $it" } ?: "Wilderness")
+                                               br
+                                               system.holder?.let {
+                                                       img(alt = it.displayName, src = it.flagUrl) {
+                                                               style = "width:4em;height:2.5em"
+                                                       }
+                                               }
                                        }
                                        
                                        selectedSystemIndicators[selection.id]?.visible = true
@@ -194,6 +237,31 @@ object CampaignUI {
                                        selectedObjectIndicators[selection.pointer]?.visible = true
                                        visibleSelectedObjectIndicators += selection.pointer
                                }
+                               is Selection.FleetPresence -> {
+                                       val fleet = selection.pointer.resolve(starCluster) ?: return@append selectionRender(clearSelection())
+                                       
+                                       p {
+                                               style = "text-align:center"
+                                               strong(classes = "heading") {
+                                                       +fleet.name
+                                               }
+                                       }
+                                       p {
+                                               style = "text-align:center"
+                                               +"${fleet.ships.size} ships"
+                                       }
+                                       p {
+                                               style = "text-align:center"
+                                               +"Operated by ${fleet.owner.loyalties.first().getDefiniteShortName()}"
+                                               br
+                                               img(alt = fleet.owner.displayName, src = fleet.owner.flagUrl) {
+                                                       style = "width:4em;height:2.5em"
+                                               }
+                                       }
+                                       
+                                       selectedFleetIndicators[selection.pointer]?.visible = true
+                                       visibleSelectedFleetIndicators += selection.pointer
+                               }
                        }
                }
        }
@@ -219,7 +287,7 @@ object CampaignUI {
                attributes["data-ship-id"] = system.id.toString()
                
                p(classes = "system-label") {
-                       val color = system.starSystem.holder?.getMapColor?.toString() ?: "#fff"
+                       val color = system.starSystem.holder?.mapColor?.toString() ?: "#fff"
                        style = "color:$color;margin:0;white-space:nowrap;width:640px;height:125px"
                        +system.starSystem.name
                }
@@ -228,7 +296,29 @@ object CampaignUI {
                        style = "text-align:center;color:#fff;margin:0;white-space:nowrap;width:640px;height:125px"
                        system.starSystem.holder?.let { flavor ->
                                img(src = flavor.flagUrl, alt = flavor.displayName) {
-                                       style = "width:200px;height:125px"
+                                       style = "width:640px;height:400px;transform-origin:top center;transform:scale(0.32)"
+                               }
+                       }
+               }
+       }
+       
+       private fun redrawSystemLabel(system: StarSystemWithId) {
+               val div = document.getElementById("system-overlay-${system.id}").unsafeCast<HTMLDivElement>()
+               
+               div.clear()
+               div.append {
+                       p(classes = "system-label") {
+                               val color = system.starSystem.holder?.mapColor?.toString() ?: "#fff"
+                               style = "color:$color;margin:0;white-space:nowrap;width:640px;height:125px"
+                               +system.starSystem.name
+                       }
+                       
+                       p(classes = "system-faction") {
+                               style = "text-align:center;color:#fff;margin:0;white-space:nowrap;width:640px;height:125px"
+                               system.starSystem.holder?.let { flavor ->
+                                       img(src = flavor.flagUrl, alt = flavor.displayName) {
+                                               style = "width:640px;height:400px;transform-origin:top center;transform:scale(0.32)"
+                                       }
                                }
                        }
                }
index 9ef38d41222f98a1e2c33ee6d4df8486abb426b0..ffa62e8ddbedf12068b358e9c0dd31c183e22a1d 100644 (file)
@@ -1,7 +1,10 @@
 package net.starshipfights.campaign
 
-private class CampaignUIResponderImpl(val clusterView: StarClusterView) : CampaignUIResponder {
+import externals.threejs.Scene
+
+private class CampaignUIResponderImpl(val clusterView: StarClusterView, val scene: Scene) : CampaignUIResponder {
        override fun getStarCluster() = clusterView
+       override fun getRenderScene() = scene
 }
 
-fun uiResponder(clusterView: StarClusterView): CampaignUIResponder = CampaignUIResponderImpl(clusterView)
+fun uiResponder(clusterView: StarClusterView, scene: Scene): CampaignUIResponder = CampaignUIResponderImpl(clusterView, scene)
index a695cedf1867bbe95af6a683585760bdfca75d19..1288bfe37fea7ca9a102fc05cc8ac8e8bbbb3ad7 100644 (file)
@@ -1,8 +1,11 @@
 package net.starshipfights.campaign
 
+import externals.threejs.Object3D
 import externals.threejs.Vector3
 import net.starshipfights.game.Faction
 import net.starshipfights.game.FactionFlavor
+import net.starshipfights.game.IntColor
+import net.starshipfights.game.ShipType
 import kotlin.math.PI
 import kotlin.math.atan2
 import kotlin.math.cos
@@ -13,24 +16,29 @@ data class FleetRenderPosition(
        val rotation: Double
 )
 
+fun Object3D.applyRenderPosition(renderPosition: FleetRenderPosition) {
+       position.copy(renderPosition.worldPos)
+       CampaignScaling.toWorldRotation(renderPosition.rotation, this)
+}
+
 enum class FleetSide {
        FRIEND,
        ENEMY;
        
-       fun getPositions(numFleets: Int, worldRadius: Double): List<FleetRenderPosition> {
+       fun getPositions(numFleets: Int, worldRadius: Double, worldCenter: Vector3): List<FleetRenderPosition> {
                val marks = (1..numFleets).map { it / (numFleets + 1.0) }
                
                val angles = marks.map { (it + 0.5) * PI }
                
                val cosFactor = when (this) {
-                       FRIEND -> 1.0
-                       ENEMY -> -1.0
+                       FRIEND -> -1.0
+                       ENEMY -> 1.0
                }
                
                return angles.map { theta ->
-                       val position = Vector3(cosFactor * cos(theta) * worldRadius, 0, sin(theta) * worldRadius)
+                       val position = Vector3(cosFactor * cos(theta) * worldRadius, 9.6, sin(theta) * worldRadius).add(worldCenter)
                        
-                       val rotation = atan2(cos(theta) * cosFactor, sin(theta))
+                       val rotation = atan2(-cos(theta) * cosFactor, -sin(theta))
                        
                        FleetRenderPosition(position, rotation)
                }
@@ -42,3 +50,68 @@ fun getFleetSide(admiralFaction: Faction, fleetOwner: FactionFlavor) =
                FleetSide.FRIEND
        else
                FleetSide.ENEMY
+
+fun getFleetSide(fleetOwner: FactionFlavor) =
+       mySide?.admiral?.faction?.let { admiralFaction ->
+               getFleetSide(admiralFaction, fleetOwner)
+       } ?: FleetSide.ENEMY
+
+val FactionFlavor.mapColor: IntColor
+       get() = when (this) {
+               FactionFlavor.MECHYRDIA -> IntColor(255, 204, 51)
+               FactionFlavor.TYLA -> IntColor(255, 204, 51)
+               FactionFlavor.OLYMPIA -> IntColor(255, 204, 51)
+               FactionFlavor.TEXANDRIA -> IntColor(255, 204, 51)
+               
+               FactionFlavor.NDRC -> IntColor(255, 204, 51)
+               FactionFlavor.CCC -> IntColor(255, 204, 51)
+               FactionFlavor.MJOLNIR_ENERGY -> IntColor(255, 204, 51)
+               
+               FactionFlavor.MASRA_DRAETSEN -> IntColor(204, 34, 34)
+               FactionFlavor.AEDON_CULTISTS -> IntColor(204, 34, 34)
+               FactionFlavor.FERTHLON_EXILES -> IntColor(204, 34, 34)
+               
+               FactionFlavor.RES_NOSTRA -> IntColor(204, 102, 153)
+               FactionFlavor.CORSAIRS -> IntColor(204, 102, 153)
+               FactionFlavor.FELINAE_FELICES -> IntColor(204, 102, 153)
+               
+               FactionFlavor.ISARNAREYKK -> IntColor(34, 221, 34)
+               FactionFlavor.SWARTAREYKK -> IntColor(34, 221, 34)
+               FactionFlavor.THEUDAREYKK -> IntColor(255, 204, 51) // Mechyrdia
+               FactionFlavor.STAHLAREYKK -> IntColor(255, 204, 51) // Also Mechyrdia
+               FactionFlavor.LYUDAREYKK -> IntColor(34, 221, 34)
+               FactionFlavor.NEUIA_FULKREYKK -> IntColor(34, 221, 34)
+               
+               FactionFlavor.CORVUS_CLUSTER_VESTIGIUM -> IntColor(108, 96, 153)
+               FactionFlavor.COLEMAN_SF_BASE_VESTIGIUM -> IntColor(108, 96, 153)
+       }
+
+val FactionFlavor.mapCounterShipClass: ShipType
+       get() = when (this) {
+               FactionFlavor.MECHYRDIA -> ShipType.VENSCA
+               FactionFlavor.TYLA -> ShipType.VENSCA
+               FactionFlavor.OLYMPIA -> ShipType.VENSCA
+               FactionFlavor.TEXANDRIA -> ShipType.VENSCA
+               
+               FactionFlavor.NDRC -> ShipType.KRIJGSCHUIT
+               FactionFlavor.CCC -> ShipType.KRIJGSCHUIT
+               FactionFlavor.MJOLNIR_ENERGY -> ShipType.KRIJGSCHUIT
+               
+               FactionFlavor.MASRA_DRAETSEN -> ShipType.AZATHOTH
+               FactionFlavor.AEDON_CULTISTS -> ShipType.AZATHOTH
+               FactionFlavor.FERTHLON_EXILES -> ShipType.AZATHOTH
+               
+               FactionFlavor.RES_NOSTRA -> ShipType.BOBCAT
+               FactionFlavor.CORSAIRS -> ShipType.BOBCAT
+               FactionFlavor.FELINAE_FELICES -> ShipType.BOBCAT
+               
+               FactionFlavor.ISARNAREYKK -> ShipType.KHORR
+               FactionFlavor.SWARTAREYKK -> ShipType.KHORR
+               FactionFlavor.THEUDAREYKK -> ShipType.KHORR
+               FactionFlavor.STAHLAREYKK -> ShipType.KHORR
+               FactionFlavor.LYUDAREYKK -> ShipType.KHORR
+               FactionFlavor.NEUIA_FULKREYKK -> ShipType.KHORR
+               
+               FactionFlavor.CORVUS_CLUSTER_VESTIGIUM -> ShipType.IOWA
+               FactionFlavor.COLEMAN_SF_BASE_VESTIGIUM -> ShipType.IOWA
+       }
index 71133261d60478dfc6e79525d2b8690255876ce5..17361a4c7adb69a8c12bd56f8793dfc3657311cc 100644 (file)
@@ -16,15 +16,18 @@ object CampaignResources {
                private set
        
        private lateinit var starTypes: Map<StarType, RenderFactory>
-       lateinit var star: CustomRenderFactory<CelestialObjectWithPointer<CelestialObject.Star>>
-               private set
+       private lateinit var star: CustomRenderFactory<CelestialObjectWithPointer<CelestialObject.Star>>
        
        private lateinit var planetTypes: Map<PlanetType, RenderFactory>
-       lateinit var planet: CustomRenderFactory<CelestialObjectWithPointer<CelestialObject.Planet>>
-               private set
+       private lateinit var planet: CustomRenderFactory<CelestialObjectWithPointer<CelestialObject.Planet>>
        
-       lateinit var starSystem: CustomRenderFactory<StarSystemWithId>
-               private set
+       private lateinit var fleetMeshesRaw: Map<ShipType, RenderFactory>
+       private lateinit var fleetMeshes: Map<FactionFlavor, RenderFactory>
+       private lateinit var fleetCounter: CustomRenderFactory<FleetPresenceWithPointer>
+       private lateinit var fleetCountersInSystem: CustomRenderFactory<StarSystemWithId>
+       private lateinit var fleetCountersInCluster: CustomRenderFactory<StarClusterView>
+       
+       private lateinit var starSystem: CustomRenderFactory<StarSystemWithId>
        
        private lateinit var warpLane: CustomRenderFactory<WarpLaneData>
        
@@ -260,12 +263,89 @@ object CampaignResources {
                                        obj
                                }
                        }
+                       
+                       launch {
+                               val shipTypes = FactionFlavor.values().map { it.mapCounterShipClass }.distinct()
+                               
+                               fleetMeshesRaw = shipTypes.associateWith { shipType ->
+                                       async { loadModel(shipType.meshName) }
+                               }.mapValues { (_, meshAsync) ->
+                                       val mesh = meshAsync.await()
+                                       mesh.scale.setScalar(0.25)
+                                       RenderFactory { mesh.clone(true) }
+                               }
+                               
+                               fleetMeshes = FactionFlavor.values().associateWith { flavor ->
+                                       val shipTypeMesh = fleetMeshesRaw.getValue(flavor.mapCounterShipClass).generate().unsafeCast<Mesh>()
+                                       
+                                       shipTypeMesh.material = shipTypeMesh.material.unsafeCast<MeshPhongMaterial>()
+                                               .apply { emissive = flavor.mapColor.shadow.to3JS() }
+                                               .forShip(flavor.mapCounterShipClass.faction, flavor)
+                                       
+                                       RenderFactory { shipTypeMesh.clone(true) }
+                               }
+                               
+                               fleetCounter = CustomRenderFactory { (ptr, fleet) ->
+                                       fleetMeshes.getValue(fleet.owner).generate().apply {
+                                               userData = FleetPresenceRender(ptr)
+                                       }
+                               }
+                               
+                               fleetCountersInSystem = CustomRenderFactory { (systemId, system) ->
+                                       val fleetList = system.fleets
+                                               .map { (id, fleet) -> FleetPresenceWithPointer(FleetPresencePointer(systemId, id), fleet) }
+                                               .sortedBy { (_, fleet) -> fleet.owner }
+                                       
+                                       val friendFleets = fleetList.filter { (_, fleet) ->
+                                               getFleetSide(fleet.owner) == FleetSide.FRIEND
+                                       }
+                                       
+                                       val enemyFleets = fleetList.filter { (_, fleet) ->
+                                               getFleetSide(fleet.owner) == FleetSide.ENEMY
+                                       }
+                                       
+                                       val worldRadius = CampaignScaling.toWorldLength(system.radius)
+                                       val worldCenter = CampaignScaling.toWorldPosition(system.position)
+                                       val friendPositions = FleetSide.FRIEND.getPositions(friendFleets.size, worldRadius, worldCenter)
+                                       val enemyPositions = FleetSide.ENEMY.getPositions(enemyFleets.size, worldRadius, worldCenter)
+                                       
+                                       val friendFleetRenders = (friendFleets.map { fleetCounter.generate(it) } zip friendPositions).map { (obj3d, pos) ->
+                                               obj3d.apply { applyRenderPosition(pos) }
+                                       }
+                                       val enemyFleetRenders = (enemyFleets.map { fleetCounter.generate(it) } zip enemyPositions).map { (obj3d, pos) ->
+                                               obj3d.apply { applyRenderPosition(pos) }
+                                       }
+                                       
+                                       Group().apply {
+                                               for (friendFleetRender in friendFleetRenders)
+                                                       add(friendFleetRender)
+                                               
+                                               for (enemyFleetRender in enemyFleetRenders)
+                                                       add(enemyFleetRender)
+                                               
+                                               userData = systemId.id
+                                       }
+                               }
+                               
+                               fleetCountersInCluster = CustomRenderFactory { cluster ->
+                                       val systemsFleets = cluster.systems.map { (id, system) ->
+                                               fleetCountersInSystem.generate(StarSystemWithId(id, system))
+                                       }
+                                       
+                                       Group().apply {
+                                               for (systemFleets in systemsFleets)
+                                                       add(systemFleets)
+                                               
+                                               userData = "fleet counters"
+                                       }
+                               }
+                       }
                }
                
                starSystem = CustomRenderFactory { (ssId, starSystem) ->
                        val torusGeom = TorusGeometry(CampaignScaling.toWorldLength(starSystem.radius), 0.25, 4, 64)
                        val torusMat = MeshBasicMaterial(configure {
-                               color = (starSystem.holder?.getMapColor ?: IntColor(255, 255, 255)).to3JS()
+                               color = (starSystem.holder?.mapColor ?: IntColor(255, 255, 255)).to3JS()
                        })
                        
                        val torus = Mesh(torusGeom, torusMat)
@@ -305,6 +385,8 @@ object CampaignResources {
                                for (lane in cluster.lanes)
                                        add(warpLane.generate(lane.resolve(cluster) ?: continue))
                                
+                               add(fleetCountersInCluster.generate(cluster))
+                               
                                userData = "star cluster"
                        }
                }
@@ -312,24 +394,24 @@ object CampaignResources {
 }
 
 object CampaignScaling {
-       private const val CELESTIAL_BODY_SIZE_TO_3JS_UNITS_FACTOR = 0.25
+       const val CELESTIAL_BODY_SIZE_PER_3JS_UNITS = 4.0
        
        fun toWorldRotation(facing: Double, obj: Object3D) {
                obj.rotateY(facing)
        }
        
-       fun toSpaceLength(length3js: Double) = length3js / CELESTIAL_BODY_SIZE_TO_3JS_UNITS_FACTOR
-       fun toWorldLength(lengthSc: Double) = lengthSc * CELESTIAL_BODY_SIZE_TO_3JS_UNITS_FACTOR
+       fun toSpaceLength(length3js: Double) = length3js * CELESTIAL_BODY_SIZE_PER_3JS_UNITS
+       fun toWorldLength(lengthSc: Double) = lengthSc / CELESTIAL_BODY_SIZE_PER_3JS_UNITS
        
        fun toSpacePosition(v3: Vector3) = Position(
-               Vec2(v3.x.toDouble(), v3.z.toDouble()) / CELESTIAL_BODY_SIZE_TO_3JS_UNITS_FACTOR
+               Vec2(v3.x.toDouble(), v3.z.toDouble()) * CELESTIAL_BODY_SIZE_PER_3JS_UNITS
        )
        
-       fun toWorldPosition(pos: Position) = (pos.vector * CELESTIAL_BODY_SIZE_TO_3JS_UNITS_FACTOR).let { (x, y) ->
+       fun toWorldPosition(pos: Position) = (pos.vector / CELESTIAL_BODY_SIZE_PER_3JS_UNITS).let { (x, y) ->
                Vector3(x, 0, y)
        }
        
-       fun toWorldScale(size: Int) = sqrt(size * 32.0) * CELESTIAL_BODY_SIZE_TO_3JS_UNITS_FACTOR
+       fun toWorldScale(size: Int) = sqrt(size * 32.0) / CELESTIAL_BODY_SIZE_PER_3JS_UNITS
 }
 
 external interface CelestialObjectRender {
@@ -378,6 +460,27 @@ val Object3D.starSystemRender: Id<StarSystem>?
        else
                parent?.starSystemRender
 
+external interface FleetPresenceRender {
+       var isFleetPresence: Boolean
+       var fleetPresenceId: String
+       var starSystemId: String
+}
+
+fun FleetPresenceRender(ptr: FleetPresencePointer) = configure<FleetPresenceRender> {
+       isFleetPresence = true
+       starSystemId = ptr.starSystem.id
+       fleetPresenceId = ptr.fleetPresence.id
+}
+
+val FleetPresenceRender.pointer: FleetPresencePointer
+       get() = FleetPresencePointer(Id(starSystemId), Id(fleetPresenceId))
+
+val Object3D.fleetPresenceRender: FleetPresencePointer?
+       get() = if (userData.isFleetPresence == true)
+               userData.unsafeCast<FleetPresenceRender>().pointer
+       else
+               parent?.fleetPresenceRender
+
 val StarClusterBackground.ambientColor: IntColor
        get() = when (this) {
                StarClusterBackground.BLUE -> IntColor(34, 51, 85)
@@ -392,3 +495,8 @@ val StarClusterBackground.ambientColor: IntColor
 
 val Object3D.isStarCluster: Boolean
        get() = userData == "star cluster"
+
+val Object3D.isStarClusterFleets: Boolean
+       get() = userData == "fleet counters"
+
+fun Object3D.isStarSystemFleets(system: Id<StarSystem>) = (parent?.isStarClusterFleets == true) && userData == system.id
index 6e7fb842442e0ab625be4fdde537d4959c71cc8e..0466bf370478417b17af8097702bb1ec8c1b8073 100644 (file)
@@ -87,6 +87,7 @@ suspend fun GameRenderInteraction.execute(scope: CoroutineScope) {
                                cameraControls.update(dt)
                                renderer.render(scene, camera)
                                GameUI.renderGameUI(cameraControls)
+                               GameUI.fitLabels()
                        }
                }
                
index 2e3430c2bc9c1f07e69b6550aa477539d8f414e8..b0241925aa6b139e3f4da95c29c7d25061696631 100644 (file)
@@ -507,8 +507,13 @@ object GameUI {
                                        position.y = 7.5
                                })
                }
-               
+       }
+       
+       private var labelsFit = false
+       fun fitLabels() {
+               if (labelsFit) return
                textFit(document.getElementsByClassName("ship-label"))
+               labelsFit = true
        }
        
        private fun DIV.drawShipLabel(state: GameState, abilities: List<PlayerAbilityType>, shipId: Id<ShipInstance>, ship: ShipInstance) {
index 0c4bbc716f794d1b1fd7415fa945d754a3e3eba6..075d084c02979bf43841125ec180d98b6e85464b 100644 (file)
@@ -149,6 +149,7 @@ fun MeshPhongMaterial.forShip(faction: Faction, flavor: FactionFlavor): ShaderMa
        return shipShaderMaterial.clone().unsafeCast<ShaderMaterial>().also { material ->
                material.uniforms["diffuse"]?.value?.unsafeCast<Color>()?.copy(color)
                material.uniforms["specular"]?.value?.unsafeCast<Color>()?.copy(specular)
+               material.uniforms["emissive"]?.value?.unsafeCast<Color>()?.copy(emissive)
                material.uniforms["shininess"]?.value = shininess.toDouble().coerceAtLeast(EPSILON)
                
                map?.let { material.uniforms["map"]?.value = it }
index 3b1e8e2a811abda0b3277f2c83123e59cb15ca37..242e05c918618180b5f814305d6bccaec0036ea8 100644 (file)
@@ -183,6 +183,11 @@ class ClusterGenerator(val settings: ClusterGenerationSettings) {
                if (detachedSystems.isEmpty())
                        return warpLanes
                
+               val lanesWithSegments = warpLanes.map { lane ->
+                       val (aId, bId) = lane
+                       lane to LineSegment(positions.getValue(aId), positions.getValue(bId))
+               }
+               
                val possibleLanes = discoveredSystems.map { a ->
                        val aPos = positions.getValue(a)
                        val (b, bPos) = detachedSystems.map { b ->
@@ -193,9 +198,15 @@ class ClusterGenerator(val settings: ClusterGenerationSettings) {
                                (aPos - bPos).magnitude
                        }!!
                        WarpLane(a, b) to LineSegment(aPos, bPos)
+               }.filter { (warpLane, segment) ->
+                       lanesWithSegments.none { (l, s) ->
+                               warpLane !in l && s in segment
+                       } && positions.none { (id, pos) ->
+                               id !in warpLane && segment.intersectsCircle(pos, MAX_SYSTEM_SIZE)
+                       }
                }.sortedBy { (_, it) -> it.length }.map { (it, _) -> it }
                
-               return fixWarpLanes(positions, warpLanes + possibleLanes.take(settings.laneDensity.numToAdd))
+               return fixWarpLanes(positions, warpLanes + possibleLanes.take(1))
        }
        
        private fun createStarSystems() = flow {
@@ -363,12 +374,6 @@ private data class UnplacedStarSystem(
 private val UnplacedStarSystem.numHabitableWorlds: Int
        get() = bodies.count { it is CelestialObject.Planet && it.type == PlanetType.TERRESTRIAL }
 
-private val UnplacedStarSystem.isEldritch: Boolean
-       get() = bodies.any { it is CelestialObject.Star && it.type == StarType.X }
-
-private val UnplacedStarSystem.numWorlds: Int
-       get() = bodies.count { it is CelestialObject.Planet }
-
 @JvmInline
 private value class UnnamedCelestialObject private constructor(private val celestialObject: CelestialObject) {
        val position: Position
@@ -470,11 +475,8 @@ private val CelestialObject.Star.surfaceLuminosity: Double
                val gray = type.lightColor.let { (r, g, b) -> r + g + b } / 765.0
                val area = 4 * PI * size // radius is proportional to sqrt(size)
                
-               // screw it
-               val hueTemperature = exp(hue)
-               
                // neutron stars look blue, but emit light as if they were gray
-               val temperature = if (type == StarType.NEUTRON_STAR) 0.6441 else hueTemperature
+               val temperature = exp(if (type == StarType.NEUTRON_STAR) 0.0 else hue)
                
                // not-so-physically-accurate variant of Stefan-Boltzmann equation
                return gray * area * temperature
diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt
new file mode 100644 (file)
index 0000000..776ee9f
--- /dev/null
@@ -0,0 +1,30 @@
+package net.starshipfights.campaign
+
+import net.starshipfights.data.Id
+import net.starshipfights.game.FactionFlavor
+
+fun StarClusterView.testPostProcess(): StarClusterView {
+       val flavors = FactionFlavor.values().toList().shuffled()
+       val ownerFlavors = sequence {
+               while (true)
+                       for (flavor in flavors)
+                               yield(flavor)
+       }.take(systems.size).toList()
+       
+       val ownedSystems = (systems.toList().shuffled() zip ownerFlavors).associate { (systemWithId, flavor) ->
+               val (systemId, system) = systemWithId
+               
+               val numOfFleets = (1..3).random()
+               val fleets = (1..numOfFleets).associate { i ->
+                       Id<FleetPresence>("${systemId.id}-fleet-$i") to FleetPresence(
+                               "Test Fleet $i",
+                               flavor,
+                               emptyMap()
+                       )
+               }
+               
+               systemId to system.copy(holder = flavor, fleets = fleets)
+       }
+       
+       return copy(systems = ownedSystems)
+}
index 3f09afe1922f46389ef3fb3a8340ecaec2d1694a..d6e0ffb797c47f4eda52f4f8c1a06cc6b8223094 100644 (file)
@@ -114,7 +114,7 @@ fun Routing.installCampaign() {
                
                val cluster = ClusterGenerator(
                        ClusterGenerationSettings(color, size, density, planets, corruption)
-               ).generateCluster()
+               ).generateCluster().testPostProcess()
                
                val clientMode = ClientMode.CampaignMap(
                        Id(""),