mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-24 03:53:12 -04:00
Support for Leader voices (#10395)
* Prepare Leader Voices: Framework * Leader Voices: Hooks and corresponding text field comments * Leader Voices: wiki * Leader Voices: oops, comments * Decouple voice play calls to make global modification easier * Move voices to own folder and give them a separate volume setting * Oops, template needed too * Oops, wiki needed too
This commit is contained in:
parent
450813fa49
commit
08c3f97f82
@ -792,6 +792,7 @@ Sound =
|
||||
Sound effects volume =
|
||||
Music volume =
|
||||
City ambient sound volume =
|
||||
Leader voices volume =
|
||||
Pause between tracks =
|
||||
Pause =
|
||||
Music =
|
||||
|
@ -46,6 +46,7 @@ class GameSettings {
|
||||
var soundEffectsVolume = 0.5f
|
||||
var citySoundsVolume = 0.5f
|
||||
var musicVolume = 0.5f
|
||||
var voicesVolume = 0.5f
|
||||
var pauseBetweenTracks = 10
|
||||
|
||||
var turnsBetweenAutosaves = 1
|
||||
|
@ -27,13 +27,22 @@ class Nation : RulesetObject() {
|
||||
|
||||
var cityStateType: String? = null
|
||||
var preferredVictoryType: String = Constants.neutralVictoryType
|
||||
var declaringWar = ""
|
||||
var attacked = ""
|
||||
var defeated = ""
|
||||
var introduction = ""
|
||||
var tradeRequest = ""
|
||||
|
||||
/// The following all have audio hooks to play corresponding leader
|
||||
/// voice clips - named <civName>.<fieldName>, e.g. "America.defeated.ogg"
|
||||
/** Shown for AlertType.WarDeclaration, when other Civs declare war on a player */
|
||||
var declaringWar = ""
|
||||
/** Shown in DiplomacyScreen when a player declares war */
|
||||
var attacked = ""
|
||||
/** Shown for AlertType.Defeated */
|
||||
var defeated = ""
|
||||
/** Shown for AlertType.FirstContact */
|
||||
var introduction = ""
|
||||
/** Shown in TradePopup when other Civs initiate trade with a player */
|
||||
var tradeRequest = ""
|
||||
/** Shown in DiplomacyScreen when a player contacts another major civ with RelationshipLevel.Afraid or better */
|
||||
var neutralHello = ""
|
||||
/** Shown in DiplomacyScreen when a player contacts another major civ with RelationshipLevel.Enemy or worse */
|
||||
var hateHello = ""
|
||||
|
||||
lateinit var outerColor: List<Int>
|
||||
|
@ -14,7 +14,7 @@ class CityAmbiencePlayer(
|
||||
val volume = UncivGame.Current.settings.citySoundsVolume
|
||||
if (volume > 0f) {
|
||||
UncivGame.Current.musicController
|
||||
.playOverlay("sounds", city.civ.getEra().citySound, volume)
|
||||
.playOverlay(city.civ.getEra().citySound, volume = volume, isLooping = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ import kotlin.math.roundToInt
|
||||
*
|
||||
* Main methods: [chooseTrack], [pause], [resume], [setModList], [isPlaying], [gracefulShutdown]
|
||||
*
|
||||
* City ambience feature: [playOverlay], [stopOverlay]
|
||||
* City ambience / Leader voice feature: [playOverlay], [stopOverlay], [playVoice]
|
||||
* * This plays entirely independent of all other functionality as linked above.
|
||||
* * Can load from internal (jar,apk) - music is always local, nothing is packaged into a release.
|
||||
*/
|
||||
@ -103,7 +103,7 @@ class MusicController {
|
||||
|
||||
//region Fields
|
||||
/** mirrors [GameSettings.musicVolume] - use [setVolume] to update */
|
||||
private var baseVolume: Float = UncivGame.Current.settings.musicVolume
|
||||
private var baseVolume: Float = settings.musicVolume
|
||||
|
||||
/** Pause in seconds between tracks unless [chooseTrack] is called to force a track change */
|
||||
var silenceLength: Float
|
||||
@ -111,7 +111,7 @@ class MusicController {
|
||||
set(value) { silenceLengthInTicks = (ticksPerSecond * value).toInt() }
|
||||
|
||||
private var silenceLengthInTicks =
|
||||
(UncivGame.Current.settings.pauseBetweenTracks * ticksPerSecond).roundToInt()
|
||||
(settings.pauseBetweenTracks * ticksPerSecond).roundToInt()
|
||||
|
||||
private var mods = HashSet<String>()
|
||||
|
||||
@ -206,6 +206,8 @@ class MusicController {
|
||||
//endregion
|
||||
//region Internal helpers
|
||||
|
||||
private val settings get() = UncivGame.Current.settings
|
||||
|
||||
private fun clearCurrent() {
|
||||
current?.clear()
|
||||
current = null
|
||||
@ -333,7 +335,7 @@ class MusicController {
|
||||
getDefault: () -> FileHandle = { getFile(folder) }
|
||||
) = sequence<FileHandle> {
|
||||
yieldAll(
|
||||
(UncivGame.Current.settings.visualMods + mods).asSequence()
|
||||
(settings.visualMods + mods).asSequence()
|
||||
.map { getFile(modPath).child(it).child(folder) }
|
||||
)
|
||||
yield(getDefault())
|
||||
@ -598,19 +600,35 @@ class MusicController {
|
||||
}
|
||||
|
||||
/** Play [name] from any mod's [folder] or internal assets,
|
||||
* fading in to [volume] then looping */
|
||||
fun playOverlay(folder: String, name: String, volume: Float) {
|
||||
* fading in to [volume] then looping if [isLooping] is set.
|
||||
* does nothing if no such file is found.
|
||||
* Note that [volume] intentionally defaults to soundEffectsVolume, not musicVolume
|
||||
* as that fits the "Leader voice" usecase better.
|
||||
*/
|
||||
fun playOverlay(
|
||||
name: String,
|
||||
folder: String = "sounds",
|
||||
volume: Float = settings.soundEffectsVolume,
|
||||
isLooping: Boolean = false,
|
||||
fadeIn: Boolean = false
|
||||
) {
|
||||
val file = getMatchingFiles(folder, name).firstOrNull() ?: return
|
||||
playOverlay(file, volume)
|
||||
playOverlay(file, volume, isLooping, fadeIn)
|
||||
}
|
||||
|
||||
/** Play [file], fading in to [volume] then looping */
|
||||
/** Called for Leader Voices */
|
||||
fun playVoice(name: String) = playOverlay(name, "voices", settings.voicesVolume)
|
||||
/** Determines if any 'voices' folder exists in any currently active mod */
|
||||
fun isVoicesAvailable() = getMusicFolders("voices").any()
|
||||
|
||||
/** Play [file], [optionally][fadeIn] fading in to [volume] then looping if [isLooping] is set */
|
||||
@Suppress("MemberVisibilityCanBePrivate") // open to future use
|
||||
fun playOverlay(file: FileHandle, volume: Float) {
|
||||
fun playOverlay(file: FileHandle, volume: Float, isLooping: Boolean, fadeIn: Boolean) {
|
||||
clearOverlay()
|
||||
MusicTrackController(volume, initialFadeVolume = 0f).load(file) {
|
||||
it.music?.isLooping = true
|
||||
MusicTrackController(volume, initialFadeVolume = if (fadeIn) 0f else 1f).load(file) {
|
||||
it.music?.isLooping = isLooping
|
||||
it.play()
|
||||
//todo Needs to be called even when no fade desired to correctly set state - Think about changing that
|
||||
it.startFade(MusicTrackController.State.FadeIn)
|
||||
overlay = it
|
||||
}
|
||||
@ -622,7 +640,7 @@ class MusicController {
|
||||
}
|
||||
|
||||
private fun MusicTrackController.overlayTick() {
|
||||
if (timerTick() == MusicTrackController.State.Idle)
|
||||
if (timerTick() == MusicTrackController.State.Idle || !isPlaying())
|
||||
clearOverlay() // means FadeOut finished
|
||||
}
|
||||
|
||||
|
@ -8,15 +8,15 @@ import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.audio.MusicController
|
||||
import com.unciv.ui.audio.MusicTrackChooserFlags
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.components.widgets.UncivSlider
|
||||
import com.unciv.ui.components.widgets.WrappableLabel
|
||||
import com.unciv.ui.components.extensions.disable
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.extensions.toImageButton
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.extensions.toTextButton
|
||||
import com.unciv.ui.components.extensions.toImageButton
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.widgets.UncivSlider
|
||||
import com.unciv.ui.components.widgets.WrappableLabel
|
||||
import com.unciv.ui.popups.Popup
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.utils.Concurrency
|
||||
import com.unciv.utils.launchOnGLThread
|
||||
import kotlin.math.floor
|
||||
@ -33,9 +33,11 @@ fun soundTab(
|
||||
addSoundEffectsVolumeSlider(this, settings)
|
||||
addCitySoundsVolumeSlider(this, settings)
|
||||
|
||||
if (UncivGame.Current.musicController.isMusicAvailable()) {
|
||||
if (UncivGame.Current.musicController.isVoicesAvailable())
|
||||
addVoicesVolumeSlider(this, settings)
|
||||
|
||||
if (UncivGame.Current.musicController.isMusicAvailable())
|
||||
addMusicControls(this, settings, music)
|
||||
}
|
||||
|
||||
if (!UncivGame.Current.musicController.isDefaultFileAvailable())
|
||||
addDownloadMusic(this, optionsPopup)
|
||||
@ -70,49 +72,42 @@ private fun addDownloadMusic(table: Table, optionsPopup: OptionsPopup) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSoundEffectsVolumeSlider(table: Table, settings: GameSettings) {
|
||||
table.add("Sound effects volume".tr()).left().fillX()
|
||||
private fun Table.addVolumeSlider(text: String, initial: Float, silent: Boolean = false, onChange: (Float)->Unit) {
|
||||
add(text.tr()).left().fillX()
|
||||
|
||||
val soundEffectsVolumeSlider = UncivSlider(
|
||||
val volumeSlider = UncivSlider(
|
||||
0f, 1.0f, 0.05f,
|
||||
initial = settings.soundEffectsVolume,
|
||||
getTipText = UncivSlider::formatPercent
|
||||
) {
|
||||
initial = initial,
|
||||
sound = if (silent) UncivSound.Silent else UncivSound.Slider,
|
||||
getTipText = UncivSlider::formatPercent,
|
||||
onChange = onChange
|
||||
)
|
||||
add(volumeSlider).pad(5f).row()
|
||||
}
|
||||
|
||||
private fun addSoundEffectsVolumeSlider(table: Table, settings: GameSettings) =
|
||||
table.addVolumeSlider("Sound effects volume", settings.soundEffectsVolume) {
|
||||
settings.soundEffectsVolume = it
|
||||
}
|
||||
table.add(soundEffectsVolumeSlider).pad(5f).row()
|
||||
}
|
||||
|
||||
private fun addCitySoundsVolumeSlider(table: Table, settings: GameSettings) {
|
||||
table.add("City ambient sound volume".tr()).left().fillX()
|
||||
|
||||
val citySoundVolumeSlider = UncivSlider(
|
||||
0f, 1.0f, 0.05f,
|
||||
initial = settings.citySoundsVolume,
|
||||
getTipText = UncivSlider::formatPercent
|
||||
) {
|
||||
private fun addCitySoundsVolumeSlider(table: Table, settings: GameSettings) =
|
||||
table.addVolumeSlider("City ambient sound volume", settings.citySoundsVolume) {
|
||||
settings.citySoundsVolume = it
|
||||
}
|
||||
table.add(citySoundVolumeSlider).pad(5f).row()
|
||||
}
|
||||
|
||||
private fun addMusicVolumeSlider(table: Table, settings: GameSettings, music: MusicController) {
|
||||
table.add("Music volume".tr()).left().fillX()
|
||||
private fun addVoicesVolumeSlider(table: Table, settings: GameSettings) =
|
||||
table.addVolumeSlider("Leader voices volume", settings.voicesVolume) {
|
||||
settings.voicesVolume = it
|
||||
}
|
||||
|
||||
val musicVolumeSlider = UncivSlider(
|
||||
0f, 1.0f, 0.05f,
|
||||
initial = settings.musicVolume,
|
||||
sound = UncivSound.Silent,
|
||||
getTipText = UncivSlider::formatPercent
|
||||
) {
|
||||
private fun addMusicVolumeSlider(table: Table, settings: GameSettings, music: MusicController) =
|
||||
table.addVolumeSlider("Music volume".tr(), settings.musicVolume, true) {
|
||||
settings.musicVolume = it
|
||||
|
||||
music.setVolume(it)
|
||||
if (!music.isPlaying())
|
||||
music.chooseTrack(flags = MusicTrackChooserFlags.setPlayDefault)
|
||||
}
|
||||
table.add(musicVolumeSlider).pad(5f).row()
|
||||
}
|
||||
|
||||
private fun addMusicPauseSlider(table: Table, settings: GameSettings, music: MusicController) {
|
||||
// map to/from 0-1-2..10-12-14..30-35-40..60-75-90-105-120
|
||||
|
@ -242,7 +242,9 @@ class DiplomacyScreen(
|
||||
diplomacyManager.declareWar()
|
||||
setRightSideFlavorText(otherCiv, otherCiv.nation.attacked, "Very well.")
|
||||
updateLeftSideTable(otherCiv)
|
||||
UncivGame.Current.musicController.chooseTrack(otherCiv.civName, MusicMood.War, MusicTrackChooserFlags.setSpecific)
|
||||
val music = UncivGame.Current.musicController
|
||||
music.chooseTrack(otherCiv.civName, MusicMood.War, MusicTrackChooserFlags.setSpecific)
|
||||
music.playVoice("${otherCiv.civName}.attacked")
|
||||
}.open()
|
||||
}
|
||||
if (isNotPlayersTurn()) declareWarButton.disable()
|
||||
|
@ -4,6 +4,7 @@ import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.civilization.AlertType
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.PopupAlert
|
||||
@ -33,9 +34,15 @@ class MajorCivDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) {
|
||||
val diplomacyTable = Table()
|
||||
diplomacyTable.defaults().pad(10f)
|
||||
|
||||
val helloText = if (otherCivDiplomacyManager.isRelationshipLevelLE(RelationshipLevel.Enemy))
|
||||
otherCiv.nation.hateHello
|
||||
else otherCiv.nation.neutralHello
|
||||
val helloText: String
|
||||
val helloVoice: String
|
||||
if (otherCivDiplomacyManager.isRelationshipLevelLE(RelationshipLevel.Enemy)) {
|
||||
helloText = otherCiv.nation.hateHello
|
||||
helloVoice = "${otherCiv.civName}.hateHello"
|
||||
} else {
|
||||
helloText = otherCiv.nation.neutralHello
|
||||
helloVoice = "${otherCiv.civName}.neutralHello"
|
||||
}
|
||||
val leaderIntroTable = LeaderIntroTable(otherCiv, helloText)
|
||||
diplomacyTable.add(leaderIntroTable).row()
|
||||
diplomacyTable.addSeparator()
|
||||
@ -85,6 +92,9 @@ class MajorCivDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) {
|
||||
if (promisesTable != null) diplomacyTable.add(promisesTable).row()
|
||||
}
|
||||
|
||||
// Starting playback here assumes the MajorCivDiplomacyTable is shown immediately
|
||||
UncivGame.Current.musicController.playVoice(helloVoice)
|
||||
|
||||
return diplomacyTable
|
||||
}
|
||||
|
||||
|
@ -189,6 +189,7 @@ internal class ModInfoAndActionPane : Table() {
|
||||
}
|
||||
if (isSubFolderNotEmpty(folder, "music")) return true
|
||||
if (isSubFolderNotEmpty(folder, "sounds")) return true
|
||||
if (isSubFolderNotEmpty(folder, "voices")) return true
|
||||
return folder.list("atlas").isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
@ -211,6 +211,7 @@ class AlertPopup(
|
||||
addGoodSizedLabel(civInfo.nation.defeated).row()
|
||||
addCloseButton("Farewell.")
|
||||
music.chooseTrack(civInfo.civName, MusicMood.Defeat, EnumSet.of(MusicTrackChooserFlags.SuffixMustMatch))
|
||||
music.playVoice("${civInfo.civName}.defeated")
|
||||
}
|
||||
|
||||
private fun addDemandToStopSettlingCitiesNear() {
|
||||
@ -253,6 +254,7 @@ class AlertPopup(
|
||||
val nation = civInfo.nation
|
||||
addLeaderName(civInfo)
|
||||
music.chooseTrack(civInfo.civName, MusicMood.themeOrPeace, MusicTrackChooserFlags.setSpecific)
|
||||
music.playVoice("${civInfo.civName}.introduction")
|
||||
if (civInfo.isCityState()) {
|
||||
addGoodSizedLabel("We have encountered the City-State of [${nation.name}]!").row()
|
||||
addCloseButton("Excellent!")
|
||||
@ -357,6 +359,7 @@ class AlertPopup(
|
||||
addCloseButton("You'll pay for this!")
|
||||
addCloseButton("Very well.")
|
||||
music.chooseTrack(civInfo.civName, MusicMood.War, MusicTrackChooserFlags.setSpecific)
|
||||
music.playVoice("${civInfo.civName}.declaringWar")
|
||||
}
|
||||
|
||||
private fun addWonderBuilt() {
|
||||
|
@ -2,18 +2,19 @@ package com.unciv.ui.screens.worldscreen
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.civilization.NotificationCategory
|
||||
import com.unciv.logic.civilization.NotificationIcon
|
||||
import com.unciv.logic.trade.TradeLogic
|
||||
import com.unciv.logic.trade.TradeOffer
|
||||
import com.unciv.logic.trade.TradeType
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.popups.Popup
|
||||
import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen
|
||||
import com.unciv.ui.screens.diplomacyscreen.LeaderIntroTable
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import com.unciv.ui.components.widgets.AutoScrollPane as ScrollPane
|
||||
@ -76,6 +77,8 @@ class TradePopup(worldScreen: WorldScreen) : Popup(worldScreen) {
|
||||
|
||||
addSeparator(Color.DARK_GRAY, height = 1f)
|
||||
|
||||
// Starting playback here assumes the TradePopup is shown immediately
|
||||
UncivGame.Current.musicController.playVoice("${requestingCiv.civName}.tradeRequest")
|
||||
addGoodSizedLabel(nation.tradeRequest).pad(15f).row()
|
||||
|
||||
addButton("Sounds good!", 'y') {
|
||||
|
@ -160,3 +160,11 @@ Legend:
|
||||
- [^3]: According to your relation to the picked player.
|
||||
- [^4]: Excluding City States.
|
||||
- [^5]: Both in the alert when another player declares War on you and declaring War yourself in Diplomacy screen.
|
||||
|
||||
## Supply Leader Voices
|
||||
|
||||
Sound files named from a Nation name and the corresponding text message's [field name](Mod-file-structure/2-Civilization-related-JSON-files.md#nationsjson),
|
||||
placed in a mod's `voices` folder, will play whenever that message is displayed. Nation name and message name must be joined with a dot '.', for example `voices/Zulu.defeated.ogg`.
|
||||
|
||||
Leader voice audio clips will be streamed, not cached, so they are allowed to be long - however, if another Leader voice or a city ambient sound needs to be played, they will be cut off without fade-out
|
||||
Also note that voices for City-State leaders work only for those messages a City-state can actually use: `attacked`, `defeated`, and `introduction`.
|
||||
|
@ -74,20 +74,20 @@ This file contains all the nations and city states, including Barbarians and Spe
|
||||
| preferredVictoryType | Enum | Default Neutral | Neutral, Cultural, Diplomatic, Domination or Scientific |
|
||||
| startIntroPart1 | String | Default empty | Introductory blurb shown to Player on game start... |
|
||||
| startIntroPart2 | String | Default empty | ... second paragraph. ***NO*** "TBD"!!! Leave empty to skip that alert. |
|
||||
| declaringWar | String | Default empty | another greeting |
|
||||
| attacked | String | Default empty | another greeting |
|
||||
| defeated | String | Default empty | another greeting |
|
||||
| introduction | String | Default empty | another greeting |
|
||||
| neutralHello | String | Default empty | another greeting |
|
||||
| hateHello | String | Default empty | another greeting |
|
||||
| tradeRequest | String | Default empty | another greeting |
|
||||
| declaringWar | String | Default empty | another greeting, voice hook supported [^V] |
|
||||
| attacked | String | Default empty | another greeting, voice hook supported [^V] |
|
||||
| defeated | String | Default empty | another greeting, voice hook supported [^V] |
|
||||
| introduction | String | Default empty | another greeting, voice hook supported [^V] |
|
||||
| neutralHello | String | Default empty | another greeting, voice hook supported [^V] |
|
||||
| hateHello | String | Default empty | another greeting, voice hook supported [^V] |
|
||||
| tradeRequest | String | Default empty | another greeting, voice hook supported [^V] |
|
||||
| innerColor | 3x Integer | Default black | R, G, B for outer ring of nation icon |
|
||||
| outerColor | 3x Integer | Required | R, G, B for inner circle of nation icon |
|
||||
| uniqueName | String | Default empty | Decorative name for the special characteristic of this Nation |
|
||||
| uniqueText | String | Default empty | Replacement text for "uniques". If empty, uniques are listed individually. |
|
||||
| uniques | List | Default empty | Properties of the civilization - see [here](../Unique-parameters.md#general-uniques) |
|
||||
| uniques | List | Default empty | Properties of the civilization - see [here](../Unique-parameters.md#general-uniques) |
|
||||
| cities | List | Default empty | City names used sequentially for newly founded cities. |
|
||||
| civilopediaText | List | Default empty | see [civilopediaText chapter](5-Miscellaneous-JSON-files.md#civilopedia-text) |
|
||||
| civilopediaText | List | Default empty | see [civilopediaText chapter](5-Miscellaneous-JSON-files.md#civilopedia-text) |
|
||||
|
||||
[^S]: A "Coast" preference (_unless_ combined with "Avoid") is translated to a complex test for coastal land tiles, tiles next to Lakes, river tiles or near-river tiles, and such civs are processed first. Other startBias entries are ignored in that case.
|
||||
Other positive (no "Avoid") startBias are processed next. Multiple positive preferences are treated equally, but get no "fallback".
|
||||
@ -95,6 +95,7 @@ This file contains all the nations and city states, including Barbarians and Spe
|
||||
Multiple "Avoid" entries are treated equally (and reduce chance for success - if no region is left avoiding _all_ specified types that civ gets a random one).
|
||||
When combining preferred terrain with "Avoid", the latter takes precedence, and preferred terrain only has minor weight when choosing between regions that are not of a type to avoid.
|
||||
These notes are **only** valid when playing on generated maps, loaded maps from map editor get no "regions" and startBias is processed differently (but you can expect single-entry startBias to work best).
|
||||
[^V]: See [Supply Leader Voices](../Images-and-Audio.md#supply-leader-voices)
|
||||
|
||||
## Policies.json
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user