From cbaa6f1198309b0bbd62b9fc86bbd84c83087eca Mon Sep 17 00:00:00 2001 From: TheSaminator Date: Sun, 3 Jul 2022 13:39:34 -0400 Subject: [PATCH] Add fleet rendering in star cluster view --- .../starshipfights/campaign/cluster_params.kt | 4 +- .../starshipfights/campaign/fleet_presence.kt | 64 +-------- .../starshipfights/campaign/star_systems.kt | 2 +- .../game/ship_faction_flavors.kt | 9 ++ .../starshipfights/campaign/campaign_main.kt | 9 +- .../campaign/campaign_selection.kt | 11 ++ .../starshipfights/campaign/campaign_ui.kt | 98 ++++++++++++- .../campaign/campaign_ui_impl.kt | 7 +- .../campaign/space_fleet_render.kt | 83 ++++++++++- .../starshipfights/campaign/space_render.kt | 134 ++++++++++++++++-- .../net/starshipfights/game/client_game.kt | 1 + .../kotlin/net/starshipfights/game/game_ui.kt | 7 +- .../game/ship_faction_flavors_js.kt | 1 + .../starshipfights/campaign/cluster_gen.kt | 24 ++-- .../starshipfights/campaign/cluster_test.kt | 30 ++++ .../campaign/endpoints_campaign.kt | 2 +- 16 files changed, 383 insertions(+), 103 deletions(-) create mode 100644 src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt diff --git a/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt b/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt index 42b6ae4..489f412 100644 --- a/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt +++ b/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt @@ -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() } diff --git a/src/commonMain/kotlin/net/starshipfights/campaign/fleet_presence.kt b/src/commonMain/kotlin/net/starshipfights/campaign/fleet_presence.kt index e1211fd..dc0308c 100644 --- a/src/commonMain/kotlin/net/starshipfights/campaign/fleet_presence.kt +++ b/src/commonMain/kotlin/net/starshipfights/campaign/fleet_presence.kt @@ -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 get() = FactionFlavor.values().filter { this in it.loyalties }.toSet() @@ -67,36 +39,6 @@ val FactionFlavor.loyalties: List 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, diff --git a/src/commonMain/kotlin/net/starshipfights/campaign/star_systems.kt b/src/commonMain/kotlin/net/starshipfights/campaign/star_systems.kt index dbfc652..c237444 100644 --- a/src/commonMain/kotlin/net/starshipfights/campaign/star_systems.kt +++ b/src/commonMain/kotlin/net/starshipfights/campaign/star_systems.kt @@ -13,7 +13,7 @@ enum class StarClusterBackground { } @Serializable -class StarClusterView( +data class StarClusterView( val background: StarClusterBackground, val systems: Map, StarSystem>, val lanes: Set diff --git a/src/commonMain/kotlin/net/starshipfights/game/ship_faction_flavors.kt b/src/commonMain/kotlin/net/starshipfights/game/ship_faction_flavors.kt index 96bd473..b862097 100644 --- a/src/commonMain/kotlin/net/starshipfights/game/ship_faction_flavors.kt +++ b/src/commonMain/kotlin/net/starshipfights/game/ship_faction_flavors.kt @@ -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( diff --git a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_main.kt b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_main.kt index 71c6503..6e7bbc2 100644 --- a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_main.kt +++ b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_main.kt @@ -13,11 +13,15 @@ import kotlinx.coroutines.launch import net.starshipfights.data.Id import net.starshipfights.game.* -suspend fun campaignMain(playingAs: Id, otherAdmirals: Map, CampaignAdmiral>, clusterToken: Id, clusterView: StarClusterView) { +var mySide: CampaignAdmiral? = null + +suspend fun campaignMain(playingAs: Id, admirals: Map, CampaignAdmiral>, clusterToken: Id, clusterView: StarClusterView) { Popup.LoadingScreen("Loading resources...") { CampaignResources.load() }.display() + mySide = admirals[playingAs] + val updateLoop = Popup.LoadingScreen>("Rendering cluster...") { delay(500L) @@ -61,7 +65,7 @@ suspend fun campaignMain(playingAs: Id, otherAdmirals: Map, otherAdmirals: Map obj3d.celestialObjectRenderImmediate?.let { ptr -> diff --git a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_selection.kt b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_selection.kt index 5c7b91c..c23df36 100644 --- a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_selection.kt +++ b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_selection.kt @@ -17,6 +17,7 @@ sealed class Selection { object None : Selection() data class System(val id: Id) : Selection() data class CelestialObject(val pointer: CelestialObjectPointer) : Selection() + data class FleetPresence(val pointer: FleetPresencePointer) : Selection() } private val selectionMutable = MutableStateFlow(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 diff --git a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui.kt b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui.kt index 76b764a..6160c3d 100644 --- a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui.kt +++ b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui.kt @@ -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() private val visibleSelectedObjectIndicators = mutableSetOf() + private val selectedFleetIndicators = mutableMapOf() + private val visibleSelectedFleetIndicators = mutableSetOf() + 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() + + 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)" + } } } } diff --git a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui_impl.kt b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui_impl.kt index 9ef38d4..ffa62e8 100644 --- a/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui_impl.kt +++ b/src/jsMain/kotlin/net/starshipfights/campaign/campaign_ui_impl.kt @@ -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) diff --git a/src/jsMain/kotlin/net/starshipfights/campaign/space_fleet_render.kt b/src/jsMain/kotlin/net/starshipfights/campaign/space_fleet_render.kt index a695ced..1288bfe 100644 --- a/src/jsMain/kotlin/net/starshipfights/campaign/space_fleet_render.kt +++ b/src/jsMain/kotlin/net/starshipfights/campaign/space_fleet_render.kt @@ -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 { + fun getPositions(numFleets: Int, worldRadius: Double, worldCenter: Vector3): List { 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 + } diff --git a/src/jsMain/kotlin/net/starshipfights/campaign/space_render.kt b/src/jsMain/kotlin/net/starshipfights/campaign/space_render.kt index 7113326..17361a4 100644 --- a/src/jsMain/kotlin/net/starshipfights/campaign/space_render.kt +++ b/src/jsMain/kotlin/net/starshipfights/campaign/space_render.kt @@ -16,15 +16,18 @@ object CampaignResources { private set private lateinit var starTypes: Map - lateinit var star: CustomRenderFactory> - private set + private lateinit var star: CustomRenderFactory> private lateinit var planetTypes: Map - lateinit var planet: CustomRenderFactory> - private set + private lateinit var planet: CustomRenderFactory> - lateinit var starSystem: CustomRenderFactory - private set + private lateinit var fleetMeshesRaw: Map + private lateinit var fleetMeshes: Map + private lateinit var fleetCounter: CustomRenderFactory + private lateinit var fleetCountersInSystem: CustomRenderFactory + private lateinit var fleetCountersInCluster: CustomRenderFactory + + private lateinit var starSystem: CustomRenderFactory private lateinit var warpLane: CustomRenderFactory @@ -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() + + shipTypeMesh.material = shipTypeMesh.material.unsafeCast() + .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? else parent?.starSystemRender +external interface FleetPresenceRender { + var isFleetPresence: Boolean + var fleetPresenceId: String + var starSystemId: String +} + +fun FleetPresenceRender(ptr: FleetPresencePointer) = configure { + 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().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) = (parent?.isStarClusterFleets == true) && userData == system.id diff --git a/src/jsMain/kotlin/net/starshipfights/game/client_game.kt b/src/jsMain/kotlin/net/starshipfights/game/client_game.kt index 6e7fb84..0466bf3 100644 --- a/src/jsMain/kotlin/net/starshipfights/game/client_game.kt +++ b/src/jsMain/kotlin/net/starshipfights/game/client_game.kt @@ -87,6 +87,7 @@ suspend fun GameRenderInteraction.execute(scope: CoroutineScope) { cameraControls.update(dt) renderer.render(scene, camera) GameUI.renderGameUI(cameraControls) + GameUI.fitLabels() } } diff --git a/src/jsMain/kotlin/net/starshipfights/game/game_ui.kt b/src/jsMain/kotlin/net/starshipfights/game/game_ui.kt index 2e3430c..b024192 100644 --- a/src/jsMain/kotlin/net/starshipfights/game/game_ui.kt +++ b/src/jsMain/kotlin/net/starshipfights/game/game_ui.kt @@ -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, shipId: Id, ship: ShipInstance) { diff --git a/src/jsMain/kotlin/net/starshipfights/game/ship_faction_flavors_js.kt b/src/jsMain/kotlin/net/starshipfights/game/ship_faction_flavors_js.kt index 0c4bbc7..075d084 100644 --- a/src/jsMain/kotlin/net/starshipfights/game/ship_faction_flavors_js.kt +++ b/src/jsMain/kotlin/net/starshipfights/game/ship_faction_flavors_js.kt @@ -149,6 +149,7 @@ fun MeshPhongMaterial.forShip(faction: Faction, flavor: FactionFlavor): ShaderMa return shipShaderMaterial.clone().unsafeCast().also { material -> material.uniforms["diffuse"]?.value?.unsafeCast()?.copy(color) material.uniforms["specular"]?.value?.unsafeCast()?.copy(specular) + material.uniforms["emissive"]?.value?.unsafeCast()?.copy(emissive) material.uniforms["shininess"]?.value = shininess.toDouble().coerceAtLeast(EPSILON) map?.let { material.uniforms["map"]?.value = it } diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt index 3b1e8e2..242e05c 100644 --- a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt +++ b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt @@ -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 index 0000000..776ee9f --- /dev/null +++ b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt @@ -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("${systemId.id}-fleet-$i") to FleetPresence( + "Test Fleet $i", + flavor, + emptyMap() + ) + } + + systemId to system.copy(holder = flavor, fleets = fleets) + } + + return copy(systems = ownedSystems) +} diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt index 3f09afe..d6e0ffb 100644 --- a/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt +++ b/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt @@ -114,7 +114,7 @@ fun Routing.installCampaign() { val cluster = ClusterGenerator( ClusterGenerationSettings(color, size, density, planets, corruption) - ).generateCluster() + ).generateCluster().testPostProcess() val clientMode = ClientMode.CampaignMap( Id(""), -- 2.25.1