diff --git a/.codespell.exclude b/.codespell.exclude index d5953e5b7daf..8fe77c20fdcb 100644 --- a/.codespell.exclude +++ b/.codespell.exclude @@ -61,6 +61,7 @@ ` "I do not know vhy you sent us to that place. But I am happy to be home. So go, and maybe one day I vill not hate to see you again."` `A familiar woman is waiting for you outside your ship. Although it's been more than seven years since you've last seen her, you recognize Hjlod instantly. Her eyes are brighter than when last you parted, and she has the same lopsided grin that you remember. ", I saw your ship landing, and thought I vould say hello."` ` She reaches into her pocket and pulls out a small wooden box, similar to the one you saw displayed at Asgard years ago. "I have been thinking," Hjlod says. "Maybe all this vas vill of Skade. Maybe you took us to Asgard to make us stronger, to make us better. It is bitter lesson, but I understand it now."` + ` She points to a small wooden craft up ahead. "I built her," she says with pride. "No more Ice-Crawler for Hjlod. I am sailor now. I take curious tourist out now and again. Much safer than Ice-Crawler, except when we capsize." She shrugs and grins. "Perhaps someday you'll be my tourist again. This time, I promise no accidents. Also, there are no Ice-Slugs to worry about." She pauses for a moment. "There are Shark-Kraken though. But don't worry. Hjlod vill take care of you."` object "Sies Upi" planet "Sies Upi" description `Sies Upi is one of the most recently colonized planets of the Gegno Vi. This hot, dry, and harsh world serves as more of an outpost than it does a colony, but nevertheless the Vi have found ways to create manageable living conditions for their settlers.` diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index b16a14a6bb85..ffe7206a57b9 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -61,7 +61,7 @@ jobs: cd_windows_x64: name: Windows - runs-on: windows-2022 + runs-on: windows-2025 env: OUTPUT: EndlessSky-win64-continuous.zip GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cd_release.yaml b/.github/workflows/cd_release.yaml index c7c0cc7f2a41..c623db1c22ee 100644 --- a/.github/workflows/cd_release.yaml +++ b/.github/workflows/cd_release.yaml @@ -44,7 +44,7 @@ jobs: release_windows_x64: name: Windows x64 - runs-on: windows-2022 + runs-on: windows-2025 env: OUTPUT: EndlessSky-win64-${{ inputs.release_version }}.zip GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60ecb0364a9d..ce5b48626a95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,7 +125,7 @@ jobs: name: Windows needs: changed if: ${{ needs.changed.outputs.game_code == 'true' || needs.changed.outputs.unit_tests == 'true' || needs.changed.outputs.cmake_files == 'true' || needs.changed.outputs.ci_config == 'true' }} - runs-on: windows-2022 + runs-on: windows-2025 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -168,7 +168,7 @@ jobs: name: Windows Clang needs: changed if: ${{ needs.changed.outputs.game_code == 'true' || needs.changed.outputs.unit_tests == 'true' || needs.changed.outputs.cmake_files == 'true' || needs.changed.outputs.ci_config == 'true' }} - runs-on: windows-2022 + runs-on: windows-2025 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -291,7 +291,7 @@ jobs: needs: [changed, build_windows] name: Data Files if: ${{ (needs.changed.outputs.game_code == 'true' || needs.changed.outputs.cmake_files == 'true' || needs.changed.outputs.data == 'true' || needs.changed.outputs.integration_tests == 'true' || needs.changed.outputs.ci_config == 'true') && always() }} - runs-on: windows-2022 + runs-on: windows-2025 env: CONTINUOUS: EndlessSky-win64-continuous.zip GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CMakeLists.txt b/CMakeLists.txt index b3b81d6bee3b..82ee3bc585a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ set(CMAKE_CONFIGURATION_TYPES "Debug" "Release" CACHE STRING "" FORCE) set(CMAKE_CXX_STANDARD 20 CACHE STRING "") set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +# We don't use modules, so avoid using unrecognised compiler-specific commands that cmake produces when they are enabled. +set(CMAKE_CXX_SCAN_FOR_MODULES OFF) # Use LTO for Release builds only. set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DEBUG FALSE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) diff --git a/copyright b/copyright index 1e80f3636a17..8c65d33c7f92 100644 --- a/copyright +++ b/copyright @@ -2818,6 +2818,12 @@ Copyright: bene-dictator License: CC-BY-SA-4.0 Comment: Derived from works by Michael Zahniser (under the same license), detailed by Saugia. +Files: + images/outfit/surtrblood* +Copyright: bene_dictator +License: CC-BY-SA-4.0 +Comment: Derived from works by Anarchist2 (under the same license). + Files: images/planet/station19* Copyright: bene-dictator @@ -3427,6 +3433,7 @@ License: CC-BY-SA-4.0 Files: images/outfit/android* + images/outfit/collar* images/outfit/mug* images/outfit/skadetear* Copyright: Anarchist2 diff --git a/data/_ui/help.txt b/data/_ui/help.txt index a9b0b8ff085b..f4e1d26eab52 100644 --- a/data/_ui/help.txt +++ b/data/_ui/help.txt @@ -90,6 +90,9 @@ help "map advanced shops" `Each ship or outfit can be clicked on, displaying that item's stats while also highlighting any planets where it is sold. Using Shift+click on a ship or outfit after you already have one selected will display that item's stats side by side with the previously selected item.` `You can also press F to search the shipyard and outfitter maps for a specific ship or outfit.` +help "map advanced stars" + `The starry map mode toggles the display of star types on the system map. This can also be triggered by pressing A while the map is open. The stars present in a system affect the amount of solar power and solar wind available for use by solar collectors and ramscoops respectively.` + help "map advanced danger" `The galaxy can be a dangerous place! Pirates raid some systems more frequently or in greater strength than others, and a poorly defended convoy risks attracting them in many systems. To get an idea of the relative danger of different systems, click on the alert icon next to the system name.` diff --git a/data/_ui/interfaces.txt b/data/_ui/interfaces.txt index e809eed2d654..c691231559cb 100644 --- a/data/_ui/interfaces.txt +++ b/data/_ui/interfaces.txt @@ -54,6 +54,7 @@ color "logbook line" .2 .2 .2 1. color "message log background" .125 .125 .125 1. color "item selected" 0. 0. 0. .3 color "outfitter difference highlight" 1. .5 0. 1. +color "volume bar background" .0937 .0937 .0937 1. # Colors used to draw certain HUD elements in-flight. color "shields" .43 .55 .85 .8 @@ -495,6 +496,11 @@ interface "controls" dimensions 90 30 color medium hover bright + button a "_Audio" + center -300 -110 + dimensions 90 30 + color medium + hover bright visible if "multiple controls pages" sprite "ui/dialog cancel" center -115 210 @@ -528,6 +534,11 @@ interface "settings" dimensions 90 30 color medium hover bright + button a "_Audio" + center -300 -110 + dimensions 90 30 + color medium + hover bright visible if "multiple settings pages" sprite "ui/dialog cancel" center -115 210 @@ -562,6 +573,11 @@ interface "plugins" center -300 -150 dimensions 90 30 color bright + button a "_Audio" + center -300 -110 + dimensions 90 30 + color medium + hover bright button o "_Open Plugins" center -195 210 dimensions 120 30 @@ -585,15 +601,391 @@ interface "plugins" +interface "audio" + sprite "ui/settings panel" + center -20 -20 + button c "_Controls" + center -300 -230 + dimensions 90 30 + color medium + hover bright + button s "_Settings" + center -300 -190 + dimensions 90 30 + color medium + hover bright + button p "_Plugins" + center -300 -150 + dimensions 90 30 + color medium + hover bright + button a "_Audio" + center -300 -110 + dimensions 90 30 + color bright + value "volume bar size" 195 + + label "Music" + center -129.5 -228 + color bright + line + center -117.5 -208.5 + dimensions 210 10.5 + color "volume bar background" + bar "music volume" + center -117.5 -208.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "music volume box" + center -117.5 -208.5 + dimensions 215 25 + visible if "music volume none" + sprite "ui/audio none" + center -238 -208 + visible if "music volume low" + sprite "ui/audio low" + center -238 -208 + visible if "music volume medium" + sprite "ui/audio medium" + center -238 -208 + visible if "music volume max" + sprite "ui/audio max" + center -238 -208 + + visible + label "UI" + center -129.5 -168 + color bright + line + center -117.5 -148.5 + dimensions 210 10.5 + color "volume bar background" + bar "ui volume" + center -117.5 -148.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "ui volume box" + center -117.5 -148.5 + dimensions 215 25 + visible if "ui volume none" + sprite "ui/audio none" + center -238 -148 + visible if "ui volume low" + sprite "ui/audio low" + center -238 -148 + visible if "ui volume medium" + sprite "ui/audio medium" + center -238 -148 + visible if "ui volume max" + sprite "ui/audio max" + center -238 -148 + + visible + label "Anti-Missiles" + center -129.5 -108 + color bright + line + center -117.5 -88.5 + dimensions 210 10.5 + color "volume bar background" + bar "anti-missile volume" + center -117.5 -88.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "anti-missile volume box" + center -117.5 -88.5 + dimensions 215 25 + visible if "anti-missile volume none" + sprite "ui/audio none" + center -238 -88 + visible if "anti-missile volume low" + sprite "ui/audio low" + center -238 -88 + visible if "anti-missile volume medium" + sprite "ui/audio medium" + center -238 -88 + visible if "anti-missile volume max" + sprite "ui/audio max" + center -238 -88 + + visible + label "Weapons" + center -129.5 -48 + color bright + line + center -117.5 -28.5 + dimensions 210 10.5 + color "volume bar background" + bar "weapon volume" + center -117.5 -28.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "weapon volume box" + center -117.5 -28.5 + dimensions 215 25 + visible if "weapon volume none" + sprite "ui/audio none" + center -238 -28 + visible if "weapon volume low" + sprite "ui/audio low" + center -238 -28 + visible if "weapon volume medium" + sprite "ui/audio medium" + center -238 -28 + visible if "weapon volume max" + sprite "ui/audio max" + center -238 -28 + + visible + label "Engines" + center -129.5 12 + color bright + line + center -117.5 32.5 + dimensions 210 10.5 + color "volume bar background" + bar "engine volume" + center -117.5 32.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "engine volume box" + center -117.5 32.5 + dimensions 215 25 + visible if "engine volume none" + sprite "ui/audio none" + center -238 32 + visible if "engine volume low" + sprite "ui/audio low" + center -238 32 + visible if "engine volume medium" + sprite "ui/audio medium" + center -238 32 + visible if "engine volume max" + sprite "ui/audio max" + center -238 32 + + visible + label "Afterburners" + center -129.5 72 + color bright + line + center -117.5 92.5 + dimensions 210 10.5 + color "volume bar background" + bar "afterburner volume" + center -117.5 92.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "afterburner volume box" + center -117.5 92.5 + dimensions 215 25 + visible if "afterburner volume none" + sprite "ui/audio none" + center -238 92 + visible if "afterburner volume low" + sprite "ui/audio low" + center -238 92 + visible if "afterburner volume medium" + sprite "ui/audio medium" + center -238 92 + visible if "afterburner volume max" + sprite "ui/audio max" + center -238 92 + + visible + label "Hyperdrives" + center -129.5 132 + color bright + line + center -117.5 152.5 + dimensions 210 10.5 + color "volume bar background" + bar "jump volume" + center -117.5 152.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "jump volume box" + center -117.5 152.5 + dimensions 215 25 + visible if "jump volume none" + sprite "ui/audio none" + center -238 152 + visible if "jump volume low" + sprite "ui/audio low" + center -238 152 + visible if "jump volume medium" + sprite "ui/audio medium" + center -238 152 + visible if "jump volume max" + sprite "ui/audio max" + center -238 152 + + visible + label "Explosions" + center 129.5 -228 + color bright + line + center 117.5 -208.5 + dimensions 210 10.5 + color "volume bar background" + bar "explosion volume" + center 117.5 -208.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "explosion volume box" + center 117.5 -208.5 + dimensions 215 25 + visible if "explosion volume none" + sprite "ui/audio none" + center 238 -208 + visible if "explosion volume low" + sprite "ui/audio low" + center 238 -208 + visible if "explosion volume medium" + sprite "ui/audio medium" + center 238 -208 + visible if "explosion volume max" + sprite "ui/audio max" + center 238 -208 + + visible + label "Scans" + center 129.5 -168 + color bright + line + center 117.5 -148.5 + dimensions 210 10.5 + color "volume bar background" + bar "scan volume" + center 117.5 -148.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "scan volume box" + center 117.5 -148.5 + dimensions 215 25 + visible if "scan volume none" + sprite "ui/audio none" + center 238 -148 + visible if "scan volume low" + sprite "ui/audio low" + center 238 -148 + visible if "scan volume medium" + sprite "ui/audio medium" + center 238 -148 + visible if "scan volume max" + sprite "ui/audio max" + center 238 -148 + + visible + label "Environment" + center 129.5 -108 + color bright + line + center 117.5 -88.5 + dimensions 210 10.5 + color "volume bar background" + bar "environment volume" + center 117.5 -88.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "environment volume box" + center 117.5 -88.5 + dimensions 215 25 + visible if "environment volume none" + sprite "ui/audio none" + center 238 -88 + visible if "environment volume low" + sprite "ui/audio low" + center 238 -88 + visible if "environment volume medium" + sprite "ui/audio medium" + center 238 -88 + visible if "environment volume max" + sprite "ui/audio max" + center 238 -88 + + visible + label "Alerts" + center 129.5 -48 + color bright + line + center 117.5 -28.5 + dimensions 210 10.5 + color "volume bar background" + bar "alert volume" + center 117.5 -28.5 + dimensions 195 0 + color "energy" + size 2 + reversed + box "alert volume box" + center 117.5 -28.5 + dimensions 215 25 + visible if "alert volume none" + sprite "ui/audio none" + center 238 -28 + visible if "alert volume low" + sprite "ui/audio low" + center 238 -28 + visible if "alert volume medium" + sprite "ui/audio medium" + center 238 -28 + visible if "alert volume max" + sprite "ui/audio max" + center 238 -28 + + + interface "preferences" button b "_Back to Menu..." center 195 210 dimensions 120 30 + value "master volume bar size" 220 + line + center 280.5 -97 + dimensions 10.5 235 + color "volume bar background" bar "volume" - from 280.5 15 - dimensions 0 -200 + center 280.5 -97 + dimensions 0 220 color "energy" size 2 + box "volume box" + center 280.5 -97 + dimensions 20.5 265 + + visible if "volume none" + sprite "ui/audio none" + center 281 35 + visible if "volume low" + sprite "ui/audio low" + center 281 35 + visible if "volume medium" + sprite "ui/audio medium" + center 281 35 + visible if "volume max" + sprite "ui/audio max" + center 281 35 @@ -1578,26 +1970,32 @@ interface "mission" bottom interface "map buttons" bottom right active if "!is shipyards" sprite "ui/wide button" - from -464 -50 to -354 0 + from -544 -50 to -434 0 button s "_Shipyards" - from -454 -40 to -364 -10 + from -534 -40 to -444 -10 active if "!is outfitters" sprite "ui/wide button" - from -364 -50 to -254 0 + from -444 -50 to -334 0 button o "_Outfitters" - from -354 -40 to -264 -10 + from -434 -40 to -344 -10 active if "!is missions" sprite "ui/dialog cancel" - from -264 -50 to -174 0 + from -344 -50 to -254 0 button i "M_issions" - from -254 -40 to -184 -10 + from -334 -40 to -264 -10 active if "!is ports" sprite "ui/dialog cancel" - from -184 -50 to -94 0 + from -264 -50 to -174 0 button p "_Ports" + from -254 -40 to -184 -10 + + active if "!is stars" + sprite "ui/dialog cancel" + from -184 -50 to -94 0 + button a "St_ars" from -174 -40 to -104 -10 active @@ -1653,9 +2051,9 @@ interface "map buttons (small screen)" bottom right active if "!is missions" sprite "ui/dialog cancel" - from -170 -40 to -80 -90 + from -170 -30 to -80 -180 button i "M_issions" - from -160 -80 to -90 -50 + from -160 -60 to -90 -150 active if "!is ports" sprite "ui/dialog cancel" @@ -1663,6 +2061,12 @@ interface "map buttons (small screen)" bottom right button p "_Ports" from -160 -40 to -90 -10 + active if "!is stars" + sprite "ui/dialog cancel" + from -170 -40 to -80 -90 + button a "St_ars" + from -160 -80 to -90 -50 + active sprite "ui/dialog cancel" from -90 -50 to 0 0 diff --git a/data/bunrodea/bunrodea weapons.txt b/data/bunrodea/bunrodea weapons.txt index 936fe1c69797..ebd6c98d8669 100644 --- a/data/bunrodea/bunrodea weapons.txt +++ b/data/bunrodea/bunrodea weapons.txt @@ -50,7 +50,7 @@ outfit "Locust Turret" sound "locust blaster" "hit effect" "bullet impact" "inaccuracy" 5 - "turret turn" 2 + "turret turn" 3.7 "velocity" 10 "random velocity" 2 "lifetime" 40 diff --git a/data/coalition/coalition weapons.txt b/data/coalition/coalition weapons.txt index e102910f87ca..0318d882d3c5 100644 --- a/data/coalition/coalition weapons.txt +++ b/data/coalition/coalition weapons.txt @@ -58,7 +58,7 @@ outfit "Bombardment Turret" sound "bombardment" "hit effect" "bombardment impact" "inaccuracy" 4 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 6 "random velocity" 1 "lifetime" 150 @@ -256,7 +256,7 @@ outfit "Heliarch Attractor" sound "heliarch attractor" "hit effect" "attractor impact" "inaccuracy" 2 - "turret turn" 2 + "turret turn" 3.6 "velocity" 640 "lifetime" 1 "reload" 1 @@ -299,7 +299,7 @@ outfit "Heliarch Repulsor" sound "heliarch repulsor" "hit effect" "repulsor impact" "inaccuracy" 3 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 560 "lifetime" 1 "reload" 1 @@ -339,7 +339,7 @@ outfit "Ion Hail Turret" "hardpoint offset" 10. sound "ion rain" "inaccuracy" 3 - "turret turn" 1.9 + "turret turn" 3.5 "velocity" 12 "reload" 15 "burst reload" 3 diff --git a/data/gegno/gegno I corroboration.txt b/data/gegno/gegno I corroboration.txt index 0e1caaf9a312..2c8a3ec574a6 100644 --- a/data/gegno/gegno I corroboration.txt +++ b/data/gegno/gegno I corroboration.txt @@ -351,7 +351,7 @@ mission "Gegno Suspicions" landing source "Giaru Gegno" to offer - "gegno: encounters" >= 3 + has "gegno: encounters" "gegno: encounters" < 6 not "Gegno Intervention: offered" not "Gegno Genocide Defense: offered" @@ -1016,6 +1016,7 @@ mission "Acquiescence" has "Gegno Intervention: offered" not "Gegno Anticipation: failed" not "Gegno Intervention: failed" + not "Gegno Genocide Defense: offered" destination "Tschyss" on offer diff --git a/data/gegno/gegno intro missions.txt b/data/gegno/gegno intro missions.txt index b822baa565b4..a46980d32fed 100644 --- a/data/gegno/gegno intro missions.txt +++ b/data/gegno/gegno intro missions.txt @@ -171,6 +171,8 @@ mission "First Contact: Gegno Vi" on defer set "First Contact: Gegno Vi: deferred" + on abort + to fail "reputation: Gegno" < -100 @@ -747,6 +749,9 @@ mission "Gegno Genocide Defense" fail "First Contact: Gegno Vi" fail "Visit Quarg in Gegno Space" fail "Vi Augen" + fail "Gegno Anticipation" + fail "Gegno Intervention" + fail "Acquiescence" "reputation: Gegno" = -1000 "reputation: Gegno Vi" = -1000 "reputation: Gegno Scin" = -1000 @@ -801,6 +806,27 @@ mission "Gegno Genocide Defense" ship "Augen" "Vytii Yrrlausei" +# Mission for if the player has clearance on a Gegno world despite having provoked civilians. + +mission "Gegno Unwelcoming" + landing + invisible + source + government "Gegno" "Gegno Vi" "Gegno Scin" + to offer + "reputation: Gegno" < 0 + on offer + conversation + `You are greeted by a large force of aliens on the surface of the planet, though they don't seem too happy to welcome you on their world - in fact, several squads in numbers you can't quickly determine rush towards you, equipped with advanced looking combat gear and weapons. As they do, countless ships begin to descend in formations from the sky above.` + to display + not "Giaru Gegno: Quarg Contact: offered" + `You are greeted by a large group of Gegno on the surface of the planet, though they don't seem too happy to welcome you on their world - in fact, several squads in numbers you can't quickly determine rush towards you, equipped with advanced looking combat gear and weapons. As they do, countless ships belonging to both the Vi and Scin factions begin to descend in formations from the sky above.` + to display + has "Giaru Gegno: Quarg Contact: offered" + ` It probably wasn't the smartest plan to purposefully land here after assaulting civilians. There's no room for talk as they immediately begin to attack, and you do your best to fight as many off as you can. While you are able to fend off several with your pistol, you are eventually overwhelmed, and the last thing you experience is a cold, sharp pain through your upper body.` + die + + # Gegno Hunter Fleets mission "Gegno Hunter Fleets" diff --git a/data/gegno/gegno outfits.txt b/data/gegno/gegno outfits.txt index 0c61e88a63e9..b937a45327b3 100644 --- a/data/gegno/gegno outfits.txt +++ b/data/gegno/gegno outfits.txt @@ -601,7 +601,7 @@ outfit "Irate Cannon Turret" "live effect" "irate smoke" 12 "hit effect" "tiny explosion" "inaccuracy" 2 - "turret turn" 2 + "turret turn" 3.6 "velocity" 22 "lifetime" 15 "reload" 10 @@ -682,7 +682,7 @@ outfit "Choleric Cannon Turret" "live effect" "choleric smoke" 24 "hit effect" "small explosion" "inaccuracy" 2 - "turret turn" 1.3 + "turret turn" 2.2 "velocity" 9 "lifetime" 55 "reload" 80 @@ -826,7 +826,7 @@ outfit "Astuit Battery" sound "astuit" "hit effect" "astuit hit" "inaccuracy" 5 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 8 "lifetime" 44 "reload" 6 @@ -865,7 +865,7 @@ outfit "Acuit Artillery" "hit effect" "acuit hit" sound "acuit" "inaccuracy" 3 - "turret turn" 0.7 + "turret turn" 1 "velocity" 18 "lifetime" 60 "reload" 60 diff --git a/data/governments.txt b/data/governments.txt index 9d9b7956a2b9..51beff62d5c3 100644 --- a/data/governments.txt +++ b/data/governments.txt @@ -1039,11 +1039,29 @@ government "Indigenous Lifeform (Astral)" destroy 0 atrocity 0 +government "Indigenous Lifeform (Subsidurial)" + # This indigenous gov is specifically for the Subsidurial, so that nothing in the game tries to hurt the poor thing. + "player reputation" 1 + "display name" "Indigenous Lifeform" + "bribe" 0 + "fine" 0 + + "penalty for" + assist 0 + disable 0 + board 0 + capture 0 + destroy 0 + atrocity 0 + government "Ka'het" swizzle 0 language "Ka'het" "default attitude" -.01 + "attitude toward" + "Author" 0 + "Indigenous Lifeform (Subsidurial)" 0 "player reputation" -1 @@ -1054,6 +1072,9 @@ government "Ka'het (Infighting)" language "Ka'het" "default attitude" -.01 + "attitude toward" + "Author" 0 + "Indigenous Lifeform (Subsidurial)" 0 "player reputation" -1 diff --git a/data/hai/hai outfits.txt b/data/hai/hai outfits.txt index aa443a0f4a5f..b5fc93ae3438 100644 --- a/data/hai/hai outfits.txt +++ b/data/hai/hai outfits.txt @@ -78,7 +78,7 @@ outfit "Pulse Turret" sound "pulse" "hit effect" "pulse impact" "inaccuracy" 1 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 15 "lifetime" 30 "reload" 10 @@ -208,7 +208,7 @@ outfit "Ionic Turret Prototype" sound "ion" "hit effect" "ionic impact" 5 "inaccuracy" 2 - "turret turn" 0.9 + "turret turn" 1.4 "velocity" 12 "lifetime" 70 "reload" 70 diff --git a/data/human/deep missions.txt b/data/human/deep missions.txt index 75a9e857d1af..bd46bdb314bb 100644 --- a/data/human/deep missions.txt +++ b/data/human/deep missions.txt @@ -2824,6 +2824,7 @@ mission "Stone of our Fathers 5" on visit dialog `You have reached , but your escort with the space reserved for Thorleif and Ragnhild has not arrived! Better depart and wait for your escorts to arrive in this star system.` on complete + event "stone waiting most" 30 payment 500000 conversation `The trip back to Norn was restful.` @@ -3945,7 +3946,7 @@ mission "Home for Skadenga 13" conversation `The Ondurdis bows to you when you arrive. "You have chosen good home for us."` scene "outfit/skadetear" - ` She has a small, wooden box with inlaid runes that looks very similar to the one you saw at Midgard. She opens the box to reveal a blue glowing stone with a single rune inscribed on it. "We call these Tears of Skade. We found few of them buried around Goddess-Stone of Nifel. They are always cold, and may be useful in warm ship." She hands you the box.` + ` She has a small, wooden box with inlaid runes that looks very similar to the one you saw at Asgard. She opens the box to reveal a blue glowing stone with a single rune inscribed on it. "We call these Tears of Skade. We found few of them buried around Goddess-Stone of Nifel. They are always cold, and may be useful in warm ship." She hands you the box.` choice ` "You honor me. Thank you."` goto honor @@ -3955,24 +3956,54 @@ mission "Home for Skadenga 13" decline label honor ` "No, it is you who honor us. We will not forget this. Now, I told you when we met I would answer question. What would you like to know?"` + label answers choice ` "How old are you?"` + goto age + to display + not "ages" ` "Who were the elves?"` goto elves + to display + not "elf" ` "Why were the Skadenga making amends?"` goto amends + to display + not "amending" + ` "This Tear of Skade, I've seen something like it before. Might you know more about these enigmatic stones?"` + goto enigmatic + to display + has "Ice Queen 6: done" + not "tears" ` "No. I am done here."` goto done + label age + action + set "ages" ` The Ondurdis' eyes sparkle mischievously. "Too old. Suffice to say, my name is Eymani, and I was Pathfinder who discovered cure to Rigellian plague. As to how I am so old, or all that I have seen: I cannot say more, for it is not my story to tell."` - goto done + goto answers label elves + action + set "elf" ` "Elves is broad term. Many creatures over long ages have been called elves. But, here, in Deep, I believe there are two kinds. First, there are alfar, known by many. They were ones who transported us here from Earth. I do not know why they did this, but I do know that it was not out of kindness. They are devious, and they are opaque."` ` The Ondurdis lowers her voice and leans in. "But there is another kind of elf here. The Hudulfolk, the Hidden. They served makers of God-Stones. They are rare, and mostly dormant. I do not think even alfar are aware of them. They cannot be trusted, for no alien can be, but are not devious. Each Hidden is different, but all are connected to natural world. When Skadenga were new upon Nifel, we had some small dealing with them before they returned to sleep of eons."` - goto done + goto answers label amends + action + set "amending" ` The Ondurdis nods slowly. "On Earth, before alfar, or elves, came to us, we had forsaken Skade. Men of Book had persuaded us to abandon old ways and embrace their new religion. We deserted our temples, forsook our trees, and thought ourselves the better for it."` ` Her old face scrunches up in anger at the memory. "When we were brought to Deep, when we saw Goddess-Stone, we realized our folly. We would atone, and we would be strong." She looks up at you, her eyes bright and her face firm. "And strong we have been."` + label enigmatic + action + set "tears" + ` "Less than I would like," she says softly. "Longer I live, more I learn how little I know. Before ships came, when we knew only ways of stone, there was different kind of power here. Fragile, gentle, stone-magic. Then came endless colonists, men from stars with ships that sailed night. Stone-magic failed and secret ways shut."` + ` She looks at you sadly. "These stones are relics from lost age. Stolen age." She pauses. "More I cannot tell, but if you bring me such stone I will look for you, and see if more I might know."` + goto answers label done + action + clear "ages" + clear "elf" + clear "amending" ` "That is enough whispering of old secrets." She straightens up and smooths her dress. "I have much to do, and I imagine so do you. Take care, . May Skade protect you." The leader of the Skadenga returns to her people on the windswept hills of the small, picturesque town of Hrithfjall.` scene "scene/sunset" ` With the conversation concluded, you wander back toward your ship as the sun sets. You hear the hustle and bustle all around you of eager people pursuing a new and exciting life. The promise of a new beginning is almost as intoxicating as the faint hint of sea-salt on the cool breeze that ruffles your clothes.` @@ -3998,8 +4029,8 @@ mission "Homecoming to Skadenga" ` (Don't visit Hrithfjall.)` decline ` It's a short journey to the outskirts of Hrithfjall. You are evidently spotted from afar, as someone is waiting for you.` - ` Hjlod's lopsided smile is warm and refreshing. "!" She says with genuine joy. "You did not say goodbye! How can I forgive?" You hug her. You walk together along the shore toward Hrithfjall. She tells you all about the growing village - how the Skadenga are thriving here, and how they've gotten along with the Ran-Folk.` - ` She points to a small wooden craft up ahead. "I built her." She says with pride. "No more Ice-Crawler for Hjlod. I am sailor now. I take curious tourist out now and again. Much safer than Ice-Crawler, except when we capsize." She shrugs and grins. "Perhaps someday you'll be my tourist again. This time, I promise no accidents. Also, there are no Ice-Slugs to worry about." She pauses for a moment. "There are Shark-Kraken though. But don't worry. Hjlod will take care of you."` + ` Hjlod's lopsided smile is warm and refreshing. "!" She says with genuine joy. "You did not say goodbye! How can I forgive?" You hug her. You walk together along the shore toward Hrithfjall. She tells you all about the growing village - how the Skadenga are thriving here, and how they've gotten along with the Ranfolk.` + ` She points to a small wooden craft up ahead. "I built her," she says with pride. "No more Ice-Crawler for Hjlod. I am sailor now. I take curious tourist out now and again. Much safer than Ice-Crawler, except when we capsize." She shrugs and grins. "Perhaps someday you'll be my tourist again. This time, I promise no accidents. Also, there are no Ice-Slugs to worry about." She pauses for a moment. "There are Shark-Kraken though. But don't worry. Hjlod vill take care of you."` ` You spend the afternoon in Hrithfjall, where you are treated like a guest of honor. The Ondurdis seems younger than you remember, and also appears quite fond of old Aldfrith. You eat strange fish, listen to unlikely stories, and laugh at outrageous jokes.` ` As the sun sinks in the sky, you announce that you should be heading back. "Wait," the Ondurdis says. "I have task for you. If you are interested."` choice diff --git a/data/human/free worlds 0 prologue.txt b/data/human/free worlds 0 prologue.txt index eca8368319bd..a969e02928b7 100644 --- a/data/human/free worlds 0 prologue.txt +++ b/data/human/free worlds 0 prologue.txt @@ -304,12 +304,12 @@ mission "FW Recon 0" ` "Are you connected to the Free Worlds?"` ` "Yes, I'm an officer in their militia." The man takes a half step back. "Is that a problem?"` choice - ` "Sorry, I really don't want to assist the rebels."` - decline ` "No problem at all. I'd be glad to help with your cause!"` accept ` "Not a problem, but I'm afraid I don't have space for you right now."` defer + ` "I don't help rebels. Find someone else."` + decline on visit dialog `You land on , but you realize that the militia officer is on one of your escorts that hasn't entered the system yet. Better depart and wait for it to arrive.` @@ -333,24 +333,22 @@ mission "FW Recon 1" `When you walk into the spaceport bar, a man in a militia uniform flags you down, and says, "Captain . One of my officers told me to keep an eye out for you. Would you be willing to do another small service for the Free Worlds?"` choice ` "Perhaps. What do you want me to do?"` - ` "Sorry, I want no part in your rebellion."` + ` "I only agreed to a transport job. I want no part in your rebellion."` decline ` "It's a rather simple matter," he says. "Most of the Republic ships that have come through this system seem to be outfitted for surveillance, not combat. But there is one in orbit right now, the , that seems to be equipped as a warship. Would you be willing to scan its outfits and report to us what you find? The outfitter stocks scanners if you don't have one, though you might want to install a few of them to improve performance. We will pay ."` choice ` "Sure, that sounds simple enough."` accept ` "Why can't you scan the ship yourself?"` - ` "Sorry, I don't want to assist rebels."` - decline ` "I'm sure you understand the... tensions between the Free Worlds and the Republic at the moment. We're just trying to look out for ourselves, but we don't want to provoke anything with the Navy. It'd be much simpler if we could get a sympathetic merchant to scan the ship for us."` choice ` "Sounds reasonable enough. I'll do it."` accept - ` "Sorry, but that sounds too risky for me to get involved in."` + ` "If shady jobs like this are what constitute 'helping the Free Worlds,' then count me out."` decline on accept - log "Volunteered to do some recon work for the Free Worlds, as long as there is nothing illegal involved." + log "Volunteered to do some recon work for the Free Worlds." npc "scan outfits" government "Republic" @@ -404,7 +402,7 @@ mission "FW Recon 2" choice ` "Sure, I'd be glad to do that."` accept - ` "Sorry, after the way this last captain responded to me scanning him, I'm not sure I want to risk it again."` + ` "Sorry. After the way the last captain responded to me scanning him, I'm not risking my head for the Free Worlds again."` decline npc "scan outfits" @@ -448,8 +446,8 @@ mission "FW Recon 3" choice ` "Okay, I'll do it."` accept - ` "Sorry, I've decided I don't really want to side with the Free Worlds."` - ` "I'm really sorry to hear that," says JJ. "We had high hopes for you."` + ` "This is too far. I'm not trespassing on Navy property for you."` + ` "I'm really sorry to hear that," says JJ. "We had high hopes for you." He leaves the bar.` decline @@ -942,7 +940,7 @@ mission "FW Escort 3" ` Tomek brings you to one of the spaceport bars, exchanging greetings with the patrons before sitting at the counter. "Here's the deal," he says. "One of our freighters, the , was just disabled in the system. We need you to fly there immediately, repair it, and bring it back here before it can be captured by pirates. Do you think you're up to the task?"` choice ` "Sure!"` - ` "Sorry, I'm just a merchant captain. The Free Worlds shouldn't be asking me to do combat missions."` + ` "Sorry, no. I'm a merchant captain with bills to pay. I'm not going to go search for your missing ships in the middle of nowhere."` decline ` "Glad to hear it," he says. "The crew of that freighter is depending on you. I know I don't have to tell you what will happen to them if pirates board them before you get there, so get moving quickly."` @@ -1057,7 +1055,7 @@ mission "FW Bounty 1" ` "How powerful of a fleet do these pirates have?"` ` "How much will you be paying me to take out this fleet?"` goto payment - ` "Sorry, I don't want to have anything to do with the Free Worlds."` + ` "I don't want to have anything to do with the Free Worlds."` decline ` "Well they aren't exactly a powerful fleet. They're just a collection of interceptors, but they're numerous and swift enough that we've had trouble taking them down before they can run or swarm our larger ships. We can pay you if you can take them out."` @@ -1275,7 +1273,7 @@ mission "FW Katya 1" choice ` "That sounds fair. I'll hear you out."` goto more - ` "Fair enough, but I don't like working with people who start conversations by pointing guns at my head. Even unloaded guns. Find someone else to do this job for you."` + ` "I don't like working with people who start conversations by pointing guns at my head. Even unloaded guns. Find someone else to do this job for you."` decline label terrorists @@ -1470,13 +1468,13 @@ mission "FW Katya Alt 2" `As you are walking through the spaceport, someone calls out, "Captain !" A Free Worlds officer is approaching you; you recognize her as the same woman who pulled a gun on you earlier as a way to "test" you. "I apologize for pulling that trick on you before," she says. "My name is Katya Reynolds. I've heard really good things about you from some other members of the Free Worlds Council, and I'm hoping you'd reconsider working with me."` choice ` "Okay, as long as you don't point any more guns at me."` - ` "Sorry, I just really don't like you. Or the Free Worlds."` + ` "No. I just don't like you. Or the Free Worlds."` decline ` "Great," she says. "Here's the deal. I have a friend named Mr. Eyes, who needs a lift from . For his safety, I'd rather not tell you anything about his work, but I assure you he's an upstanding citizen. A little unpopular with big corporations and certain corrupt local governments, though. Can you give him a lift?"` choice ` "Sure, I would be glad to."` accept - ` "Sorry, this sounds a bit too shady to me."` + ` "This sounds too shady. Find someone else to help you."` decline on visit @@ -1648,15 +1646,17 @@ mission "FW Katya 3" ` "That sounds like a terrible idea."` goto terrible - ` Eyes laughs, spitting out his milkshake in the process. "We won't be asking you to do anything of that sort," Katya responds somewhat aggressively. "Look, if we want to track down who dropped those bombs then we need to get our hands on better scanning equipment, equipment that the Navy has on their unmanned surveillance drones, and you're the one with a ship unconnected to the Free Worlds. All you need to do is find a lone surveillance drone to plunder. Legally the Navy can't do anything about it, and no one will be hurt in the process. If we succeed in finding who's responsible for the bombings, we could prevent war between the Republic and Free Worlds, saving thousands or even potentially millions of lives. So are you with us or not?"` + ` Eyes laughs, spitting out his milkshake in the process. "We won't be asking you to do anything of that sort," Katya responds somewhat aggressively. "Look, if we want to track down who dropped those bombs then we need to get our hands on better scanning equipment, equipment that the Navy has on their unmanned surveillance drones, and you're the one with a ship unconnected to the Free Worlds. All you need to do is find a lone surveillance drone to plunder. Legally the Navy can't do anything about it, and no one will be hurt in the process. If we succeed in finding who's responsible for the bombings, we could prevent war between the Republic and the Free Worlds, saving thousands or even potentially millions of lives. So are you with us or not?"` goto decision label terrible - ` "Are you saying we're going to need to find a different captain to work with?" says Katya. "That would be a big disappointment."` + ` "Are you saying we're going to need to find a different captain to work with?" says Katya. "If we want to track down who dropped those bombs then we need to get our hands on better scanning equipment, equipment that the Navy has on their unmanned surveillance drones. All you need to do is find a lone surveillance drone to plunder. Legally the Navy can't do anything about it, and no one will be hurt in the process. If we succeed in finding who's responsible for the bombings, we could prevent war between the Republic and the Free Worlds, saving thousands or even potentially millions of lives. So are you with us or not?"` label decision choice ` "Fine, I'll do it. But I still think it's an unwise idea."` - ` "I'm sorry, but this is a bit farther than I'm willing to go. Count me out."` + ` "Even still, that doesn't justify destroying Navy property. Count me out."` + decline + ` "If this is the sort of reasoning the Free Worlds runs on, then you can kiss goodbye to working with me ever again."` decline label agree @@ -1885,14 +1885,14 @@ mission "FW Katya 6" ` "Sure, I definitely want to help solve this case!"` goto sure ` "Why these worlds specifically? Why not check worlds in Free space if the Republic thinks you did it?"` - ` "Sorry, I'm through with helping the Free Worlds. I don't think we have a chance of beating the Navy to the punch."` + ` "I'm through with helping the Free Worlds. I don't think we have a chance of beating the Navy to the punch."` decline ` "I suppose that showing a lack of evidence for nuclear testing done on Free planets would be circumstantial evidence for us," Eyes says, "but we're specifically hunting for the culprit, so these are our best bets as far as nearby mining worlds go. And searching these three worlds would be quicker than searching all of Free space."` ` "Another thing we have to keep in mind is that we may not be able to operate outside of Free space forever," Katya responds. "I pray that we find answers before it comes to it, but war between the Free Worlds and Republic would cut us off from freely searching most of the galaxy."` choice ` "Okay, I'll help you search these worlds."` - ` "Sorry, but I don't think this is the best course of action. You'll have to find someone else to help you with this."` + ` "I don't think this is the best course of action. You'll have to find someone else to help you with this."` decline label sure @@ -1970,7 +1970,7 @@ mission "FW Katya 7" ` "Sounds like a good possibility. Let's give it a try!"` goto agree ` "But is part of the Free Worlds. What happens if we do find evidence there?"` - ` "Sorry, I've got some other missions I need to run instead of continuing this wild goose chase."` + ` "I've got other missions I need to run instead of continuing this wild goose chase."` decline ` "The plan hasn't changed," Eyes responds. "We find where the testing occurred, and that will lead us to the culprits. Even if it turns out that someone connected to the Free Worlds are responsible, they'll be turned over to the Republic."` diff --git a/data/human/human missions.txt b/data/human/human missions.txt index 987730e40cba..b38afc1f1745 100644 --- a/data/human/human missions.txt +++ b/data/human/human missions.txt @@ -7139,6 +7139,7 @@ mission "Adelita 1" and has "FW Katya 4: done" has "visited planet: Hope" + has "Stone to Hope: done" ` "I haven't been there myself, but I know people who have been affected by it."` to display has "FW Katya 4: done" @@ -9345,3 +9346,853 @@ mission "Timothy Radrickson 7: Grave" label end ` The shadows turn a darker shade of purple as you leave the graveyard of the forgotten dead for the familiar confines of your ship.` decline + + + +event "stone waiting most" + + +mission "Stone to Hope" + name "Bring Tosti's stone to " + description "Take Tosti's family stone to , where the Snaiwaz clan used to live before they immigrated to ." + to offer + random < 75 + has "Stone of our Fathers 5: done" + has "event: stone waiting most" + not "Ice Queen: offered" + source "Norn" + destination "Hope" + on offer + conversation + `There is a certain comfort to be found in the familiar swaying of Norn's spaceport. The creaking bridges no longer feel intimidating or threatening, only welcoming. There's never many staff here, but the few you do run into greet you warmly. Word has apparently spread that you've all but been adopted into the Hlewagasti family.` + ` Helm, with his sandy hair and short beard, approaches you with a young man in tow. "! It is well to see you again.` + ` "So..." he says conspiratorially, "... there is no date yet, but a certain red-haired stoic and his exotic Alfheimian girlfriend may be looking at wedding dates."` + choice + ` "That's great news!"` + ` "Do you know when?"` + ` "Who are you talking about?"` + goto who + ` "I don't really care."` + goto cold + + ` "Like I said, there is no date yet, and with those two it could be years before they finally land on a date. Time's different out on the algae islands." He shrugs.` + ` "But I thought you'd appreciate a small update on Thorleif." He smiles and then glances at the young man next to him. "Anyways, that's not actually why I sought you out."` + goto hopestone + + label who + ` "You don't remember Thorleif? My cousin out on the algae islands?" He frowns, but regains his composure and glances at the young man next to him. "Anyways, that's not actually why I sought you out."` + goto hopestone + + label cold + ` Helm frowns and takes a step back like he's been struck. After a moment he regains his composure and replies, "If you say so. In that case I have another matter to discuss with you."` + + label hopestone + ` Standing beside Helm is an unremarkable young man with a sunburnt face, calloused hands, and threadbare garb. "Ca-Captain ?" he says nervously, "I'm sorry to be a bothering you, ser. But I heard tell what you did for Thorleif, and I thought maybe you were the like to do a good turn by me."` + ` Sensing his discomfort, Helm puts an arm on the young man's shoulder. "It's alright, Tosti, we're all friends on Norn."` + ` The young man staightens up a bit at Helm's reassurance and looks you in the eyes. "My da, well, he liked the waves well enough, but never really settled here, you see? In his heart that is.` + ` "He never really left our old home. We were refugees and were ever so glad to be taken in by Norn, but he never felt so tall as when he thought about Hope, about home."` + choice + ` "What are you going on about?"` + goto finish + ` (Wait for him to get the point.)` + goto finish + ` "Leave me alone. I'm not interested."` + ` Crestfallen, the young man stumbles over his words attempting to apologize for the intrusion and slinks away from you in an ungraceful hurry.` + ` Helm watches him go with an unhappy frown upon his face. "You could have at least heard him out."` + ` He shakes his head and turns from you before saying, "I thought better of you."` + decline + + label finish + ` Tosti suddenly stops, and takes a deep breath. "We're from Hope, ser. My family. We're refugees and we had to leave everything behind. But da, he never really left Hope."` + ` The young refugee pulls out a stone criss-crossed in tiny chiseled letters. The familiar engraved story of a life now ended. "This is his stone, and well, he - he'd want his stone left on Hope. I was hoping that was something you could do."` + ` With the words he swallows and looks at you earnestly.` + label hopeoptions + choice + ` "I can take the stone to Hope."` + goto hopeyes + ` "I can take the stone to Hope, for a price."` + goto hopeprice + ` "I thought Family-Cairns were exclusive to the Deep. Why would there be one on Hope?"` + goto whyhope + to display + not "stones" + ` "Hire a courier. I have more important things to do."` + ` "No."` + + label hopeno + action + clear "stones" + ` Helm sighs and tries to pat the young man on the back before he entirely deflates. "Captain is a very busy man. I am sure we can find someone else to make the journey. There is no shame in asking for help."` + ` Tosti is red and shaking, and mumbles some apologies before slinking away from you and Helm.` + ` Helm gives you a nod and says, "Thank you for listening, at least."` + decline + + label whyhope + action + set "stones" + ` "Lots of the first folks who settled Hope came from the Deep." Helm answers, getting a far off look in his eyes. "It wasn't like most worlds in the south. It was going to be different. At least, that's what they believed."` + ` He shrugs, "Many of them didn't forget where they came from. Brought the old ways with them. Some refugees, like Tosti's family, even found their way back here when Hope was lost.` + ` "Anyways, that's why you can find Family-Cairns on Hope - because our people were there. Now, getting back to the matter at hand, you willing to help Tosti out?"` + goto hopeoptions + + label hopeprice + ` He grimaces, "I am sorry, ser. Work is hard to come by for us refugees. I thought - I thought I'd just ask. In case you were near. In case it was within your power is all."` + choice + ` "I might be in the area. No promises though."` + ` "Alright. I'll do it."` + ` "Sorry. I don't work for free."` + goto hopeno + + label hopeyes + action + clear "stones" + ` The young man's eyes light up. "Really? Truly? I didn't dare hope..."` + ` His body weight shifts and it looks as though he's about to hug you before he abruptly stops himself and tries to regain a little composure.` + ` "Thorleif was right about you," he says as he presses the small, cool stone into your hand. "The Family-Cairn is not far from where the last spaceport used to be. Just by the cliffs, and the family name was Snaiwaz."` + ` With the stone firmly in your fist, he thanks you one last time, and then takes his leave.` + ` Helm silently watches him go and then turns back to you, shaking his head. "You're not like any off-worlder I've ever known. Most would have laughed in that boy's face, let alone done him a good turn for free."` + ` He smiles at you and says, "You really are one of us."` + accept + + +mission "Stone to Ice Queen" + landing + name "Bring Ildico to " + description "You found a woman named Ildico Drifa in an abandoned manor on . She has asked you to bring her to so she can escape the abandoned planet." + source "Hope" + destination "Clark" + passengers 1 + to offer + has "Stone to Hope: done" + on offer + conversation "Ice Queen" + on complete + conversation "Ice Queen End" + + +mission "Ice Queen" + landing + name "Bring Ildico to " + description "You found a woman named Ildico Drifa in an abandoned manor on . She has asked you to bring her to so she can escape the abandoned planet." + source "Hope" + destination "Clark" + passengers 1 + to offer + has "Expedition to Hope 2: done" + not "Stone to Hope: done" + random < 5 + on offer + conversation "Ice Queen" + on complete + conversation "Ice Queen End" + + +conversation "Ice Queen" + branch "has stone" + has "Stone to Hope: done" + `There is nothing here. Only unceasing desolation. The slow churning of hateful ice that swallowed the dream of a world. Yet, here you are. You've landed along the equator, where the environment is not quite so hostile, and where patches of unconquered ice-free rock still reign.` + ` Beyond your ship are the ruins of the last unfrozen city. Where the final refugees huddled together awaiting their saviors, the Republic Navy, and were whisked away to other more welcoming worlds.` + choice + ` (Explore the area.)` + goto explore + ` (Leave this world.)` + ` There is nothing here anyone could want. You leave this abandoned world to its glaciers and ghosts.` + decline + + label explore + ` You break out the cold gear and dress yourself in warm layers with sturdy boots. Furtive frost cracks and breaks as your ship's door opens, and you step out into the cold air. This world's dim light reflects painfully off the ice and snow, momentarily blinding you.` + ` Once outside in the sharp, crisp air it's easy to find the dismal ruins of this world's last spaceport. The trek is slow and clumsy in the unfamiliar gravity and granular snow.` + ` You work around the silent spaceport, where the half-buried hangars are littered with the forgotten mementos of desperate refugees, and many of the buildings are lost beneath the encroaching ice. Eventually you see a cliff edge with dozens of cairns piled along it. Silent watchers of the churning glaciers.` + ` The sun begins to set, and you can already feel the temperature beginning to plummet. There's not much else to see in this abandoned world, and it may be time to head back.` + goto building + + label "has stone" + `There is nothing here. Only unceasing desolation. The slow churning of hateful ice that swallowed the dream of a world. Yet, here you are. You've landed along the equator, where the environment is not quite so hostile, and where patches of unconquered ice-free rock still reign.` + ` Beyond your ship are the ruins of the last unfrozen city. Where the final refugees huddled together awaiting their saviors, the Republic Navy, and were whisked away to other more welcoming worlds.` + choice + ` (Search for the Snaiwaz Family-Cairn.)` + goto cairnsearch + ` (Toss the stone out your hatch and leave this world.)` + + action + set "chucked tosti's stone" + ` The stone of Tosti's father tumbles out of the ship and comes to rest in a heap of cluttered refuse. This is as close as it will get to its Family-Cairn, and far closer than Tosti could have gotten it on his own.` + decline + + label cairnsearch + ` You break out the cold gear and dress yourself in warm layers with sturdy boots. Furtive frost cracks and breaks as your ship's door opens, and you step out into the cold air. This world's dim light reflects painfully off the ice and snow, momentarily blinding you.` + ` Once outside in the sharp, crisp air it's easy to find the dismal ruins of this world's last spaceport. The trek is slow and clumsy in the unfamiliar gravity and granular snow.` + ` You work around the silent spaceport, where the half-buried hangars are littered with the forgotten mementos of desperate refugees, and many of the buildings are lost beneath the encroaching ice. Eventually you find the cliffs that Tosti spoke of, and the many dozens of Family-Cairns piled along side it. Silent watchers of the churning glaciers.` + ` You see a nearly toppled Family-Cairn with the name "Snaiwaz" at its base.` + choice + ` (Place Tosti's father's stone.)` + goto placestone + ` (Throw the stone off the side of the cliff.)` + + action + set "threw tosti's stone" + ` You hurl the stone far beyond the cliff side and down into glacial slopes beneath it, forever separated from its kindred-stones.` + decline + + label placestone + action + set "placed tosti's stone" + ` Carefully you place Tosti's stone with the others. The act is greeted only by the deafening silence and the icy air of the abandoned world.` + ` The sun is setting and your task here is done. You start making your way back through ice and ruin to your ship.` + + label building + ` As you pass around one corner of the sagging spaceport, you notice a large, ice-free building in the distance with a light burning within.` + choice + ` (Enter the building.)` + goto litbuilding + ` (Keep heading toward your ship.)` + + ` Whatever is going on in that building, it has nothing to do with you, and so you ignore it and return to your waiting ship.` + decline + + label litbuilding + ` You enter into the sturdy and unfrozen building at the edge of the spaceport.` + ` Faded wealth still clings to peeling walls of this once spacious manor. It is evident that it was once the heart and hearth of a wealthy family in an earlier, warmer time. The home feels like a tomb or a museum; it seems as if everything here has been left in a certain way for a long time. But as you work deeper into its nested walls you begin to see signs of much more recent life.` + ` Eventually you find your way to the kitchen, which appears to be the epicenter of activity in this household. You see assorted survival rations and gear strewn about everywhere. More alarmingly, you also see a young woman sitting on the floor with her back against a counter.` + choice + ` "Who are you?"` + ` "Are you okay?"` + ` "What are you doing here?"` + + ` The woman has short jet black hair. She's wearing a military grade, all-purpose survival suit, the kind you might see on deeply embedded special forces. The only weapon on her is a decorated knife at her belt. She looks pale and frail, probably malnourished. But her icy eyes are clear, and she locks them onto you.` + ` "You come into my home, and expect me to answer your questions?" Her voice is steady.` + ` Slowly, awkwardly, she pulls herself to her feet, keeping one hand on the counter for balance. She rests her other hand over her knife.` + ` "You are lucky that you did not barge in here a year ago, or even a month ago." Despite her bravado there is a deep wariness behind her words. "Now tell me, who are you, and what are you doing here?" she asks, through gritted teeth.` + choice + ` (Tell her who you are, and how you were just delivering a stone.)` + to display + has "Stone to Hope: done" + ` (Tell her who you are, and how you were just out exploring.)` + ` (Tell her it's none of her business.)` + + ` The woman nods slowly, and then painfully, slides back down to the ground.` + ` Her eyes fall to the floor. "I have no choice but to trust you. I have lived here all my life, but my equipment has failed. I never wanted to leave, I was never meant to leave, but I won't let the ice win. I won't let it take me, too."` + ` She looks back up at you with her piercing eyes. "I am Ildico, last of House Drifa, daughter of Deoric, once one of the most powerful men on Hope. Everyone fled the ice. The Republic Navy made it easy. Enter their ships and be shipped off to paradise, to live as nobodies at the sufferance of strangers.` + ` "But we were House Drifa. More than anyone else we built this world, and we would not abandon it to be someone else's dredges. My father bought supplies, equipment, necessities from the Navy. Enough so that me, my brother, and my mother could survive here."` + ` She pauses, as though unsure she wants to continue; then finally, in a less firm voice: "And then my father left. He said that dozens of worlds, worse off than ours, were terraformed every day. He said all they needed was a little convincing, and that he would come back with the support to terraform, and save, Hope. And then... he never came back."` + ` Ildico sighs. "A treasure hunter killed my brother a few years back. My mother... she is also dead, and I am starving. I have no way to contact anyone else, and no one else is left.` + ` "So, what do you say," she says while leaning her head back, clearly exhausted by this exchange. "Will you kill me? Take advantage of me? Save me? Or leave me... to die?"` + choice + ` "Come with me. It's time you gave up on Hope."` + goto comewithme + ` "I'll get you out of here. After that, no promises."` + goto comewithme + ` "I don't have time for wretches. You're on your own."` + ` "How about I kill you, and take your stuff?"` + + ` Ildico, her eyes still closed, makes a swift motion with her hand and flicks her wrist toward you. Before you have a chance to react, her knife is embedded deep in your throat.` + ` You're on the ground now, with ruby red blood oozing out over the cold floor. You hear a strange moan escaping from your mouth, and Ildico's hands are in your pockets removing everything you have of value.` + ` As the world fades to black, you hear her talking, her words sound as though they are coming from a great distance. "Like I said, I won't die here. Thanks for the ship."` + ` Your vision grows darker until only a single point of light remains. You don't feel cold, or pain, or anything at all. You just feel heavy. So heavy. Too heavy. So, you let go.` + die + + label comewithme + ` You help her back onto her feet, and with her leaning on you, the two of you make your way back outside and through the perilous cold to your ship.` + ` Once aboard, after she's eaten and regained a little of her strength, she comes to talk with you.` + ` "Thank you for saving me. There are places I need to go. But I know I cannot force you to take me. And I have no credits to pay you. If you could take me to , I could make my own way from there."` + choice + ` "Anywhere you need to go, I can take you."` + goto wheresheneedstogo + ` "Okay, I'll take you to , but after that you're on your own."` + accept + + label wheresheneedstogo + ` Ildico narrows her eyes. "Nothing is free."` + choice + ` "I just want to make the universe a better place."` + ` "It's karma. What goes around comes around."` + ` "One day you'll be able to pay me back."` + ` "It's how they do things on Norn."` + to display + has "Stone to Hope: done" + ` "My reasons are my own."` + + action + set "helpildico" + ` Ildico doesn't react for a moment, seemingly taking in your words.` + ` At length she nods slowly. "We'll see."` + ` She pauses again and then shrugs. "It's not like I have anything to lose... very well. I need to discover what happened to my father. I know he first went to Clark to meet with a terraforming firm there. That's where I start my search."` + launch + +conversation "Ice Queen End" + branch helpildico + has "helpildico" + `Your thrusters kick up fresh dust as you land on your assigned concrete pad in the sprawling spaceport. Ildico, looking less pale and significantly more sturdy, waits by the exit.` + ` "Thank you for getting me out of there. After all those years on the ice this place will be no challenge for me. Perhaps someday our paths will cross again. If they do, I will remember this small kindness."` + ` She gives you a stern nod, and then exits your ship and into the chaos of Clark.` + goto end + + label helpildico + `Your thrusters kick up fresh dust as you land on your assigned concrete pad on the fringes of the sprawling spaceport. Ildico, looking less pale and significantly more sturdy, waits by the exit.` + ` "The terraforming firm is certain to have an office, or booth, in the ramshackle spaceport here. Let's pay them a visit."` + label end + + +mission "Ice Queen 2" + name "Find Ildico's Father" + description "Look for Ildico's father on , where he went after he left Clark." + source "Clark" + destination "Glory" + passengers 1 + to offer + has "helpildico" + or + has "Ice Queen: done" + has "Stone to Ice Queen: done" + + on offer + conversation + `Pushing through the boomtown hustle and bustle, you and Ildico make your way to a series of kiosks, booths, and hastily constructed offices that serve a variety of needs and functions in the ever-growing world of Clark.` + ` Among these you find the offices of "Clark's Third Law Terraformists." Ildico steps in front of you and enters first.` + ` An older man with greying hair and a bit of a paunch looks up from his desk. He stands up genially as both of you approach and says, "Howdy friends, I am Eustace Goodfellow, and welcome to our cozy establishment. Do you have need of a little terraforming?" Eustace flashes a wide open smile.` + ` "Yes. I'd like to terraform the planet Hope." Ildico says flatly.` + ` "Well, uh, ma'am. We don't, that is to say, we're not that kind of terraforming firm. We do small jobs. Touch ups. We can't... we can't terraform a world, and I don't..." Eustace looks at both of you carefully before saying, "I don't think you could pay for it either."` + ` Ildico leaps over the desk and grabs Eustace by his collar. "Is that what you told my father, Lord of House Drifa? Did you send him away like some misbegotten pauper?"` + ` Eustace pales and holds out his hands. "Please, let me go. I meant no insult."` + choice + ` (Don't interfere.)` + ` (Tell Ildico to calm down.)` + ` Ildico releases his collar, but remains only inches from his face. "Did Deoric Drifa come here years ago asking to terraform Hope?"` + ` Eustace takes a step back, and nods vigorously. "Yes, yes. I remember him. You don't forget a man like him, or a request like his. He asked about terraforming Hope. As I said earlier, we aren't that kind of firm. I told him he'd have to talk to the Academy of Planetary Sciences on for that kinda work. And then he left.` + ` "Please." Eustace says, backing up further. "That's all I know about him. I never saw him again."` + ` Ildico stares at him for a few more moments, and then turns and passes you on her way to the exit.` + ` She opens the door and nods to you. "We're going to ."` + accept + + on complete + conversation + `To some Glory is a testament to human ingenuity, skill, and potential. It is the dream made real, the promise proven true. A world perfectly sculpted to suit the needs, desires, and tastes of humanity. Nothing here is left to chance; every inch carefully, artfully, manifested by man's will.` + ` And yet, to others, it is grotesque, a mockery of nature. A lie made into a monument. As you deal with the Republic spaceport officials you notice the plastic-perfect denizens walking around, staring at your defective face and your workworn ship.` + ` "There is no beauty here," Ildico says vacantly when you're finished with the officials. "Only vanity."` + ` The spaceport's transport terminal has a direct rail link to the Academy of Planetary Sciences. Perhaps you could find out what happened to Ildico's father in the Academy.` + + +mission "Ice Queen 3" + name "Find Ildico's Father" + description "Professor McGiven told you that Ildico's father now works for the Republic on . Bring Ildico there to see if you can visit him." + to offer + has "Ice Queen 2: done" + source "Glory" + destination "Chiron" + passengers 1 + on offer + conversation + `The Academy of Planetary Sciences is a campus the size of a small nation-state on Earth. Various artificially created and maintained microclimates abound as you travel between miniature mountains and micro-savannahs; between marshland and jungle, desert and forest. Impossible machines - some larger than skyscrapers - litter the twisted and ever-changing landscape, and terraforming satellites make meteor showers overhead in the dawn light. This is a place where anything is possible, and land bends eagerly to the whims of the terraforming wizards.` + ` Eventually, Ildico and you make it to the primary reception complex, where after hours of filling out inane forms, and waiting in different meaningless queues, you finally reach someone who can help you. An old visitor list shows that one Deoric Drifa had visited a Professor Ian McGiven, Second Degree Terraformer and World Warming specialist in JZ86 Cantos-B of Cooling Block 9 in the Everest Facility.` + ` The receptionist helpfully draws you a map to where you're going. She also lets you know that in order to visit him you'll need to fill out six other forms first. You get to work.` + choice + ` (Fill out the forms.)` + choice + ` (Fill out more forms.)` + choice + ` (Continue filling out forms.)` + + ` The receptionist brings a few of the earlier forms you filled out and helpfully points out a few mistakes for you to fix.` + choice + ` (Fix the forms you already filled out.)` + choice + ` (Fill out the final set of forms.)` + + ` A few painfully tedious hours later, all the right forms have been filled out and you're met by an intern in a small vehicle.` + ` She glances at her electronic clipboard. "Captain ? Lady Drifa? I am here to escort you to the Everest Facility." She opens the door and gestures to you two. "Please climb aboard. I know the confines are quite small, but the E.F. is only a few kilometers away."` + ` Ildico glares at the young woman suspiciously, then turns to you. "Can we trust her?"` + ` The intern looks confused and a little hurt, but says nothing.` + choice + ` "We can trust her. We're in no danger here."` + goto nodanger + ` "I'll protect you. Don't worry."` + goto protect + ` "We have no choice if you want answers."` + ` Ildico nods and then enters the vehicle and you follow behind her.` + goto ridealong + + label nodanger + ` Ildico barks out a laugh. "So naive. We are always in danger. But for answers I will risk it."` + ` She carefully checks the inside of the vehicle before carefully climbing in.` + choice + ` (Follow her in.)` + goto ridealong + + label protect + ` Ildico laughs heartily and sheds a couple tears. "You? Protect me?" She wipes her tears as her mirth subsides and her cold intense coutenance reasserts itself.` + ` "No, . I will protect you." Her voice is deadly serious. She then inspects the inside of the vehicle carefully and takes a seat. "It seems safe. And I need answers."` + choice + ` (Follow her in.)` + + label ridealong + ` After a brief ride through the campus you are escorted deeper and deeper into the maze that is the Everest Facility. Another half an hour of hallways, and turns, and stairs and ups and downs, you finally find yourself inside Ian McGiven's office.` + ` The man hears your inquiry and nods earnestly. "Ah, yes, of course I remember the fellow! What an interesting meeting that was," Ian says from behind his small desk.` + ` "Obviously, the details are confidential, but in very broad generalities, he wanted me to warm up a freezing world."` + ` Ildico doesn't blink; on second thought, you're not sure you've ever seen her blink. "Hope. Yes, and could you warm it?"` + ` Ian laughs. "Oh, of course, a job like that would have been quite simple."` + ` "Then why didn't you?" The tension is obvious in Ildico's voice.` + ` Ian shrugs. "Though he could afford it, there were other concerns."` + ` Ildico steps forward and looms over Ian like an angry shadow. It appears that it is taking all of her willpower to keep her from ripping this man apart. "Concerns?"` + ` "Y-yes. I don't know the specifics. You have to understand, here at the Academy it's not just about money. It's about politics, and power. I am just a Second Degree Terraformer, and after the initial meeting I wasn't privy to the details. But someone, or some government or corporation didn't want it. If you want to know more you'll have to ask him yourself."` + ` Ildico seems to deflate. "Ask him? Is... is he here?"` + ` The terraformer shakes his head. "Oh, no. He doesn't work for the Academy. But after the rejection he went to the heart of the Republic, I assume to find out why. He now works for them. I see his name from time to time. Just head to the the Republic's main office for the Department of Migration, which is located on Chiron."` + ` Without saying another word, Ildico turns and leaves. You thank Ian for his help and follow her out.` + accept + + +mission "Ice Queen 4" + landing + invisible + source "Chiron" + passengers 1 + to offer + has "Ice Queen 3: done" + on offer + event "deoricdeeds" 700 + conversation + `It's easy to imagine Earth once looked and felt much like Chiron does today. A vibrant world, full of engaged and motivated people, all in a hurry to change the universe - or at least, to run it.` + ` The Department of Migration, Stellar Populations and Advanced Interplanetary Economics is easy enough to find. What's surprising is how quickly you are ushered into a waiting room outside the office of one Mr. Drifa, Sub-Executive of Multiplanetary Pricing.` + ` After a brief wait, a young secretary announces that Mr. Drifa is ready to see you, and brings you and Ildico into his office.` + ` "So what can I help you with..." Deoric Drifa's voice trails off as he sees Ildico.` + ` Ildico tries to step forward than stops herself. She freezes up.` + ` "It can't be," the elder Drifa whispers.` + ` "Why. Didn't. You. Come?" Ildico forces out each word.` + ` "I sent for you. They told me you were all dead. They found bodies... and the generator blown."` + ` "The accident..." Ildico says more to herself than anyone else. And then in that instant she unfreezes, emotions roil through her, and she runs to her father, with everything forgiven.` + ` After a long tear-filled embrace she disengages and wipes her eyes. "Early on the generator blew and we had to evacuate the compound. Some people died... including Hogne." She glances at you and says, "He was my older brother."` + ` She continues, "I salvaged what I could and set up at home. I've been there the whole time."` + ` "It's my fault," Deoric says. "I should have gone and looked for you myself. I would have known where to look."` + ` They talk about family, the time apart, and what happened back on Hope. Finally, Ildico turns her attention back to you.` + ` "Thank you for bringing me here, . For sticking with me. I appreciate it." She gives you the first real genuine smile you've seen on her face.` + choice + ` "No problem, Ildico."` + ` "You're welcome, Ildico."` + + ` She looks at you for just a moment, as if in thought, and then says, "If we ever meet again, call me Ildy. It's what my friends call me."` + ` With that you take your leave.` + decline + + +event "deoricdeeds" + + +mission "Ice Queen 5" + landing + name "Visit Ildico" + description "Ildico has asked you to visit her on ." + source + government "Republic" "Free Worlds" "Syndicate" "Neutral" "Independent" + destination "Chiron" + to offer + has "event: deoricdeeds" + + on offer + dialog + `Your inbox blinks with a new message from Ildico on Chiron:` + `` + ` "I need to see you. Please come. My address is attached.` + ` -your friend` + ` Ildy"` + + on complete + dialog + `The myriad lights of urban settlements twinkle in the dusk as you approach the capital, and main spaceport, of Chiron. Crickets, brought from old Earth, chirp in the twilight air. Your ship parked, you'll need to make your way to the main spaceport to hire transportation to Ildico's address.` + + +mission "Ice Queen 6" + name "Take Ildico to " + description "Smuggle Ildico and her child to ." + source "Chiron" + destination "Haven" + passengers 2 + illegal 1250000 `Ildico Drifa is dragged away in chains for high crimes against the Republic. You can only watch in silence as they are removed from your ship and her infant child is taken into custody. You end up fined for your actions.` + stealth + to offer + has "Ice Queen 5: done" + + on offer + conversation + `The directions Ildico gave you take you beyond the bustling city and out into the the large estates owned by the Republic political elite; Drifa Manor sits atop a low hill with what must be a very expensive view of New Sydney. The architecture of the building is reminiscent of upper-class villas you've seen in the Deep.` + ` As you approach the main gate you find it wide open, and the attached security booth is abandoned. You could just walk right in.` + choice + ` (Walk right in.)` + goto walk + ` (Investigate before entering.)` + ` The gate itself appears to have been opened from the inside, not broken into, and there are no signs of a struggle in the security booth. The employee parking beyond is also completely empty. It seems everyone left fast and didn't bother to close the gate behind them. You're entirely alone out here.` + choice + ` (Walk right in.)` + goto walk + ` (Leave.)` + ` Whatever this is; you decide you aren't interested. Ildico will just have to fend for herself.` + decline + + label walk + ` The spacious building is eerily empty of servants. Doors are left often, incomplete tasks clutter the floor. Whatever happened here, the people left in a hurry.` + ` There's a loud crashing noise in the floor above you. You ascend the central grand staircase following the noise.` + ` There are streaks of crimson blood across the opulent decor and a pool of it settles in the center of the room. Lavish baroque furniture is stained with the carnage.` + ` At the center of it all is a ravaged dead body on the floor, and above it stands a bloody, grinning Ildico.` + choice + ` "Ildico, what's going on?"` + ` "Are you hurt? Do you need help?"` + ` "What have you done?"` + ` (Say nothing.)` + + ` She ignores you, her attention focused elsewhere.` + ` "You thought," Ildico says as she catches her breath, "that you could sell me? That you could give me to this bureaucrat who signed Hope's death warrant?" She kicks the lifeless body at her feet. "That I would be your pawn, your tool, your object in order to gain favor and promotion?"` + ` The raven-haired woman laughs, as she cleans off her bloody dagger, and takes a step toward a figure cowering in the corner. The man you recognize as her father.` + ` "Wh-what have you done? You've ruined everything. I was so close. I would have had it all." Deoric blinks, his face sets in hard, and he straightens up, he looks as if he just remembered who he is.` + ` "You stupid, selfish, arrogant brat. You just needed to do as you were told. To play your pathetic part. You couldn't, could you? You were always worthless. If only your brother had survived." Deoric picks up a broken furniture leg and confidently wields it like a club.` + ` At this Ildico's wicked grin widens deeper. "My brother? Dear Hogne. Father, I killed him. He couldn't handle the ice, the isolation, the hopelessness of Hope. He lost his mind. He tried to make me his wife. And mother? She cracked long before that. An entitled debutant like her had no chance in that hellhole, and I? I needed to eat."` + ` The color drains out of Deoric's face. "Monster." He says. "I will put an end to you. I will start a new family, as I should have before."` + ` Deoric lunges at Ildico, but before he can take more then a single step, Ildico's wrist flicks and her knife slams into Deoric's throat. He falls to the ground, bleeding out, dying.` + ` Ildico stands over him. "You are a coward. A miserable disgusting man." She spits on him, and pulls her dagger from his heaving neck. He tries to speak as his dying body squirms, but no words materialize and he goes still.` + ` She turns to you. "I am now a fugitive, and you are my only friend. I need you to get us out of here."` + ` Ildico walks back to corner far from any of the fighting, and retrieves a bundled baby you'd neglected to notice until now.` + choice + ` "You got it. Let's go."` + ` "Anything for you, Ildy."` + ` "Sure, but you owe me. Big."` + ` "No way. You're a murderer. I want nothing to do with you."` + goto leave + ` "Stop right there. I am putting you under citizen's arrest until the police arrive."` + goto arrest + + ` She nods, her face softens, and tears glitter around her eyes. "Thank you, I knew- I knew I could count on you."` + ` Ildico recollects herself for a moment before pointing to a briefcase lying by a table. "Grab that, and let's go."` + choice + ` "Go where?"` + goto where + ` "What's in the briefcase?"` + ` The recently widowed woman gives you a suspicious look, but then nods. "What I need to survive.` + ` "Now grab it, and let's go."` + choice + ` "Go where?"` + goto where + ` (Grab the briefcase.)` + ` "We need to go where the Republic cannot. Take us to ." She gives you a serious look and adds, "And do not let anyone scan your ship until we get there."` + ` The three of you escape out into the night and onto your ship.` + launch + + label where + ` She sighs. "Not the Republic, nor the Free Worlds. After tonight I will be hunted. We need to go to ." She looks at you seriously and says, "And do not let anyone scan your ship until we get there."` + ` The three of you escape out into the night and onto your ship.` + launch + + label arrest + ` Ildico puts her baby back down, and for just a second you see agony in her eyes. Killing her father or her arranged husband didn't break her mask, but somehow this does. A tear rolls down her cheek. She moves, too fast for you to react, and her dagger is in your throat.` + ` You lay on the ground, bleeding out, dying. Ildico standing above you, a broken expression on her face. "I thought I had one friend. What a fool I was. I'll never make that mistake again."` + ` She leaves you to die. The world grows dim, and hazy. You remember... you remember... someone...` + die + + label leave + ` Ildico puts her baby back down, and for just a second you see agony in her eyes. Killing her father or her arranged husband didn't break her mask, but somehow this does. A tear rolls down her cheek.` + ` Her hand wavers, trembles, over her knife, and then she drops it, and her head. "So then go."` + decline + + on complete + outfit "Blood of Surtr" + payment 500000 + event "pirateildy" 611 + conversation + `Having evaded Republic patrols on the lookout for the "Murderous Heiress," you make it to the pirate planet of Haven. Ildico watches the dismal landscape as you land and says, almost curtly, "It'll do."` + ` She opens the briefcase she took off Chiron, revealing a storage device and an oranate wooden box.` + ` Ildico gives you a sly smile. "It took years, but the majority of the Drifa fortune has been smuggled into this storage device. They thought I was meek, malleable, and pitiful. Not a threat. They were fools."` + ` She holds up the box. "And in this, well, this may take some explaining. Before we migrated to Hope, the Drifa were old nobility in the Deep. Legend says we ruled Valhalla before the first ships came. Whatever the truth, my ancestors had this in their possession as a sign of their royalty, this bauble."` + ` Ildico opens the rune-carved lid of the box, revealing a faintly glowing red rock. "It is called the Blood of Surtr, and it was said to have been the symbol of our rulership, and more importantly, a source of power."` + ` She shrugs. "I know it sounds far-fetched. But I had this with me on Hope, and I don't think I would have survived without it. When the generators went down, this kept the lights on.` + ` "But its time with me is over. I want you, the last friend of House Drifa, and the only person who has shown me any kindness in many years, to have it.` + scene "outfit/surtrblood" + ` "I'll also transfer a small sum of money for your troubles. I fear I will be needing most of my family's fortune, but hopefully what little I can offer will help."` + choice + ` "Thank you."` + goto "gotta go" + ` "Is it wise for you and your child to be left alone in a place like Haven?"` + + ` She laughs. "After surviving on Hope and Chiron? Haven will be a cakewalk."` + goto end + + label "gotta go" + ` "No, it is my appreciation that has to be given. You have done what few would have the conviction to do."` + + label end + ` She takes her child in one hand, and the briefcase in the other, and walks off your ship.` + + +event "pirateildy" + + +mission "Ice Queen 7" + landing + name "Meet Ildy on " + description "See what Ildico needs on ." + source + government "Republic" "Free Worlds" "Syndicate" "Neutral" "Independent" + destination "Haven" + to offer + has "event: pirateildy" + on offer + dialog + `Your inbox blinks with a new message from Ildico from Haven:` + `` + ` "It's been too long, and I could use your help. Please, come to .` + ` - Your friend,` + ` Ildy"` + + on complete + dialog + `Even from orbit, Haven looks ugly, and as you approach the planet it doesn't get any better. Upon landing you're alerted Ildico is waiting for you in the spaceport.` + + +mission "Ice Queen 8" + name "Defeat Ildico's rivals" + description "Ildico has requested you to destroy a fleet of Marauder ships near the system. Once you're done, return to ." + source "Haven" + to offer + has "Ice Queen 7: done" + on offer + conversation + `You arrive at the address Ildico gave you; a large warehouse on the edge of the spaceport. When you step into the dimly lit building, your eyes struggle to adjust, and gradually you realize you're face-to-face with a group of dirty burly men and women leveling a half-dozen weapons right at you.` + ` "Looks like someone's lost," a voice rises from the group, though you can't tell who's speaking.` + ` "They look right fancy. Good thing we found them."` + ` "Whaddya say, Frank, kill 'em now? Or keep 'em for a bit? Maybe their worth something? Maybe we can have some fun with 'em."` + choice + ` "I am looking for Ildy."` + ` "Ildy asked me to come here."` + ` "Ildy, help!"` + ` (Grab your sidearm and shoot your way out.)` + goto why? + + ` The band of cut-throats looks at you, confused, and then most of them lower their weapons.` + ` "Boss," one of them hollers, "this stranger says they're here for you, I think."` + ` Footsteps echo on the opposite side of the warehouse as someone approaches.` + ` One of the men starts laughing. "Ildy?! Haha, never thought to call her that. Hey Ildy! How come you never told us to call you that? Or that some kind of special pet name?"` + ` The man flashes an unsavory grin at you, with his rotting teeth on full display.` + ` In the dim light the footsteps materialize into a woman in her mid-thirties with hair darker than vacuum. Ildico Drifa gives you a warm smile, then lifts up a gun and fires.` + choice + ` (Duck.)` + goto duck + ` (Jump to the side.)` + goto jump + ` (Do nothing.)` + ` You don't flinch as the rotten-toothed man besides you crumples to the ground, a bullet hole in his head.` + goto onlyfriends + label jump + ` You jump to the side, knocking against some empty barrels, only to realize she wasn't aiming at you. The rotten-toothed man lies dead on the ground, a bullet hole in his head.` + goto onlyfriends + label duck + ` You duck, startled by the attack, only to realize she wasn't aiming at you. The rotten-toothed man lies dead on the ground, a bullet hole in his head.` + label onlyfriends + ` She looks to the rest of her crew, "Only my friends call me Ildy. And I have only one friend."` + ` "Yes, boss."` + ` "Got it, boss."` + ` "Sure thing, Cap."` + ` "Whatever you say, boss."` + ` The group of pirates melt away into the warehouse, one of them dragging the dead body, leaving you and Ildy relatively alone.` + ` "I'm glad you came, ." Ildico puts a friendly hand on your shoulder and squeezes.` + ` She turns and waves toward the warehouse. "This is my temporary headquarters. I have few other safe-houses, and I own part of a hangar elsewhere. Not too many ships yet, but that will change soon."` + ` Ildico holsters her gun right next to where she keeps her ornate dagger and continues. "But making moves is hard. Everyone wants to be on top. Which is why I need you."` + ` She looks you in the eye and says, "There's a rival gang right next door. They have a lot more firepower, but if they could be removed, we could easily double our territory.` + ` "I don't have the ships," she glances at her gang in the distance, "or the quality of crew, to deal with them.` + ` "But I think you do. I can't afford to pay you right now, things are pretty fluid, but I'll owe you. So, are you in?"` + choice + ` "Sure, I'll help."` + ` "Fine. But you're going to owe me big when this is all over."` + ` "Anything for you, Ildy."` + ` "Not interested."` + goto uninterested + + ` Her smile lights up the dim room. "I always know I can count on you. And only you."` + ` "I fed the gang some false information. They have their entire fleet hunting some juicy merchant ships that don't actually exist, so they'll be surprised when they find you instead."` + ` "Check out the nearby systems and look for their flagship, the . Kill them all, and come back here. To me."` + accept + + label uninterested + ` Ildico frowns. She looks down at her feet for a moment, and when she looks back up her face is a mask of rage and hurt. "Then get out."` + decline + + label why? + ` Acting defiantly in the face of so much danger, you reach for your sidearm. Unfortunately, it's in vain, because as soon as you feel the pistol grip in your hand you simultaneously feel the burning sensations of a flurry of bullets entering your body and tearing apart your insides.` + die + + npc kill + government "Bounty" + personality heroic nemesis staying target + system + distance 1 2 + fleet "Marauder fleet IV" + dialog phrase "generic hunted bounty fleet eliminated dialog" + + on visit + dialog phrase "generic fleet bounty hunting on visit" + + on complete + conversation + `There's been intense fighting around the district where Ildico's warehouse was located. A few gunshots echo as you make your way through the rubble and around the fires.` + ` A group of injured but high-spirited pirates are set up outside the building, and there are bodies littering the nearby streets. An unharmed but bloody Ildico walks up to you, smiling. She is trailed by a small incongruous toddler with short black hair and lopsided grin.` + ` "Perfect. They never had a chance." She wipes the blood off her dagger, and notices your gaze.` + ` "The safest place for her was by my side," she says matter-of-factly to you, then she kneels down to her daughter. "Randalin, this is . You met them long ago when you were a baby. is my friend."` + ` She stands up again and looks back at you. "And , this is Randalin. She's grown a bit since last you saw her."` + choice + ` "Hi, Randalin. It's wonderful to meet you."` + goto hirand + ` (Nod at the child.)` + goto onlyyou + ` "She was with you for the battle?"` + ` Ildico shrugs. "Who would I trust with her? Who could I trust with her? Only you, , and you were elsewhere."` + goto onlyyou + + label hirand + ` The little girl lifts a chubby fist up to you.` + choice + ` (Fist bump her.)` + goto bump + ` (Shake her fist.)` + ` You awkwardly take her fist in your hand and shake it up and down. Randalin giggles, and babbles something incoherent.` + goto words + + label bump + ` Her small fist bumps against yours. She then gives you an easy grin and babbles something incoherent.` + + label words + ` "She's not quite got the hang of words yet. But they'll come," Ildico says approvingly.` + + label onlyyou + ` Ilidco puts a hand on Randalin's shoulder and draws her close. She turns her attention back to the battlefield.` + ` "This is only the beginning, ." Her eyes blaze as she speaks, "House Drifa will rise again. You'll see. You'll be there."` + ` She glances back toward her mulling forces. "Thanks again, it means a lot that I can count on you, but I have to work to do. Until next time."` + ` Ildico returns to her group, barking orders and gesturing at the rubble, leaving you to your own devices.` + + +mission "Ice Queen: News" + source + government "Republic" "Free Worlds" "Syndicate" "Neutral" "Independent" + near "Arneb" 10 + to offer + has "Ice Queen 8: done" + random < 3 + on offer + conversation + `A news kiosk is doing a segment on the growing threat of pirates around Haven. A certain name catches your eye, and you read:` + ` Self-styled "pirate queen" Ildico Drifa continues to expand her territory and reach. While there is some concern this growing centralization of power could result in greater threats to trade and stability in the Far North, most analysts think it is quite unlikely.` + ` Professor Grenwich Millstone, foremost authority on the economy and history of the northern pirate worlds, had this to say about Drifa:` + ` "It is patently inconceivable that any single magnate could consolidate power on Haven. It is cultural anathema, and economically impractical. Frankly, all this talk of a so-called 'pirate queen' is just sensationalist journalism trying to sell the news. In a few weeks another pirate captain will be the new bogeyman, and then another."` + ` The segment ends, and the news kiosk automatically scrolls down to the next article.` + decline + + +mission "Skadenga: Blood of Surtr" + landing + source "Windblain" + to offer + has "Ice Queen 6: done" + has "outfit: Blood of Surtr" + has "Stones of Skadenga 3: offered" + on offer + conversation + `As your ship descends through Windblain's cloudy atmosphere, you consider landing near Hrithfjall and asking the Ondurdis about the Blood of Surtr.` + choice + ` (Take the Blood of Surtr to Hrithfjall.)` + goto hrithland + ` (Ask about the Blood of Surtr another time.)` + defer + ` (Don't visit Hrithfjall at all.)` + decline + + ` You fly past Hrithfjall without landing and continue on your way.` + defer + + label hrithland + ` You pass numerous new homes built of driftwood and sea-stone as you approach the old village center of Hrithfjall. There is laughter and song in the air and children running everywhere; it always seems larger every time you visit.` + ` At the old village hall you find the Ondurdis and Aldfrith deep in conversation with a band of people who stick out beside the Skadenga and indigenous Ranfolk; these people appear to be foreigners. The Ondurdis's old eyes light up and she gives you a grandmotherly smile as you approach.` + ` "!" the Ondurdis exclaims happily. "My heart sings to see you."` + choice + ` "Who are they?"` + goto who + ` "I've come to show you something."` + goto something + ` "What do you know about other stones like the Tear of Skade?"` + to display + not "tears" + ` "On Skade? Only Tears of Skade. But on other worlds? Maybe."` + goto something + + label who + ` The Ondurdis steps away from the group and takes you aside, leaving Aldfrith to continue talking with them.` + ` "They are from Alfheim. They say their culture and world dying and they look for new home before old one gone." She pats your arm gently.` + ` "Word spreads of Hrithfjell and in some corners of Deep old ways still remembered. So they come." Her eyes gleam, "We are like fire on glacier. All see, all come. For warmth, for happiness, for life.` + ` "But, , you have not come all this way to talk with old woman. What brings you here?"` + + label something + action + clear "tears" + ` You show her the Blood of Surtr, and tell her the story of House Drifa taking it from the Deep.` + ` The Ondurdis nods slowly. "I only know of Tears of Skade. Our Goddess cried - maybe others did as well?"` + ` She turns and motions to Aldfrith who steps over to you. She asks, "Did Ran ever cry cold tears?"` + + label aldtime + ` Aldfrith inspects the Blood of Surtr thoughtfully, and then considers the Ondurdis's words as he strokes his white beard. "The sea has no mercy, and no memory. Ran would never cry."` + ` He chews his lip and then says slowly and carefully, "But she did leave us scales."` + ` He nods to himself and becomes more confident. "Long ago we would find the Scales of Ran on the beaches nearest her God-Stone. The stories say that they made a bubble of breathable air around those who carried them, and allowed us to explore the secrets of her depths."` + choice + ` "What happened to them?"` + ` "Where could I find them?"` + + ` Aldfrith shakes his head slowly. "In the time of my grandfather the men from the Deep came and took the Scales off world. They said they were too valuable to be left in the hands of savages, and since then we've never found another Scale of Ran. I think she was displeased we allowed them to be taken."` + ` The old man pauses for a moment and then says, "Although... I do remember when I was a child the folk from the Deep coming and visiting us again. They had questions about... our baubles. I think they said they did not work? They thought they were just useless charms. Perhaps the Scales stopped working?"` + ` He shrugs. "I am sorry, , that I cannot tell you more."` + ` The Ondurdis puts a hand on his back. "I value your stories." She turns her attention back to you, "Stone-magic fades when outsiders come, this is known. Maybe all God-Stones once gave such gifts, but no longer."` + ` She points at the Blood of Surtr. "Surtr, is known, he is volcano that erupts on ice. He is black fire that swallows world. But he had no God-Stone on Nifel." She pauses and looks at Aldfrith.` + ` "Oh, uh, and none here. Ran, like the sea, is jealous and would share no world with another." Aldfrith says quickly.` + ` "Maybe on other world, there is Surtr God-Stone, and from it he bleeds. More than that, I do not know," she says apologetically.` + ` You thank the Ondurdis and Aldfrith for their gracious help and interesting stories. The Ondurdis hastily prepares a small meal, believing it bad manners to let a guest leave unfed, and wishes you well in your future travels among the lofty stars.` + decline + + +mission "Stone to Hope: Thanks" + source "Norn" + to offer + has "Stone to Ice Queen: offered" + on offer + conversation + `In the quiet, creaking spaceport, it's easy enough to spot Helm doing some much needed maintenance on the ships that have landed here for the night. The last time you met him, he asked you to bring a stone to Hope. Do you want to check in with him?` + choice + ` (Go to Helm and report on the stone.)` + goto tell + ` (Leave Helm alone.)` + decline + + label tell + ` As you say your hello you accidentally startle Helm, who's in the middle of recalibrating a repulsor.` + ` "Woah! ! No need to sneak up on me!" He stands up to greet you. "What's new?"` + choice + ` "Is Tosti around? I found his Family-Cairn and left the stone there."` + goto where + to display + has "placed tosti's stone" + ` "It's done. I chucked the stone out of a hatch, but it's at least on Hope now."` + goto threw + to display + has "chucked tosti's stone" + ` "I threw the stone off a cliff, but it's at least on Hope now."` + goto threw + to display + has "threw tosti's stone" + ` "Where's Tosti?"` + label where + action + clear "placed tosti's stone" + ` "Ah, Tosti's out on the seas. The lad got himself a job working on a fishing trawler. It'll likely be a few years before he'll call to port here again."` + ` Helm gives you a friendly smile. "Still, you've done him a good turn. I'll have a message passed along to him so he knows he can rest easy."` + ` He returns to tinkering with the repulsor but continues, "There's no money or reward, but word will spread and people here will appreciate it all the same. Here on Norn we have long memories."` + decline + + label threw + action + clear "threw tosti's stone" + clear "chucked tosti's stone" + ` Helm stops what's he doing and looks up at you searchingly. "Best keep that to yourself, then. You of all people should know that folks around here take stone-bearing pretty seriously."` + ` He sighs, "Still, better than not being on Hope at all, I suppose. As for Tosti, he's out on the seas. Got himself a job working on a fishing trawler. It'd likely be a few years before he'll call to port here again."` + ` He turns and looks out over Norn's endless oceans. "I'll have a message sent to him so he knows he can rest easy. I'll of course leave out the part about you tossing the stone. No reason to give him grief."` + ` He returns to tinkering with the repulsor but continues, "There's no money or reward, but a good deed is its own reward, isn't it?` + ` "At least that's what I believe," he says more to himself than to you.` + decline diff --git a/data/human/outfits.txt b/data/human/outfits.txt index bf903f42bb03..ef81dad16118 100644 --- a/data/human/outfits.txt +++ b/data/human/outfits.txt @@ -466,7 +466,7 @@ outfit "Antigrav Glass" outfit "Puny" category "Unique" "display name" `"Puny"` - thumbnail "outfit/unknown" + thumbnail "outfit/collar" "unique" 1 description `A poodle you rescued from a dubious racing complex on Freedom. She appears to have formed a strong bond with you, willing to stick by your side.` description ` Her history matches with Pookie's, but Pookie's owner claimed that Puny was not hers. Puny is also vastly less urine-prone; you suspect that the race organizers may have toilet-trained her.` @@ -478,6 +478,14 @@ outfit "Tear of Skade" "unique" 1 description "A strange glowing stone gifted to you by the Skadenga. It lacks any power source, yet is quite cold." +outfit "Blood of Surtr" + category "Unique" + thumbnail "outfit/surtrblood" + "energy generation" 2 + "heat generation" 4 + "unique" 1 + description "A strange glowing stone gifted to you by Ildico Drifa. It hums with energy and heat." + outfit "Local Map" category "Special" diff --git a/data/human/weapons.txt b/data/human/weapons.txt index 4d7a54212af6..7bb2d9724a1a 100644 --- a/data/human/weapons.txt +++ b/data/human/weapons.txt @@ -84,7 +84,7 @@ outfit "Blaster Turret" sound "blaster" "hit effect" "blaster impact" "inaccuracy" 3 - "turret turn" 2 + "turret turn" 3.6 "velocity" 10.625 "lifetime" 48 "reload" 6 @@ -110,7 +110,7 @@ outfit "Quad Blaster Turret" sound "blaster" "hit effect" "blaster impact" "inaccuracy" 3 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 10.625 "lifetime" 48 "reload" 3 @@ -202,7 +202,7 @@ outfit "Modified Blaster Turret" sound "mod blaster" "hit effect" "blaster impact" "inaccuracy" 4 - "turret turn" 2 + "turret turn" 3.6 "velocity" 10 "lifetime" 48 "reload" 6 @@ -253,7 +253,7 @@ outfit "Laser Turret" sound "laser" "hit effect" "beam laser impact" "inaccuracy" .5 - "turret turn" 2.2 + "turret turn" 4 "velocity" 300 "lifetime" 1 "reload" 1 @@ -313,7 +313,7 @@ outfit "Heavy Laser Turret" sound "heavy laser" "hit effect" "heavy laser impact" "inaccuracy" .4 - "turret turn" 1.3 + "turret turn" 2.2 "velocity" 400 "lifetime" 1 "reload" 1 @@ -377,7 +377,7 @@ outfit "Mining Laser Turret" sound "mining laser" "hit effect" "beam laser impact" "inaccuracy" .35 - "turret turn" 2.4 + "turret turn" 4.4 "velocity" 200 "lifetime" 1 "reload" 1 @@ -458,7 +458,7 @@ outfit "Electron Turret" sound "electron beam" "hit effect" "electron impact" "inaccuracy" .3 - "turret turn" 0.8 + "turret turn" 1.2 "velocity" 450 "lifetime" 1 "reload" 1 @@ -630,7 +630,7 @@ outfit "Proton Turret" sound "proton" "hit effect" "proton impact" 3 "inaccuracy" 4 - "turret turn" 1.4 + "turret turn" 2.4 "submunition" "Proton Fragment" 3 "velocity" 24 "lifetime" 2 @@ -683,7 +683,7 @@ outfit "Plasma Turret" sound "plasma" "hit effect" "plasma explosion" "inaccuracy" 2 - "turret turn" 1 + "turret turn" 1.6 "velocity" 12 "lifetime" 40 "reload" 9 @@ -1100,7 +1100,7 @@ outfit "Javelin Turret" icon "icon/javelin turret" "hit effect" "tiny explosion" "inaccuracy" 4 - "turret turn" 2 + "turret turn" 3.6 "velocity" 10 "lifetime" 60 "reload" 7.5 @@ -1561,7 +1561,7 @@ outfit "Gatling Turret" ammo "Gatling Gun Ammo" 2 icon "icon/gat turret" "inaccuracy" 2 - "turret turn" 2 + "turret turn" 3.6 "velocity" 16 "lifetime" 1 "reload" 3 diff --git a/data/incipias/incipias outfits.txt b/data/incipias/incipias outfits.txt index 8aaa302f09b5..33fec4fc37e5 100644 --- a/data/incipias/incipias outfits.txt +++ b/data/incipias/incipias outfits.txt @@ -67,7 +67,7 @@ outfit "Stagnation Beam" sound "stagnation beam" "hit effect" "stagnation beam impact" "inaccuracy" .5 - "turret turn" 2.1 + "turret turn" 3.8 "velocity" 600 "lifetime" 1 "reload" 1 diff --git a/data/kahet/kahet outfits.txt b/data/kahet/kahet outfits.txt index 837854863db9..7eb254bed270 100644 --- a/data/kahet/kahet outfits.txt +++ b/data/kahet/kahet outfits.txt @@ -149,7 +149,7 @@ outfit "Ka'het Ravager Turret" sound "disruptor" "hit effect" "ravager impact" "inaccuracy" .2 - "turret turn" 2.3 + "turret turn" 4.2 "velocity" 480 "lifetime" 1 "reload" 1 @@ -205,7 +205,7 @@ outfit "Ka'het Annihilator Turret" "hardpoint offset" 15. "hit effect" "bullet impact" "inaccuracy" 1 - "turret turn" 1.8 + "turret turn" 3.2 "range override" 630 "velocity override" 18 "reload" 10 diff --git a/data/korath/korath culture conversations.txt b/data/korath/korath culture conversations.txt new file mode 100644 index 000000000000..56592b913461 --- /dev/null +++ b/data/korath/korath culture conversations.txt @@ -0,0 +1,76 @@ +# Copyright (c) 2024 by Arachi-Lover +# +# Endless Sky is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later version. +# +# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + +mission "Korath Far'en Lai Prayer" + minor + invisible + to offer + day >= 13 + day < 18 + random < 35 + source + attributes "efret" + not attributes "station" + on offer + conversation + `The gloomy feel of the local spaceport appears to be somehow even more pronounced today. Very few Korath are doing anything but the most basic maintenance of the local facilities, as if they all had their attention elsewhere. It doesn't take you long to notice the gathering of a considerable number of Korath by one large, blackened side of an abandoned building, which seems to be the reason for the drop in the local morale.` + ` As you approach the group, you notice a few Korath children running their hands over the dark wall, scratching it carefully with what must be something akin to crayons, or some material that could perform the same function. After they play around for a while, you notice all their drawings seem to take a similar shape: a beige or brownish circle, with some shy spots of blue, and rare bits of green. Contrasted by the onyx background of the building wall, you easily make it out to be a planet.` + ` Up close, you see many adult and elderly Korath, gathered as if in a prayer group around a shrine of sorts, just a few steps away from the building. They are mostly silent but for a few brief gasps. The shrine seems to be simply a pile of common crates and boxes, hidden by a dusty canvas that's not large enough to cover all of the cubic components of the pyramid-like structure.` + ` At the bottom of the shrine, the Korath seem to have deposited a plethora of personal belongings - clothing pieces, tools, pictures and coins, books and crystals, large fragments of cracked eggshells, flasks of oils or other substances, small jewelry pieces, and all manner of beads. The items seem to pile on and on endlessly, as every passing Korath in the spaceport makes a point to pass by the shrine and leave something as an offering.` + ` On the first level of crates, you first see a number of wooden, stone, and metal slabs, with various scratch marks over them. Upon closer inspection, you see that from certain angles, the marks combine to form shapes like those of the planet drawn by the children. The marks also form many other planets, with clearly defined oceans and continents. There are stars, systems, symbols you can't recognize, and a great number of tablets with nothing but writing.` + ` One of the oldest Korath in the gathering, who had been shivering with his head in his hands since before you approached, lets out an agonizing, bark-like yelp in a frayed voice. He looks up to the top of the shrine. Standing at navel height, one lone crate supports only a few items. A large, colorful feather, an amazingly complex seashell, and a mysteriously beautiful flower sit on the top crate, all surrounding one single, lone candle, which looks as if it's been burning for hours on end.` + ` You wonder how this one candle could be burning for so long out in the open, and think that perhaps the Korath simply relight it whenever it is put out by a passing gust. Only at that thought do you notice that, today, there is no wind whatsoever on .` + ` Seemingly only taking notice of you now, one of the younger Korath sitting by the shrine slowly rises, making her way over to you, and briefly holds up her hands with both palms facing you. She speaks quickly, and you cannot understand most of it, but you do recognize the words "humani efreti" at the end of what you guess was a question.` + choice + ` "What exactly is all of this?"` + ` (Gesture inquisitively at the shrine.)` + ` "Sorry, I didn't mean to interrupt. I'll be on my way."` + goto end + ` Either due to somehow having understood your meaning, or simply having correctly guessed the reason for your curiosity, the Korath begins to attempt to explain the gathering. She grabs some of the crayon-like objects and beckons you a little closer to the wall, where she begins to draw many spiral arms in white, converging in what looks like a large maw at the center. She repeatedly jabs the crayon at a point close to the center, then at you, then at herself. "," she says.` + ` She then draws on the wall with a different crayon of a light-green coloring. The green seems to make a path to another dot, closer to the maw, though she takes great care to draw something like a barrier around the position - as if shielding it from the center of her spiral map. She stares at the dot thoughtfully for a few seconds, as if in disbelief of what she had just drawn herself. "Far'en Lai," she finally says, her voice cracking slightly, still facing the dot. Then, she looks back and points directly at the burning candle. "Far'en Lai," she says again, before looking back at you.` + ` Reaching in her garments, she pulls out what looks to be some form of communication device. She considers it briefly, then gestures for you to follow her back to the shrine, where she carefully places the object down onto the pile of offerings. She then looks at you, as if wondering if you would follow the gesture.` + choice + ` (Offer 100 credits.)` + to display + "credits" >= 100 + goto "small credits" + ` (Offer 10,000 credits.)` + to display + "credits" >= 10000 + goto "medium credits" + ` (Offer 10,000,000 credits.)` + to display + "credits" >= 10000000 + goto "large credits" + ` (Offer a picture of my home planet.)` + ` "I'm sorry, but I have nothing to offer. Goodbye."` + goto end + ` You remember the picture of your hometown back in you tend to carry around, and start searching for it. Sure enough, you find it inside your back pocket, straighten it out as best as you can, and go to add it to the pile. However, the Korath stops you, holding your arm and stretching her hand carefully as if asking for the picture. You hand it to her, and she brings it very close to her eyes. The elderly Korath who wailed before appears beside her, and she shows him the picture. He seems to stare at it for minutes on end, and you begin to wonder if you somehow offended them with your choice. Then, he hands her back the picture, and seems to nod slightly. She heads close to the shrine and places the picture at the top level, with the feather, shell, and flower. The elderly Korath faces you, looking deeply into your eyes, then raises his palms toward you, letting out a faint bark. You notice all other Korath around the shrine are doing the same. When they've all acknowledged your contribution, the Korath who drew the spiral arms comes to you again, looking thoroughly impressed.` + goto end + label "small credits" + action + payment -100 + ` You add the credit chip to the objects by the shrine, and she seems to nod approvingly before looking at you once more, seemingly satisfied with your understanding of the gesture.` + goto end + label "medium credits" + action + payment -10000 + ` You add the credit chips to the objects by the shrine, and she and a few of the other Korath seem to bow lightly to you. She turns to you once more, looking satisfied with your understanding of the gesture.` + goto end + label "large credits" + action + payment -10000000 + ` You attempt to carefully place the credit chips onto the pile of objects by the shrine, but despite your efforts a few end up slipping out and dropping down rather noisily. The Korath, however, seem in shock rather than disturbed at your addition. Many of them raise their hands up, their palms to you in greeting or in thanks. You look back at the Korath who drew the spiral arms, who seems to be beaming.` + label end + ` She raises her hands up towards you again, as if bidding you farewell, as you head away from the shrine. You notice many of the Korath around the spaceport take a few glances at you, but still seem focused entirely on the shrine regardless of what activity they may be performing.` + decline diff --git a/data/korath/korath weapons.txt b/data/korath/korath weapons.txt index 82ad0324d3a2..3368c98dc161 100644 --- a/data/korath/korath weapons.txt +++ b/data/korath/korath weapons.txt @@ -32,7 +32,7 @@ outfit "Grab-Strike Turret" "hit effect" "grab-strike impact" "die effect" "grab-strike impact" "inaccuracy" 1 - "turret turn" 1 + "turret turn" 1.6 "velocity" 10 "lifetime" 100 "reload" 20 @@ -78,7 +78,7 @@ outfit "Shunt-Strike Turret" "hit effect" "shunt-strike hit" "die effect" "shunt-strike sparkle" "inaccuracy" 1 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 10 "lifetime" 81 "reload" 30 @@ -132,7 +132,7 @@ outfit "Banisher Grav-Turret" sound "banisher" "hit effect" "banisher impact" "inaccuracy" .4 - "turret turn" 1.8 + "turret turn" 3.2 "velocity" 590 "lifetime" 1 "reload" 1 @@ -255,7 +255,7 @@ outfit "Blaze-Pike Turret" sound "blaze-pike" "hit effect" "blaze-pike hit" "inaccuracy" 0.45 - "turret turn" 0.8 + "turret turn" 1.2 "velocity" 450 "lifetime" 1 "reload" 1 @@ -293,7 +293,7 @@ outfit "Korath Inferno" sound "korath inferno" "hit effect" "korath inferno hit" "inaccuracy" 0.5 - "turret turn" 0.6 + "turret turn" 0.8 "velocity" 550 "lifetime" 1 "reload" 1 @@ -372,7 +372,7 @@ outfit "Thermal Repeater Turret" sound "repeater" "hit effect" "repeater impact" "inaccuracy" 3 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 13 "lifetime" 40 "reload" 5 @@ -654,7 +654,7 @@ outfit "Shield Disruptor Turret" sound "disruptor" "hit effect" "disruptor impact" "inaccuracy" 1 - "turret turn" 1 + "turret turn" 1.6 "velocity" 480 "lifetime" 1 "reload" 1 @@ -718,7 +718,7 @@ outfit "Husk-Slice Turret" sound "slicer" "hit effect" "slicer impact" "inaccuracy" 0 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 520 "lifetime" 1 "reload" 1.62 @@ -758,7 +758,7 @@ outfit "Digger Mining Turret" sound "korath digger" "hit effect" "korath digger hit" "inaccuracy" 1.5 - "turret turn" 2.4 + "turret turn" 4.4 "velocity" 480 "lifetime" 1 "reload" 1 @@ -826,7 +826,7 @@ outfit "Langrage Hyper-Heaver" icon "icon/korath heaver" "hit effect" "heaver hyperspace hit" "inaccuracy" 0.5 - "turret turn" 1.3 + "turret turn" 2.2 velocity 30 lifetime 8 "reload" 40 @@ -1084,7 +1084,7 @@ outfit "Firestorm Battery" "hardpoint sprite" "hardpoint/firestorm battery" sound "firestorm" ammo "Firestorm Torpedo" - "turret turn" 1.5 + "turret turn" 2.6 icon "icon/firestorm torpedo" "hit effect" "firestorm ring" 40 "die effect" "small explosion" diff --git a/data/persons.txt b/data/persons.txt index 40768b710922..74543b4cc81f 100644 --- a/data/persons.txt +++ b/data/persons.txt @@ -910,7 +910,7 @@ outfit "LG Gridfire Turret" person "Subsidurial" - government "Indigenous Lifeform" + government "Indigenous Lifeform (Subsidurial)" frequency 1000 personality timid unconstrained coward appeasing uninterested mining harvests mute diff --git a/data/pug/pug outfits.txt b/data/pug/pug outfits.txt index 776c8fdf5c04..217a1a9e149a 100644 --- a/data/pug/pug outfits.txt +++ b/data/pug/pug outfits.txt @@ -54,7 +54,7 @@ outfit "Pug Zapper Turret" sound "zapper" "hit effect" "zapper impact" "inaccuracy" .3 - "turret turn" 1 + "turret turn" 1.6 "velocity" 320 "lifetime" 1 "reload" 1 diff --git a/data/quarg/quarg outfits.txt b/data/quarg/quarg outfits.txt index 39d37e946bb0..bc8fb7ca6acb 100644 --- a/data/quarg/quarg outfits.txt +++ b/data/quarg/quarg outfits.txt @@ -50,7 +50,7 @@ outfit "Quarg Skylance" sound "skylance" "hit effect" "skylance impact" "inaccuracy" .4 - "turret turn" 1.5 + "turret turn" 2.6 "velocity" 500 "lifetime" 1 "reload" 1 diff --git a/data/remnant/remnant outfits.txt b/data/remnant/remnant outfits.txt index 80451b72bd00..0722f9df2252 100644 --- a/data/remnant/remnant outfits.txt +++ b/data/remnant/remnant outfits.txt @@ -76,7 +76,7 @@ outfit "Inhibitor Turret" sound "inhibitor" "hit effect" "inhibitor impact" 3 "inaccuracy" .5 - "turret turn" 2 + "turret turn" 3.6 "velocity" 36 "random velocity" .5 "lifetime" 24 @@ -152,7 +152,7 @@ outfit "Thrasher Turret" sound "thrasher" "hit effect" "thrasher impact" "inaccuracy" 6 - "turret turn" 1.2 + "turret turn" 2 "velocity" 10 "lifetime" 36 "reload" 5 diff --git a/data/sheragi/sheragi outfits.txt b/data/sheragi/sheragi outfits.txt index 880c66503090..6a9ba9270ff5 100644 --- a/data/sheragi/sheragi outfits.txt +++ b/data/sheragi/sheragi outfits.txt @@ -294,7 +294,7 @@ outfit "Particle Waveform Turret" "fire effect" "lightning flare" "hit effect" "pwave impact" "inaccuracy" 1.5 - "turret turn" 0.8 + "turret turn" 1.2 "velocity" 50 "lifetime" 18 "reload" 60 diff --git a/data/successors/successor jobs.txt b/data/successors/successor jobs.txt index d12e69469c66..f73b71b748df 100644 --- a/data/successors/successor jobs.txt +++ b/data/successors/successor jobs.txt @@ -209,7 +209,7 @@ mission "Successors: Bold Tourists Round Trip" job repeat name "Extreme sightseeing on " - description "These sightseers will pay you to bring them and their safety equipment to and back home by ." + description "These sightseers will pay you to bring them and their safety equipment to and back home to by ." deadline cargo "Isolation Suits" 1 passengers 2 4 .5 @@ -237,7 +237,7 @@ mission "Successors: Tourist Expedition" job repeat name "Sightseeing expedition" - description "These sightseers will pay you to take them to before returning them home by ." + description "These sightseers will pay you to take them to before returning them home to by ." deadline deadline 4 passengers 2 5 .5 diff --git a/data/successors/successor weapons.txt b/data/successors/successor weapons.txt index 6ed062689e41..6012c34dff3e 100644 --- a/data/successors/successor weapons.txt +++ b/data/successors/successor weapons.txt @@ -82,7 +82,7 @@ outfit "Bimodal Coilgun Turret" "velocity" 30 "lifetime" 15 "range override" 900 - "turret turn" 0.7 + "turret turn" 1 "burst count" 24 "burst reload" 3 "reload" 15 @@ -229,7 +229,7 @@ outfit "Pulse Laser Turret" "hit effect" "successor pulse laser impact" "inaccuracy" 0.4 "velocity" 490 - "turret turn" 1.5 + "turret turn" 2.6 "lifetime" 1 "burst count" 9 "burst reload" 1 @@ -306,7 +306,7 @@ outfit "Overcharged Laser Turret" "inaccuracy" 5.5 "velocity" 490 "lifetime" 1 - "turret turn" 1.7 + "turret turn" 3 "burst count" 4 "burst reload" 1 "reload" 7 diff --git a/data/wanderer/wanderer outfits.txt b/data/wanderer/wanderer outfits.txt index a1f3d9cd2193..df91817fb3fb 100644 --- a/data/wanderer/wanderer outfits.txt +++ b/data/wanderer/wanderer outfits.txt @@ -66,7 +66,7 @@ outfit "Sunbeam Turret" sound "sunbeam" "hit effect" "sunbeam impact" "inaccuracy" .2 - "turret turn" 1.1 + "turret turn" 1.8 "velocity" 540 "lifetime" 1 "reload" 1 @@ -96,7 +96,7 @@ outfit "Dual Sunbeam Turret" sound "sunbeam" "hit effect" "sunbeam impact" 2 "inaccuracy" .2 - "turret turn" 0.7 + "turret turn" 1 "velocity" 540 "lifetime" 1 "reload" 1 @@ -171,7 +171,7 @@ outfit "Moonbeam Turret" "fire effect" "moonbeam fleck" "hit effect" "moonbeam impact" "inaccuracy" 2.0 - "turret turn" 1.3 + "turret turn" 2.2 "velocity" 490 "lifetime" 1 # A full cycle is 36 frames. Beam is active 83% of the time. diff --git a/images/outfit/collar.png b/images/outfit/collar.png new file mode 100644 index 000000000000..9e69ae01e742 Binary files /dev/null and b/images/outfit/collar.png differ diff --git a/images/outfit/surtrblood.png b/images/outfit/surtrblood.png new file mode 100644 index 000000000000..e5fc1096e82f Binary files /dev/null and b/images/outfit/surtrblood.png differ diff --git a/images/ui/audio low.png b/images/ui/audio low.png new file mode 100644 index 000000000000..4a59f6e99b84 Binary files /dev/null and b/images/ui/audio low.png differ diff --git a/images/ui/audio max.png b/images/ui/audio max.png new file mode 100644 index 000000000000..c6af92889238 Binary files /dev/null and b/images/ui/audio max.png differ diff --git a/images/ui/audio medium.png b/images/ui/audio medium.png new file mode 100644 index 000000000000..5b7fc2fe4126 Binary files /dev/null and b/images/ui/audio medium.png differ diff --git a/images/ui/audio none.png b/images/ui/audio none.png new file mode 100644 index 000000000000..c99b95d70994 Binary files /dev/null and b/images/ui/audio none.png differ diff --git a/images/ui/plugins panel.png b/images/ui/plugins panel.png index 37378d08f8fa..4ee4e76ae350 100644 Binary files a/images/ui/plugins panel.png and b/images/ui/plugins panel.png differ diff --git a/images/ui/settings panel.png b/images/ui/settings panel.png index 47543fdc2333..f1778d4df30b 100644 Binary files a/images/ui/settings panel.png and b/images/ui/settings panel.png differ diff --git a/sounds/archonteleport@3x.wav b/sounds/archonteleport@3x.wav new file mode 100644 index 000000000000..605b3e2b0bcf Binary files /dev/null and b/sounds/archonteleport@3x.wav differ diff --git a/sounds/asteroid crunch large@3x.wav b/sounds/asteroid crunch large@3x.wav new file mode 100644 index 000000000000..78ebc3409997 Binary files /dev/null and b/sounds/asteroid crunch large@3x.wav differ diff --git a/sounds/asteroid crunch medium@3x.wav b/sounds/asteroid crunch medium@3x.wav new file mode 100644 index 000000000000..be69bba5bdcb Binary files /dev/null and b/sounds/asteroid crunch medium@3x.wav differ diff --git a/sounds/asteroid crunch small@3x.wav b/sounds/asteroid crunch small@3x.wav new file mode 100644 index 000000000000..5cf3e01c6c2d Binary files /dev/null and b/sounds/asteroid crunch small@3x.wav differ diff --git a/sounds/dragonflame@3x.wav b/sounds/dragonflame@3x.wav new file mode 100644 index 000000000000..cd7bffff5dd7 Binary files /dev/null and b/sounds/dragonflame@3x.wav differ diff --git a/sounds/explosion ayym@3x.wav b/sounds/explosion ayym@3x.wav new file mode 100644 index 000000000000..2bb0af776c8b Binary files /dev/null and b/sounds/explosion ayym@3x.wav differ diff --git a/sounds/explosion huge@3x.wav b/sounds/explosion huge@3x.wav new file mode 100644 index 000000000000..6bcd0877d7f5 Binary files /dev/null and b/sounds/explosion huge@3x.wav differ diff --git a/sounds/explosion jje@3x.wav b/sounds/explosion jje@3x.wav new file mode 100644 index 000000000000..df47907d887f Binary files /dev/null and b/sounds/explosion jje@3x.wav differ diff --git a/sounds/explosion large@3x.wav b/sounds/explosion large@3x.wav new file mode 100644 index 000000000000..95b0a3a1b3d7 Binary files /dev/null and b/sounds/explosion large@3x.wav differ diff --git a/sounds/explosion medium@3x.wav b/sounds/explosion medium@3x.wav new file mode 100644 index 000000000000..d387baa5d24a Binary files /dev/null and b/sounds/explosion medium@3x.wav differ diff --git a/sounds/explosion nuke@3x.wav b/sounds/explosion nuke@3x.wav new file mode 100644 index 000000000000..1ac730a4bd37 Binary files /dev/null and b/sounds/explosion nuke@3x.wav differ diff --git a/sounds/explosion small@3x.wav b/sounds/explosion small@3x.wav new file mode 100644 index 000000000000..ffbcfb7602b9 Binary files /dev/null and b/sounds/explosion small@3x.wav differ diff --git a/sounds/explosion tiny@3x.wav b/sounds/explosion tiny@3x.wav new file mode 100644 index 000000000000..bdf71c9ea315 Binary files /dev/null and b/sounds/explosion tiny@3x.wav differ diff --git a/sounds/final explosion large@3x.wav b/sounds/final explosion large@3x.wav new file mode 100644 index 000000000000..83504711bef0 Binary files /dev/null and b/sounds/final explosion large@3x.wav differ diff --git a/sounds/final explosion medium@3x.wav b/sounds/final explosion medium@3x.wav new file mode 100644 index 000000000000..ad9ed4a904f1 Binary files /dev/null and b/sounds/final explosion medium@3x.wav differ diff --git a/sounds/final explosion small@3x.wav b/sounds/final explosion small@3x.wav new file mode 100644 index 000000000000..12ce07ad7fb1 Binary files /dev/null and b/sounds/final explosion small@3x.wav differ diff --git a/sounds/finisher@3x.wav b/sounds/finisher@3x.wav new file mode 100644 index 000000000000..401ee2abd728 Binary files /dev/null and b/sounds/finisher@3x.wav differ diff --git a/sounds/firestorm hit@3x.wav b/sounds/firestorm hit@3x.wav new file mode 100644 index 000000000000..4559e574d375 Binary files /dev/null and b/sounds/firestorm hit@3x.wav differ diff --git a/sounds/heavy rocket hit@3x.wav b/sounds/heavy rocket hit@3x.wav new file mode 100644 index 000000000000..59e3fb44f35e Binary files /dev/null and b/sounds/heavy rocket hit@3x.wav differ diff --git a/sounds/human launch@3x.wav b/sounds/human launch@3x.wav new file mode 100644 index 000000000000..c9d60a84233b Binary files /dev/null and b/sounds/human launch@3x.wav differ diff --git a/sounds/hyperdrive in@3x.wav b/sounds/hyperdrive in@3x.wav new file mode 100644 index 000000000000..bf78dc49fcf4 Binary files /dev/null and b/sounds/hyperdrive in@3x.wav differ diff --git a/sounds/hyperdrive out@3x.wav b/sounds/hyperdrive out@3x.wav new file mode 100644 index 000000000000..4d99bd946603 Binary files /dev/null and b/sounds/hyperdrive out@3x.wav differ diff --git a/sounds/hyperdrive@3x.wav b/sounds/hyperdrive@3x.wav new file mode 100644 index 000000000000..c8d845889568 Binary files /dev/null and b/sounds/hyperdrive@3x.wav differ diff --git a/sounds/jump drive@3x.wav b/sounds/jump drive@3x.wav new file mode 100644 index 000000000000..68ae63d4b1ea Binary files /dev/null and b/sounds/jump drive@3x.wav differ diff --git a/sounds/jump in@3x.wav b/sounds/jump in@3x.wav new file mode 100644 index 000000000000..bc9523bd02bb Binary files /dev/null and b/sounds/jump in@3x.wav differ diff --git a/sounds/jump out@3x.wav b/sounds/jump out@3x.wav new file mode 100644 index 000000000000..892ae4989ffd Binary files /dev/null and b/sounds/jump out@3x.wav differ diff --git a/sounds/korath launch@3x.wav b/sounds/korath launch@3x.wav new file mode 100644 index 000000000000..b4a3371ad6d9 Binary files /dev/null and b/sounds/korath launch@3x.wav differ diff --git a/sounds/missile hit@3x.wav b/sounds/missile hit@3x.wav new file mode 100644 index 000000000000..2ea63b310e0b Binary files /dev/null and b/sounds/missile hit@3x.wav differ diff --git a/sounds/nuke@3x.wav b/sounds/nuke@3x.wav new file mode 100644 index 000000000000..3001b78478ba Binary files /dev/null and b/sounds/nuke@3x.wav differ diff --git a/sounds/organ drive in@3x.wav b/sounds/organ drive in@3x.wav new file mode 100644 index 000000000000..56a00846a469 Binary files /dev/null and b/sounds/organ drive in@3x.wav differ diff --git a/sounds/organ drive out@3x.wav b/sounds/organ drive out@3x.wav new file mode 100644 index 000000000000..0af334e8b297 Binary files /dev/null and b/sounds/organ drive out@3x.wav differ diff --git a/sounds/organ drive@3x.wav b/sounds/organ drive@3x.wav new file mode 100644 index 000000000000..75e8800d1897 Binary files /dev/null and b/sounds/organ drive@3x.wav differ diff --git a/sounds/remnant launch external@3x.wav b/sounds/remnant launch external@3x.wav new file mode 100644 index 000000000000..006b482ce417 Binary files /dev/null and b/sounds/remnant launch external@3x.wav differ diff --git a/sounds/remnant launch@3x.wav b/sounds/remnant launch@3x.wav new file mode 100644 index 000000000000..bd8b0e2d4617 Binary files /dev/null and b/sounds/remnant launch@3x.wav differ diff --git a/sounds/rocket@3x.wav b/sounds/rocket@3x.wav new file mode 100644 index 000000000000..ef2167959c6e Binary files /dev/null and b/sounds/rocket@3x.wav differ diff --git a/sounds/star tail@3x.wav b/sounds/star tail@3x.wav new file mode 100644 index 000000000000..84531e63a21d Binary files /dev/null and b/sounds/star tail@3x.wav differ diff --git a/sounds/takeoff@3x.wav b/sounds/takeoff@3x.wav new file mode 100644 index 000000000000..2cf44e600cd9 Binary files /dev/null and b/sounds/takeoff@3x.wav differ diff --git a/sounds/thorax cannon@3x.wav b/sounds/thorax cannon@3x.wav new file mode 100644 index 000000000000..b42a2d5b2db4 Binary files /dev/null and b/sounds/thorax cannon@3x.wav differ diff --git a/sounds/torpedo hit@3x.wav b/sounds/torpedo hit@3x.wav new file mode 100644 index 000000000000..fb4c8c293d50 Binary files /dev/null and b/sounds/torpedo hit@3x.wav differ diff --git a/sounds/torpedo@3x.wav b/sounds/torpedo@3x.wav new file mode 100644 index 000000000000..6d85df2499b1 Binary files /dev/null and b/sounds/torpedo@3x.wav differ diff --git a/sounds/typhoon@3x.wav b/sounds/typhoon@3x.wav new file mode 100644 index 000000000000..47b9397589f6 Binary files /dev/null and b/sounds/typhoon@3x.wav differ diff --git a/source/AI.cpp b/source/AI.cpp index ac8cb15906f8..c1915e7f46b1 100644 --- a/source/AI.cpp +++ b/source/AI.cpp @@ -488,7 +488,7 @@ void AI::IssueMoveTarget(const Point &target, const System *moveToSystem) newOrders.point = target; newOrders.targetSystem = moveToSystem; string description = "moving to the given location"; - description += player.GetSystem() == moveToSystem ? "." : (" in the " + moveToSystem->Name() + " system."); + description += player.GetSystem() == moveToSystem ? "." : (" in the " + moveToSystem->DisplayName() + " system."); IssueOrders(newOrders, description); } @@ -4167,7 +4167,7 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) bool oxfordComma = (count > 2); for(const Planet *planet : destinations) { - message += planet->Name(); + message += planet->DisplayName(); --count; if(count > 1) message += ", "; @@ -4178,7 +4178,7 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) Messages::Add(message, Messages::Importance::Info); if(Preferences::GetNotificationSetting() == Preferences::NotificationSetting::BOTH) - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::ALERT); } // If any destination was found, find the corresponding stellar object // and set it as your ship's target planet. @@ -4395,7 +4395,7 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) if(target) message.clear(); else if(!message.empty()) - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); Messages::Importance messageImportance = Messages::Importance::High; @@ -4418,10 +4418,10 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) message = "The authorities on this " + next->GetPlanet()->Noun() + " refuse to clear you to land here."; messageImportance = Messages::Importance::Highest; - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); } else if(next != target) - message = "Switching landing targets. Now landing on " + next->Name() + "."; + message = "Switching landing targets. Now landing on " + next->DisplayName() + "."; } else if(message.empty()) { @@ -4458,14 +4458,14 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) { message = "There are no planets in this system that you can land on."; messageImportance = Messages::Importance::Highest; - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); } else if(!target->GetPlanet()->CanLand()) { message = "The authorities on this " + target->GetPlanet()->Noun() + " refuse to clear you to land here."; messageImportance = Messages::Importance::Highest; - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); } else if(!types.empty()) { @@ -4481,10 +4481,10 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) message += ' ' + *it++ + ','; message += " or " + *it; } - message += " in this system. Landing on " + target->Name() + "."; + message += " in this system. Landing on " + target->DisplayName() + "."; } else - message = "Landing on " + target->Name() + "."; + message = "Landing on " + target->DisplayName() + "."; } if(!message.empty()) Messages::Add(message, messageImportance); @@ -4516,13 +4516,13 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) { // The player is guaranteed to have a travel plan for isWormhole to be true. Messages::Add("Landing on a local wormhole to navigate to the " - + player.TravelPlan().back()->Name() + " system.", Messages::Importance::High); + + player.TravelPlan().back()->DisplayName() + " system.", Messages::Importance::High); } if(ship.GetTargetSystem() && !isWormhole) { string name = "selected star"; if(player.KnowsName(*ship.GetTargetSystem())) - name = ship.GetTargetSystem()->Name(); + name = ship.GetTargetSystem()->DisplayName(); if(activeCommands.Has(Command::FLEET_JUMP)) Messages::Add("Engaging fleet autopilot to jump to the " + name + " system." @@ -4667,7 +4667,7 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) const Planet *planet = player.TravelDestination(); if(planet && planet->IsInSystem(ship.GetSystem()) && planet->IsAccessible(&ship)) { - Messages::Add("Autopilot: landing on " + planet->Name() + ".", Messages::Importance::High); + Messages::Add("Autopilot: landing on " + planet->DisplayName() + ".", Messages::Importance::High); autoPilot |= Command::LAND; ship.SetTargetStellar(ship.GetSystem()->FindStellar(planet)); } @@ -4706,25 +4706,25 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) { Messages::Add("You do not have a hyperdrive installed.", Messages::Importance::Highest); autoPilot.Clear(); - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); } else if(!ship.JumpNavigation().JumpFuel(ship.GetTargetSystem())) { Messages::Add("You cannot jump to the selected system.", Messages::Importance::Highest); autoPilot.Clear(); - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); } else if(!ship.JumpsRemaining() && !ship.IsEnteringHyperspace()) { Messages::Add("You do not have enough fuel to make a hyperspace jump.", Messages::Importance::Highest); autoPilot.Clear(); - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); } else if(ship.IsLanding()) { Messages::Add("You cannot jump while landing.", Messages::Importance::Highest); autoPilot.Clear(Command::JUMP); - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail"), SoundCategory::UI); } else { diff --git a/source/BoardingPanel.cpp b/source/BoardingPanel.cpp index fb8b1c264201..79448fb81a7c 100644 --- a/source/BoardingPanel.cpp +++ b/source/BoardingPanel.cpp @@ -16,6 +16,7 @@ this program. If not, see . #include "BoardingPanel.h" #include "text/alignment.hpp" +#include "audio/Audio.h" #include "CargoHold.h" #include "Depreciation.h" #include "Dialog.h" @@ -62,6 +63,7 @@ BoardingPanel::BoardingPanel(PlayerInfo &player, const shared_ptr &victim) : player(player), you(player.FlagshipPtr()), victim(victim), attackOdds(*you, *victim), defenseOdds(*victim, *you) { + Audio::Pause(); // The escape key should close this panel rather than bringing up the main menu. SetInterruptible(false); @@ -117,6 +119,13 @@ BoardingPanel::BoardingPanel(PlayerInfo &player, const shared_ptr &victim) +BoardingPanel::~BoardingPanel() +{ + Audio::Resume(); +} + + + // Draw the panel. void BoardingPanel::Draw() { diff --git a/source/BoardingPanel.h b/source/BoardingPanel.h index 990362de03ad..f21aa1a83ac2 100644 --- a/source/BoardingPanel.h +++ b/source/BoardingPanel.h @@ -36,6 +36,7 @@ class Ship; class BoardingPanel : public Panel { public: BoardingPanel(PlayerInfo &player, const std::shared_ptr &victim); + virtual ~BoardingPanel() override; virtual void Draw() override; diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 8cd37dfa564a..717e8b1b2c31 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -8,7 +8,7 @@ if(MSVC) target_compile_definitions(EndlessSkyLib PUBLIC "_UNICODE" "UNICODE") else() target_compile_options(EndlessSkyLib PUBLIC - "-Wall" "-pedantic-errors" "-Wold-style-cast" "-fno-rtti") + "-Wall" "-pedantic-errors" "-Wold-style-cast" "$<$:-fno-rtti>") endif() # Every source file (and header file) should be listed here, except main.cpp. @@ -334,6 +334,7 @@ target_sources(EndlessSkyLib PRIVATE audio/Music.h audio/Sound.cpp audio/Sound.h + audio/SoundCategory.h comparators/ByGivenOrder.h comparators/ByName.h comparators/BySeriesAndIndex.h diff --git a/source/ConversationPanel.cpp b/source/ConversationPanel.cpp index e3be3c8b34b6..45d73c0037fc 100644 --- a/source/ConversationPanel.cpp +++ b/source/ConversationPanel.cpp @@ -16,6 +16,7 @@ this program. If not, see . #include "ConversationPanel.h" #include "text/alignment.hpp" +#include "audio/Audio.h" #include "BoardingPanel.h" #include "Color.h" #include "Command.h" @@ -68,6 +69,7 @@ ConversationPanel::ConversationPanel(PlayerInfo &player, const Conversation &con #if defined _WIN32 PATH_LENGTH = Files::Saves().string().size(); #endif + Audio::Pause(); // These substitutions need to be applied on the fly as each paragraph of // text is prepared for display. subs[""] = player.FirstName(); @@ -94,6 +96,13 @@ ConversationPanel::ConversationPanel(PlayerInfo &player, const Conversation &con +ConversationPanel::~ConversationPanel() +{ + Audio::Resume(); +} + + + void ConversationPanel::SetCallback(function fun) { callback = std::move(fun); @@ -234,7 +243,7 @@ bool ConversationPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &comm // fields are currently active. The name text entry fields are active if // choices is empty and we aren't at the end of the conversation. if(command.Has(Command::MAP) && (!choices.empty() || node < 0)) - GetUI()->Push(new MapDetailPanel(player, system)); + GetUI()->Push(new MapDetailPanel(player, system, true)); if(node < 0) { // If the conversation has ended, the only possible action is to exit. diff --git a/source/ConversationPanel.h b/source/ConversationPanel.h index f078257c185a..e5a5dd2ea36c 100644 --- a/source/ConversationPanel.h +++ b/source/ConversationPanel.h @@ -46,6 +46,8 @@ class ConversationPanel : public Panel { const Mission *caller = nullptr, const System *system = nullptr, const std::shared_ptr &ship = nullptr, bool useTransactions = false); + virtual ~ConversationPanel() override; + template void SetCallback(T *t, void (T::*fun)(int)); void SetCallback(std::function fun); diff --git a/source/CoreStartData.cpp b/source/CoreStartData.cpp index 2d4b26039923..cf176a66d7a8 100644 --- a/source/CoreStartData.cpp +++ b/source/CoreStartData.cpp @@ -49,7 +49,7 @@ void CoreStartData::Save(DataWriter &out) const out.Write("start", identifier); out.BeginChild(); { - out.Write("system", system->Name()); + out.Write("system", system->TrueName()); out.Write("planet", planet->TrueName()); if(date) out.Write("date", date.Day(), date.Month(), date.Year()); diff --git a/source/Dialog.cpp b/source/Dialog.cpp index 398db716ce7d..95663e48fbba 100644 --- a/source/Dialog.cpp +++ b/source/Dialog.cpp @@ -15,6 +15,7 @@ this program. If not, see . #include "Dialog.h" +#include "audio/Audio.h" #include "Color.h" #include "Command.h" #include "Conversation.h" @@ -150,6 +151,13 @@ Dialog::Dialog(const string &text, PlayerInfo &player, const System *system, Tru +Dialog::~Dialog() +{ + Audio::Resume(); +} + + + // Draw this panel. void Dialog::Draw() { @@ -313,7 +321,7 @@ bool Dialog::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, bool i GetUI()->Pop(this); } else if((key == 'm' || command.Has(Command::MAP)) && system && player) - GetUI()->Push(new MapDetailPanel(*player, system)); + GetUI()->Push(new MapDetailPanel(*player, system, true)); else return false; @@ -355,6 +363,7 @@ bool Dialog::Click(int x, int y, int clicks) // Common code from all three constructors: void Dialog::Init(const string &message, Truncate truncate, bool canCancel, bool isMission) { + Audio::Pause(); SetInterruptible(isMission); this->isMission = isMission; diff --git a/source/Dialog.h b/source/Dialog.h index 633012479d6a..e8901ec41b72 100644 --- a/source/Dialog.h +++ b/source/Dialog.h @@ -47,7 +47,7 @@ class Dialog : public Panel { // Mission accept / decline dialog. Dialog(const std::string &text, PlayerInfo &player, const System *system = nullptr, Truncate truncate = Truncate::NONE, bool allowsFastForward = false); - virtual ~Dialog() = default; + virtual ~Dialog() override; // Three different kinds of dialogs can be constructed: requesting numerical // input, requesting text input, or not requesting any input at all. In any diff --git a/source/Effect.cpp b/source/Effect.cpp index 19115b841047..c56ec711f0a0 100644 --- a/source/Effect.cpp +++ b/source/Effect.cpp @@ -18,8 +18,25 @@ this program. If not, see . #include "audio/Audio.h" #include "DataNode.h" +#include + using namespace std; +namespace { + const map categoryNames = { + {"ui", SoundCategory::UI}, + {"anti-missile", SoundCategory::ANTI_MISSILE}, + {"weapon", SoundCategory::WEAPON}, + {"engine", SoundCategory::ENGINE}, + {"afterburner", SoundCategory::AFTERBURNER}, + {"jump", SoundCategory::JUMP}, + {"explosion", SoundCategory::EXPLOSION}, + {"scan", SoundCategory::SCAN}, + {"environment", SoundCategory::ENVIRONMENT}, + {"alert", SoundCategory::ALERT} + }; +} + const string &Effect::Name() const @@ -47,6 +64,13 @@ void Effect::Load(const DataNode &node) LoadSprite(child); else if(child.Token(0) == "sound" && child.Size() >= 2) sound = Audio::Get(child.Token(1)); + else if(child.Token(0) == "sound category" && child.Size() >= 2) + { + if(categoryNames.contains(child.Token(1))) + soundCategory = categoryNames.at(child.Token(1)); + else + child.PrintTrace("Unknown sound category \"" + child.Token(1) + "\""); + } else if(child.Token(0) == "lifetime" && child.Size() >= 2) lifetime = child.Value(1); else if(child.Token(0) == "random lifetime" && child.Size() >= 2) diff --git a/source/Effect.h b/source/Effect.h index 60f8233be0ab..e76f0c01fcd6 100644 --- a/source/Effect.h +++ b/source/Effect.h @@ -15,6 +15,8 @@ this program. If not, see . #pragma once +#include "audio/SoundCategory.h" + #include "Angle.h" #include "Body.h" @@ -50,6 +52,7 @@ class Effect : public Body { std::string name; const Sound *sound = nullptr; + SoundCategory soundCategory = SoundCategory::EXPLOSION; // Parameters used for randomizing spin and velocity. The random angle is // added to the parent angle, and then a random velocity in that direction diff --git a/source/Engine.cpp b/source/Engine.cpp index 6869ad82d8a8..f7919e192ea3 100644 --- a/source/Engine.cpp +++ b/source/Engine.cpp @@ -765,7 +765,7 @@ void Engine::Step(bool isActive) info.SetString("flagship name", flagship.Name()); } if(currentSystem) - info.SetString("location", currentSystem->Name()); + info.SetString("location", currentSystem->DisplayName()); info.SetString("date", player.GetDate().ToString()); if(flagship) { @@ -886,7 +886,7 @@ void Engine::Step(bool isActive) object->GetPlanet() && object->GetPlanet()->CanLand(*flagship) ? "Can land on:" : "Cannot land on:"; info.SetString("navigation mode", navigationMode); - const string &name = object->Name(); + const string &name = object->DisplayName(); info.SetString("destination", name); targets.push_back({ @@ -900,7 +900,7 @@ void Engine::Step(bool isActive) { info.SetString("navigation mode", "Hyperspace:"); if(player.CanView(*flagship->GetTargetSystem())) - info.SetString("destination", flagship->GetTargetSystem()->Name()); + info.SetString("destination", flagship->GetTargetSystem()->DisplayName()); else info.SetString("destination", "unexplored system"); } @@ -1520,7 +1520,7 @@ void Engine::EnterSystem() Audio::PlayMusic(system->MusicName()); GameData::SetHaze(system->Haze(), false); - Messages::Add("Entering the " + system->Name() + " system on " + Messages::Add("Entering the " + system->DisplayName() + " system on " + today.ToString() + (system->IsInhabited(flagship) ? "." : ". No inhabited planets detected."), Messages::Importance::Daily); @@ -1737,10 +1737,10 @@ void Engine::CalculateStep() // No sounds. } else if(jumpSounds.empty()) - Audio::Play(Audio::Get(isJumping ? "jump drive" : "hyperdrive")); + Audio::Play(Audio::Get(isJumping ? "jump drive" : "hyperdrive"), SoundCategory::JUMP); else for(const auto &sound : jumpSounds) - Audio::Play(sound.first); + Audio::Play(sound.first, SoundCategory::JUMP); } // Check if the flagship just entered a new system. if(flagship && playerSystem != flagship->GetSystem()) @@ -1888,22 +1888,22 @@ void Engine::CalculateStep() if(ship->ThrustMagnitude() && !ship->EnginePoints().empty()) { for(const auto &it : ship->Attributes().FlareSounds()) - Audio::Play(it.first, ship->Position()); + Audio::Play(it.first, ship->Position(), SoundCategory::ENGINE); } else if(ship->IsReversing() && !ship->ReverseEnginePoints().empty()) { for(const auto &it : ship->Attributes().ReverseFlareSounds()) - Audio::Play(it.first, ship->Position()); + Audio::Play(it.first, ship->Position(), SoundCategory::ENGINE); } else if(ship->IsLatThrusting() && !ship->LateralEnginePoints().empty()) { for(const auto &it : ship->Attributes().LateralFlareSounds()) - Audio::Play(it.first, ship->Position()); + Audio::Play(it.first, ship->Position(), SoundCategory::ENGINE); } if(ship->IsSteering() && !ship->SteeringEnginePoints().empty()) { for(const auto &it : ship->Attributes().SteeringFlareSounds()) - Audio::Play(it.first, ship->Position()); + Audio::Play(it.first, ship->Position(), SoundCategory::ENGINE); } } else @@ -1916,22 +1916,22 @@ void Engine::CalculateStep() if(flagship->IsThrusting() && !flagship->EnginePoints().empty()) { for(const auto &it : flagship->Attributes().FlareSounds()) - Audio::Play(it.first); + Audio::Play(it.first, SoundCategory::ENGINE); } else if(flagship->IsReversing() && !flagship->ReverseEnginePoints().empty()) { for(const auto &it : flagship->Attributes().ReverseFlareSounds()) - Audio::Play(it.first); + Audio::Play(it.first, SoundCategory::ENGINE); } else if(flagship->IsLatThrusting() && !flagship->LateralEnginePoints().empty()) { for(const auto &it : flagship->Attributes().LateralFlareSounds()) - Audio::Play(it.first); + Audio::Play(it.first, SoundCategory::ENGINE); } if(flagship->IsSteering() && !flagship->SteeringEnginePoints().empty()) { for(const auto &it : flagship->Attributes().SteeringFlareSounds()) - Audio::Play(it.first); + Audio::Play(it.first, SoundCategory::ENGINE); } } // Draw the projectiles. @@ -2007,10 +2007,10 @@ void Engine::MoveShip(const shared_ptr &ship) // No sounds. } else if(jumpSounds.empty()) - Audio::Play(Audio::Get(isJump ? "jump out" : "hyperdrive out"), position); + Audio::Play(Audio::Get(isJump ? "jump out" : "hyperdrive out"), position, SoundCategory::JUMP); else for(const auto &sound : jumpSounds) - Audio::Play(sound.first, position); + Audio::Play(sound.first, position, SoundCategory::JUMP); } // Did this ship just jump into the player's system? @@ -2023,10 +2023,10 @@ void Engine::MoveShip(const shared_ptr &ship) // No sounds. } else if(jumpSounds.empty()) - Audio::Play(Audio::Get(isJump ? "jump in" : "hyperdrive in"), position); + Audio::Play(Audio::Get(isJump ? "jump in" : "hyperdrive in"), position, SoundCategory::JUMP); else for(const auto &sound : jumpSounds) - Audio::Play(sound.first, position); + Audio::Play(sound.first, position, SoundCategory::JUMP); } } @@ -2326,12 +2326,12 @@ void Engine::HandleMouseClicks() if(&object == flagship->GetTargetStellar()) { if(!planet->CanLand(*flagship)) - Messages::Add("The authorities on " + planet->Name() + Messages::Add("The authorities on " + planet->DisplayName() + " refuse to let you land.", Messages::Importance::Highest); else if(!flagship->IsDestroyed()) { activeCommands |= Command::LAND; - Messages::Add("Landing on " + planet->Name() + ".", Messages::Importance::High); + Messages::Add("Landing on " + planet->DisplayName() + ".", Messages::Importance::High); } } else @@ -2840,7 +2840,7 @@ void Engine::FillRadar() else if(hasHostiles && !hadHostiles) { if(Preferences::PlayAudioAlert()) - Audio::Play(Audio::Get("alarm")); + Audio::Play(Audio::Get("alarm"), SoundCategory::ALERT); alarmTime = 300; hadHostiles = true; } diff --git a/source/EscortDisplay.cpp b/source/EscortDisplay.cpp index 1ae6e0d89acd..3a9b0e66f8ee 100644 --- a/source/EscortDisplay.cpp +++ b/source/EscortDisplay.cpp @@ -179,7 +179,7 @@ EscortDisplay::Icon::Icon(const Ship &ship, bool isHere, bool systemNameKnown, b cannotJump(fleetIsJumping && !ship.IsHyperspacing() && !ship.JumpsRemaining()), isSelected(isSelected), cost(ship.Cost()), - system((!isHere && ship.GetSystem()) ? (systemNameKnown ? ship.GetSystem()->Name() : "???") : ""), + system((!isHere && ship.GetSystem()) ? (systemNameKnown ? ship.GetSystem()->DisplayName() : "???") : ""), low{ship.Shields(), ship.Hull(), ship.Energy(), min(ship.Heat(), 1.), ship.Fuel()}, high(low), ships(1, &ship) diff --git a/source/Files.cpp b/source/Files.cpp index 1d89591b6a22..fc0d5a69d0f9 100644 --- a/source/Files.cpp +++ b/source/Files.cpp @@ -88,9 +88,11 @@ void Files::Init(const char * const *argv) { // Find the path to the resource directory. This will depend on the // operating system, and can be overridden by a command line argument. - resources = SDL_GetBasePath(); - if(resources.empty()) + char *basePath = SDL_GetBasePath(); + if(!basePath) throw runtime_error("Unable to get path to resource directory!"); + resources = basePath; + SDL_free(basePath); if(Exists(resources)) resources = filesystem::canonical(resources); @@ -296,6 +298,12 @@ time_t Files::Timestamp(const filesystem::path &filePath) void Files::Copy(const filesystem::path &from, const filesystem::path &to) { +#ifdef _WIN32 + // Due to a mingw bug, the overwrite_existing flag is not respected on Windows. + // TODO: remove once it is fixed. + if(Exists(to)) + Delete(to); +#endif copy(from, to, filesystem::copy_options::overwrite_existing); } diff --git a/source/Fleet.cpp b/source/Fleet.cpp index 4967b455ea8d..6dcc127af89a 100644 --- a/source/Fleet.cpp +++ b/source/Fleet.cpp @@ -314,7 +314,7 @@ void Fleet::Enter(const System &system, list> &ships, const Pla { // Log this error. Logger::LogError("Fleet::Enter: Unable to find valid stellar object for planet \"" - + planet->TrueName() + "\" in system \"" + system.Name() + "\""); + + planet->TrueName() + "\" in system \"" + system.TrueName() + "\""); return; } diff --git a/source/GameAction.cpp b/source/GameAction.cpp index 7c7ad06aaa19..8ebb18f313bf 100644 --- a/source/GameAction.cpp +++ b/source/GameAction.cpp @@ -287,9 +287,9 @@ void GameAction::Save(DataWriter &out) const for(auto &&it : events) out.Write("event", it.first->Name(), it.second.first, it.second.second); for(const System *system : mark) - out.Write("mark", system->Name()); + out.Write("mark", system->TrueName()); for(const System *system : unmark) - out.Write("unmark", system->Name()); + out.Write("unmark", system->TrueName()); for(const string &name : fail) out.Write("fail", name); if(failCaller) @@ -330,10 +330,10 @@ string GameAction::Validate() const // Marked and unmarked system must be valid. for(auto &&system : mark) if(!system->IsValid()) - return "system \"" + system->Name() + "\""; + return "system \"" + system->TrueName() + "\""; for(auto &&system : unmark) if(!system->IsValid()) - return "system \"" + system->Name() + "\""; + return "system \"" + system->TrueName() + "\""; // It is OK for this action to try to fail a mission that does not exist. // (E.g. a plugin may be designed for interoperability with other plugins.) diff --git a/source/GameData.cpp b/source/GameData.cpp index 54c9747c8d45..9a2e371354af 100644 --- a/source/GameData.cpp +++ b/source/GameData.cpp @@ -451,12 +451,12 @@ void GameData::WriteEconomy(DataWriter &out) using Purchase = pair>; WriteSorted(purchases, [](const Purchase *lhs, const Purchase *rhs) - { return lhs->first->Name() < rhs->first->Name(); }, + { return lhs->first->TrueName() < rhs->first->TrueName(); }, [&out](const Purchase &pit) { // Write purchases for all systems, even ones from removed plugins. for(const auto &cit : pit.second) - out.Write(pit.first->Name(), cit.first, cit.second); + out.Write(pit.first->TrueName(), cit.first, cit.second); }); out.EndChild(); } @@ -472,7 +472,7 @@ void GameData::WriteEconomy(DataWriter &out) if(!sit.second.IsValid() && !sit.second.HasTrade()) continue; - out.WriteToken(sit.second.Name()); + out.WriteToken(sit.second.TrueName()); for(const auto &cit : GameData::Commodities()) out.WriteToken(static_cast(sit.second.Supply(cit.name))); out.Write(); diff --git a/source/GameEvent.cpp b/source/GameEvent.cpp index 7840234328e0..cedf26be59a2 100644 --- a/source/GameEvent.cpp +++ b/source/GameEvent.cpp @@ -141,12 +141,12 @@ void GameEvent::Save(DataWriter &out) const conditionsToApply.Save(out); for(auto &&system : systemsToUnvisit) - out.Write("unvisit", system->Name()); + out.Write("unvisit", system->TrueName()); for(auto &&planet : planetsToUnvisit) out.Write("unvisit planet", planet->TrueName()); for(auto &&system : systemsToVisit) - out.Write("visit", system->Name()); + out.Write("visit", system->TrueName()); for(auto &&planet : planetsToVisit) out.Write("visit planet", planet->TrueName()); @@ -202,8 +202,8 @@ string GameEvent::IsValid() const for(auto &&systems : {systemsToVisit, systemsToUnvisit}) for(auto &&system : systems) - if(!system->IsValid() && !deferred["system"].contains(system->Name())) - return "contains invalid system \"" + system->Name() + "\"."; + if(!system->IsValid() && !deferred["system"].contains(system->TrueName())) + return "contains invalid system \"" + system->TrueName() + "\"."; for(auto &&planets : {planetsToVisit, planetsToUnvisit}) for(auto &&planet : planets) if(!planet->IsValid() && !deferred["planet"].contains(planet->TrueName())) diff --git a/source/HailPanel.cpp b/source/HailPanel.cpp index 9380cff925f2..0ff68c41b8c3 100644 --- a/source/HailPanel.cpp +++ b/source/HailPanel.cpp @@ -16,6 +16,7 @@ this program. If not, see . #include "HailPanel.h" #include "text/alignment.hpp" +#include "audio/Audio.h" #include "Dialog.h" #include "shader/DrawList.h" #include "text/Font.h" @@ -47,6 +48,7 @@ using namespace std; HailPanel::HailPanel(PlayerInfo &player, const shared_ptr &ship, function bribeCallback) : player(player), ship(ship), bribeCallback(std::move(bribeCallback)), facing(ship->Facing()) { + Audio::Pause(); SetInterruptible(false); const Government *gov = ship->GetGovernment(); @@ -134,11 +136,12 @@ HailPanel::HailPanel(PlayerInfo &player, const shared_ptr &ship, function< HailPanel::HailPanel(PlayerInfo &player, const StellarObject *object) : player(player), object(object), planet(object->GetPlanet()), facing(object->Facing()) { + Audio::Pause(); SetInterruptible(false); const Government *gov = planet ? planet->GetGovernment() : player.GetSystem()->GetGovernment(); if(planet) - header = gov->GetName() + " " + planet->Noun() + " \"" + planet->Name() + "\":"; + header = gov->GetName() + " " + planet->Noun() + " \"" + planet->DisplayName() + "\":"; hasLanguage = (gov->Language().empty() || player.Conditions().Get("language: " + gov->Language())); // If the player is hailing a planet, determine if a mission grants them clearance before checking @@ -175,6 +178,13 @@ HailPanel::HailPanel(PlayerInfo &player, const StellarObject *object) +HailPanel::~HailPanel() +{ + Audio::Resume(); +} + + + void HailPanel::Draw() { DrawBackdrop(); @@ -392,7 +402,7 @@ bool HailPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, boo else { planet->Bribe(); - Messages::Add("You bribed the authorities on " + planet->Name() + " " + Messages::Add("You bribed the authorities on " + planet->DisplayName() + " " + Format::CreditString(bribe) + " to permit you to land." , Messages::Importance::High); } diff --git a/source/HailPanel.h b/source/HailPanel.h index 2534c902be6e..e36b9abc2581 100644 --- a/source/HailPanel.h +++ b/source/HailPanel.h @@ -41,6 +41,8 @@ class HailPanel : public Panel { std::function bribeCallback); HailPanel(PlayerInfo &player, const StellarObject *object); + virtual ~HailPanel() override; + virtual void Draw() override; diff --git a/source/Hardpoint.cpp b/source/Hardpoint.cpp index 0de00a4cd8d0..eff195c25d78 100644 --- a/source/Hardpoint.cpp +++ b/source/Hardpoint.cpp @@ -474,7 +474,7 @@ void Hardpoint::Fire(Ship &ship, const Point &start, const Angle &aim) // Anti-missile sounds can be specified either in the outfit itself or in // the effect they create. if(outfit->WeaponSound()) - Audio::Play(outfit->WeaponSound(), start); + Audio::Play(outfit->WeaponSound(), start, IsSpecial() ? SoundCategory::ANTI_MISSILE : SoundCategory::WEAPON); // Apply any "kick" from firing this weapon. double force = outfit->FiringForce(); if(force) diff --git a/source/LocationFilter.cpp b/source/LocationFilter.cpp index 7297fff794b0..cad9e2c743cb 100644 --- a/source/LocationFilter.cpp +++ b/source/LocationFilter.cpp @@ -227,7 +227,7 @@ void LocationFilter::Save(DataWriter &out) const out.BeginChild(); { for(const System *system : systems) - out.Write(system->Name()); + out.Write(system->TrueName()); } out.EndChild(); } @@ -272,7 +272,7 @@ void LocationFilter::Save(DataWriter &out) const out.EndChild(); } if(center) - out.Write("near", center->Name(), centerMinDistance, centerMaxDistance); + out.Write("near", center->TrueName(), centerMinDistance, centerMaxDistance); } out.EndChild(); } diff --git a/source/MapDetailPanel.cpp b/source/MapDetailPanel.cpp index 8826b0d12c22..8615dac785c1 100644 --- a/source/MapDetailPanel.cpp +++ b/source/MapDetailPanel.cpp @@ -92,18 +92,18 @@ double MapDetailPanel::planetPanelHeight = 0.; -MapDetailPanel::MapDetailPanel(PlayerInfo &player, const System *system) - : MapPanel(player, system ? MapPanel::SHOW_REPUTATION : player.MapColoring(), system) +MapDetailPanel::MapDetailPanel(PlayerInfo &player, const System *system, bool fromMission) + : MapPanel(player, system ? MapPanel::SHOW_REPUTATION : player.MapColoring(), system, fromMission) { } -MapDetailPanel::MapDetailPanel(const MapPanel &panel) - : MapPanel(panel) +MapDetailPanel::MapDetailPanel(const MapPanel &panel, bool isStars) + : MapPanel(panel), isStars(isStars) { // Use whatever map coloring is specified in the PlayerInfo. - commodity = player.MapColoring(); + commodity = isStars ? -8 : player.MapColoring(); } @@ -121,6 +121,7 @@ void MapDetailPanel::Step() { DoHelp("map advanced danger"); DoHelp("map advanced ports"); + DoHelp("map advanced stars"); } if(!player.GetPlanet()) DoHelp("map"); @@ -135,7 +136,7 @@ void MapDetailPanel::Draw() DrawInfo(); DrawOrbits(); DrawKey(); - FinishDrawing("is ports"); + FinishDrawing(isStars ? "is stars" : "is ports"); } @@ -204,6 +205,7 @@ bool MapDetailPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command { DoHelp("map advanced danger", true); DoHelp("map advanced ports", true); + DoHelp("map advanced stars", true); if(!player.GetPlanet()) DoHelp("map", true); } @@ -374,17 +376,26 @@ bool MapDetailPanel::Click(int x, int y, int clicks) if(y >= tradeY && y < tradeY + 200) { // The player clicked on a tradable commodity. Color the map by its price. + isStars = false; SetCommodity((y - tradeY) / 20); return true; } // Clicking the system name activates the view of the player's reputation with various governments. // But the bit to the left will show danger of pirate/raid fleets instead. else if(y < governmentY && y > governmentY - 30) + { + isStars = false; SetCommodity(x < Screen::Left() + mapInterface->GetValue("text margin") ? SHOW_DANGER : SHOW_REPUTATION); + } + // Clicking the government name activates the view of system / planet ownership. else if(y >= governmentY && y < governmentY + 25) + { + isStars = false; SetCommodity(SHOW_GOVERNMENT); + } + } if(y <= Screen::Top() + planetPanelHeight + 30 && x <= Screen::Left() + planetCardWidth + arrowOffset + 10) { @@ -393,12 +404,14 @@ bool MapDetailPanel::Click(int x, int y, int clicks) MapPlanetCard::ClickAction clickAction = card.Click(x, y, clicks); if(clickAction == MapPlanetCard::ClickAction::GOTO_SHIPYARD) { + isStars = false; GetUI()->Pop(this); GetUI()->Push(new MapShipyardPanel(*this, true)); break; } else if(clickAction == MapPlanetCard::ClickAction::GOTO_OUTFITTER) { + isStars = false; GetUI()->Pop(this); GetUI()->Push(new MapOutfitterPanel(*this, true)); break; @@ -419,6 +432,7 @@ bool MapDetailPanel::Click(int x, int y, int clicks) { // The player has clicked within the "orbits" scene. // Select the nearest planet to the click point. + isStars = false; Point click = Point(x, y); selectedPlanet = nullptr; double distance = numeric_limits::infinity(); @@ -530,9 +544,10 @@ void MapDetailPanel::DrawKey() "", // Special should never be active in this mode. "Government:", "System:", - "Danger level:" + "Danger level:", + "" // Temporary blank tile for the starry map mode. }; - const string &header = HEADER[-min(0, max(-7, commodity))]; + const string &header = HEADER[-min(0, max(-8, commodity))]; font.Draw(header, pos + headerOff, medium); pos.Y() += 20.; @@ -653,16 +668,21 @@ void MapDetailPanel::DrawKey() pos.Y() += 20.; } } - - if(commodity != SHOW_DANGER) + else if(commodity == SHOW_STARS) + { + // The starry map mode leave the legend panel blank. + } + if(commodity != SHOW_DANGER && commodity != SHOW_STARS) { RingShader::Draw(pos, OUTER, INNER, UninhabitedColor()); font.Draw("Uninhabited", pos + textOff, dim); pos.Y() += 20.; } - - RingShader::Draw(pos, OUTER, INNER, UnexploredColor()); - font.Draw("Unexplored", pos + textOff, dim); + if(commodity != SHOW_STARS) + { + RingShader::Draw(pos, OUTER, INNER, UnexploredColor()); + font.Draw("Unexplored", pos + textOff, dim); + } } @@ -752,7 +772,7 @@ void MapDetailPanel::DrawInfo() SpriteShader::Draw(alertSprite, uiPoint + Point(-textMargin / 2., -7. + font.Height() / 2.), alertScale); string systemName = player.KnowsName(*selectedSystem) ? - selectedSystem->Name() : "Unexplored System"; + selectedSystem->DisplayName() : "Unexplored System"; const auto alignLeft = Layout(145, Truncate::BACK); font.Draw({systemName, alignLeft}, uiPoint + Point(0., -7.), medium); @@ -962,7 +982,7 @@ void MapDetailPanel::DrawOrbits() habitColor[6]); // Draw the name of the selected planet. - const string &name = selectedPlanet ? selectedPlanet->Name() : selectedSystem->Name(); + const string &name = selectedPlanet ? selectedPlanet->DisplayName() : selectedSystem->DisplayName(); Point namePos(Screen::Right() - 190., Screen::Top() + 7.); font.Draw({name, {180, Alignment::CENTER, Truncate::BACK}}, namePos, *GameData::Colors().Get("medium")); @@ -974,5 +994,8 @@ void MapDetailPanel::DrawOrbits() void MapDetailPanel::SetCommodity(int index) { commodity = index; + if(index != SHOW_STARS) + isStars = false; + player.SetMapColoring(commodity); } diff --git a/source/MapDetailPanel.h b/source/MapDetailPanel.h index 7a1369671bb5..9b7715c52c43 100644 --- a/source/MapDetailPanel.h +++ b/source/MapDetailPanel.h @@ -37,8 +37,8 @@ class System; // click on a planet to view its description. class MapDetailPanel : public MapPanel { public: - explicit MapDetailPanel(PlayerInfo &player, const System *system = nullptr); - explicit MapDetailPanel(const MapPanel &panel); + explicit MapDetailPanel(PlayerInfo &player, const System *system = nullptr, bool fromMission = false); + explicit MapDetailPanel(const MapPanel &panel, bool isStars); virtual void Step() override; virtual void Draw() override; @@ -78,6 +78,7 @@ class MapDetailPanel : public MapPanel { // Which panel is being hovered over and should be affected by up and down keys. bool isPlanetViewSelected = false; + bool isStars = false; ScrollVar scroll; ScrollBar scrollbar; diff --git a/source/MapPanel.cpp b/source/MapPanel.cpp index 01dc7c377078..3a225f9db113 100644 --- a/source/MapPanel.cpp +++ b/source/MapPanel.cpp @@ -17,6 +17,7 @@ this program. If not, see . #include "text/alignment.hpp" #include "Angle.h" +#include "audio/Audio.h" #include "shader/BatchDrawList.h" #include "CargoHold.h" #include "Dialog.h" @@ -250,14 +251,16 @@ const float MapPanel::LINK_OFFSET = 7.f; -MapPanel::MapPanel(PlayerInfo &player, int commodity, const System *special) +MapPanel::MapPanel(PlayerInfo &player, int commodity, const System *special, bool fromMission) : player(player), distance(player), playerSystem(*player.GetSystem()), selectedSystem(special ? special : player.GetSystem()), specialSystem(special), playerJumpDistance(System::DEFAULT_NEIGHBOR_DISTANCE), - commodity(commodity) + commodity(commodity), + fromMission(fromMission) { + Audio::Pause(); SetIsFullScreen(true); SetInterruptible(false); // Recalculate the fog each time the map is opened, just in case the player @@ -293,6 +296,13 @@ MapPanel::MapPanel(PlayerInfo &player, int commodity, const System *special) +MapPanel::~MapPanel() +{ + Audio::Resume(); +} + + + void MapPanel::Step() { if(recentering > 0) @@ -414,7 +424,7 @@ void MapPanel::FinishDrawing(const string &buttonCondition) if(HasMultipleLandablePlanets(*hoverSystem) || t.outfits.size() > 1) for(const auto &it : t.outfits) - tooltip += "\n - " + to_string(it.second) + " on " + it.first->Name(); + tooltip += "\n - " + to_string(it.second) + " on " + it.first->DisplayName(); } hoverText.Wrap(tooltip); @@ -472,7 +482,7 @@ void MapPanel::DrawMiniMap(const PlayerInfo &player, float alpha, const System * const System &system = *jump[i]; const Government *gov = system.GetGovernment(); Point from = system.Position() - center + drawPos; - const string &name = player.KnowsName(system) ? system.Name() : UNKNOWN_SYSTEM; + const string &name = player.KnowsName(system) ? system.DisplayName() : UNKNOWN_SYSTEM; font.Draw(name, from + Point(OUTER, -.5 * font.Height()), lineColor); // Draw the origin and destination systems, since they @@ -599,7 +609,12 @@ bool MapPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, bool else if(key == 'p' && buttonCondition != "is ports") { GetUI()->Pop(this); - GetUI()->Push(new MapDetailPanel(*this)); + GetUI()->Push(new MapDetailPanel(*this, false)); + } + else if(key == 'a' && buttonCondition != "is stars") + { + GetUI()->Pop(this); + GetUI()->Push(new MapDetailPanel(*this, true)); } else if(key == 'f') { @@ -610,9 +625,9 @@ bool MapPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, bool else if(key == 'x') player.SetStarryMap(!mapIsStarry); else if(key == SDLK_PLUS || key == SDLK_KP_PLUS || key == SDLK_EQUALS) - player.SetMapZoom(min(static_cast(mapInterface->GetValue("max zoom")), player.MapZoom() + 1)); + player.SetMapZoom(min(mapInterface->GetValue("max zoom"), player.MapZoom() + 1)); else if(key == SDLK_MINUS || key == SDLK_KP_MINUS) - player.SetMapZoom(max(static_cast(mapInterface->GetValue("min zoom")), player.MapZoom() - 1)); + player.SetMapZoom(max(mapInterface->GetValue("min zoom"), player.MapZoom() - 1)); else return false; @@ -696,9 +711,9 @@ bool MapPanel::Scroll(double dx, double dy) Point anchor = mouse / Zoom() - center; const Interface *mapInterface = GameData::Interfaces().Get("map"); if(dy > 0.) - player.SetMapZoom(min(static_cast(mapInterface->GetValue("max zoom")), player.MapZoom() + 1)); + player.SetMapZoom(min(mapInterface->GetValue("max zoom"), player.MapZoom() + 1)); else if(dy < 0.) - player.SetMapZoom(max(static_cast(mapInterface->GetValue("min zoom")), player.MapZoom() - 1)); + player.SetMapZoom(max(mapInterface->GetValue("min zoom"), player.MapZoom() - 1)); // Now, Zoom() has changed (unless at one of the limits). But, we still want // anchor to be the same, so: @@ -806,7 +821,11 @@ void MapPanel::Select(const System *system) { if(!system) return; + selectedSystem = system; + // Update the cache to apply any visual changes needed after the selected system was changed. + UpdateCache(); + vector &plan = player.TravelPlan(); Ship *flagship = player.Flagship(); if(!flagship || (!plan.empty() && system == plan.front())) @@ -872,6 +891,7 @@ void MapPanel::Find(const string &name) { bestIndex = index; selectedSystem = &system; + UpdateCache(); CenterOnSystem(selectedSystem); if(!index) { @@ -891,6 +911,7 @@ void MapPanel::Find(const string &name) { bestIndex = index; selectedSystem = planet.GetSystem(); + UpdateCache(); CenterOnSystem(selectedSystem); if(!index) { @@ -1129,8 +1150,8 @@ void MapPanel::UpdateCache() const bool canViewSystem = player.CanView(system); nodes.emplace_back(system.Position(), color, - player.KnowsName(system) ? system.Name() : "", - (&system == &playerSystem) ? closeNameColor : farNameColor, + player.KnowsName(system) ? system.DisplayName() : "", + (&system == &playerSystem || &system == selectedSystem) ? closeNameColor : farNameColor, canViewSystem ? system.GetGovernment() : nullptr, canViewSystem ? system.GetMapIcons() : unmappedSystem); } @@ -1260,7 +1281,7 @@ void MapPanel::DrawSelectedSystem() if(!player.KnowsName(*selectedSystem)) text = "Selected system: unexplored system"; else - text = "Selected system: " + selectedSystem->Name(); + text = "Selected system: " + selectedSystem->DisplayName(); int jumps = 0; const vector &plan = player.TravelPlan(); @@ -1420,24 +1441,19 @@ void MapPanel::DrawSystems() // Draw the circles for the systems. BatchDrawList starBatch; double zoom = Zoom(); - const float ringFade = mapIsStarry ? 1.5 - 1.25 * zoom : 1.; for(const Node &node : nodes) { Point pos = zoom * (node.position + center); - if(!mapIsStarry) + if(commodity != SHOW_STARS) RingShader::Draw(pos, OUTER, INNER, node.color); else { - // System rings fade as you zoom in if starry map is enabled. - const float alpha = max(ringFade, node.mapIcons.empty() ? .9f : 0.f); - RingShader::Draw(pos, OUTER, INNER, node.color.Additive(alpha)); - // Ensures every multiple-star system has a characteristic, deterministic rotation. Angle starAngle = 0; Angle angularSpacing = 0; Point starOffset = Point(0, 0); - const int starsToDraw = min(static_cast(node.mapIcons.size()), MAX_STARS); + const int starsToDraw = min(node.mapIcons.size(), MAX_STARS); if(starsToDraw > 1) { starAngle = node.name.length() + node.position.Length(); @@ -1451,7 +1467,7 @@ void MapPanel::DrawSystems() starAngle += angularSpacing; const Sprite *star = node.mapIcons[i]; const Body starBody(star, pos + zoom * starOffset * starAngle.Unit(), - Point(0, 0), 0, sqrt(zoom) / 2, min(zoom + 0.3, 0.75)); + Point(0, 0), 0, sqrt(max(zoom, 0.5)) / 2, min(zoom + 0.25, 0.75)); starBatch.Add(starBody); } } diff --git a/source/MapPanel.h b/source/MapPanel.h index 41c94e78f8ae..71523b524fb7 100644 --- a/source/MapPanel.h +++ b/source/MapPanel.h @@ -50,6 +50,7 @@ class MapPanel : public Panel { static const int SHOW_GOVERNMENT = -5; static const int SHOW_REPUTATION = -6; static const int SHOW_DANGER = -7; + static const int SHOW_STARS = -8; static const float OUTER; static const float INNER; @@ -69,7 +70,9 @@ class MapPanel : public Panel { public: - explicit MapPanel(PlayerInfo &player, int commodity = SHOW_REPUTATION, const System *special = nullptr); + explicit MapPanel(PlayerInfo &player, int commodity = SHOW_REPUTATION, + const System *special = nullptr, bool fromMission = false); + virtual ~MapPanel() override; virtual void Step() override; virtual void Draw() override; @@ -165,6 +168,8 @@ class MapPanel : public Panel { // else gets in the way of its default position. int selectedSystemOffset = 0; + bool fromMission = false; + private: void DrawTravelPlan(); // Display the name of and distance to the selected system. diff --git a/source/MapPlanetCard.cpp b/source/MapPlanetCard.cpp index 606c4b148e45..65c76900a2c0 100644 --- a/source/MapPlanetCard.cpp +++ b/source/MapPlanetCard.cpp @@ -42,7 +42,7 @@ namespace { MapPlanetCard::MapPlanetCard(const StellarObject &object, unsigned number, bool hasVisited, const MapDetailPanel *parent) - : parent(parent), number(number), hasVisited(hasVisited), planetName(object.Name()) + : parent(parent), number(number), hasVisited(hasVisited), planetName(object.DisplayName()) { planet = object.GetPlanet(); hasSpaceport = planet->HasServices(); diff --git a/source/MenuAnimationPanel.cpp b/source/MenuAnimationPanel.cpp index 4ac2027fe53d..ca89bd540048 100644 --- a/source/MenuAnimationPanel.cpp +++ b/source/MenuAnimationPanel.cpp @@ -27,7 +27,7 @@ MenuAnimationPanel::MenuAnimationPanel() { SetTrapAllEvents(false); - Audio::Play(Audio::Get("landing")); + Audio::Play(Audio::Get("landing"), SoundCategory::UI); } diff --git a/source/MenuPanel.cpp b/source/MenuPanel.cpp index ed2beb488ecf..67f63cfabfb7 100644 --- a/source/MenuPanel.cpp +++ b/source/MenuPanel.cpp @@ -139,9 +139,9 @@ void MenuPanel::Draw() info.SetString("ship", flagship.Name()); } if(player.GetSystem()) - info.SetString("system", player.GetSystem()->Name()); + info.SetString("system", player.GetSystem()->DisplayName()); if(player.GetPlanet()) - info.SetString("planet", player.GetPlanet()->Name()); + info.SetString("planet", player.GetPlanet()->DisplayName()); info.SetString("credits", Format::Credits(player.Accounts().Credits())); info.SetString("date", player.GetDate().ToString()); info.SetString("playtime", Format::PlayTime(player.GetPlayTime())); diff --git a/source/MessageLogPanel.cpp b/source/MessageLogPanel.cpp index f1239b5b91e3..08d7492175e9 100644 --- a/source/MessageLogPanel.cpp +++ b/source/MessageLogPanel.cpp @@ -16,6 +16,7 @@ this program. If not, see . #include "MessageLogPanel.h" #include "text/alignment.hpp" +#include "audio/Audio.h" #include "Color.h" #include "Command.h" #include "shader/FillShader.h" @@ -44,11 +45,19 @@ namespace { MessageLogPanel::MessageLogPanel() : messages(Messages::GetLog()), width(GameData::Interfaces().Get("message log")->GetValue("width")) { + Audio::Pause(); SetInterruptible(false); } +MessageLogPanel::~MessageLogPanel() +{ + Audio::Resume(); +} + + + void MessageLogPanel::Draw() { // Dim out everything outside this panel. diff --git a/source/MessageLogPanel.h b/source/MessageLogPanel.h index eca814a87c8d..663e8f6b04b6 100644 --- a/source/MessageLogPanel.h +++ b/source/MessageLogPanel.h @@ -25,6 +25,7 @@ this program. If not, see . class MessageLogPanel : public Panel { public: MessageLogPanel(); + virtual ~MessageLogPanel() override; virtual void Draw() override; diff --git a/source/Minable.cpp b/source/Minable.cpp index 60aa8b784f70..a4eb2f1fdd66 100644 --- a/source/Minable.cpp +++ b/source/Minable.cpp @@ -77,10 +77,13 @@ void Minable::Load(const DataNode &node) displayName = child.Token(1); else if(key == "noun") noun = child.Token(1); - // A full sprite definition (frame rate, etc.) is not needed, because - // the frame rate will be set randomly and it will always be looping. else if(key == "sprite") - SetSprite(SpriteSet::Get(child.Token(1))); + { + LoadSprite(child); + for(const DataNode &grand : child) + if(grand.Token(0) == "frame rate" || grand.Token(0) == "frame time") + useRandomFrameRate = false; + } else if(key == "hull") hull = child.Value(1); else if(key == "random hull") @@ -182,7 +185,8 @@ void Minable::Place(double energy, double beltRadius) // Start the object off with a random facing angle and spin rate. angle = Angle::Random(); spin = Angle::Random(energy) - Angle::Random(energy); - SetFrameRate(Random::Real() * 4. * energy + 5.); + if(useRandomFrameRate) + SetFrameRate(Random::Real() * 4. * energy + 5.); // Choose a random direction for the angle of periapsis. rotation = Random::Real() * 2. * PI; diff --git a/source/Minable.h b/source/Minable.h index 98bdaa4d078b..eaa2c69adeb5 100644 --- a/source/Minable.h +++ b/source/Minable.h @@ -130,4 +130,5 @@ class Minable : public Body { std::map explosions; // The expected value of the payload of this minable. int64_t value = 0.; + bool useRandomFrameRate = true; }; diff --git a/source/Mission.cpp b/source/Mission.cpp index 2c5f38283e07..e3bbc0b4e4e1 100644 --- a/source/Mission.cpp +++ b/source/Mission.cpp @@ -231,6 +231,8 @@ void Mission::Load(const DataNode &node) location = SHIPYARD; else if(child.Token(0) == "outfitter") location = OUTFITTER; + else if(child.Token(0) == "job board") + location = JOB_BOARD; else if(child.Token(0) == "repeat") repeat = (child.Size() == 1 ? 0 : static_cast(child.Value(1))); else if(child.Token(0) == "clearance") @@ -386,6 +388,8 @@ void Mission::Save(DataWriter &out, const string &tag) const out.Write("shipyard"); else if(location == OUTFITTER) out.Write("outfitter"); + else if(location == JOB_BOARD) + out.Write("job board"); else if(location == ASSISTING) out.Write("assisting"); else if(location == BOARDING) @@ -453,11 +457,11 @@ void Mission::Save(DataWriter &out, const string &tag) const out.EndChild(); } if(destination) - out.Write("destination", destination->Name()); + out.Write("destination", destination->TrueName()); for(const System *system : waypoints) - out.Write("waypoint", system->Name()); + out.Write("waypoint", system->TrueName()); for(const System *system : visitedWaypoints) - out.Write("waypoint", system->Name(), "visited"); + out.Write("waypoint", system->TrueName(), "visited"); for(const Planet *planet : stopovers) out.Write("stopover", planet->TrueName()); @@ -465,9 +469,9 @@ void Mission::Save(DataWriter &out, const string &tag) const out.Write("stopover", planet->TrueName(), "visited"); for(const System *system : markedSystems) - out.Write("mark", system->Name()); + out.Write("mark", system->TrueName()); for(const System *system : unmarkedSystems) - out.Write("mark", system->Name(), "unmarked"); + out.Write("mark", system->TrueName(), "unmarked"); for(const NPC &npc : npcs) npc.Save(out); @@ -1444,11 +1448,11 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b subs[""] = (result.passengers == 1) ? "passenger" : "passengers"; subs[""] = (result.passengers == 1) ? "a passenger" : (subs[""] + " passengers"); if(player.GetPlanet()) - subs[""] = player.GetPlanet()->Name(); + subs[""] = player.GetPlanet()->DisplayName(); else if(boardingShip) subs[""] = boardingShip->Name(); - subs[""] = result.destination ? result.destination->Name() : ""; - subs[""] = result.destination ? result.destination->GetSystem()->Name() : ""; + subs[""] = result.destination ? result.destination->DisplayName() : ""; + subs[""] = result.destination ? result.destination->GetSystem()->DisplayName() : ""; subs[""] = subs[""] + " in the " + subs[""] + " system"; subs[""] = result.deadline.ToString(); subs[""] = result.deadline.LongString(); @@ -1471,8 +1475,8 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b stopovers += result; planets += result; } - stopovers += planet->Name() + " in the " + planet->GetSystem()->Name() + " system"; - planets += planet->Name(); + stopovers += planet->DisplayName() + " in the " + planet->GetSystem()->DisplayName() + " system"; + planets += planet->DisplayName(); } subs[""] = stopovers; subs[""] = planets; @@ -1486,7 +1490,7 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b { if(count++) systems += (&system != last) ? ", " : (count > 2 ? ", and " : " and "); - systems += system->Name(); + systems += system->DisplayName(); } return systems; }; @@ -1540,7 +1544,7 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b } if(oit != onEnter.end()) { - Logger::LogError("Instantiation Error: Action \"on enter '" + oit->first->Name() + "'\" in mission \"" + Logger::LogError("Instantiation Error: Action \"on enter '" + oit->first->TrueName() + "'\" in mission \"" + Identifier() + "\" uses invalid " + std::move(reason)); return result; } diff --git a/source/Mission.h b/source/Mission.h index 17dab0f683b0..d26e1d18e81c 100644 --- a/source/Mission.h +++ b/source/Mission.h @@ -89,7 +89,7 @@ class Mission { bool IsMinor() const; // Find out where this mission is offered. - enum Location {SPACEPORT, LANDING, JOB, ASSISTING, BOARDING, SHIPYARD, OUTFITTER}; + enum Location {SPACEPORT, LANDING, JOB, ASSISTING, BOARDING, SHIPYARD, OUTFITTER, JOB_BOARD}; bool IsAtLocation(Location location) const; // Information about what you are doing. diff --git a/source/MissionPanel.cpp b/source/MissionPanel.cpp index c3c44000e733..737121bb4f9e 100644 --- a/source/MissionPanel.cpp +++ b/source/MissionPanel.cpp @@ -197,9 +197,39 @@ MissionPanel::MissionPanel(const MapPanel &panel) void MissionPanel::Step() { MapPanel::Step(); + + // If a job or mission that launches the player triggers, + // immediately close the map. + if(player.ShouldLaunch()) + GetUI()->Pop(this); + if(GetUI()->IsTop(this) && player.GetPlanet() && player.GetDate() >= player.StartData().GetDate() + 12) DoHelp("map advanced"); DoHelp("jobs"); + + if(GetUI()->IsTop(this) && !fromMission) + { + Mission *mission = player.MissionToOffer(Mission::JOB_BOARD); + if(mission) + { + // Prevent dragging while a mission is offering, to make sure + // the screen fully pans to the destination system. + canDrag = false; + + // Only highlight and pan to the destination if the mission is visible. + // Otherwise, pan to the player's current system. + specialSystem = mission->IsVisible() ? mission->Destination()->GetSystem() : nullptr; + CenterOnSystem(specialSystem ? specialSystem : player.GetSystem()); + + mission->Do(Mission::OFFER, player, GetUI()); + } + else + { + canDrag = true; + specialSystem = nullptr; + player.HandleBlockedMissions(Mission::JOB_BOARD, GetUI()); + } + } } @@ -557,7 +587,7 @@ bool MissionPanel::Drag(double dx, double dy) min(accepted.size() * 20. + 160. - Screen::Height(), acceptedScroll - dy)); } - else + else if(canDrag) MapPanel::Drag(dx, dy); return true; @@ -618,11 +648,13 @@ void MissionPanel::SetSelectedScrollAndCenter(bool immediate) if(availableIt != available.end()) { selectedSystem = availableIt->Destination()->GetSystem(); + UpdateCache(); ScrollMissionList(available, availableIt, availableScroll, false); } else if(acceptedIt != accepted.end()) { selectedSystem = acceptedIt->Destination()->GetSystem(); + UpdateCache(); ScrollMissionList(accepted, acceptedIt, acceptedScroll, true); } diff --git a/source/MissionPanel.h b/source/MissionPanel.h index 7a181d965fd7..8e0d74fed691 100644 --- a/source/MissionPanel.h +++ b/source/MissionPanel.h @@ -90,6 +90,8 @@ class MissionPanel : public MapPanel { double availableScroll = 0.; double acceptedScroll = 0.; + bool canDrag = true; + int dragSide = 0; int hoverSortCount = 0; int hoverSort = -1; // 0 to 3 for each UI element diff --git a/source/NPC.cpp b/source/NPC.cpp index 3abab1458517..33b036e6bdce 100644 --- a/source/NPC.cpp +++ b/source/NPC.cpp @@ -379,7 +379,7 @@ string NPC::Validate(bool asTemplate) const // A null system reference is allowed, since it will be set during // instantiation if not given explicitly. if(system && !system->IsValid()) - return "system \"" + system->Name() + "\""; + return "system \"" + system->TrueName() + "\""; // A planet is optional, but if given must be valid. if(planet && !planet->IsValid()) diff --git a/source/Planet.cpp b/source/Planet.cpp index 4ce128383c80..7be904515eb9 100644 --- a/source/Planet.cpp +++ b/source/Planet.cpp @@ -58,7 +58,7 @@ void Planet::Load(const DataNode &node, Set &wormholes) { if(node.Size() < 2) return; - name = node.Token(1); + trueName = node.Token(1); // The planet's name is needed to save references to this object, so a // flag is used to test whether Load() was called at least once for it. isDefined = true; @@ -97,7 +97,9 @@ void Planet::Load(const DataNode &node, Set &wormholes) if(removeAll || overwriteAll) { // Clear the data of the given type. - if(key == "music") + if(key == "display name") + displayName.clear(); + else if(key == "music") music.clear(); else if(key == "attributes") attributes.clear(); @@ -174,6 +176,8 @@ void Planet::Load(const DataNode &node, Set &wormholes) child.PrintTrace("Error: Cannot \"remove\" a specific value from the given key:"); continue; } + else if(key == "display name") + displayName = value; else if(key == "landscape") landscape = SpriteSet::Get(value); else if(key == "music") @@ -304,12 +308,16 @@ void Planet::FinishLoading(Set &wormholes) { wormhole = wormholes.Get(TrueName()); wormhole->LoadFromPlanet(*this); - Logger::LogError("Warning: deprecated automatic generation of wormhole \"" + name + "\" from a multi-system planet."); + Logger::LogError("Warning: deprecated automatic generation of wormhole \"" + + trueName + "\" from a multi-system planet."); } // If the wormhole was autogenerated we need to update it to // match the planet's state. else if(wormhole && wormhole->IsAutogenerated()) wormhole->LoadFromPlanet(*this); + // If this planet is not a wormhole, make sure it has a display name. + else if(!wormhole && displayName.empty()) + displayName = trueName; } @@ -324,25 +332,27 @@ bool Planet::IsValid() const -// Get the name of the planet. -const string &Planet::Name() const +// Get the name used for this planet in the data files. +const string &Planet::TrueName() const { - return IsWormhole() ? wormhole->Name() : name; + return trueName; } void Planet::SetName(const string &name) { - this->name = name; + trueName = name; + if(displayName.empty()) + displayName = trueName; } -// Get the name used for this planet in the data files. -const string &Planet::TrueName() const +// Get the display name of the planet. +const string &Planet::DisplayName() const { - return name; + return IsWormhole() ? wormhole->DisplayName() : displayName; } @@ -703,7 +713,7 @@ void Planet::DeployDefense(list> &ships) const if(defenseFleets[defenseDeployed]->IsValid()) defenseFleets[defenseDeployed]->Enter(*GetSystem(), defenders, this); else - Logger::LogError("Warning: skipped an incomplete defense fleet of planet \"" + name + "\"."); + Logger::LogError("Warning: skipped an incomplete defense fleet of planet \"" + trueName + "\"."); ships.insert(ships.begin(), defenders.begin(), end); // All defenders use a special personality. diff --git a/source/Planet.h b/source/Planet.h index b08aa5a19a5a..17650a7986a1 100644 --- a/source/Planet.h +++ b/source/Planet.h @@ -60,14 +60,14 @@ class Planet { // Check if both this planet and its containing system(s) have been defined. bool IsValid() const; - // Get the name of the planet (all wormholes use the same name). - // When saving missions or writing the player's save, the reference name + // Get the name used for this planet in the data files. + // When saving missions or writing the player's save, the true name // associated with this planet is used even if the planet was not fully // defined (i.e. it belongs to an inactive plugin). - const std::string &Name() const; - void SetName(const std::string &name); - // Get the name used for this planet in the data files. const std::string &TrueName() const; + void SetName(const std::string &name); + // Get the display name of the planet (all wormholes use the same name). + const std::string &DisplayName() const; // Return the description text for the planet, but not the spaceport: const Paragraphs &Description() const; // Get the landscape sprite. @@ -158,7 +158,8 @@ class Planet { private: bool isDefined = false; - std::string name; + std::string trueName; + std::string displayName; Paragraphs description; Port port; const Sprite *landscape = nullptr; diff --git a/source/PlanetLabel.cpp b/source/PlanetLabel.cpp index 86a5aff37503..253d51c18485 100644 --- a/source/PlanetLabel.cpp +++ b/source/PlanetLabel.cpp @@ -75,7 +75,7 @@ PlanetLabel::PlanetLabel(const vector &labels, const System &system : object(&object) { const Planet &planet = *object.GetPlanet(); - name = planet.Name(); + name = planet.DisplayName(); if(planet.IsWormhole()) color = *planet.GetWormhole()->GetLinkColor(); else if(planet.GetGovernment()) diff --git a/source/PlanetPanel.cpp b/source/PlanetPanel.cpp index 95306daced1f..ba08c4d65f22 100644 --- a/source/PlanetPanel.cpp +++ b/source/PlanetPanel.cpp @@ -18,6 +18,7 @@ this program. If not, see . #include "Information.h" #include "text/alignment.hpp" +#include "audio/Audio.h" #include "BankPanel.h" #include "Command.h" #include "ConversationPanel.h" @@ -73,6 +74,13 @@ PlanetPanel::PlanetPanel(PlayerInfo &player, function callback) +PlanetPanel::~PlanetPanel() +{ + Audio::Resume(); +} + + + void PlanetPanel::Step() { // If the player is dead, pop the planet panel. diff --git a/source/PlanetPanel.h b/source/PlanetPanel.h index 498e99744cec..37d361c7651a 100644 --- a/source/PlanetPanel.h +++ b/source/PlanetPanel.h @@ -37,6 +37,7 @@ class System; class PlanetPanel : public Panel { public: PlanetPanel(PlayerInfo &player, std::function callback); + virtual ~PlanetPanel() override; virtual void Step() override; virtual void Draw() override; diff --git a/source/PlayerInfo.cpp b/source/PlayerInfo.cpp index 222db96aaa5d..482ffd24c642 100644 --- a/source/PlayerInfo.cpp +++ b/source/PlayerInfo.cpp @@ -1529,7 +1529,7 @@ void PlayerInfo::Land(UI *ui) if(!freshlyLoaded) { - Audio::Play(Audio::Get("landing")); + Audio::Play(Audio::Get("landing"), SoundCategory::ENGINE); Audio::PlayMusic(planet->MusicName()); } @@ -1675,7 +1675,7 @@ bool PlayerInfo::TakeOff(UI *ui, const bool distributeCargo) return false; shouldLaunch = false; - Audio::Play(Audio::Get("takeoff")); + Audio::Play(Audio::Get("takeoff"), SoundCategory::ENGINE); // Jobs are only available when you are landed. availableJobs.clear(); @@ -2413,7 +2413,7 @@ void PlayerInfo::AddPlayerSubstitutions(map &subs) const subs[""] = flag->DisplayModelName(); } - subs[""] = GetSystem()->Name(); + subs[""] = GetSystem()->DisplayName(); subs[""] = GetDate().ToString(); subs[""] = GetDate().LongString(); } @@ -3825,7 +3825,7 @@ void PlayerInfo::RegisterDerivedConditions() { if(!previousSystem) return false; - return name == "previous system: " + previousSystem->Name(); + return name == "previous system: " + previousSystem->TrueName(); }; previousSystemProvider.SetGetFunction(previousSystemFun); @@ -3835,7 +3835,7 @@ void PlayerInfo::RegisterDerivedConditions() { if(!flagship || !flagship->GetSystem()) return false; - return name == "flagship system: " + flagship->GetSystem()->Name(); + return name == "flagship system: " + flagship->GetSystem()->TrueName(); }; flagshipSystemProvider.SetGetFunction(flagshipSystemFun); @@ -4081,7 +4081,8 @@ void PlayerInfo::CreateMissions() { bool hasLowerPriorityLocation = it->IsAtLocation(Mission::SPACEPORT) || it->IsAtLocation(Mission::SHIPYARD) - || it->IsAtLocation(Mission::OUTFITTER); + || it->IsAtLocation(Mission::OUTFITTER) + || it->IsAtLocation(Mission::JOB_BOARD); if(hasLowerPriorityLocation && !it->HasPriority()) it = availableMissions.erase(it); else @@ -4362,9 +4363,9 @@ void PlayerInfo::Save(DataWriter &out) const out.Write("date", date.Day(), date.Month(), date.Year()); out.Write("system entry method", EntryToString(entry)); if(previousSystem) - out.Write("previous system", previousSystem->Name()); + out.Write("previous system", previousSystem->TrueName()); if(system) - out.Write("system", system->Name()); + out.Write("system", system->TrueName()); if(planet) out.Write("planet", planet->TrueName()); if(planet && planet->CanUseServices()) @@ -4375,7 +4376,7 @@ void PlayerInfo::Save(DataWriter &out) const if(shouldLaunch) out.Write("launching"); for(const System *system : travelPlan) - out.Write("travel", system->Name()); + out.Write("travel", system->TrueName()); if(travelDestination) out.Write("travel destination", travelDestination->TrueName()); // Detect which ship number is the current flagship, for showing on LoadPanel. @@ -4607,10 +4608,10 @@ void PlayerInfo::Save(DataWriter &out) const // Save a list of systems the player has visited. WriteSorted(visitedSystems, [](const System *const *lhs, const System *const *rhs) - { return (*lhs)->Name() < (*rhs)->Name(); }, + { return (*lhs)->TrueName() < (*rhs)->TrueName(); }, [&out](const System *system) { - out.Write("visited", system->Name()); + out.Write("visited", system->TrueName()); }); // Save a list of planets the player has visited. @@ -4633,13 +4634,13 @@ void PlayerInfo::Save(DataWriter &out) const { // Sort by system name and then by outfit name. if(lhs->first != rhs->first) - return lhs->first->Name() < rhs->first->Name(); + return lhs->first->TrueName() < rhs->first->TrueName(); else return lhs->second->TrueName() < rhs->second->TrueName(); }, [&out](const HarvestLog &it) { - out.Write(it.first->Name(), it.second->TrueName()); + out.Write(it.first->TrueName(), it.second->TrueName()); }); } out.EndChild(); diff --git a/source/PlayerInfoPanel.cpp b/source/PlayerInfoPanel.cpp index 83f9111e6ff4..0cb242a7c520 100644 --- a/source/PlayerInfoPanel.cpp +++ b/source/PlayerInfoPanel.cpp @@ -16,6 +16,7 @@ this program. If not, see . #include "PlayerInfoPanel.h" #include "text/alignment.hpp" +#include "audio/Audio.h" #include "Command.h" #include "text/Font.h" #include "text/FontSet.h" @@ -102,7 +103,7 @@ namespace { return false; else if(rhs->GetSystem() == nullptr) return true; - return lhs->GetSystem()->Name() < rhs->GetSystem()->Name(); + return lhs->GetSystem()->DisplayName() < rhs->GetSystem()->DisplayName(); } bool CompareShields(const shared_ptr &lhs, const shared_ptr &rhs) @@ -178,11 +179,19 @@ PlayerInfoPanel::PlayerInfoPanel(PlayerInfo &player) PlayerInfoPanel::PlayerInfoPanel(PlayerInfo &player, InfoPanelState panelState) : player(player), panelState(panelState) { + Audio::Pause(); SetInterruptible(false); } +PlayerInfoPanel::~PlayerInfoPanel() +{ + Audio::Resume(); +} + + + void PlayerInfoPanel::Step() { // If the player has acquired a second ship for the first time, explain to @@ -780,7 +789,7 @@ void PlayerInfoPanel::DrawFleet(const Rectangle &bounds) table.Draw(ship.DisplayModelName()); const System *system = ship.GetSystem(); - table.Draw(system ? (player.KnowsName(*system) ? system->Name() : "???") : ""); + table.Draw(system ? (player.KnowsName(*system) ? system->DisplayName() : "???") : ""); string shields = to_string(static_cast(100. * max(0., ship.Shields()))) + "%"; table.Draw(shields); diff --git a/source/PlayerInfoPanel.h b/source/PlayerInfoPanel.h index 938daf180f9e..1ccbde23d1bc 100644 --- a/source/PlayerInfoPanel.h +++ b/source/PlayerInfoPanel.h @@ -37,6 +37,7 @@ class PlayerInfoPanel : public Panel { public: explicit PlayerInfoPanel(PlayerInfo &player); explicit PlayerInfoPanel(PlayerInfo &player, InfoPanelState panelState); + virtual ~PlayerInfoPanel() override; virtual void Step() override; virtual void Draw() override; diff --git a/source/Preferences.cpp b/source/Preferences.cpp index 552b03bdcd48..62735284bfcb 100644 --- a/source/Preferences.cpp +++ b/source/Preferences.cpp @@ -44,7 +44,7 @@ namespace { int dateFormatIndex = 0; const vector NOTIF_OPTIONS = {"off", "message", "both"}; - int notifOptionsIndex = 0; + int notifOptionsIndex = 1; size_t zoomIndex = 4; constexpr double VOLUME_SCALE = .25; @@ -60,6 +60,21 @@ namespace { const vector CAMERA_ACCELERATION_SETTINGS = {"off", "on", "reversed"}; int cameraAccelerationIndex = 1; + const map VOLUME_SETTINGS = { + {"volume", SoundCategory::MASTER}, + {"music volume", SoundCategory::MUSIC}, + {"ui volume", SoundCategory::UI}, + {"anti-missile volume", SoundCategory::ANTI_MISSILE}, + {"weapon volume", SoundCategory::WEAPON}, + {"engine volume", SoundCategory::ENGINE}, + {"afterburner volume", SoundCategory::AFTERBURNER}, + {"jump volume", SoundCategory::JUMP}, + {"explosion volume", SoundCategory::EXPLOSION}, + {"scan volume", SoundCategory::SCAN}, + {"environment volume", SoundCategory::ENVIRONMENT}, + {"alert volume", SoundCategory::ALERT} + }; + class OverlaySetting { public: OverlaySetting() = default; @@ -186,8 +201,8 @@ void Preferences::Load() Screen::SetRaw(node.Value(1), node.Value(2)); else if(node.Token(0) == "zoom" && node.Size() >= 2) Screen::SetZoom(node.Value(1)); - else if(node.Token(0) == "volume" && node.Size() >= 2) - Audio::SetVolume(node.Value(1) * VOLUME_SCALE); + else if(VOLUME_SETTINGS.contains(node.Token(0)) && node.Size() >= 2) + Audio::SetVolume(node.Value(1) * VOLUME_SCALE, VOLUME_SETTINGS.at(node.Token(0))); else if(node.Token(0) == "scroll speed" && node.Size() >= 2) scrollSpeed = node.Value(1); else if(node.Token(0) == "boarding target") @@ -273,7 +288,8 @@ void Preferences::Save() { DataWriter out(Files::Config() / "preferences.txt"); - out.Write("volume", Audio::Volume() / VOLUME_SCALE); + for(const auto &[name, category] : VOLUME_SETTINGS) + out.Write(name, Audio::Volume(category) / VOLUME_SCALE); out.Write("window size", Screen::RawWidth(), Screen::RawHeight()); out.Write("zoom", Screen::UserZoom()); out.Write("scroll speed", scrollSpeed); diff --git a/source/PreferencesPanel.cpp b/source/PreferencesPanel.cpp index 8e5744e6795f..fa123a706661 100644 --- a/source/PreferencesPanel.cpp +++ b/source/PreferencesPanel.cpp @@ -88,6 +88,21 @@ namespace { const int SETTINGS_PAGE_COUNT = 2; // Hovering a preference for this many frames activates the tooltip. const int HOVER_TIME = 30; + + const map volumeBars = { + {"volume", SoundCategory::MASTER}, + {"music volume", SoundCategory::MUSIC}, + {"ui volume", SoundCategory::UI}, + {"anti-missile volume", SoundCategory::ANTI_MISSILE}, + {"weapon volume", SoundCategory::WEAPON}, + {"engine volume", SoundCategory::ENGINE}, + {"afterburner volume", SoundCategory::AFTERBURNER}, + {"jump volume", SoundCategory::JUMP}, + {"explosion volume", SoundCategory::EXPLOSION}, + {"scan volume", SoundCategory::SCAN}, + {"environment volume", SoundCategory::ENVIRONMENT}, + {"alert volume", SoundCategory::ALERT} + }; } @@ -141,7 +156,21 @@ void PreferencesPanel::Draw() GameData::Background().Draw(Point(), Point()); Information info; - info.SetBar("volume", Audio::Volume()); + + for(const auto &[bar, category] : volumeBars) + { + double volume = Audio::Volume(category); + info.SetBar(bar, volume); + if(volume > .75) + info.SetCondition(bar + " max"); + else if(volume > .5) + info.SetCondition(bar + " medium"); + else if(volume > .25) + info.SetCondition(bar + " low"); + else + info.SetCondition(bar + " none"); + } + if(Plugins::HasChanged()) info.SetCondition("show plugins changed"); if(CONTROLS_PAGE_COUNT > 1) @@ -157,7 +186,7 @@ void PreferencesPanel::Draw() if(currentSettingsPage + 1 < SETTINGS_PAGE_COUNT) info.SetCondition("show next settings"); GameData::Interfaces().Get("menu background")->Draw(info, this); - string pageName = (page == 'c' ? "controls" : page == 's' ? "settings" : "plugins"); + string pageName = (page == 'c' ? "controls" : page == 's' ? "settings" : page == 'p' ? "plugins" : "audio"); GameData::Interfaces().Get(pageName)->Draw(info, this); GameData::Interfaces().Get("preferences")->Draw(info, this); @@ -176,6 +205,10 @@ void PreferencesPanel::Draw() } else if(page == 'p') DrawPlugins(); + else if(page == 'a') + { + // The entire audio panel is defined in interfaces, so this is a dummy. + } } @@ -197,7 +230,7 @@ bool PreferencesPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &comma HandleConfirm(); else if(key == 'b' || command.Has(Command::MENU) || (key == 'w' && (mod & (KMOD_CTRL | KMOD_GUI)))) Exit(); - else if(key == 'c' || key == 's' || key == 'p') + else if(key == 'c' || key == 's' || key == 'p' || key == 'a') { page = key; hoverItem.clear(); @@ -252,14 +285,19 @@ bool PreferencesPanel::Click(int x, int y, int clicks) { EndEditing(); - if(x >= 265 && x < 295 && y >= -220 && y < 70) + Point point(x, y); + const Interface *preferencesUI = GameData::Interfaces().Get("preferences"); + Rectangle volumeBox = preferencesUI->GetBox("volume box"); + if(volumeBox.Contains(point)) { - Audio::SetVolume((17 - y) / 200.); - Audio::Play(Audio::Get("warder")); + double barSize = preferencesUI->GetValue("master volume bar size"); + double volume = (volumeBox.Center().Y() - point.Y()) / barSize + .5; + + Audio::SetVolume(volume, SoundCategory::MASTER); + Audio::Play(Audio::Get("warder"), SoundCategory::MASTER); return true; } - Point point(x, y); for(unsigned index = 0; index < zones.size(); ++index) if(zones[index].Contains(point)) editing = selected = index; @@ -292,6 +330,25 @@ bool PreferencesPanel::Click(int x, int y, int clicks) } } } + else if(page == 'a') + { + const Interface *audioUI = GameData::Interfaces().Get("audio"); + double barSize = audioUI->GetValue("volume bar size"); + for(const auto &[name, category] : volumeBars) + { + if(category != SoundCategory::MASTER) + { + Rectangle barZone = audioUI->GetBox(name + " box"); + if(barZone.Contains(point)) + { + double volume = (point.X() - barZone.Center().X()) / barSize + .5; + Audio::SetVolume(volume, category); + Audio::Play(Audio::Get("warder"), category); + return true; + } + } + } + } return true; } diff --git a/source/Ship.cpp b/source/Ship.cpp index f31abbf72597..c1e9a32fa83f 100644 --- a/source/Ship.cpp +++ b/source/Ship.cpp @@ -944,7 +944,7 @@ void Ship::FinishLoading(bool isNewInstance) if(!isNewInstance && targetSystem) { string message = "Warning: " + string(isYours ? "player-owned " : "NPC ") - + trueModelName + " \"" + name + "\": Cannot reach target system \"" + targetSystem->Name(); + + trueModelName + " \"" + name + "\": Cannot reach target system \"" + targetSystem->TrueName(); if(!currentSystem) { Logger::LogError(message + "\" (no current system)."); @@ -953,7 +953,7 @@ void Ship::FinishLoading(bool isNewInstance) else if(!currentSystem->Links().contains(targetSystem) && (!navigation.JumpRange() || !currentSystem->JumpNeighbors(navigation.JumpRange()).contains(targetSystem))) { - Logger::LogError(message + "\" by hyperlink or jump from system \"" + currentSystem->Name() + ".\""); + Logger::LogError(message + "\" by hyperlink or jump from system \"" + currentSystem->TrueName() + ".\""); targetSystem = nullptr; } } @@ -1201,18 +1201,18 @@ void Ship::Save(DataWriter &out) const if(formationPattern) out.Write("formation", formationPattern->Name()); if(currentSystem) - out.Write("system", currentSystem->Name()); + out.Write("system", currentSystem->TrueName()); else { // A carried ship is saved in its carrier's system. shared_ptr parent = GetParent(); if(parent && parent->currentSystem) - out.Write("system", parent->currentSystem->Name()); + out.Write("system", parent->currentSystem->TrueName()); } if(landingPlanet) out.Write("planet", landingPlanet->TrueName()); if(targetSystem) - out.Write("destination system", targetSystem->Name()); + out.Write("destination system", targetSystem->TrueName()); if(isParked) out.Write("parked"); } @@ -2015,10 +2015,10 @@ int Ship::Scan(const PlayerInfo &player) auto playScanSounds = [](const map &sounds, Point &position) { if(sounds.empty()) - Audio::Play(Audio::Get("scan"), position); + Audio::Play(Audio::Get("scan"), position, SoundCategory::SCAN); else for(const auto &sound : sounds) - Audio::Play(sound.first, position); + Audio::Play(sound.first, position, SoundCategory::SCAN); }; if(attributes.Get("silent scans")) { diff --git a/source/StartConditions.cpp b/source/StartConditions.cpp index 5b1e42871187..d427049c12de 100644 --- a/source/StartConditions.cpp +++ b/source/StartConditions.cpp @@ -188,8 +188,8 @@ void StartConditions::FinishLoading() // may now be invalid, meaning the CoreStartData would actually send the start to New Boston instead // of what was displayed. StartInfo &unlocked = infoByState[StartState::UNLOCKED]; - unlocked.planet = GetPlanet().Name(); - unlocked.system = GetSystem().Name(); + unlocked.planet = GetPlanet().DisplayName(); + unlocked.system = GetSystem().DisplayName(); unlocked.date = GetDate(); unlocked.credits = Format::Credits(GetAccounts().Credits()); unlocked.debt = Format::Credits(GetAccounts().TotalDebt()); diff --git a/source/StellarObject.cpp b/source/StellarObject.cpp index 7b98178185da..3d163c1bf7df 100644 --- a/source/StellarObject.cpp +++ b/source/StellarObject.cpp @@ -72,10 +72,10 @@ const Planet *StellarObject::GetPlanet() const // Only planets that you can land on have names. -const string &StellarObject::Name() const +const string &StellarObject::DisplayName() const { static const string UNKNOWN = "???"; - return (planet && !planet->Name().empty()) ? planet->Name() : UNKNOWN; + return (planet && !planet->DisplayName().empty()) ? planet->DisplayName() : UNKNOWN; } diff --git a/source/StellarObject.h b/source/StellarObject.h index 60c73bd10609..2d551ca4d134 100644 --- a/source/StellarObject.h +++ b/source/StellarObject.h @@ -52,7 +52,7 @@ class StellarObject : public Body { const Planet *GetPlanet() const; // Only planets that you can land on have names. - const std::string &Name() const; + const std::string &DisplayName() const; // If it is impossible to land on this planet, get the message // explaining why (e.g. too hot, too cold, etc.). const std::string &LandingMessage() const; diff --git a/source/System.cpp b/source/System.cpp index d3e844adfc54..8598af27afbb 100644 --- a/source/System.cpp +++ b/source/System.cpp @@ -96,7 +96,7 @@ void System::Load(const DataNode &node, Set &planets) { if(node.Size() < 2) return; - name = node.Token(1); + trueName = node.Token(1); isDefined = true; // For the following keys, if this data node defines a new value for that @@ -134,7 +134,9 @@ void System::Load(const DataNode &node, Set &planets) if(removeAll || overwriteAll) { // Clear the data of the given type. - if(key == "government") + if(key == "display name") + displayName.clear(); + else if(key == "government") government = nullptr; else if(key == "music") music.clear(); @@ -291,7 +293,7 @@ void System::Load(const DataNode &node, Set &planets) LoadFleet(child, fleet); if(fleet.Category().empty()) - fleet.Category() = value + "@" + name; + fleet.Category() = value + "@" + trueName; } } else if(key == "raid") @@ -376,6 +378,8 @@ void System::Load(const DataNode &node, Set &planets) child.PrintTrace("Cannot \"remove\" a specific value from the given key:"); continue; } + else if(key == "display name" && hasValue) + displayName = value; else if(key == "pos" && child.Size() >= 3) { position.Set(child.Value(valueIndex), child.Value(valueIndex + 1)); @@ -486,6 +490,9 @@ void System::Load(const DataNode &node, Set &planets) // Systems without an asteroid belt defined default to a radius of 1500. if(belts.empty()) belts.emplace_back(1, 1500.); + + if(displayName.empty()) + displayName = trueName; } @@ -583,17 +590,26 @@ bool System::IsValid() const -// Get this system's name. -const string &System::Name() const +const string &System::TrueName() const { - return name; + return trueName; +} + + + +void System::SetName(const string &name) +{ + trueName = name; + if(displayName.empty()) + displayName = trueName; } -void System::SetName(const std::string &name) +// Get this system's display name. +const string &System::DisplayName() const { - this->name = name; + return displayName; } @@ -614,12 +630,15 @@ const Government *System::GetGovernment() const } + // Get this system's map icons. const vector &System::GetMapIcons() const { return mapIcons; } + + // Get the name of the ambient audio to play in this system. const string &System::MusicName() const { @@ -1101,7 +1120,7 @@ void System::UpdateNeighbors(const Set &systems, double distance) { const System &other = it.second; // Skip systems that have no name or that are inaccessible. - if(it.first.empty() || other.Name().empty() || other.Inaccessible()) + if(it.first.empty() || other.TrueName().empty() || other.Inaccessible()) continue; if(&other != this && other.Position().Distance(position) <= distance) diff --git a/source/System.h b/source/System.h index eb95c89be361..9e3d368ad83a 100644 --- a/source/System.h +++ b/source/System.h @@ -78,9 +78,10 @@ class System { void Unlink(System *other); bool IsValid() const; - // Get this system's name and position (in the star map). - const std::string &Name() const; + const std::string &TrueName() const; void SetName(const std::string &name); + // Get this system's name and position (in the star map). + const std::string &DisplayName() const; const Point &Position() const; // Get this system's government. const Government *GetGovernment() const; @@ -209,8 +210,9 @@ class System { private: bool isDefined = false; bool hasPosition = false; + std::string trueName; // Name and position (within the star map) of this system. - std::string name; + std::string displayName; Point position; const Government *government = nullptr; std::vector mapIcons; diff --git a/source/UniverseObjects.cpp b/source/UniverseObjects.cpp index d3da069bf7cb..38c0667924bf 100644 --- a/source/UniverseObjects.cpp +++ b/source/UniverseObjects.cpp @@ -173,7 +173,7 @@ void UniverseObjects::UpdateSystems() for(auto &it : systems) { // Skip systems that have no name. - if(it.first.empty() || it.second.Name().empty()) + if(it.first.empty() || it.second.TrueName().empty()) continue; it.second.UpdateSystem(systems, neighborDistances); @@ -294,7 +294,7 @@ void UniverseObjects::CheckReferences() Logger::LogError("Warning: shipyard \"" + it.first + "\" is referred to, but has no ships."); // System names are used by a number of classes. for(auto &&it : systems) - if(it.second.Name().empty() && !NameIfDeferred(deferred["system"], it)) + if(it.second.TrueName().empty() && !NameIfDeferred(deferred["system"], it)) NameAndWarn("system", it); // Hazards are never serialized. for(const auto &it : hazards) @@ -302,7 +302,7 @@ void UniverseObjects::CheckReferences() Warn("hazard", it.first); // Wormholes are never serialized. for(const auto &it : wormholes) - if(it.second.Name().empty()) + if(it.second.DisplayName().empty()) Warn("wormhole", it.first); // Formation patterns are not serialized, but their usage is. for(auto &&it : formations) diff --git a/source/Visual.cpp b/source/Visual.cpp index 63c8ab694d6d..bc9cd8992430 100644 --- a/source/Visual.cpp +++ b/source/Visual.cpp @@ -46,7 +46,7 @@ Visual::Visual(const Effect &effect, Point pos, Point vel, Angle facing, Point h velocity += angle.Unit() * Random::Real() * effect.randomVelocity; if(effect.sound) - Audio::Play(effect.sound, position); + Audio::Play(effect.sound, position, effect.soundCategory); if(effect.randomFrameRate) AddFrameRate(effect.randomFrameRate * Random::Real()); diff --git a/source/Wormhole.cpp b/source/Wormhole.cpp index fc692a8c223f..7e0e61b3c012 100644 --- a/source/Wormhole.cpp +++ b/source/Wormhole.cpp @@ -103,9 +103,9 @@ void Wormhole::Load(const DataNode &node) else if(key == "display name") { if(remove) - name = "???"; + displayName = "???"; else if(hasValue) - name = value; + displayName = value; else child.PrintTrace("Missing value for attribute:"); } @@ -164,9 +164,9 @@ const Planet *Wormhole::GetPlanet() const -const string &Wormhole::Name() const +const string &Wormhole::DisplayName() const { - return name; + return displayName; } diff --git a/source/Wormhole.h b/source/Wormhole.h index 02a7fd934bf9..26998adb91f7 100644 --- a/source/Wormhole.h +++ b/source/Wormhole.h @@ -42,7 +42,7 @@ class Wormhole { // Returns the planet corresponding to this wormhole. const Planet *GetPlanet() const; // Returns this wormhole's name. - const std::string &Name() const; + const std::string &DisplayName() const; // Whether this wormhole's link appears on the map. bool IsMappable() const; // Returns this wormhole's arrow color. @@ -70,7 +70,7 @@ class Wormhole { bool isAutogenerated = false; const Planet *planet = nullptr; - std::string name = "???"; + std::string displayName = "???"; bool mappable = false; ExclusiveItem linkColor; std::unordered_map links; diff --git a/source/audio/Audio.cpp b/source/audio/Audio.cpp index ae16d538242e..386017336697 100644 --- a/source/audio/Audio.cpp +++ b/source/audio/Audio.cpp @@ -42,11 +42,12 @@ namespace { // when those sounds actually start playing. class QueueEntry { public: - void Add(Point position); + void Add(Point position, SoundCategory category); void Add(const QueueEntry &other); Point sum; double weight = 0.; + SoundCategory category = SoundCategory::MASTER; }; // OpenAL only allows a certain number of distinct sound sources. To work @@ -55,15 +56,17 @@ namespace { // be recycled once they are no longer playing. class Source { public: - Source(const Sound *sound, unsigned source); + Source(const Sound *sound, unsigned source, SoundCategory category, bool isFastForward); void Move(const QueueEntry &entry) const; unsigned ID() const; const Sound *GetSound() const; + SoundCategory Category() const; private: const Sound *sound = nullptr; unsigned source = 0; + SoundCategory category = SoundCategory::MASTER; }; // Thread entry point for loading the sound files. @@ -77,7 +80,11 @@ namespace { ALCdevice *device = nullptr; ALCcontext *context = nullptr; bool isInitialized = false; - double volume = .125; + + // We keep track of the volume levels requested, and the volume levels + // currently set in OpenAL. + map volume{{SoundCategory::MASTER, .125}}; + map cachedVolume; // This queue keeps track of sounds that have been requested to play. Each // added sound is "deferred" until the next audio position update to make @@ -114,8 +121,8 @@ namespace { // The number of Pause vs Resume requests received. int pauseChangeCount = 0; // If we paused the audio multiple times, only resume it after the same number of Resume() calls. - // We start with -1, so when MenuPanel opens up the first time, it doesn't pause the loading sounds. - int pauseCount = -1; + // We start with -2, so when MenuPanel and PlanetPanel opens up the first time, it doesn't pause the loading sounds. + int pauseCount = -2; } @@ -140,7 +147,7 @@ void Audio::Init(const vector &sources) ALfloat zero[3] = {0., 0., 0.}; ALfloat orientation[6] = {0., 0., -1., 0., 1., 0.}; - alListenerf(AL_GAIN, volume); + alListenerf(AL_GAIN, volume[SoundCategory::MASTER]); alListenerfv(AL_POSITION, zero); alListenerfv(AL_VELOCITY, zero); alListenerfv(AL_ORIENTATION, orientation); @@ -160,7 +167,7 @@ void Audio::Init(const vector &sources) // folder, without the ".wav" or "~.wav" suffix. string name = (path.parent_path() / path.stem()).lexically_relative(root).generic_string(); if(name.ends_with('~')) - name.resize(name.length() -1); + name.resize(name.length() - 1); loadQueue[name] = path; } } @@ -217,19 +224,20 @@ double Audio::GetProgress() // Get the volume. -double Audio::Volume() +double Audio::Volume(SoundCategory category) { - return volume; + if(!volume.contains(category)) + volume[category] = 1.; + + return volume[category]; } // Set the volume (to a value between 0 and 1). -void Audio::SetVolume(double level) +void Audio::SetVolume(double level, SoundCategory category) { - volume = min(1., max(0., level)); - if(isInitialized) - alListenerf(AL_GAIN, volume); + volume[category] = clamp(level, 0., 1.); } @@ -262,28 +270,28 @@ void Audio::Update(const Point &listenerPosition) // Play the given sound, at full volume. -void Audio::Play(const Sound *sound) +void Audio::Play(const Sound *sound, SoundCategory category) { - Play(sound, listener); + Play(sound, listener, category); } // Play the given sound, as if it is at the given distance from the // "listener". This will make it softer and change the left / right balance. -void Audio::Play(const Sound *sound, const Point &position) +void Audio::Play(const Sound *sound, const Point &position, SoundCategory category) { - if(!isInitialized || !sound || !sound->Buffer() || !volume) + if(!isInitialized || !sound || !sound->Buffer() || !volume[SoundCategory::MASTER]) return; // Place sounds from the main thread directly into the queue. They are from // the UI, and the Engine may not be running right now to call Update(). if(this_thread::get_id() == mainThreadID) - soundQueue[sound].Add(position - listener); + soundQueue[sound].Add(position - listener, category); else { unique_lock lock(audioMutex); - deferred[sound].Add(position - listener); + deferred[sound].Add(position - listener, category); } } @@ -326,13 +334,58 @@ void Audio::Resume() -// Begin playing all the sounds that have been added since the last time -// this function was called. -void Audio::Step() +/// Begin playing all the sounds that have been added since the last time +/// this function was called. +/// If the game is in fast forward mode, the fast version of sounds is played. +void Audio::Step(bool isFastForward) { if(!isInitialized) return; + for(const auto &[category, expected] : volume) + if(cachedVolume[category] != expected) + { + cachedVolume[category] = expected; + if(category == SoundCategory::MASTER) + alListenerf(AL_GAIN, expected); + else if(category == SoundCategory::MUSIC) + alSourcef(musicSource, AL_GAIN, expected); + else + for(const Source &source : sources) + if(source.Category() == category) + alSourcef(source.ID(), AL_GAIN, expected); + } + + if(pauseChangeCount > 0) + { + if(pauseCount += pauseChangeCount) + { + ALint state; + for(const Source &source : sources) + { + alGetSourcei(source.ID(), AL_SOURCE_STATE, &state); + if(state == AL_PLAYING) + alSourcePause(source.ID()); + } + } + } + else if(pauseChangeCount < 0) + { + // Check that the game is not paused after this request. Also don't allow the pause count to go into negatives. + if(pauseCount && (pauseCount += pauseChangeCount) <= 0) + { + pauseCount = 0; + ALint state; + for(const Source &source : sources) + { + alGetSourcei(source.ID(), AL_SOURCE_STATE, &state); + if(state == AL_PAUSED) + alSourcePlay(source.ID()); + } + } + } + pauseChangeCount = 0; + vector newSources; // For each sound that is looping, see if it is going to continue. For other // sounds, check if they are done playing. @@ -417,7 +470,7 @@ void Audio::Step() recycledSources.pop_back(); } // Begin playing this sound. - sources.emplace_back(it.first, source); + sources.emplace_back(it.first, source, it.second.category, isFastForward); sources.back().Move(it.second); alSourcePlay(source); } @@ -459,36 +512,6 @@ void Audio::Step() if(state != AL_PLAYING && state != AL_PAUSED) alSourcePlay(musicSource); } - - if(pauseChangeCount > 0) - { - if(pauseCount += pauseChangeCount) - { - ALint state; - for(const Source &source : sources) - { - alGetSourcei(source.ID(), AL_SOURCE_STATE, &state); - if(state == AL_PLAYING) - alSourcePause(source.ID()); - } - } - } - else if(pauseChangeCount < 0) - { - // Check that the game is not paused after this request. Also don't allow the pause count to go into negatives. - if(pauseCount && (pauseCount += pauseChangeCount) <= 0) - { - pauseCount = 0; - ALint state; - for(const Source &source : sources) - { - alGetSourcei(source.ID(), AL_SOURCE_STATE, &state); - if(state == AL_PAUSED) - alSourcePlay(source.ID()); - } - } - } - pauseChangeCount = 0; } @@ -535,6 +558,8 @@ void Audio::Quit() { ALuint id = it.second.Buffer(); alDeleteBuffers(1, &id); + id = it.second.Buffer3x(); + alDeleteBuffers(1, &id); } sounds.clear(); @@ -563,7 +588,8 @@ void Audio::Quit() namespace { // Add a new source to this queue entry. Sources are weighted based on their // position, and multiple sources can be added together in the same entry. - void QueueEntry::Add(Point position) + // The preserved category is the category of the last source. + void QueueEntry::Add(Point position, SoundCategory category) { // A distance of 500 counts as 1 OpenAL unit of distance. position *= .002; @@ -572,6 +598,7 @@ namespace { double d = 1. / (1. + position.Dot(position)); sum += d * position; weight += d; + this->category = category; } @@ -581,24 +608,24 @@ namespace { { sum += other.sum; weight += other.weight; + category = other.category; } // This is a wrapper for an OpenAL audio source. - Source::Source(const Sound *sound, unsigned source) - : sound(sound), source(source) + Source::Source(const Sound *sound, unsigned source, SoundCategory category, bool isFastForward) + : sound(sound), source(source), category(category) { // Give each source a small, random pitch variation. Otherwise, multiple // instances of the same sound playing at slightly different times // overlap and create a "grinding" interference sound. alSourcef(source, AL_PITCH, 1. + (Random::Real() - Random::Real()) * .04); - alSourcef(source, AL_GAIN, 1.); alSourcef(source, AL_REFERENCE_DISTANCE, 1.); alSourcef(source, AL_ROLLOFF_FACTOR, 1.); alSourcef(source, AL_MAX_DISTANCE, 100.); alSourcei(source, AL_LOOPING, sound->IsLooping()); - alSourcei(source, AL_BUFFER, sound->Buffer()); + alSourcei(source, AL_BUFFER, (isFastForward && sound->Buffer3x()) ? sound->Buffer3x() : sound->Buffer()); } @@ -631,6 +658,14 @@ namespace { + // Get the category of this sound. + SoundCategory Source::Category() const + { + return category; + } + + + // Thread entry point for loading sounds. void Load() { @@ -651,6 +686,10 @@ namespace { name = loadQueue.begin()->first; path = loadQueue.begin()->second; + // @3x sounds should be merged with their regular variant here. + if(name.ends_with("@3x")) + name.resize(name.size() - 3); + // Since we need to unlock the mutex below, create the map entry to // avoid a race condition when accessing sounds' size. sound = &sounds[name]; diff --git a/source/audio/Audio.h b/source/audio/Audio.h index e06453f5f9f1..107d5ef7184c 100644 --- a/source/audio/Audio.h +++ b/source/audio/Audio.h @@ -15,6 +15,8 @@ this program. If not, see . #pragma once +#include "SoundCategory.h" + #include #include #include @@ -41,8 +43,8 @@ class Audio { static double GetProgress(); // Get or set the volume (between 0 and 1). - static double Volume(); - static void SetVolume(double level); + static double Volume(SoundCategory category); + static void SetVolume(double level, SoundCategory category); // Get a pointer to the named sound. The name is the path relative to the // "sound/" folder, and without ~ if it's on the end, or the extension. @@ -55,11 +57,11 @@ class Audio { static void Update(const Point &listenerPosition); // Play the given sound, at full volume. - static void Play(const Sound *sound); + static void Play(const Sound *sound, SoundCategory category); // Play the given sound, as if it is at the given distance from the // "listener". This will make it softer and change the left / right balance. - static void Play(const Sound *sound, const Point &position); + static void Play(const Sound *sound, const Point &position, SoundCategory category); // Play the given music. An empty string means to play nothing. static void PlayMusic(const std::string &name); @@ -70,9 +72,10 @@ class Audio { // you have to call Resume() the same number of times to resume the sound sources. static void Resume(); - // Begin playing all the sounds that have been added since the last time - // this function was called. - static void Step(); + /// Begin playing all the sounds that have been added since the last time + /// this function was called. + /// If the game is in fast forward mode, the fast version of sounds is played. + static void Step(bool isFastForward); // Shut down the audio system (because we're about to quit). static void Quit(); diff --git a/source/audio/Music.cpp b/source/audio/Music.cpp index 5ee1739ac1cd..5fb8bbb68466 100644 --- a/source/audio/Music.cpp +++ b/source/audio/Music.cpp @@ -53,7 +53,8 @@ void Music::Init(const vector &sources) if(Format::LowerCase(path.extension().string()) != ".mp3") continue; - paths[(path.parent_path() / path.stem()).generic_string()] = path; + string name = (path.parent_path() / path.stem()).lexically_relative(root).generic_string(); + paths[name] = path; } } } diff --git a/source/audio/Sound.cpp b/source/audio/Sound.cpp index d880c7bdf368..ca5069e55604 100644 --- a/source/audio/Sound.cpp +++ b/source/audio/Sound.cpp @@ -43,6 +43,8 @@ bool Sound::Load(const filesystem::path &path, const string &name) this->name = name; isLooped = path.stem().string().ends_with('~'); + bool isFast = isLooped ? path.stem().string().ends_with("@3x~") : path.stem().string().ends_with("@3x"); + unsigned &buf = isFast ? buffer3x : buffer; File in(path); if(!in) @@ -56,9 +58,9 @@ bool Sound::Load(const filesystem::path &path, const string &name) if(fread(&data[0], 1, bytes, in) != bytes) return false; - if(!buffer) - alGenBuffers(1, &buffer); - alBufferData(buffer, AL_FORMAT_MONO16, &data.front(), bytes, frequency); + if(!buf) + alGenBuffers(1, &buf); + alBufferData(buf, AL_FORMAT_MONO16, &data.front(), bytes, frequency); return true; } @@ -79,6 +81,13 @@ unsigned Sound::Buffer() const +unsigned Sound::Buffer3x() const +{ + return buffer3x; +} + + + bool Sound::IsLooping() const { return isLooped; diff --git a/source/audio/Sound.h b/source/audio/Sound.h index 2ba31ec69599..26bb134ef10e 100644 --- a/source/audio/Sound.h +++ b/source/audio/Sound.h @@ -29,11 +29,13 @@ class Sound { const std::string &Name() const; unsigned Buffer() const; + unsigned Buffer3x() const; bool IsLooping() const; private: std::string name; unsigned buffer = 0; + unsigned buffer3x = 0; bool isLooped = false; }; diff --git a/source/audio/SoundCategory.h b/source/audio/SoundCategory.h new file mode 100644 index 000000000000..30c84554486f --- /dev/null +++ b/source/audio/SoundCategory.h @@ -0,0 +1,27 @@ +/* SoundCategory.h +Copyright (c) 2024 by tibetiroka + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#pragma once + + + +enum class SoundCategory { + // The master controls affect all categories + MASTER, + // The background music category + MUSIC, + // The sound effect types + UI, ANTI_MISSILE, WEAPON, ENGINE, AFTERBURNER, JUMP, EXPLOSION, SCAN, ENVIRONMENT, ALERT +}; diff --git a/source/main.cpp b/source/main.cpp index 72002a3f4581..f5739eee9603 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -210,7 +210,7 @@ int main(int argc, char *argv[]) Audio::Init(GameData::Sources()); if(isTesting && !noTestMute) - Audio::SetVolume(0); + Audio::SetVolume(0, SoundCategory::MASTER); // This is the main loop where all the action begins. GameLoop(player, queue, conversation, testToRunName, debugMode); @@ -402,7 +402,7 @@ void GameLoop(PlayerInfo &player, TaskQueue &queue, const Conversation &conversa } } - Audio::Step(); + Audio::Step(isFastForward); // Events in this frame may have cleared out the menu, in which case // we should draw the game panels instead: @@ -462,7 +462,7 @@ void GameLoop(PlayerInfo &player, TaskQueue &queue, const Conversation &conversa if(!isHeadless) { - Audio::Step(); + Audio::Step(isFastForward); // Events in this frame may have cleared out the menu, in which case // we should draw the game panels instead: diff --git a/source/test/Test.cpp b/source/test/Test.cpp index 49125ba8ac04..c30fdac629b3 100644 --- a/source/test/Test.cpp +++ b/source/test/Test.cpp @@ -105,7 +105,7 @@ namespace { string description = "name: " + ship.Name(); const System *system = ship.GetSystem(); const Planet *planet = ship.GetPlanet(); - description += ", system: " + (system ? system->Name() : ""); + description += ", system: " + (system ? system->TrueName() : ""); description += ", planet: " + (planet ? planet->TrueName() : ""); description += ", hull: " + Format::Number(ship.Hull()); description += ", shields: " + Format::Number(ship.Shields()); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3f5507ff6a07..f0da0f9f7a75 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,6 +63,7 @@ list(APPEND INTEGRATION_TESTS integration/config/plugins/integration-tests/data/tests/tests_framework.txt integration/config/plugins/integration-tests/data/tests/tests_hyperjumps_to_autocondition.txt integration/config/plugins/integration-tests/data/tests/tests_illegal_atrocity.txt + integration/config/plugins/integration-tests/data/tests/tests_job_board_missions.txt integration/config/plugins/integration-tests/data/tests/tests_landing.txt integration/config/plugins/integration-tests/data/tests/tests_take_ships.txt integration/config/plugins/integration-tests/data/tests/tests_save_load.txt diff --git a/tests/integration/config/plugins/integration-tests/data/tests/tests_common.txt b/tests/integration/config/plugins/integration-tests/data/tests/tests_common.txt index 2ec4a650ce16..add9e7f88f46 100644 --- a/tests/integration/config/plugins/integration-tests/data/tests/tests_common.txt +++ b/tests/integration/config/plugins/integration-tests/data/tests/tests_common.txt @@ -818,7 +818,7 @@ test "Trigger Prime Spaceport Mission" key p -test "Skip Spaceport Mission" +test "Skip Mission" status partial description "Just skip a mission by pressing return twice." sequence diff --git a/tests/integration/config/plugins/integration-tests/data/tests/tests_conditional_choice.txt b/tests/integration/config/plugins/integration-tests/data/tests/tests_conditional_choice.txt index e09e62d1faa5..df3fc2f43ff2 100644 --- a/tests/integration/config/plugins/integration-tests/data/tests/tests_conditional_choice.txt +++ b/tests/integration/config/plugins/integration-tests/data/tests/tests_conditional_choice.txt @@ -187,7 +187,7 @@ test "Conditional Test" apply "TEST: CONDITIONAL CHOICE" = 1 call "Trigger Ruin Spaceport Mission" - call "Skip Spaceport Mission" + call "Skip Mission" call "Depart" call "Test Conditions" assert @@ -204,7 +204,7 @@ test "Conditional Test Goto" apply "TEST: CONDITIONAL CHOICE GOTO" = 1 call "Trigger Ruin Spaceport Mission" - call "Skip Spaceport Mission" + call "Skip Mission" call "Depart" call "Test Conditions" assert @@ -225,7 +225,7 @@ test "Conditional Test Out of Bound" # but if we can somehow access the condition below with the down key then its a problem. input key "Down" - call "Skip Spaceport Mission" + call "Skip Mission" call "Depart" call "Test Conditions" assert diff --git a/tests/integration/config/plugins/integration-tests/data/tests/tests_job_board_missions.txt b/tests/integration/config/plugins/integration-tests/data/tests/tests_job_board_missions.txt new file mode 100644 index 000000000000..7c84ffd4e844 --- /dev/null +++ b/tests/integration/config/plugins/integration-tests/data/tests/tests_job_board_missions.txt @@ -0,0 +1,198 @@ +# Copyright (c) 2024 by Anarchist2 +# +# Endless Sky is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later version. +# +# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + +test-data "Job Board Mission Save" + category "savegame" + contents + pilot Bobbi Bughunter + date 16 11 3013 + system "Terra Incognita" + planet Ruin + clearance + ship "Star Barge" + name "Buggy Barge" + sprite "ship/star barge" + attributes + category "Light Freighter" + cost 190000 + mass 10 + bunks 3 + "cargo space" 50 + drag 1 + "engine capacity" 400 + "fuel capacity" 300 + "heat dissipation" 0.8 + hull 1000 + "outfit space" 1300 + "required crew" 1 + shields 600 + "turret mounts" 1 + "weapon capacity" 200 + "thrust" 50 + "turn" 1000 + outfits + Hyperdrive + "nGVF-BB Fuel Cell" + "LP036a Battery Pack" + crew 1 + fuel 300 + shields 600 + hull 1000 + position 0 0 + engine -9 38 1 + engine 9 38 1 + turret 0 -8 + leak leak 60 50 + explode "tiny explosion" 10 + explode "small explosion" 10 + system "Terra Incognita" + planet Ruin + account + credits 100000000 + score 400 + history + visited "Terra Incognita" + "visited planet" Ruin + changes + planet Ruin + port + services "job board" + + + +test-data "Job Board Mission" + category mission + contents + mission "TEST: JOB BOARD MISSION" + "job board" + source "Ruin" + to offer + has "TEST: JOB BOARD MISSION OFFER" + on offer + conversation + `This is a job board mission.` + decline + on decline + set "succeed test" + + + +test "Trigger Job Board Mission From Planet" + status partial + description "Launch, land, and go to the job board to trigger any potential missions in the job board." + sequence + call "Depart" + navigate + "travel destination" Ruin + call "Land" + input + key j + + +test "Trigger Job Board Mission From Map" + status partial + description "Launch, land, open up the map and go to the mission panel to trigger any potential missions in the job board." + sequence + call "Depart" + navigate + "travel destination" Ruin + call "Land" + input + key m + input + key i + + +test "Go To Mission Panel" + status partial + description "Open up the map mid-conversation and go to the mission panel, then close it." + sequence + input + key m + input + key i + input + key d + + +test "Exit Map" + status partial + description "Leave the map screen." + sequence + input + key d + + +test "Test Job Board Mission" + status partial + description "Set a few conditions so they are displayed on errors, clarifying things up." + sequence + apply + # Test accepted. If not there it wasn't offered. + "test: succeed test" = "succeed test" + # if we launched + "test: flagship landed" = "flagship landed" + + +test "Job Board Mission From Planet Test" + status active + description "Test if a job board mission can offer by entering the mission panel from a planet." + sequence + inject "Job Board Mission" + inject "Job Board Mission Save" + call "Load First Savegame" + apply + "TEST: JOB BOARD MISSION OFFER" = 1 + call "Trigger Job Board Mission From Planet" + call "Skip Mission" + call "Depart" + call "Test Job Board Mission" + assert + "succeed test" == 1 + + +test "Job Board Mission From Map Test" + status active + description "Test if a job board mission can offer by entering the mission panel from the map." + sequence + inject "Job Board Mission" + inject "Job Board Mission Save" + call "Load First Savegame" + apply + "TEST: JOB BOARD MISSION OFFER" = 1 + call "Trigger Job Board Mission From Map" + call "Skip Mission" + call "Exit Map" + call "Depart" + call "Test Job Board Mission" + assert + "succeed test" == 1 + + +test "Job Board During Mission Test" + status active + description "Test if the map and mission panel can be accessed mid-conversation without triggering a second mission." + sequence + inject "Job Board Mission" + inject "Job Board Mission Save" + call "Load First Savegame" + apply + "TEST: JOB BOARD MISSION OFFER" = 1 + call "Trigger Job Board Mission From Planet" + call "Go To Mission Panel" + call "Skip Mission" + call "Exit Map" + call "Depart" + call "Test Job Board Mission" + assert + "succeed test" == 1 diff --git a/tests/integration/config/plugins/integration-tests/data/tests/tests_shipyard_outfitter_missions.txt b/tests/integration/config/plugins/integration-tests/data/tests/tests_shipyard_outfitter_missions.txt index 7f38ae941c21..6807eda9d557 100644 --- a/tests/integration/config/plugins/integration-tests/data/tests/tests_shipyard_outfitter_missions.txt +++ b/tests/integration/config/plugins/integration-tests/data/tests/tests_shipyard_outfitter_missions.txt @@ -129,17 +129,6 @@ test "Trigger Outfitter Mission" key o -test "End Missions" - status partial - description "Just skip a mission by pressing return twice." - sequence - # Press accept (or decline, depending on if it is shown) and then skip the dialog once. - input - key "Return" - input - key "Return" - - test "Test Shipyard And Outfitter Mission" status partial description "Set a few conditions so they are displayed on errors, clarifying things up." @@ -161,7 +150,7 @@ test "Shipyard Mission Test" apply "TEST: SHIPYARD MISSION OFFER" = 1 call "Trigger Shipyard Mission" - call "End Missions" + call "Skip Mission" call "Depart" call "Test Shipyard And Outfitter Mission" assert @@ -178,7 +167,7 @@ test "Outfitter Mission Test" apply "TEST: OUTFITTER MISSION OFFER" = 1 call "Trigger Outfitter Mission" - call "End Missions" + call "Skip Mission" call "Depart" call "Test Shipyard And Outfitter Mission" assert diff --git a/tests/integration/config/plugins/integration-tests/data/tests/tests_to_accept.txt b/tests/integration/config/plugins/integration-tests/data/tests/tests_to_accept.txt index 07de20d93377..8a7062371725 100644 --- a/tests/integration/config/plugins/integration-tests/data/tests/tests_to_accept.txt +++ b/tests/integration/config/plugins/integration-tests/data/tests/tests_to_accept.txt @@ -110,6 +110,6 @@ test "To Accept Blocks Accepts When Conditions Fail" apply "TEST: TO ACCEPT BLOCKED MISSION OFFER" = 1 call "Trigger Prime Spaceport Mission" - call "Skip Spaceport Mission" + call "Skip Mission" assert "fail test" != 1 diff --git a/utils/contentStyle.json b/utils/contentStyle.json index 5e37adfbdbb2..7c5be7624025 100644 --- a/utils/contentStyle.json +++ b/utils/contentStyle.json @@ -68,6 +68,14 @@ "correction": { "replaceWith": "\\1\\2" } + }, + { + "description": "trailing punctuation after a paranthesized paragraph", + "regex": "([`\"])(\\s*)\\((.*)\\)([.!?,]+)\\1", + "except": [], + "correction": { + "replaceWith": "\\1\\2(\\3\\4)\\1" + } } ] },