soundrts-1.4.5.1

49 views
Skip to first unread message

修君

unread,
Jul 9, 2026, 1:13:34 PMJul 9
to soundRTSChat
.. contents::


1.4.5.1
--------

Bug fixes and voice/audio UX improvements:

**Fix: nameless fog ghost after unit suicide**

- **Symptom**: After a unit suicides, Tab-cycling targets in the same square could still select an object with no readable name.
- **Cause**: After death ``place is None``, fog-of-war memory was not cleared in time; memory objects could have a ``title`` (fog suffix) but an empty ``short_title``, yet Tab still treated them as selectable.
- **Fix**: ``perception.py`` forgets memory when ``initial_model.place is None``; units leaving perception are not memorized when ``place is None`` or when they are the player's own dead units; ``game_unit_control.py`` ``is_visible`` requires a non-empty ``short_title``.
- **Tests**: ``test_suicide_fog_ghost.py`` (corpse fog memory and ambient audio paths preserved).

**Fix: wall HP flickering up and down while attacking**

- **Symptom**: Attacking ``wall`` and other ``is_repairable`` buildings could make HP or life-change sounds rise and fall intermittently.
- **Cause**: Walls inherit ``is_repairable=True`` from buildings, so attack / repair / capture-threshold logic could interact; fog HP sync (``_sync_memory_hp_from_live``) without carrying ``previous_hp`` across perception/memory view swaps caused false life-change feedback.
- **Fix**: ``world_order.py`` / ``worldcreature.py`` / ``worldworker.py`` — enemy repairable buildings default to ``go``, imperative default to ``attack``; repair paths guarded with ``not is_an_enemy(target)``; ``game_navigation.py`` preserves HP tracking on fog updates (``_take_hp_tracking`` / ``_apply_hp_tracking``).
- **Tests**: ``test_imperative_attack.py`` (imperative attack on walls).

**Improvement: unit behavior voice descriptions**

- After Tab-selecting a target, Ctrl+Backspace or go + Ctrl+Enter confirms "attack \<target\>" instead of "go" for enemy units/buildings.
- Hotkey group select (e.g. F for footmen): "You control N footmen attacking the town hall"; if moving while fighting, appends "go to c6".
- **Code**: ``clientgameentity/base.py`` ``_attack_action_title_msg``; ``properties.py`` ``orders_txt``; ``game_orders.py`` ``_say_validate_confirmation`` / ``_say_default_confirmation``; ``game_unit_control.py`` ``say_group``.
- **Tests**: ``test_attack_orders_txt.py``, ``test_imperative_attack.py``.

**Improvement: layered battle shouts**

- Three layers: ``shout_bg`` (battlefield background), ``shout_unit`` (unit voice), ``shout_event`` (first clash / charge / crit highlights); global and per-square cooldowns; ``formation_sound_queue`` staggers bursts so shouts do not stack with hit sounds in the same frame.
- **Code**: ``battle_shout_audio.py``, ``combat.py``, ``formation_sound_queue.py``.
- **Docs**: ``mod/battle-shouts.rst``.
- **Tests**: ``test_battle_shout_audio.py``.

**Improvement: P0–P2 audio priority scheme**

- **P0 ambient** (negative to low positive, e.g. -20, -10): footsteps, looping ambience, background shouts; may be preempted by higher layers.
- **P1 combat** (0–14, ``shout_combat_priority`` scales with headcount): hits, wounds, unit shouts.
- **P2 alerts** (10–16): level-up, morph, event shouts; kept when channels are scarce.
- **Code**: ``lib/sound.py`` ``SoundManager.find_a_channel`` preempts lower-priority sources; ``audio.py`` footsteps at ``priority=-10``; TTS stays on channel 0.

**Attempted to fix the Windows and Mac builds on GitHub CI, thanks to fcnjd for the contribution.

github

https://github.com/tuohai/soundrts-ultimate-version

Gabriele Battaglia

unread,
Jul 9, 2026, 1:25:53 PMJul 9
to soundr...@googlegroups.com
Hi,

Thanks for these updates! You are doing a great job with this game.

I wanted to ask if there is a plan to localize the game into different languages. I'm not sure if the Italian translation is currently up to date, but I suspect it might need some work.

Since I also develop in Python, I wanted to suggest a method I use daily in my projects: you could easily create a tool that uses Google APIs to automatically translate all the strings in the project. It’s a very efficient way to handle localization.

Thanks for your time and for all your hard work.

Best regards,

Gabriele Battaglia

Inviato dalla nave Gabryphone17ProMax.

--
You received this message because you are subscribed to the Google Groups "soundRTSChat" group.
To unsubscribe from this group and stop receiving emails from it, send an email to soundrtschat...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/soundrtschat/3df153df-3c2b-49a3-bee4-afeb2c7e845en%40googlegroups.com.

修君

unread,
Jul 10, 2026, 2:45:36 AMJul 10
to soundRTSChat
ok, I will try.

Gabriele Battaglia

unread,
Jul 10, 2026, 3:04:00 AMJul 10
to 修君, soundr...@googlegroups.com

Il 10/07/2026 08:45, 修君 ha scritto:
> ok, I will try.


Here is the code:


import os
import re
import sys
import time

try:
    import polib
    from deep_translator import GoogleTranslator
except ImportError:
    print("Errore: Librerie mancanti. Installa con: pip install polib
deep_translator")
    sys.exit(1)


def translate_po_file(po_file_path, target_lang):
    """
    Traduce tutte le stringhe non ancora tradotte in un file .po,
    preservando i segnaposto tipo {player} o {t}.
    """
    if not os.path.exists(po_file_path):
        print(f"Errore: File {po_file_path} non trovato.")
        return

    print(f"Caricamento file: {po_file_path}...")
    po = polib.pofile(po_file_path)
    translator = GoogleTranslator(source="it", target=target_lang)

    # Regex per trovare i segnaposto tipo {nome_variabile}
    placeholder_regex = re.compile(r"\{[^}]+\}")

    untranslated_entries = [e for e in po if not e.msgstr and e.msgid]
    total = len(untranslated_entries)

    if total == 0:
        print("Tutte le stringhe sono già tradotte.")
        return

    print(
        f"Trovate {total} stringhe da tradurre in '{target_lang}'.
Inizio processo..."
    )

    count = 0
    for entry in untranslated_entries:
        original = entry.msgid

        # 1. Protezione segnaposto
        placeholders = placeholder_regex.findall(original)
        protected_text = original
        for i, ph in enumerate(placeholders):
            protected_text = protected_text.replace(ph, f"VAR{i}QQ")

        try:
            # 2. Traduzione
            translated_text = translator.translate(protected_text)

            if translated_text is None:
                print(f"\nAvviso: Traduzione vuota per '{original}'.")
                translated_text = ""

            # 3. Ripristino segnaposto
            for i, ph in enumerate(placeholders):
                translated_text = translated_text.replace(f"VAR{i}QQ", ph)

            entry.msgstr = translated_text
            count += 1

            if count % 10 == 0:
                print(f"Progresso: {count}/{total}...")
                # Piccolo delay per non farsi bannare dall'API gratuita
                time.sleep(0.5)

        except Exception as e:
            print(f"\nErrore durante la traduzione di '{original}': {e}")
            entry.msgstr = ""  # Previene crash di polib
            continue

    # Salvataggio
    po.save()
    print(f"\nLavoro completato! Tradotte {count} stringhe.")
    print(f"File salvato: {po_file_path}")
    print("Ricorda di compilare il file .mo usando: pybabel compile -d
locales")


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Uso: python translator_tool.py <percorso_file_po>
<codice_lingua>")
        print(
            "Esempio: python translator_tool.py
locales/pt/LC_MESSAGES/messages.po pt"
        )
    else:
        path = sys.argv[1]
        lang = sys.argv[2]
        translate_po_file(path, lang)

修君

unread,
Jul 10, 2026, 6:07:36 AMJul 10
to soundRTSChat
please download again.
I have updated the translations for 10 languages in res, added Vietnamese translation, and also updated and expanded the help documentation for 3 languages in doc.

jasperja...@gmail.com

unread,
Jul 10, 2026, 5:28:51 PMJul 10
to soundRTSChat
Hi

I once again want to thank you for the great work you are doing with this game. It is just getting better with each update. Just a question regarding Chapter 13 in the campain, I destroyed all enemies and gave the potion to the contact, but the mission does not end. How do I continue to the next mission?

修君

unread,
Jul 11, 2026, 3:41:51 AMJul 11
to soundRTSChat
This was a bug and has now been fixed. Please download it again.
In addition, some defensive mechanisms have been added; for example, a message will be shown when certain terrain is impassable.

修君

unread,
Jul 11, 2026, 11:51:38 AMJul 11
to soundRTSChat
please download again.


- ``rules.txt`` ``class terrain`` now supports ``cover <ground> <air>``, same as ``speed``: a map line ``terrain marsh h8`` inherits default cover; per-square map ``cover`` lines still override.
- Terrain can modify **unit types** via ``speed_vs``, ``cover_vs``, ``dodge_vs``, ``mdg_vs``, ``rdg_vs``, ``mdg_cd_vs``, ``rdg_cd_vs`` (e.g. ``speed_vs knight .25 archer .5``). You may use ``*_vs`` alone without a global ``speed``/``cover``.
- Those ``*_vs`` fields and unit ``mdg_on_terrain`` / ``rdg_on_terrain`` / ``mdg_cd_on_terrain`` / ``rdg_cd_on_terrain`` (and ``charge_*_terrain``) now use **0–1 decimal percents** (``.5`` = ±50%%, ``.1`` = ±10%%) relative to the unit's current base damage or cooldown.
- ``speed_on_terrain`` remains an **absolute speed** override (unlike percent ``speed_vs``).
- Map ``speed`` / ``cover`` still apply to **all** units on a square; per-unit differences belong in terrain or unit defs in ``rules.txt``.
- **Code**: ``worldterrain.py``, ``lib/square_terrain_rules.py``, ``world/world_map.py``, ``combat/hit_miss.py``, ``combat/damage_calculation.py``, ``combat/attack_action.py``, ``worldunit/world_movement.py``; random maps emit ``cover`` lines (``rmg_templates.terrain_cover_line``).
- **Docs**: ``mod/building-land-terrain.rst``; ``res/ui/editor_palette.txt`` comments.
- **Tests**: ``test_terrain_cover_defaults.py``, ``test_terrain_unit_vs.py``, ``test_unit_on_terrain_percent.py``; ``test_combat_terrain_modifiers.py`` updated to percent cases.


Bug fixes and voice/audio UX improvements:

**Fix: melee/ranged attack cooldown (``mdg_cd`` / ``rdg_cd``) slower than rules specify**

- **Symptom**: With 1 second cooldown in rules (e.g. peasant ``mdg_cd 1``), actual attack interval was noticeably longer than in 1.3.8.1 (~1.5 s vs ~1.2 s; the latter is only 300 ms tick quantization).
- **Cause**: (1) When ``mdg_ready`` / ``rdg_ready`` is 0, the prep branch still consumed an extra tick before striking; (2) instant hits (``mdg_delay`` / ``rdg_delay`` 0) were forced through a 100 ms minimum delay in ``_schedule_ballistic_hit``; (3) ``attack_action.aim()`` and ``damage_effects._schedule_ballistic_hit`` both set cooldown, with the second write after the delay extending ``next_attack_time`` further.
- **Fix**: Skip prep when ``ready=0`` and attack immediately; no 100 ms floor for instant hits; set cooldown only once in ``attack_action.aim()`` when the attack starts.
- **Note**: ``charge_mdg_cd`` / ``charge_rdg_cd`` use a separate path (immediate ``receive_hit``, no prep/ballistic scheduling) and were not affected by these three issues; mixed charge + normal-attack pacing improves indirectly via the normal-attack CD fix.
- **Code**: ``combat/attack_action.py``, ``combat/damage_effects.py``.
- **Tests**: ``test_attack_cooldown_timing.py``.

修君

unread,
Jul 12, 2026, 2:21:24 PMJul 12
to soundRTSChat
please download again.

Honestly, I’m not sure how well the changes in this update will work in actual gameplay. They are mainly related to random maps and include many elements inspired by Civilization V. Anyone interested is welcome to give them a try.

Release notes
==============

.. contents::


1.4.5.1
--------

**Improve: random-map team modes and one-vs-many**

- **Free-for-all**: each player starts in a unique alliance (true FFA); no longer keeps the training-game default where all AIs share one team.
- **New one-vs-many**: player 1 alone vs all other players allied; 3 players get FFA / one-vs-many; 4 players get FFA / 2v2 / one-vs-many.
- **Share code**: team field abbreviation ``o`` for ``one_vs_many``.
- **Code**: ``randommap.py``, ``randommap_menu.py``, ``msgparts.py``; TTS 5750.
- **Docs**: ``player/random-map-play.rst``, ``mod/randommap.rst`` (all languages).
- **Tests**: ``test_ffa_assigns_unique_alliances``, ``test_one_vs_many_allies_all_except_player1``, ``test_team_modes_for_players``.

**New: RMG strategic layer expansion (territory, citizens, policy switching, AI strategy, cross-match hero growth)**

- **City territory and tile purchase**: each city owns its home square; ``rmg_buy_tile`` buys adjacent unclaimed main squares (first tile 20 gold, then +10 per tile).
- **Citizens and tile improvements**: ``rmg_assign_gold/wood/food/culture`` assign citizens; ``rmg_build_mine/lumber_mill/farm`` build improvements; worked tiles pay into the 60-second ``rmg_strategic_tick``.
- **In-match policy switching**: at most two active; researching a third replaces the oldest; unlocked policies switch free via ``rmg_switch_*``.
- **AI policy combinations**: aggressive → commerce + tradition; ≥2 enemies → diplomacy + commerce; otherwise tradition + commerce; researches prerequisite tech chain in order.
- **Local single-player RMG hero persistence**: peak level and XP saved at match end and restored at start to ``rmg_heroes/<mod>/<faction>.json``; not used in multiplayer/replays.
- **Code**: ``soundrts/rmg_systems.py``, ``soundrts/rmg_progress.py``, ``soundrts/worldorders/strategic.py``, ``soundrts/worldplayercomputer.py``, ``soundrts/game.py``.
- **Voice**: ``res/ui/tts.txt`` / ``res/ui-zh/tts.txt`` 5718–5728; matching command titles in ``res/ui/style.txt``.
- **Docs**: ``player/rmg-strategic-systems.rst``, ``player/homm-civ5-play.rst``.
- **Tests**: ``test_rmg_systems.py`` (territory, improvements, policy replacement, AI combos, hero profile, command registration).

**Fix: RMG strategic research appearing on normal maps (town hall)**

- **Symptom**: On hand-made or classic maps, the town hall research menu still listed urban planning, policy cards, and other ``rmg_*`` technologies.
- **Cause**: ``rules.txt`` listed ``rmg_*`` on ``townhall`` ``can_research``, and the rules-loaded list shadowed ``Building.can_research`` ``@property``; the research menu read the static list instead of ``effective_can_research()``.
- **Fix**: (1) ``townhall`` ``can_research`` keeps only generic tech such as ``hunting_techniques``; RMG maps inject ``STRATEGIC_RESEARCH_TYPES`` via ``effective_can_research``. (2) Like ``can_train`` → ``_rules_can_train``, store ``can_research`` as ``_rules_can_research`` so the ``@property`` works again.
- **Code**: ``definitions.py``, ``world_build_rules.py``, ``world_objects.py``, ``worldplayercomputer.py``, ``attributes/utils.py``, ``res/rules.txt``.
- **Tests**: ``test_townhall_can_research_property_respects_rmg_flag``, ``test_strategic_research_is_only_exposed_on_rmg_cities``.

**Improvement: culture and diplomacy points are viewable**

- On RMG strategic maps: global **B** announces culture points, **Shift+B** diplomacy points (non-RMG maps beep).
- With your city selected (town hall / keep / castle), open the attributes screen (Alt+V): **U** for culture, **Y** for diplomacy.
- The 60-second ``rmg_strategic_tick`` voice for city count remains; if resource-change alerts are on, culture/diplomacy changes are announced too.
- **Code**: ``clientgame/game_resources.py``, ``attributes/basic_attributes.py``, ``res/ui/global_bindings.txt``, ``res/ui/legacy_bindings.txt``, ``res/ui/tts.txt`` / ``res/ui-zh/tts.txt`` (5716–5717), ``hotkey_editor.py``, ``hotkey_catalogs.py``.
- **Docs**: ``player/rmg-strategic-systems.rst`` (all locales).
- **Tests**: ``test_culture_and_diplomacy_status_helpers``, ``test_city_attributes_include_strategic_points``.

**Improvement: terrain cover, per-unit modifiers, and percent notation**


- ``rules.txt`` ``class terrain`` now supports ``cover <ground> <air>``, same as ``speed``: a map line ``terrain marsh h8`` inherits default cover; per-square map ``cover`` lines still override.
- Terrain can modify **unit types** via ``speed_vs``, ``cover_vs``, ``dodge_vs``, ``mdg_vs``, ``rdg_vs``, ``mdg_cd_vs``, ``rdg_cd_vs`` (e.g. ``speed_vs knight .25 archer .5``). You may use ``*_vs`` alone without a global ``speed``/``cover``.
- Those ``*_vs`` fields and unit ``mdg_on_terrain`` / ``rdg_on_terrain`` / ``mdg_cd_on_terrain`` / ``rdg_cd_on_terrain`` (and ``charge_*_terrain``) now use **0–1 decimal percents** (``.5`` = ±50%%, ``.1`` = ±10%%) relative to the unit's current base damage or cooldown.
- ``speed_on_terrain`` remains an **absolute speed** override (unlike percent ``speed_vs``).
- Map ``speed`` / ``cover`` still apply to **all** units on a square; per-unit differences belong in terrain or unit defs in ``rules.txt``.
- **Code**: ``worldterrain.py``, ``lib/square_terrain_rules.py``, ``world/world_map.py``, ``combat/hit_miss.py``, ``combat/damage_calculation.py``, ``combat/attack_action.py``, ``worldunit/world_movement.py``; random maps emit ``cover`` lines (``rmg_templates.terrain_cover_line``).
- **Docs**: ``mod/building-land-terrain.rst``; ``res/ui/editor_palette.txt`` comments.
- **Tests**: ``test_terrain_cover_defaults.py``, ``test_terrain_unit_vs.py``, ``test_unit_on_terrain_percent.py``; ``test_combat_terrain_modifiers.py`` updated to percent cases.

Bug fixes and voice/audio UX improvements:

**Fix: melee/ranged attack cooldown (``mdg_cd`` / ``rdg_cd``) slower than rules specify**

- **Symptom**: With 1 second cooldown in rules (e.g. peasant ``mdg_cd 1``), actual attack interval was noticeably longer than in 1.3.8.1 (~1.5 s vs ~1.2 s; the latter is only 300 ms tick quantization).
- **Cause**: (1) When ``mdg_ready`` / ``rdg_ready`` is 0, the prep branch still consumed an extra tick before striking; (2) instant hits (``mdg_delay`` / ``rdg_delay`` 0) were forced through a 100 ms minimum delay in ``_schedule_ballistic_hit``; (3) ``attack_action.aim()`` and ``damage_effects._schedule_ballistic_hit`` both set cooldown, with the second write after the delay extending ``next_attack_time`` further.
- **Fix**: Skip prep when ``ready=0`` and attack immediately; no 100 ms floor for instant hits; set cooldown only once in ``attack_action.aim()`` when the attack starts.
- **Note**: ``charge_mdg_cd`` / ``charge_rdg_cd`` use a separate path (immediate ``receive_hit``, no prep/ballistic scheduling) and were not affected by these three issues; mixed charge + normal-attack pacing improves indirectly via the normal-attack CD fix.
- **Code**: ``combat/attack_action.py``, ``combat/damage_effects.py``.
- **Tests**: ``test_attack_cooldown_timing.py``.

**Fix: Computer player crash during perception update (missing ``_buckets``)**

- **Symptom**: During a match (especially with ``computer_only`` map AIs, allied AI teammates, or after loading a save), the main loop could crash in the perception phase with ``AttributeError: 'Computer' object has no attribute '_buckets'``.
- **Cause**: The spatial grid index ``_buckets`` was initialized only in the wrapper ``Player.__init__``; save/load strips that cache field; allied-vision bulk visibility (``bulk_visibility_check``) calls allies' ``_potential_neighbors``, which raises if a ``Computer`` has no ``_buckets`` yet.
- **Fix**: Pre-initialize ``_buckets`` in ``BasePlayer.__init__`` with other perception caches; ``_potential_neighbors`` falls back to an empty dict when missing; ``update_alliance`` clears the ``allied_vision`` instance cache so stale ally lists are not reused after alliance changes.
- **Code**: ``worldplayerbase/base.py``, ``worldplayerbase/perception.py``, ``worldplayerbase/__init__.py``.
- **Tests**: ``test_meteors_computer_only.py``, ``test_phase3_parity.py``, ``test_neutral_passive_creep.py``.

**Improvement: go-order rejection and voice feedback on impassable terrain**

- When a ground unit orders ``go`` / ``patrol`` to a square with ``is_ground 0``, or an air unit to ``is_air 0``, the order is rejected at queue time with "ground is impassable" or "air is impassable" (``order_impossible`` + ``ground_impassable`` / ``air_impassable``).
- Terrain with a ``passable_units`` whitelist: units not on the list are rejected on ``go`` with "\<unit type\>, cannot pass" (e.g. "footman, cannot pass", "knight, cannot pass"); whitelisted types (including via ``is_a``) still work.
- Existing checks unchanged: pure water for ground units, land for water units, unfinished bridge scaffold, etc.
- **Code**: ``worldorders/base.py`` (``_ground_air_impassable_reason``, ``_terrain_impassable_reason``); ``lib/square_terrain_rules.py`` (``terrain_name_at_square``, ``passable_units_denied_reason``); ``clientgameentity/events.py`` (unit title + "cannot pass" in ``on_order_impossible``).
- **Voice**: ``res/ui/style.txt`` ``messages`` — ``ground_impassable`` 4979, ``air_impassable`` 5700, ``passable_units_denied`` 5701; EN/ZH ``tts.txt`` entries included.
- **Docs**: ``mod/building-land-terrain.rst`` passability section.
- **Tests**: ``test_water_impassable_order.py``.


**Fix: nameless fog ghost after unit suicide**

- **Symptom**: After a unit suicides, Tab-cycling targets in the same square could still select an object with no readable name.
- **Cause**: After death ``place is None``, fog-of-war memory was not cleared in time; memory objects could have a ``title`` (fog suffix) but an empty ``short_title``, yet Tab still treated them as selectable.
- **Fix**: ``perception.py`` forgets memory when ``initial_model.place is None``; units leaving perception are not memorized when ``place is None`` or when they are the player's own dead units; ``game_unit_control.py`` ``is_visible`` requires a non-empty ``short_title``.
- **Tests**: ``test_suicide_fog_ghost.py`` (corpse fog memory and ambient audio paths preserved).

**Fix: wall HP flickering up and down while attacking**

- **Symptom**: Attacking ``wall`` and other ``is_repairable`` buildings could make HP or life-change sounds rise and fall intermittently.
- **Cause**: Walls inherit ``is_repairable=True`` from buildings, so attack / repair / capture-threshold logic could interact; fog HP sync (``_sync_memory_hp_from_live``) without carrying ``previous_hp`` across perception/memory view swaps caused false life-change feedback.
- **Fix**: ``world_order.py`` / ``worldcreature.py`` / ``worldworker.py`` — enemy repairable buildings default to ``go``, imperative default to ``attack``; repair paths guarded with ``not is_an_enemy(target)``; ``game_navigation.py`` preserves HP tracking on fog updates (``_take_hp_tracking`` / ``_apply_hp_tracking``).
- **Tests**: ``test_imperative_attack.py`` (imperative attack on walls).

**Fix: normal go order incorrectly interrupting imperative attack**

- **Symptom**: While a unit is force-attacking a target (e.g. town hall), issuing a normal ``go`` stopped the attack, yet group select (e.g. F) still announced "attacking the town hall, go to \<square\>" — behavior and voice were inconsistent.
- **Cause**: ``take_order`` with ``forget_previous=True`` called ``cancel_all_orders()``, removing the imperative attack and queuing ``go``, while ``AttackAction`` could remain on the unit.
- **Fix**: While an imperative order is active, normal commands (except ``stop``) are auto-queued (``forget_previous=False``) without replacing the imperative head; the unit finishes the forced attack before executing the follow-up. Only **one** queued command is allowed after an imperative order; a new normal command **replaces** the existing queued one (same as 1.3.8.1).
- **Code**: ``worldunit/world_order.py`` ``take_order``.
- **Tests**: ``test_imperative_attack.py`` (``test_normal_go_queues_behind_imperative_attack``, ``test_only_one_queued_order_behind_imperative_attack``, etc.).

**Fix: force attack on already-captured building still triggers capture**

- **Symptom**: After capturing an enemy capturable building (``capture_hp_threshold`` 100, e.g. barracks), force-attacking that building still issued capture instead of dealing damage, repeatedly playing capture sounds.
- **Cause**: "Capture on contact" routing used ``is_an_enemy()``; during force attack that method returns ``True`` for friendly captured buildings too (via ``_player_ordered_attack_on`` treating allied targets as attackable).
- **Fix**: Added ``should_capture_on_contact()`` using ``player.player_is_an_enemy(target.player)`` for genuine enemy checks; same guard in ``_perform_capture()``.
- **Code**: ``worldaction.py``, ``combat/attack_action.py``, ``worldunit/world_order.py``.
- **Tests**: ``test_capture_default_order.py`` (``test_imperative_attack_on_captured_barracks_deals_damage_not_capture``).

**Fix: computer transport boats park loaded at the enemy shore without unloading**

- **Symptom**: On water maps such as ``jl7``, even a nightmare computer could sail a ``boat`` full of soldiers to the player's shore and never issue ``unload`` / ``unload_all``, so troops never landed to fight.
- **Cause**: ``_try_transport_assaults`` only schedules idle ground soldiers outside transports; cargo with ``is_inside`` is ignored. After load/sail, if unload was missing or failed, a packed idle transport had no recovery path to unload.
- **Fix**: ``_try_amphibious_landings`` now calls ``_try_unload_idle_loaded_transports`` first: idle water/air transports carrying ground units get ``unload_all`` onto adjacent passable land (preferring land nearer enemy targets); if not yet adjacent, ``go`` to unload water then unload.
- **Code**: ``worldplayercomputer.py`` (``_enemy_land_assault_targets``, ``_choose_unload_land_for_transport``, ``_try_unload_idle_loaded_transports``).
- **Tests**: ``test_ai_jl7_amphibious_unload.py`` (nightmare AI regression: packed boat at the door must issue ``unload_all``).


**Improvement: unit behavior voice descriptions**

- After Tab-selecting a target, Ctrl+Backspace or go + Ctrl+Enter confirms "attack \<target\>" instead of "go" for enemy units/buildings.
- Hotkey group select (e.g. F for footmen): "You control N footmen attacking the town hall"; if moving while fighting, appends "go to c6".
- **Code**: ``clientgameentity/base.py`` ``_attack_action_title_msg``; ``properties.py`` ``orders_txt``; ``game_orders.py`` ``_say_validate_confirmation`` / ``_say_default_confirmation``; ``game_unit_control.py`` ``say_group``.
- **Tests**: ``test_attack_orders_txt.py``, ``test_imperative_attack.py``.

**Improvement: layered battle shouts**

- Three layers: ``shout_bg`` (battlefield background), ``shout_unit`` (unit voice), ``shout_event`` (first clash / charge / crit highlights); global and per-square cooldowns; ``formation_sound_queue`` staggers bursts so shouts do not stack with hit sounds in the same frame.
- **Code**: ``battle_shout_audio.py``, ``combat.py``, ``formation_sound_queue.py``.
- **Docs**: ``mod/battle-shouts.rst``.
- **Tests**: ``test_battle_shout_audio.py``.

**Improvement: P0–P2 audio engine refactor**

- **Correction**: early drafts wrongly described P0–P2 as ambient/combat/alert *priority tiers*; they are **three refactor phases** for the audio engine, separate from layered battle shouts above and from ``psounds.play(..., priority=…)`` preemption. See ``mod/audio-management.rst``.
- **P0 structure**: ``lib/music_resolver.py`` centralizes menu/game/battle/victory/defeat lookup; ``sound_cache.clear_decoded()`` on mod/map switches; instance-state fixes for ``SoundSource`` / ``SoundManager``.
- **P1 UX**: separate ``audio/sfx_volume`` from voice ``main_volume``; non-blocking voice wait (event pump); unified menu-music fallback.
- **P2 polish**: ambient LFO smoothing; ``lib/battle_music.py`` state machine; ``music_resolver`` cleanup; game SFX under ``ui/`` supports ``.ogg`` / ``.wav`` / ``.mp3`` (``.ogg`` preferred) plus hot preload (``preload_sounds`` / ``tick_preload``).
- **Hotkeys**: Home/End for game SFX; Alt+Home/Alt+End for music.
- **Tests**: ``test_music_resolver.py``, ``test_audio_settings.py``, ``test_voice_pump.py``, ``test_ambient_stereo_volume.py``, ``test_battle_music.py``, ``test_sfx_formats.py``.
Release notes
==============

.. contents::


1.4.5.1
--------

**Improve: random-map team modes and one-vs-many**

- **Free-for-all**: each player starts in a unique alliance (true FFA); no longer keeps the training-game default where all AIs share one team.
- **New one-vs-many**: player 1 alone vs all other players allied; 3 players get FFA / one-vs-many; 4 players get FFA / 2v2 / one-vs-many.
- **Share code**: team field abbreviation ``o`` for ``one_vs_many``.
- **Code**: ``randommap.py``, ``randommap_menu.py``, ``msgparts.py``; TTS 5750.
- **Docs**: ``player/random-map-play.rst``, ``mod/randommap.rst`` (all languages).
- **Tests**: ``test_ffa_assigns_unique_alliances``, ``test_one_vs_many_allies_all_except_player1``, ``test_team_modes_for_players``.

**New: RMG strategic layer expansion (territory, citizens, policy switching, AI strategy, cross-match hero growth)**

- **City territory and tile purchase**: each city owns its home square; ``rmg_buy_tile`` buys adjacent unclaimed main squares (first tile 20 gold, then +10 per tile).
- **Citizens and tile improvements**: ``rmg_assign_gold/wood/food/culture`` assign citizens; ``rmg_build_mine/lumber_mill/farm`` build improvements; worked tiles pay into the 60-second ``rmg_strategic_tick``.
- **In-match policy switching**: at most two active; researching a third replaces the oldest; unlocked policies switch free via ``rmg_switch_*``.
- **AI policy combinations**: aggressive → commerce + tradition; ≥2 enemies → diplomacy + commerce; otherwise tradition + commerce; researches prerequisite tech chain in order.
- **Local single-player RMG hero persistence**: peak level and XP saved at match end and restored at start to ``rmg_heroes/<mod>/<faction>.json``; not used in multiplayer/replays.
- **Code**: ``soundrts/rmg_systems.py``, ``soundrts/rmg_progress.py``, ``soundrts/worldorders/strategic.py``, ``soundrts/worldplayercomputer.py``, ``soundrts/game.py``.
- **Voice**: ``res/ui/tts.txt`` / ``res/ui-zh/tts.txt`` 5718–5728; matching command titles in ``res/ui/style.txt``.
- **Docs**: ``player/rmg-strategic-systems.rst``, ``player/homm-civ5-play.rst``.
- **Tests**: ``test_rmg_systems.py`` (territory, improvements, policy replacement, AI combos, hero profile, command registration).

**Fix: RMG strategic research appearing on normal maps (town hall)**

- **Symptom**: On hand-made or classic maps, the town hall research menu still listed urban planning, policy cards, and other ``rmg_*`` technologies.
- **Cause**: ``rules.txt`` listed ``rmg_*`` on ``townhall`` ``can_research``, and the rules-loaded list shadowed ``Building.can_research`` ``@property``; the research menu read the static list instead of ``effective_can_research()``.
- **Fix**: (1) ``townhall`` ``can_research`` keeps only generic tech such as ``hunting_techniques``; RMG maps inject ``STRATEGIC_RESEARCH_TYPES`` via ``effective_can_research``. (2) Like ``can_train`` → ``_rules_can_train``, store ``can_research`` as ``_rules_can_research`` so the ``@property`` works again.
- **Code**: ``definitions.py``, ``world_build_rules.py``, ``world_objects.py``, ``worldplayercomputer.py``, ``attributes/utils.py``, ``res/rules.txt``.
- **Tests**: ``test_townhall_can_research_property_respects_rmg_flag``, ``test_strategic_research_is_only_exposed_on_rmg_cities``.

**Improvement: culture and diplomacy points are viewable**

- On RMG strategic maps: global **B** announces culture points, **Shift+B** diplomacy points (non-RMG maps beep).
- With your city selected (town hall / keep / castle), open the attributes screen (Alt+V): **U** for culture, **Y** for diplomacy.
- The 60-second ``rmg_strategic_tick`` voice for city count remains; if resource-change alerts are on, culture/diplomacy changes are announced too.
- **Code**: ``clientgame/game_resources.py``, ``attributes/basic_attributes.py``, ``res/ui/global_bindings.txt``, ``res/ui/legacy_bindings.txt``, ``res/ui/tts.txt`` / ``res/ui-zh/tts.txt`` (5716–5717), ``hotkey_editor.py``, ``hotkey_catalogs.py``.
- **Docs**: ``player/rmg-strategic-systems.rst`` (all locales).
- **Tests**: ``test_culture_and_diplomacy_status_helpers``, ``test_city_attributes_include_strategic_points``.

**Improvement: terrain cover, per-unit modifiers, and percent notation**


- ``rules.txt`` ``class terrain`` now supports ``cover <ground> <air>``, same as ``speed``: a map line ``terrain marsh h8`` inherits default cover; per-square map ``cover`` lines still override.
- Terrain can modify **unit types** via ``speed_vs``, ``cover_vs``, ``dodge_vs``, ``mdg_vs``, ``rdg_vs``, ``mdg_cd_vs``, ``rdg_cd_vs`` (e.g. ``speed_vs knight .25 archer .5``). You may use ``*_vs`` alone without a global ``speed``/``cover``.
- Those ``*_vs`` fields and unit ``mdg_on_terrain`` / ``rdg_on_terrain`` / ``mdg_cd_on_terrain`` / ``rdg_cd_on_terrain`` (and ``charge_*_terrain``) now use **0–1 decimal percents** (``.5`` = ±50%%, ``.1`` = ±10%%) relative to the unit's current base damage or cooldown.
- ``speed_on_terrain`` remains an **absolute speed** override (unlike percent ``speed_vs``).
- Map ``speed`` / ``cover`` still apply to **all** units on a square; per-unit differences belong in terrain or unit defs in ``rules.txt``.
- **Code**: ``worldterrain.py``, ``lib/square_terrain_rules.py``, ``world/world_map.py``, ``combat/hit_miss.py``, ``combat/damage_calculation.py``, ``combat/attack_action.py``, ``worldunit/world_movement.py``; random maps emit ``cover`` lines (``rmg_templates.terrain_cover_line``).
- **Docs**: ``mod/building-land-terrain.rst``; ``res/ui/editor_palette.txt`` comments.
- **Tests**: ``test_terrain_cover_defaults.py``, ``test_terrain_unit_vs.py``, ``test_unit_on_terrain_percent.py``; ``test_combat_terrain_modifiers.py`` updated to percent cases.

Bug fixes and voice/audio UX improvements:

**Fix: melee/ranged attack cooldown (``mdg_cd`` / ``rdg_cd``) slower than rules specify**

- **Symptom**: With 1 second cooldown in rules (e.g. peasant ``mdg_cd 1``), actual attack interval was noticeably longer than in 1.3.8.1 (~1.5 s vs ~1.2 s; the latter is only 300 ms tick quantization).
- **Cause**: (1) When ``mdg_ready`` / ``rdg_ready`` is 0, the prep branch still consumed an extra tick before striking; (2) instant hits (``mdg_delay`` / ``rdg_delay`` 0) were forced through a 100 ms minimum delay in ``_schedule_ballistic_hit``; (3) ``attack_action.aim()`` and ``damage_effects._schedule_ballistic_hit`` both set cooldown, with the second write after the delay extending ``next_attack_time`` further.
- **Fix**: Skip prep when ``ready=0`` and attack immediately; no 100 ms floor for instant hits; set cooldown only once in ``attack_action.aim()`` when the attack starts.
- **Note**: ``charge_mdg_cd`` / ``charge_rdg_cd`` use a separate path (immediate ``receive_hit``, no prep/ballistic scheduling) and were not affected by these three issues; mixed charge + normal-attack pacing improves indirectly via the normal-attack CD fix.
- **Code**: ``combat/attack_action.py``, ``combat/damage_effects.py``.
- **Tests**: ``test_attack_cooldown_timing.py``.

**Fix: Computer player crash during perception update (missing ``_buckets``)**

- **Symptom**: During a match (especially with ``computer_only`` map AIs, allied AI teammates, or after loading a save), the main loop could crash in the perception phase with ``AttributeError: 'Computer' object has no attribute '_buckets'``.
- **Cause**: The spatial grid index ``_buckets`` was initialized only in the wrapper ``Player.__init__``; save/load strips that cache field; allied-vision bulk visibility (``bulk_visibility_check``) calls allies' ``_potential_neighbors``, which raises if a ``Computer`` has no ``_buckets`` yet.
- **Fix**: Pre-initialize ``_buckets`` in ``BasePlayer.__init__`` with other perception caches; ``_potential_neighbors`` falls back to an empty dict when missing; ``update_alliance`` clears the ``allied_vision`` instance cache so stale ally lists are not reused after alliance changes.
- **Code**: ``worldplayerbase/base.py``, ``worldplayerbase/perception.py``, ``worldplayerbase/__init__.py``.
- **Tests**: ``test_meteors_computer_only.py``, ``test_phase3_parity.py``, ``test_neutral_passive_creep.py``.

**Improvement: go-order rejection and voice feedback on impassable terrain**

- When a ground unit orders ``go`` / ``patrol`` to a square with ``is_ground 0``, or an air unit to ``is_air 0``, the order is rejected at queue time with "ground is impassable" or "air is impassable" (``order_impossible`` + ``ground_impassable`` / ``air_impassable``).
- Terrain with a ``passable_units`` whitelist: units not on the list are rejected on ``go`` with "\<unit type\>, cannot pass" (e.g. "footman, cannot pass", "knight, cannot pass"); whitelisted types (including via ``is_a``) still work.
- Existing checks unchanged: pure water for ground units, land for water units, unfinished bridge scaffold, etc.
- **Code**: ``worldorders/base.py`` (``_ground_air_impassable_reason``, ``_terrain_impassable_reason``); ``lib/square_terrain_rules.py`` (``terrain_name_at_square``, ``passable_units_denied_reason``); ``clientgameentity/events.py`` (unit title + "cannot pass" in ``on_order_impossible``).
- **Voice**: ``res/ui/style.txt`` ``messages`` — ``ground_impassable`` 4979, ``air_impassable`` 5700, ``passable_units_denied`` 5701; EN/ZH ``tts.txt`` entries included.
- **Docs**: ``mod/building-land-terrain.rst`` passability section.
- **Tests**: ``test_water_impassable_order.py``.


**Fix: nameless fog ghost after unit suicide**

- **Symptom**: After a unit suicides, Tab-cycling targets in the same square could still select an object with no readable name.
- **Cause**: After death ``place is None``, fog-of-war memory was not cleared in time; memory objects could have a ``title`` (fog suffix) but an empty ``short_title``, yet Tab still treated them as selectable.
- **Fix**: ``perception.py`` forgets memory when ``initial_model.place is None``; units leaving perception are not memorized when ``place is None`` or when they are the player's own dead units; ``game_unit_control.py`` ``is_visible`` requires a non-empty ``short_title``.
- **Tests**: ``test_suicide_fog_ghost.py`` (corpse fog memory and ambient audio paths preserved).

**Fix: wall HP flickering up and down while attacking**

- **Symptom**: Attacking ``wall`` and other ``is_repairable`` buildings could make HP or life-change sounds rise and fall intermittently.
- **Cause**: Walls inherit ``is_repairable=True`` from buildings, so attack / repair / capture-threshold logic could interact; fog HP sync (``_sync_memory_hp_from_live``) without carrying ``previous_hp`` across perception/memory view swaps caused false life-change feedback.
- **Fix**: ``world_order.py`` / ``worldcreature.py`` / ``worldworker.py`` — enemy repairable buildings default to ``go``, imperative default to ``attack``; repair paths guarded with ``not is_an_enemy(target)``; ``game_navigation.py`` preserves HP tracking on fog updates (``_take_hp_tracking`` / ``_apply_hp_tracking``).
- **Tests**: ``test_imperative_attack.py`` (imperative attack on walls).

**Fix: normal go order incorrectly interrupting imperative attack**

- **Symptom**: While a unit is force-attacking a target (e.g. town hall), issuing a normal ``go`` stopped the attack, yet group select (e.g. F) still announced "attacking the town hall, go to \<square\>" — behavior and voice were inconsistent.
- **Cause**: ``take_order`` with ``forget_previous=True`` called ``cancel_all_orders()``, removing the imperative attack and queuing ``go``, while ``AttackAction`` could remain on the unit.
- **Fix**: While an imperative order is active, normal commands (except ``stop``) are auto-queued (``forget_previous=False``) without replacing the imperative head; the unit finishes the forced attack before executing the follow-up. Only **one** queued command is allowed after an imperative order; a new normal command **replaces** the existing queued one (same as 1.3.8.1).
- **Code**: ``worldunit/world_order.py`` ``take_order``.
- **Tests**: ``test_imperative_attack.py`` (``test_normal_go_queues_behind_imperative_attack``, ``test_only_one_queued_order_behind_imperative_attack``, etc.).

**Fix: force attack on already-captured building still triggers capture**

- **Symptom**: After capturing an enemy capturable building (``capture_hp_threshold`` 100, e.g. barracks), force-attacking that building still issued capture instead of dealing damage, repeatedly playing capture sounds.
- **Cause**: "Capture on contact" routing used ``is_an_enemy()``; during force attack that method returns ``True`` for friendly captured buildings too (via ``_player_ordered_attack_on`` treating allied targets as attackable).
- **Fix**: Added ``should_capture_on_contact()`` using ``player.player_is_an_enemy(target.player)`` for genuine enemy checks; same guard in ``_perform_capture()``.
- **Code**: ``worldaction.py``, ``combat/attack_action.py``, ``worldunit/world_order.py``.
- **Tests**: ``test_capture_default_order.py`` (``test_imperative_attack_on_captured_barracks_deals_damage_not_capture``).

**Fix: computer transport boats park loaded at the enemy shore without unloading**

- **Symptom**: On water maps such as ``jl7``, even a nightmare computer could sail a ``boat`` full of soldiers to the player's shore and never issue ``unload`` / ``unload_all``, so troops never landed to fight.
- **Cause**: ``_try_transport_assaults`` only schedules idle ground soldiers outside transports; cargo with ``is_inside`` is ignored. After load/sail, if unload was missing or failed, a packed idle transport had no recovery path to unload.
- **Fix**: ``_try_amphibious_landings`` now calls ``_try_unload_idle_loaded_transports`` first: idle water/air transports carrying ground units get ``unload_all`` onto adjacent passable land (preferring land nearer enemy targets); if not yet adjacent, ``go`` to unload water then unload.
- **Code**: ``worldplayercomputer.py`` (``_enemy_land_assault_targets``, ``_choose_unload_land_for_transport``, ``_try_unload_idle_loaded_transports``).
- **Tests**: ``test_ai_jl7_amphibious_unload.py`` (nightmare AI regression: packed boat at the door must issue ``unload_all``).


**Improvement: unit behavior voice descriptions**

- After Tab-selecting a target, Ctrl+Backspace or go + Ctrl+Enter confirms "attack \<target\>" instead of "go" for enemy units/buildings.
- Hotkey group select (e.g. F for footmen): "You control N footmen attacking the town hall"; if moving while fighting, appends "go to c6".
- **Code**: ``clientgameentity/base.py`` ``_attack_action_title_msg``; ``properties.py`` ``orders_txt``; ``game_orders.py`` ``_say_validate_confirmation`` / ``_say_default_confirmation``; ``game_unit_control.py`` ``say_group``.
- **Tests**: ``test_attack_orders_txt.py``, ``test_imperative_attack.py``.

**Improvement: layered battle shouts**

- Three layers: ``shout_bg`` (battlefield background), ``shout_unit`` (unit voice), ``shout_event`` (first clash / charge / crit highlights); global and per-square cooldowns; ``formation_sound_queue`` staggers bursts so shouts do not stack with hit sounds in the same frame.
- **Code**: ``battle_shout_audio.py``, ``combat.py``, ``formation_sound_queue.py``.
- **Docs**: ``mod/battle-shouts.rst``.
- **Tests**: ``test_battle_shout_audio.py``.

**Improvement: P0–P2 audio engine refactor**

- **Correction**: early drafts wrongly described P0–P2 as ambient/combat/alert *priority tiers*; they are **three refactor phases** for the audio engine, separate from layered battle shouts above and from ``psounds.play(..., priority=…)`` preemption. See ``mod/audio-management.rst``.
- **P0 structure**: ``lib/music_resolver.py`` centralizes menu/game/battle/victory/defeat lookup; ``sound_cache.clear_decoded()`` on mod/map switches; instance-state fixes for ``SoundSource`` / ``SoundManager``.
- **P1 UX**: separate ``audio/sfx_volume`` from voice ``main_volume``; non-blocking voice wait (event pump); unified menu-music fallback.
- **P2 polish**: ambient LFO smoothing; ``lib/battle_music.py`` state machine; ``music_resolver`` cleanup; game SFX under ``ui/`` supports ``.ogg`` / ``.wav`` / ``.mp3`` (``.ogg`` preferred) plus hot preload (``preload_sounds`` / ``tick_preload``).
- **Hotkeys**: Home/End for game SFX; Alt+Home/Alt+End for music.
- **Tests**: ``test_music_resolver.py``, ``test_audio_settings.py``, ``test_voice_pump.py``, ``test_ambient_stereo_volume.py``, ``test_battle_music.py``, ``test_sfx_formats.py``.

修君

unread,
Jul 12, 2026, 2:22:26 PMJul 12
to soundRTSChat

RMG hero and civilization strategic systems
============================================

This layer adds Heroes of Might and Magic-style hero progression and
Civilization-style city management to SoundRTS random maps (Random Map
Generator, RMG). The game remains real-time strategy: yields and combat
resolve on game time, not turns.


----


1. How to enable
----------------

From the main menu choose **Start a game → Random map** and begin a match.
Every newly generated RMG map writes ``rmg_strategic_systems 1`` and enables
heroes, city yields, culture, diplomacy points, technologies, and policies.

Hand-made maps and non-RMG sessions do not enable these rules by default. If a
mod does not define ``rmg_hero``, the generator skips hero placement without
breaking map load.


----


2. Hero progression
-------------------

Each player starts with one ``rmg_hero``:

- Starts at level 1, maximum level 8.
- Gains experience in combat and levels up automatically.
- Each level increases hit points, melee damage, and mana capacity.
- Heroes have their own mana pool; skills spend mana and mana regenerates.
- At most one RMG hero per player at a time.
- In **local single-player RMG**, the highest level and experience reached are
  saved per mod and faction and restored in the next match. Multiplayer and
  replays do not read local hero saves (avoids client desync).

Cross-match hero profile
~~~~~~~~~~~~~~~~~~~~~~~~

- Save path: under the user config directory, sibling to ``achievements``:
  ``rmg_heroes/<mod_key>/<faction>.json`` (e.g. ``human.json``).
- Written at match end with peak level and XP; applied at the next match start
  to ``rmg_hero``, including unlocked level skills.
- Only **local single-player random maps** (``TrainingGame``). Campaign,
  multiplayer, replay, and spectator modes do not use this file.
- Separate from campaign ``campaign_carryover``: ``rmg_hero`` keeps
  ``campaign_carryover 0`` in rules; RMG persistence uses dedicated JSON, not
  ``campaigns.ini``.

Skill tree
~~~~~~~~~~

.. list-table::
   :header-rows: 1

   * - Level
     - Skill
     - Mana cost
     - Effect
   * - 2
     - Arcane bolt
     - 20
     - Magic damage to one target
   * - 4
     - Whirlwind
     - 35
     - Damage enemies in melee range
   * - 6
     - Meteor shower
     - 60
     - Area damage at long range

Skills unlock automatically at the listed levels. Select the hero and use the
skill command menu; insufficient mana blocks casting.


----


3. City expansion and tile yields
---------------------------------

Town halls, keeps, and castles count as cities. Mod-compatible bases that both
**provide survival** and **store resources** are also treated as cities.

Each city owns its home square as territory. Select a city, use **Purchase
tile**, then pick a main map square adjacent to that city's current territory.
Tiles cannot be double-claimed. The first purchased tile costs 20 gold; each
additional purchase adds 10 gold. A newly built city always claims its home
square.

Every 60 seconds, each living city and each worked tile pays out once.

Base yield per city
~~~~~~~~~~~~~~~~~~~

Each city pays per tick:

- 6 gold
- 4 wood
- 4 food
- 4 culture
- 1 diplomacy point

City terrain bonus
~~~~~~~~~~~~~~~~~~

The city's RMG terrain adds to the base yield:

.. list-table::
   :header-rows: 1

   * - Terrain
     - Extra yield
   * - Hill, plateau, high rocky plain, mountain pass
     - +3 gold
   * - Forest, dense forest, marsh
     - +3 wood
   * - Plain, town, meadows
     - +3 food
   * - Lake, river, creek, ford
     - +1 gold, +2 food

Resource yields count toward **total gathered** and can satisfy RMG economic
victory. After each tick the local player hears city count, culture total, and
diplomacy total.

Citizens and tile improvements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Select a city and use **Assign citizen to gold / wood / food / culture**, then
pick an owned tile. If citizen slots are full, the oldest assignment is
released:

- 1 base slot per city.
- Urban planning and civic administration each add 1 slot.
- Keep or castle adds 1 more slot.

Worked tiles pay terrain-based yields and can build a **mine**, **lumber
mill**, or **farm** for +3 gold, +3 wood, or +3 food per tick. Costs: mine
15 gold + 10 wood; lumber mill 10 gold + 15 wood; farm 10 gold + 5 wood + 10
food.

Worked tile base yield (every 60 s, before improvements and focus):

- 1 gold, 1 wood, 1 food per tile.
- Same terrain bonuses as the city table above.
- Citizen focus: +2 gold, +2 wood, +2 food, or +3 culture (culture focus does
  not add the +2 resource bonus).

Purchase cost: first expansion 20 gold, then +10 gold per tile bought (city
home square is free).


3.1 City strategic commands
---------------------------

With a town hall, keep, or castle selected in an RMG match, the command menu
includes (select target square, then confirm):

.. list-table::
   :header-rows: 1

   * - Command (voice)
     - Keyword
     - Effect
   * - Purchase tile (5718)
     - ``rmg_buy_tile``
     - Buy an adjacent unowned main square
   * - Assign citizen to gold (5719)
     - ``rmg_assign_gold``
     - Work a owned tile for gold
   * - Assign citizen to wood (5720)
     - ``rmg_assign_wood``
     - Work for wood
   * - Assign citizen to food (5721)
     - ``rmg_assign_food``
     - Work for food
   * - Assign citizen to culture (5722)
     - ``rmg_assign_culture``
     - +3 culture per minute on that tile
   * - Build mine (5723)
     - ``rmg_build_mine``
     - +3 gold per tick on that tile
   * - Build lumber mill (5724)
     - ``rmg_build_lumber_mill``
     - +3 wood per tick
   * - Build farm (5725)
     - ``rmg_build_farm``
     - +3 food per tick
   * - Activate tradition policy (5726)
     - ``rmg_switch_tradition``
     - Switch among unlocked policies at no culture cost
   * - Activate commerce policy (5727)
     - ``rmg_switch_commerce``
     - Same
   * - Activate diplomacy policy (5728)
     - ``rmg_switch_diplomacy``
     - Same

Policy switch commands appear only for researched policies that are not
currently active. Invalid targets announce command failure.


----


4. Technology tree
------------------

Research at any city:

.. list-table::
   :header-rows: 1

   * - Technology
     - Requires
     - Effect
   * - Urban planning
     - —
     - +2 gold, wood, and food per city per tick
   * - Civic administration
     - Urban planning
     - +2 culture per city per tick
   * - Foreign service
     - Civic administration
     - +1 diplomacy point per city per tick

Technologies cost normal gold, wood, and food. Non-RMG games hide ``rmg_``
research entries.


----


5. Culture and policy cards
---------------------------

Culture is an in-match strategic stat (not shown on the resource bar). Cities
generate culture each minute; adopting a policy spends culture once.

Policy cards
~~~~~~~~~~~~

.. list-table::
   :header-rows: 1

   * - Policy
     - Culture cost
     - Requires
     - Effect
   * - Tradition
     - 40
     - Urban planning
     - +50% culture yield
   * - Commerce
     - 80
     - Civic administration
     - +25% city gold, wood, and food
   * - Diplomacy
     - 120
     - Foreign service
     - Double diplomacy point yield

At most **two** policies are active. Researching a third unlocks it and
**replaces the oldest active** policy. After that, any city can **Activate
tradition / commerce / diplomacy policy** to switch among unlocked policies
for free; replaced policies stay unlocked.

Policies appear in research only when you have enough culture.

Computer players pick fixed combinations by situation and research
prerequisites in order (urban planning → policy → civic administration → …):

.. list-table::
   :header-rows: 1

   * - AI / situation
     - Preferred pair
   * - Aggressive (``aggressive`` / ``rush`` / ``hard`` …)
     - Commerce + tradition
   * - Standard with ≥ 2 enemies
     - Diplomacy + commerce
   * - Other
     - Tradition + commerce

AI skips policies outside its plan and does not queue the same policy while
culture is insufficient.


----


6. Diplomacy points
-------------------

Cities generate diplomacy points each minute; foreign service and the diplomacy
policy increase output.

In RMG with strategic systems, **sending an alliance request** costs 20
diplomacy points:

- No points spent if you cannot afford it.
- Points are deducted only when the request is sent.
- Accept, decline, withdraw, or leave an alliance is free.
- The 60-second cooldown per target still applies.
- Nomadic or city-less mod setups are not blocked by diplomacy costs.

Neutral creeps cannot join diplomacy.


6.1 Viewing culture and diplomacy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Culture and diplomacy **do not** use the main resource bar (Z / X / Shift+Z) or
stay announced like wood or food. On RMG matches with ``rmg_strategic_systems``:

.. list-table::
   :header-rows: 1

   * - Method
     - Action
   * - Global hotkeys
     - **B** — current culture; **Shift+B** — current diplomacy points
   * - City attributes
     - Select your town hall / keep / castle, open attributes (Alt+V), **U** for culture, **Y** for diplomacy
   * - Periodic voice
     - Every 60 s ``rmg_strategic_tick`` still reports city count, culture total, diplomacy total
   * - Change alerts
     - If resource-change alerts are enabled, culture/diplomacy changes are voiced too

On non-RMG maps, **B** / **Shift+B** only beep.


----


7. Mod compatibility
--------------------

RMG gameplay architecture
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Random maps combine an **engine framework** (generator, four victory modes,
trigger API, optional Civ-style strategic systems) with **rules and template
data**. Default values and whether strategic systems are enabled live in
``rules.txt`` ``def parameters``; ``cfg/randommap/*.txt`` templates override
them per skirmish setup. Mods extend RMG by editing rules and templates—no
Python changes. Hand-written ``map.txt`` files allow fully custom victory
conditions.

Strategic numbers, tile improvements, diplomatic trades, and victory goals are
all rule-driven in ``rules.txt``.

Global parameters (``def parameters``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. list-table::
   :header-rows: 1

   * - Key
     - Default
     - Meaning
   * - ``rmg_diplomacy_request_cost``
     - 20
     - Diplomacy spent to send an alliance request
   * - ``rmg_tile_purchase_base``
     - 20
     - Gold for the first purchased tile
   * - ``rmg_tile_purchase_step``
     - 10
     - Extra gold per additional purchased tile
   * - ``rmg_policy_slot_limit``
     - 2
     - Active policy cards at once
   * - ``rmg_trade_cooldown``
     - 60
     - Default diplomatic trade cooldown (seconds)
   * - ``rmg_economic_goal``
     - 3000
     - Economic victory: total ``resource1`` gathered
   * - ``rmg_economic_goal_fast`` / ``_macro`` / ``_lanes``
     - 2000 / 5000 / 2500
     - Per-template economic goals
   * - ``rmg_survival_seconds``
     - 900
     - Survival hold time (seconds, non-fast templates)
   * - ``rmg_survival_seconds_fast``
     - 600
     - Survival hold time for fast template
   * - ``rmg_exploration_ruin_pairs_small`` / ``_medium`` / ``_large``
     - 1 / 2 / 2
     - Symmetric ancient-ruin *pairs* (each pair = two mirrored ruins)
   * - ``rmg_exploration_ruin_pairs_bonus``
     - 1
     - Extra pairs when exploration victory mode is selected
   * - ``rmg_strategic_systems``
     - 1
     - Enable Civ-style RMG systems on generated maps

Tile improvements (``rmg_tile_*`` buildings)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

On any RMG tile building:

- ``rmg_tile_improvement 1`` — marks a strategic tile improvement
- ``rmg_improvement_key mine`` — short internal key (optional; default strips ``rmg_tile_`` prefix)
- ``rmg_tile_yield 3 0 0 0 0`` — per 60 s while a worker is on tile: gold / wood / food / culture / diplomacy

Build cost and time still use normal ``cost`` / ``time_cost``.

Diplomatic trades (``rmg_trade_*`` upgrades)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Built-in example:

.. code-block:: text

   def rmg_trade_resource2
   class upgrade
   rmg_trade 1
   rmg_trade_id resource2
   rmg_trade_pay 50 0 0
   rmg_trade_gain 0 100 0

- ``rmg_trade_id`` — wire token (``resource1`` / ``resource2`` / ``resource3`` recommended)
- ``rmg_trade_pay`` — visible resources paid by buyer
- ``rmg_trade_gain`` — visible resources received from AI
- ``rmg_trade_diplomacy_cost`` — extra diplomacy cost (optional)
- ``rmg_trade_alliance 1`` — alliance on success (open-borders style)
- ``rmg_trade_cooldown 90`` — per-trade cooldown override (seconds, optional)

F12 hotkeys still map to ``resource2`` / ``resource3`` / ``open_borders``; new
trades need ``diplomacy_bindings.txt`` entries and
``diplomacy trade <rmg_trade_id> <player>``.

Victory modes and custom challenges (``cfg/randommap/*.txt``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Per-template overrides (example):

.. code-block:: text

   random_map_template
   name space_survival
   extends fast
   title space survival
   default_victory_mode survival
   survival_seconds 1200
   exploration_ruin_pairs 2
   economic_goal 8000
   strategic_systems 0

- ``default_victory_mode`` — auto-select victory mode when this template is chosen
- ``strategic_systems 0`` — disable culture/diplomacy/tile purchase (pure RTS, e.g. sci-fi reskin)
- Command centres need not be ``townhall``: any building with ``provides_survival 1`` and ``storable_resource_types``

**Fifth and custom victory modes** — ``victory_triggers`` block (full trigger lines):

.. code-block:: text

   victory_triggers
   trigger players (timer 60 60) (if (has_gathered 5000 resource2) (victory))
   end_victory_triggers

The generator writes your victory triggers and still adds ``no_building_left`` /
``no_unit_left`` defeat triggers. See ``res/randommap/example.txt``.

Other compatibility notes
~~~~~~~~~~~~~~~~~~~~~~~~~

Rule-driven behaviour:

- ``rmg_hero`` must exist for a starting hero.
- Non-standard command centres with ``provides_survival`` and
  ``storable_resource_types`` count as cities.
- Cities gain RMG tech and policy research dynamically.
- Non-RMG maps filter ``rmg_`` research.
- Rules store building ``can_research`` as ``_rules_can_research`` so ``Building.can_research`` ``@property`` works; ``townhall`` no longer lists ``rmg_*`` in rules—``effective_can_research()`` injects them on RMG maps only.
- Mods with fewer than three resources only get yields for existing slots.

Key rule fields:

.. code-block:: text

   culture_cost 40
   rmg_policy 1

Map triggers may also use:

.. code-block:: text

   (rmg_strategic_tick)
   (rmg_has_culture 100)
   (rmg_has_diplomacy 20)
   (rmg_grant_culture 25)
   (rmg_grant_diplomacy 10)


----


8. Implementation
-----------------

- Strategic runtime: ``soundrts/rmg_systems.py``
- City commands: ``soundrts/worldorders/strategic.py``
- Cross-match hero saves: ``soundrts/rmg_progress.py``
- RMG map hook: ``soundrts/randommap.py``
- Map flag: ``soundrts/world/world_map.py``
- Culture on research: ``soundrts/worldorders/production.py``
- Diplomacy spend: ``soundrts/worldplayerbase/base.py``
- AI policy order: ``soundrts/worldplayercomputer.py``
- Hero save hooks: ``soundrts/game.py``
- Triggers: ``soundrts/worldplayerbase/triggers.py``
- Rules: ``res/rules.txt``
- Voice/style: ``res/ui/style.txt``, ``res/ui/tts.txt``, ``res/ui-zh/tts.txt`` (5702–5728; culture/diplomacy status 5716–5717)
- Culture/diplomacy UI: ``soundrts/clientgame/game_resources.py``, ``soundrts/attributes/basic_attributes.py``
- Tests: ``soundrts/tests/test_rmg_systems.py``, ``soundrts/tests/test_randommap.py``


----


9. Current boundaries
---------------------

Still real-time minute ticks — no Civ5 turns, citizen population growth, road
maintenance, or diplomacy UI. Territory is tracked per RMG main square and
does not change unit movement rights. Cross-match hero growth applies only to
local single-player random maps, not multiplayer.
在2026年7月11日星期六 UTC+8 23:51:38<修君> 写道:

lucia greco

unread,
Jul 13, 2026, 11:26:47 AMJul 13
to soundr...@googlegroups.com
hi please do  not just say   download again but add the link as well some times the link is buried deep in my inbox and then when i find it it says not found 


lucia Greco
http://accessaces.com
follow me on twitter @accessaces


--
You received this message because you are subscribed to the Google Groups "soundRTSChat" group.
To unsubscribe from this group and stop receiving emails from it, send an email to soundrtschat...@googlegroups.com.

Gabriele Battaglia

unread,
Jul 13, 2026, 11:56:32 AMJul 13
to soundr...@googlegroups.com
Yep, I agree with Lucia.

It would be better if you remind the link, time to time. Otherwise, you
may erase all the useless quoted text within the mail and put your
github link within the groups singature. Better would be the link to the
last release, so we can jump all the stuff for those who are not
interested on downloading the source code.

I also would like to strongly recommend you to increase the version
number at every byte you touch. It will be more easy for all to kee
track on which version we are on.

Just my idea.

Gabe.
> <https://groups.google.com/d/msgid/soundrtschat/3c10f2bd-43f0-4579-8f6d-cdbac20b1750n%40googlegroups.com?utm_medium=email&utm_source=footer>.
>
> --
> You received this message because you are subscribed to the Google
> Groups "soundRTSChat" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to soundrtschat...@googlegroups.com.
> To view this discussion visit
> https://groups.google.com/d/msgid/soundrtschat/CAOLcwjMNAfwX0gUWAxKOQ4BCsqn7-JMWZ9Dg9N%2BXt5nmrv%3DuVA%40mail.gmail.com
> <https://groups.google.com/d/msgid/soundrtschat/CAOLcwjMNAfwX0gUWAxKOQ4BCsqn7-JMWZ9Dg9N%2BXt5nmrv%3DuVA%40mail.gmail.com?utm_medium=email&utm_source=footer>.

--
Gabriele Battaglia (IZ4APU)
--... ...-- -.. . .. --.. ....- .- .--. ..- - ..- . .
Sent from my Giant desktop PC.

Julian Dreykorn

unread,
Jul 13, 2026, 11:56:50 AMJul 13
to soundr...@googlegroups.com, 修君

Hi Tuohai, done as promised. I just submitted PR #1 (hey, premier!) to improve the CI builds, more in the description. Now the published artifacts, and in the future maybe releases also published via workflows, can be downloaded and used directly. I hope you're OK with that way of submitting changes.
Again thanks for all your work.
Best, Julian

--
You received this message because you are subscribed to the Google Groups "soundRTSChat" group.
To unsubscribe from this group and stop receiving emails from it, send an email to soundrtschat...@googlegroups.com.

Julian Dreykorn

unread,
Jul 13, 2026, 12:15:11 PMJul 13
to soundr...@googlegroups.com, Gabriele Battaglia
Hi, I made the mistake to direct reply to the initial mail, so let me
break that down.

So, the normal version releases are - supposed to be - published on
GitHub, and most top you'll always find the latest release. Here's the link:

https://github.com/tuohai/soundrts-ultimate-version/releases

However, e.g. latest update didn't get a new version, and no new GitHub
release either. So, my just mentioned PR changes the GitHub builds, and
the latest builds are now here:

https://github.com/fcnjd/soundrts-ultimate-version/actions/runs/29263168017

Go to the artifacts heading, and pick the archive corresponding to your
OS. However I'd like to say that this link is unique for this special
build, once a new update will be pushed there'll be a new one.
Therefore, yes I agree with the point that a new release with
incremented version number would be helpful. I didn't yet work on that
since I wasn't sure if Tuohai is accepting contributions, but if so I'd
be happy to also automate the release procedure via GitHub CI, as now
work the builds. If desired, we could even trigger that on every commit,
so you automatically get a new release build, but that's something that
needs discussion prior to enabling.

Hope that makes things clearer.

Best, Julian

修君

unread,
Jul 13, 2026, 12:47:49 PMJul 13
to soundRTSChat

Okay, I’ll update the version number next time.
Also, thanks to Julian Dreykorn. I welcome contributions.

Julian Dreykorn

unread,
Jul 13, 2026, 1:04:48 PMJul 13
to soundr...@googlegroups.com, 修君

Awesome. In fact there's another commit I just added to the PR, something that's now waiting on SoundRTS original for almost three years now. Change is small: Config is also allowed by environment variables, making it easy to spin up a server noninteractively e.g. via Docker compoes.

Best, Julian

jasperja...@gmail.com

unread,
Jul 13, 2026, 4:03:09 PMJul 13
to soundRTSChat

Hi
Chapter 16 of campain is having same issue Chapter 13 had where I give the wond to the mage and then the game does not continue. I also found it very convenient that you did not change the version number, so I was able to continue my saved games after downloading update. Thanks again for all your hard work with this. The updates are really awesome.
Reply all
Reply to author
Forward
0 new messages