Compare commits

..

No commits in common. "main" and "sparta_adjustments" have entirely different histories.

34 changed files with 252 additions and 914 deletions

View File

@ -18,7 +18,6 @@ jobs:
- name: Get changed files - name: Get changed files
id: files id: files
run: | run: |
git fetch origin ${{ github.event.pull_request.base.ref }}
files=$(git diff-tree --no-commit-id --name-only -r HEAD origin/${{ github.event.pull_request.base.ref }}) files=$(git diff-tree --no-commit-id --name-only -r HEAD origin/${{ github.event.pull_request.base.ref }})
files=$(echo "$files" | grep 'simulation/templates/' | grep '.xml' || true) files=$(echo "$files" | grep 'simulation/templates/' | grep '.xml' || true)
files=$(echo "$files" | awk '{ gsub("community-mod/","", $0); print $0 }') files=$(echo "$files" | awk '{ gsub("community-mod/","", $0); print $0 }')
@ -47,10 +46,9 @@ jobs:
needs: check_version_increment needs: check_version_increment
env: env:
MOD_NAME: 0ad-community-mod MOD_NAME: 0ad-community-mod
if: github.ref == 'refs/heads/signed' && github.event_name == 'push' if: github.ref == 'refs/heads/signed'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
fetch-depth: 0
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
- run: pip3 install requests - run: pip3 install requests
- name: Install minisign - name: Install minisign
@ -74,12 +72,10 @@ jobs:
echo "Tag ${{ env.VERSION }} already exists" echo "Tag ${{ env.VERSION }} already exists"
exit 1 exit 1
fi fi
- name: Package Pyromod - uses: https://gitea.wildfiregames.com/Stan/gitea-action-build-pyromod@main
uses: https://gitea.wildfiregames.com/Stan/gitea-action-build-pyromod@f37c93f16d4992177c1115016d9a2501339972a0
with: with:
name: ${{ env.MOD_NAME }} name: ${{ env.MOD_NAME }}
version: ${{ env.MOD_VERSION }} version: ${{ env.MOD_VERSION }}
directory: community-mod
- name: Create sha256sum - name: Create sha256sum
run: | run: |
OUTPUT_FILE="${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.pyromod" OUTPUT_FILE="${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.pyromod"
@ -90,20 +86,7 @@ jobs:
cp -v ${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.pyromod ../${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip cp -v ${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.pyromod ../${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip
- name: Sign - name: Sign
run: | run: |
echo ${{ secrets.MINISIGN_KEY_PW }} | minisign -S \ echo ${{ secrets.MINISIGN_KEY_PW }} | minisign -S -s signature-file.pem -m "${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip" -x signature.minisign
-s signature-file.pem \
-m "${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip" \
-x ${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip.minisign
- name: Upload to Modio - name: Upload to Modio
run: | run: |
MOD_FILE_PATH="${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip" MOD_VERSION="${{ env.MOD_VERSION }}" python3 -m scripts.modio MOD_FILE_PATH="${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip" MOD_VERSION="${{ env.MOD_VERSION }}" python3 -m scripts.modio
- name: Release PyroMod
uses: akkuman/gitea-release-action@v1
with:
files: |-
$OUTPUT_FILE.sha256sum
${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip
${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip.minisign
target_commitish: ${{ env.MOD_VERSION }}
tag_name: ${{ env.MOD_VERSION }}
name: Community Mod ${{ env.MOD_VERSION }}

View File

@ -16,15 +16,3 @@ repos:
- id: yamllint - id: yamllint
args: args:
- -s - -s
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.1
hooks:
- id: ruff
args:
- --output-format=full
exclude: ^source/tools/webservices/
- id: ruff-format
args:
- --check
- --target-version
- py311

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<actor version="1">
<castshadow/>
<group>
<variant file="biped/base_healer_male.xml">
<mesh>skeletal/new/m_dress.dae</mesh>
<props>
<prop actor="props/units/heads/new/head_hele_healer.xml" attachpoint="head"/>
<prop actor="props/units/staff.xml" attachpoint="weapon_R"/>
</props>
</variant>
</group>
<group>
<variant frequency="1" name="healer-robe-1">
<props>
</props>
<textures>
<texture file="skeletal/athen/philosopher_01.png" name="baseTex"/>
<texture file="default_norm.png" name="normTex"/>
<texture file="skeletal/athen/philosopher_01_spec.png" name="specTex"/>
</textures>
</variant>
</group>
<group>
<variant frequency="1" name="Idle"/>
<variant file="biped/death_infantry.xml"/>
</group>
<material>player_trans_norm_spec.xml</material>
</actor>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1,204 +1,6 @@
[font="sans-bold-24"] Community-mod changelog [font="sans-bold-24"] Community-mod changelog
[font="sans-bold-20"] [font="sans-bold-20"]
#Version 12 (September 10, 2024) #Version 10 (July 16, 2024)
[font="sans-16"]
- Double the Experience required for Athenian Hoplites to convert into Champion Spearmen.
- Remove walls destroying trees upon completion due to crashes.
[font="sans-bold-20"]
#Version 11 (September 9, 2024)
[font="sans-16"]
- Slighty damage buff to all archer units.
- Allow walls and palisades to be built over trees, destroying trees upon completion.
- Rams now has less pierce armor and increased hack armor.
- Training time 25% faster for all champions that are not trained from barracks and stables.
[icon="tech_loom" displace="0 8"][font="sans-bold-18"] The Loom
[font="sans-16"]
Cost: reduced from [icon="icon_food"] 150 to [icon="icon_food"] 100.
[icon="tech_cartography" displace="0 8"][font="sans-bold-18"] Cartography
[font="sans-16"]
Now available at both Market and CC
[font="sans-bold-18"] Heroes update
[icon="han_hero_wei_qing" displace="0 8"] [font="sans-bold-18"] Wei Qing Han Hero
[font="sans-16"]
[font="sans-bold-16"]Mandate of Heaven[font="sans-16"] aura (-10% melee and range enemy cavalry attack): increased from 30 to 45 meters.
[icon="pers_hero_xerxes" displace="0 8"] [font="sans-bold-18"] Xerxes Persian Hero
[font="sans-16"]
[font="sans-bold-16"]Administrator[font="sans-16"] aura: increased build rate from +15% to +25%. Aura radius increased to 100 meters.
Added [font="sans-bold-16"]Invader of Greece[font="sans-16"] aura: Siege Rams, Elephants, and Champion Infantry +25% Health.
[icon="kush_hero_arakamani" displace="0 8"] [font="sans-bold-18"] Arakamani Kushites Hero
[font="sans-16"]
Removed [font="sans-bold-16"]Defier of Tradition[font="sans-16"] aura.
[font="sans-bold-16"]Follower of Apedemak[font="sans-16"] aura: Temple of Apedemak −30% resource costs and build time. Apedemak Temple Guard −30% resource costs and training time.
[font="sans-bold-16"]Slaughter of the Faithful[font="sans-16"] aura: Enemy Healers healing strength from -50% to -100%
[icon="kush_hero_nastasen" displace="0 8"] [font="sans-bold-18"] Arakamani Kushites Hero
[font="sans-16"]
[font="sans-bold-16"]Savior of Kush[font="sans-16"] aura now affects Soldiers and Elephants. Increased melee and ranged damage from +10% to +15%. Gain +50% resource loot.
[icon="maur_pillar_ashoka" displace="0 8"] [font="sans-bold-18"] Pillar of Ashoka
[font="sans-16"]
[font="sans-bold-16"]Edict of Ashoka[font="sans-16"] aura: Now affects Traders and Humans. +10% movement speed, +15 Health. Decreased radius from 75 to 65.
[icon="rome_ship_trireme_big" displace="0 18"][font="sans-bold-18"] Naval rebalance
[font="sans-16"]
- Fireships now behave like a melee ship that can also apply a burning status to its enemies. Add fuel to the fire ship to get it burning really hot which causes the ship to deal area damage upon its destruction. From full HP, the fire ship burns up in about 10 seconds. If the ship has less HP, it will blow up sooner.
- Footprints are smaller for slightly better pathing.
- All ships reduced population cost from 2 to 1, except for Siege ships.
[font="sans-bold-18"] Scout ships
[font="sans-16"]
Attack damage and range increased.
Faster acceleration movement speed.
[font="sans-bold-18"] Arrow ships
[font="sans-16"]
Attack damage and range increased.
Faster prepare attack time from 0.5 to 0.25 seconds.
Cost: from [icon="icon_wood"] 120 [icon="icon_metal"] 80 to [icon="icon_wood"] 120 [icon="icon_metal"] 100
Loot: from [icon="icon_wood"] 12 [icon="icon_metal"] 8 to [icon="icon_wood"] 12 [icon="icon_metal"] 10
[font="sans-bold-18"] Ram ships
[font="sans-16"]
Hack damage increased from 280 to 350
Crush damage increased from 20 to 40
Cost: from [icon="icon_food"] 50 [icon="icon_wood"] 100 [icon="icon_metal"] 50 to [icon="icon_food"] 50 [icon="icon_wood"] 100 [icon="icon_metal"] 25
[font="sans-bold-18"] Siege ships
[font="sans-16"]
Attack range from 2 to 4
Crush damage increased from 50 to 100
[icon="emblem_athen" displace="0 18"][font="sans-bold-18"] Athenians update
[font="sans-16"]
Added new phase 2 healing hero unit and two uniques technologies
Spearmen may promote to champions with 200 xp
- Units
[icon="athen_hero_hippocrates" displace="0 8"] [font="sans-bold-18"] Hippocrates
[font="sans-16"]
[font="sans-bold-16"]Officer Accommodation[font="sans-16"] aura: Garrisoned Heroes +6 health regeneration rate.
[font="sans-bold-16"]Regeneration[font="sans-16"] aura: Humans +0.5 health regeneration rate.
[font="sans-bold-16"]Medicine Father[font="sans-16"] aura: Own and Allied Healers +2 heal health.
- Technologies
[icon="tech_ostracism" displace="0 8"] [font="sans-bold-18"] Ostracism
[font="sans-16"]
Citizen soldiers +5% health, but Heroes −40% health.
Cost: [icon="icon_food"] 300 [icon="icon_metal"] 300. [icon="icon_time"]: 60 seconds. Phase: [icon="phase_3" displace="0 2"]
[icon="tech_pheidan_workshop" displace="0 8"] [font="sans-bold-18"] Pheidan Workshop
[font="sans-16"]
Temples and Wonder −50% stone cost and build time.
Cost: [icon="icon_stone"] 300. [icon="icon_time"]: 40 seconds. Phase: [icon="phase_2" displace="0 2"]
[icon="emblem_romans" displace="0 18"][font="sans-bold-18"] Romans update
[font="sans-16"]
Roman army camp and siege walls do not decay
Unlock Onager from the arsenal
- Civ bonus
[icon="civbonus_fertility" displace="0 8"][font="sans-bold-18"] Fertility
[font="sans-16"]
Women may be trained from houses without fertility festival.
[icon="civbonus_engineers" displace="0 8"][font="sans-bold-18"] Legionary Engineers
[font="sans-16"]
Siege Catapults and Onagers 10% faster fire rate and +20% movement speed.
- Units
[icon="rome_siege_onager" displace="0 8"][font="sans-bold-18"] Onagers
[font="sans-16"]
New unit good versus units. Not so good versus buildings.
Shorter range. 4 meters area splash damage.
- Technologies
[icon="tech_roman_roads" displace="0 8"][font="sans-bold-18"] Roman Roads
[font="sans-16"]
All Land Units +5% movement speed.
Cost: [icon="icon_stone"] 500. [icon="icon_time"]: 60 seconds. Phase: [icon="phase_2" displace="0 2"]
[icon="tech_marian_reforms" displace="0 8"][font="sans-bold-18"] Marian Reforms
[font="sans-16"]
Convert Citizen Swordsmen and Skirmishers to Marian Legionaries, and Citizen Spearmen to conscripts which cannot promote. Allows to train Onagers and Centurions, lose access to Italic Heavy Infantry.
Cost: [icon="icon_food"] 1200. [icon="icon_metal"] 1000. [icon="icon_time"]: 60 seconds. Phase: [icon="phase_3" displace="0 2"]
[icon="emblem_spartans" displace="0 18"][font="sans-bold-18"] Spartans adjustments
[font="sans-16"]
- Units
[icon="spart_neodamodes" displace="0 8"] [font="sans-bold-18"] Neodamodes
[font="sans-16"]
[icon="icon_time"]: from 10 to 12 seconds.
[icon="spart_spartiates" displace="0 8"] [font="sans-bold-18"] Champion Spartiates
[font="sans-16"]
Removed phase [icon="phase_1" displace="0 2"] and [icon="phase_2" displace="0 2"] train time penalty
- Buildings
[icon="spart_syssition" displace="0 8"] [font="sans-bold-18"] Syssition
[font="sans-16"]
Cost: from [icon="icon_stone"] 200 [icon="icon_metal"] 200 to [icon="icon_stone"] 150 [icon="icon_metal"] 150
- Technologies
[icon="tech_tyrtean-paeans" displace="0 8"][font="sans-bold-18"] Tyrtean Paeans
[font="sans-16"]
Now available at [icon="phase_1" displace="0 2"]
Cost: from [icon="icon_food"] 400 [icon="icon_metal"] 200 to [icon="icon_food"] 200 [icon="icon_metal"] 100. [icon="icon_time"]: from 50 to 30 seconds.
[icon="emblem_macedonians" displace="0 18"][font="sans-bold-18"] Macedonians
[font="sans-16"]
Arsenal moved to [icon="phase_2" displace="0 2"] and allow to train Bolt Shooters.
[icon="emblem_han" displace="0 18"][font="sans-bold-18"] Hans
[font="sans-16"]
[icon="han_crossbowman" displace="0 8"][font="sans-bold-18"] Crossbowmen
[font="sans-16"]
Pierce damage increased from 23 to 24.
Movement speed increased from 9.6 to 10.0
[font="sans-bold-20"]
#Version 10 (July 15, 2024)
[font="sans-16"] [font="sans-16"]
- Woodlines a little larger in badosu maps - Woodlines a little larger in badosu maps

View File

@ -33,22 +33,7 @@
sprite="stretched:session/portraits/emblems/emblem_carthaginians.png" sprite="stretched:session/portraits/emblems/emblem_carthaginians.png"
size="48 48" size="48 48"
/> />
<icon name="emblem_athen"
sprite="stretched:session/portraits/emblems/emblem_athenians.png"
size="48 48"
/>
<icon name="emblem_romans"
sprite="stretched:session/portraits/emblems/emblem_romans.png"
size="48 48"
/>
<icon name="emblem_macedonians"
sprite="stretched:session/portraits/emblems/emblem_macedonians.png"
size="48 48"
/>
<icon name="emblem_han"
sprite="stretched:session/portraits/emblems/emblem_han.png"
size="48 48"
/>
<!-- Phases --> <!-- Phases -->
@ -58,25 +43,17 @@
/> />
<icon name="phase_2" <icon name="phase_2"
sprite="stretched:session/portraits/technologies/town_phase.png" sprite="stretched:session/portraits/technologies/town_phase.png"
size="16 16" size="32 32"
/> />
<icon name="phase_3" <icon name="phase_3"
sprite="stretched:session/portraits/technologies/city_phase.png" sprite="stretched:session/portraits/technologies/city_phase.png"
size="16 16" size="32 32"
/> />
<!-- Civ Bonuses --> <!-- Civ Bonuses -->
<icon name="civbonus_agoge" <icon name="civbonus_agoge"
sprite="stretched:session/portraits/technologies/agoge.png" sprite="stretched:session/portraits/technologies/agoge.png"
size="32 32" size="32 32"
/> />
<icon name="civbonus_fertility"
sprite="stretched:session/portraits/technologies/wives_festival.png"
size="32 32"
/>
<icon name="civbonus_engineers"
sprite="stretched:session/portraits/technologies/engineering.png"
size="32 32"
/>
<!-- Structures --> <!-- Structures -->
@ -84,10 +61,7 @@
sprite="stretched:session/portraits/structures/temple_epic.png" sprite="stretched:session/portraits/structures/temple_epic.png"
size="32 32" size="32 32"
/> />
<icon name="maur_pillar_ashoka"
sprite="stretched:session/portraits/structures/ashoka_pillar.png"
size="32 32"
/>
<icon name="wall" <icon name="wall"
sprite="stretched:session/portraits/structures/wall.png" sprite="stretched:session/portraits/structures/wall.png"
size="32 32" size="32 32"
@ -108,7 +82,8 @@
sprite="stretched:session/portraits/technologies/anvil.png" sprite="stretched:session/portraits/technologies/anvil.png"
size="32 32" size="32 32"
/> />
<!-- Civ Bonuses --> <!-- Civ Bonuses -->
<icon name="civbonus_agoge" <icon name="civbonus_agoge"
@ -176,11 +151,6 @@
sprite="stretched:session/portraits/technologies/loom.png.cached.dds" sprite="stretched:session/portraits/technologies/loom.png.cached.dds"
size="32 32" size="32 32"
/> />
<icon name="tech_cartography"
sprite="stretched:session/portraits/technologies/cartography.png"
size="32 32"
/>
<icon name="tech_unlock-neodamodes" <icon name="tech_unlock-neodamodes"
sprite="stretched:session/portraits/technologies/helmet_corinthian_bronze_old.png" sprite="stretched:session/portraits/technologies/helmet_corinthian_bronze_old.png"
size="32 32" size="32 32"
@ -201,50 +171,12 @@
sprite="stretched:session/portraits/technologies/accuracy_bolt.png" sprite="stretched:session/portraits/technologies/accuracy_bolt.png"
size="32 32" size="32 32"
/> />
<icon name="tech_ostracism"
sprite="stretched:session/portraits/technologies/patriotism.png"
size="32 32"
/>
<icon name="tech_pheidan_workshop"
sprite="stretched:session/portraits/technologies/vestals.png"
size="32 32"
/>
<icon name="tech_roman_roads"
sprite="stretched:session/portraits/technologies/masonry_marble.png"
size="32 32"
/>
<icon name="tech_marian_reforms"
sprite="stretched:session/portraits/technologies/shield_scutum.png"
size="32 32"
/>
<!-- Units --> <!-- Units -->
<icon name="han_hero_wei_qing"
sprite="stretched:session/portraits/units/han/hero_wei_qing.png"
size="32 32"
/>
<icon name="han_crossbowman"
sprite="stretched:session/portraits/units/han/infantry_crossbowman.png"
size="32 32"
/>
<icon name="kush_hero_arakamani"
sprite="stretched:session/portraits/units/kush_hero_arakamani.png"
size="32 32"
/>
<icon name="kush_hero_nastasen"
sprite="stretched:session/portraits/units/kush_hero_nastasen.png"
size="32 32"
/>
<icon name="pers_hero_xerxes"
sprite="stretched:session/portraits/units/pers_hero_xerxes.png"
size="32 32"
/>
<icon name="pers_immortal_inf" <icon name="pers_immortal_inf"
sprite="stretched:session/portraits/units/pers_champion_infantry.png" sprite="stretched:session/portraits/units/pers_champion_infantry.png"
size="32 32" size="32 32"
@ -253,7 +185,10 @@
sprite="stretched:session/portraits/units/pers_champion_infantry_archer.png" sprite="stretched:session/portraits/units/pers_champion_infantry_archer.png"
size="32 32" size="32 32"
/> />
<icon name="pers_ship_bireme"
sprite="stretched:session/portraits/units/pers_ship_bireme.png"
size="32 32"
/>
<icon name="rome_siege_ballista" <icon name="rome_siege_ballista"
sprite="stretched:session/portraits/units/rome_siege_ballista.png" sprite="stretched:session/portraits/units/rome_siege_ballista.png"
size="32 32" size="32 32"
@ -262,15 +197,6 @@
sprite="stretched:session/portraits/units/rome_siege_scorpio.png" sprite="stretched:session/portraits/units/rome_siege_scorpio.png"
size="32 32" size="32 32"
/> />
<icon name="rome_siege_ram.png"
sprite="stretched:session/portraits/units/rome_siege_ram.png"
size="32 32"
/>
<icon name="rome_siege_onager"
sprite="stretched:session/portraits/units/rome_siege_onager.png"
size="32 32"
/>
<icon name="spart_hero_agis" <icon name="spart_hero_agis"
sprite="stretched:session/portraits/units/spart_hero_agis.png" sprite="stretched:session/portraits/units/spart_hero_agis.png"
size="32 32" size="32 32"
@ -279,58 +205,11 @@
sprite="stretched:session/portraits/units/spart/infantry_spearman_neodamodes.png" sprite="stretched:session/portraits/units/spart/infantry_spearman_neodamodes.png"
size="32 32" size="32 32"
/> />
<icon name="spart_spartiates"
sprite="stretched:session/portraits/units/spart_champion_infantry_spear.png"
size="32 32"
/>
<icon name="elephants" <icon name="elephants"
sprite="stretched:session/portraits/structures/stable_elephant.png" sprite="stretched:session/portraits/structures/stable_elephant.png"
size="32 32" size="32 32"
/> />
<icon name="athen_hero_hippocrates"
sprite="stretched:session/portraits/units/athen/hero_hippocrates.png"
size="32 32"
/>
<icon name="mace_bolts"
sprite="stretched:session/portraits/units/rome_siege_scorpio_packed.png"
size="32 32"
/>
<!-- Ships -->
<icon name="pers_ship_bireme"
sprite="stretched:session/portraits/units/pers_ship_bireme.png"
size="32 32"
/>
<icon name="rome_ship_bireme"
sprite="stretched:session/portraits/units/rome_ship_bireme.png"
size="32 32"
/>
<icon name="rome_ship_trireme"
sprite="stretched:session/portraits/units/rome_ship_trireme.png"
size="32 32"
/>
<icon name="rome_ship_trireme_big"
sprite="stretched:session/portraits/units/rome_ship_trireme.png"
size="48 48"
/>
<!-- Auras -->
<icon name="aura_buildgather_bonus"
sprite="stretched:session/auras/buildgather_bonus.png"
size="16 16"
/>
<icon name="aura_build_bonus"
sprite="stretched:session/auras/build_bonus.png"
size="16 16"
/>
<icon name="aura_health_bonus"
sprite="stretched:session/auras/health_bonus.png"
size="16 16"
/>
<!-- Old Icons --> <!-- Old Icons -->

View File

@ -1,6 +1,6 @@
{ {
"name": "community-mod", "name": "community-mod",
"version": "0.26.12", "version": "0.26.11",
"label": "0 A.D. Community Mod", "label": "0 A.D. Community Mod",
"url": "https://gitlab.com/0ad/0ad-community-mod-a26", "url": "https://gitlab.com/0ad/0ad-community-mod-a26",
"description": "The Community Mod is a community-led effort to improve the gameplay of 0 A.D., officially backed by the 0 A.D. team.", "description": "The Community Mod is a community-led effort to improve the gameplay of 0 A.D., officially backed by the 0 A.D. team.",

View File

@ -81,8 +81,7 @@ BuildRestrictions.prototype.Init = function()
* (template name should be "preview|"+templateName), as otherwise territory * (template name should be "preview|"+templateName), as otherwise territory
* checks for buildings with territory influence will not work as expected. * checks for buildings with territory influence will not work as expected.
*/ */
BuildRestrictions.prototype.CheckPlacement = function() BuildRestrictions.prototype.CheckPlacement = function () {
{
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building"; var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building";
@ -101,8 +100,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; // Fail return result; // Fail
// TODO: AI has no visibility info // TODO: AI has no visibility info
if (!cmpPlayer.IsAI()) if (!cmpPlayer.IsAI()) {
{
// Check whether it's in a visible or fogged region // Check whether it's in a visible or fogged region
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
@ -110,8 +108,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; // Fail return result; // Fail
var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden"); var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden");
if (!explored) if (!explored) {
{
result.message = markForTranslation("%(name)s cannot be built in unexplored area"); result.message = markForTranslation("%(name)s cannot be built in unexplored area");
return result; // Fail return result; // Fail
} }
@ -119,21 +116,20 @@ BuildRestrictions.prototype.CheckPlacement = function()
// Check obstructions and terrain passability // Check obstructions and terrain passability
var passClassName = ""; var passClassName = "";
switch (this.template.PlacementType) switch (this.template.PlacementType) {
{ case "shore":
case "shore": passClassName = "building-shore";
passClassName = "building-shore"; break;
break;
case "land-shore": case "land-shore":
// 'default-terrain-only' is everywhere a normal unit can go, ignoring // 'default-terrain-only' is everywhere a normal unit can go, ignoring
// obstructions (i.e. on passable land, and not too deep in the water) // obstructions (i.e. on passable land, and not too deep in the water)
passClassName = "default-terrain-only"; passClassName = "default-terrain-only";
break; break;
case "land": case "land":
default: default:
passClassName = "building-land"; passClassName = "building-land";
} }
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
@ -141,30 +137,38 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; // Fail return result; // Fail
if (this.template.Category == "Wall") if (this.template.Category == "Wall") {
{ // allow walls to be built on top of trees
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
let areWoodEnts = collisions.length > 0;
for (let ent of collisions) {
// don't let entities with unitmotion prevent us from laying a foundation
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
let cmpResource = Engine.QueryInterface(ent, IID_ResourceSupply);
if ((!cmpResource || cmpResource.GetType().generic !== "wood") && !cmpUnitMotion) {
areWoodEnts = false;
break;
}
}
// for walls, only test the center point // for walls, only test the center point
var ret = cmpObstruction.CheckFoundation(passClassName, true); var ret = areWoodEnts ? "success" : cmpObstruction.CheckFoundation(passClassName, true);
} }
else else {
{
var ret = cmpObstruction.CheckFoundation(passClassName, false); var ret = cmpObstruction.CheckFoundation(passClassName, false);
} }
if (ret != "success") if (ret != "success") {
{ switch (ret) {
switch (ret) case "fail_error":
{ case "fail_no_obstruction":
case "fail_error": error("CheckPlacement: Error returned from CheckFoundation");
case "fail_no_obstruction": break;
error("CheckPlacement: Error returned from CheckFoundation"); case "fail_obstructs_foundation":
break; result.message = markForTranslation("%(name)s cannot be built on another building or resource");
case "fail_obstructs_foundation": break;
result.message = markForTranslation("%(name)s cannot be built on another building or resource"); case "fail_terrain_class":
break; // TODO: be more specific and/or list valid terrain?
case "fail_terrain_class": result.message = markForTranslation("%(name)s cannot be built on invalid terrain");
// TODO: be more specific and/or list valid terrain?
result.message = markForTranslation("%(name)s cannot be built on invalid terrain");
} }
return result; // Fail return result; // Fail
} }
@ -183,8 +187,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
var isNeutral = tileOwner == 0; var isNeutral = tileOwner == 0;
var invalidTerritory = ""; var invalidTerritory = "";
if (isOwn) if (isOwn) {
{
if (!this.HasTerritory("own")) if (!this.HasTerritory("own"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "own"); invalidTerritory = markForTranslationWithContext("Territory type", "own");
@ -192,8 +195,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own"); invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own");
} }
else if (isMutualAlly) else if (isMutualAlly) {
{
if (!this.HasTerritory("ally")) if (!this.HasTerritory("ally"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "allied"); invalidTerritory = markForTranslationWithContext("Territory type", "allied");
@ -201,22 +203,19 @@ BuildRestrictions.prototype.CheckPlacement = function()
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied"); invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied");
} }
else if (isNeutral) else if (isNeutral) {
{
if (!this.HasTerritory("neutral")) if (!this.HasTerritory("neutral"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "neutral"); invalidTerritory = markForTranslationWithContext("Territory type", "neutral");
} }
else else {
{
// consider everything else enemy territory // consider everything else enemy territory
if (!this.HasTerritory("enemy")) if (!this.HasTerritory("enemy"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "enemy"); invalidTerritory = markForTranslationWithContext("Territory type", "enemy");
} }
if (invalidTerritory) if (invalidTerritory) {
{
result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s"); result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s");
result.translateParameters.push("territoryType"); result.translateParameters.push("territoryType");
result.translateParameters.push("validTerritories"); result.translateParameters.push("validTerritories");
@ -227,10 +226,8 @@ BuildRestrictions.prototype.CheckPlacement = function()
} }
// Check special requirements // Check special requirements
if (this.template.PlacementType == "shore") if (this.template.PlacementType == "shore") {
{ if (!cmpObstruction.CheckShorePlacement()) {
if (!cmpObstruction.CheckShorePlacement())
{
result.message = markForTranslation("%(name)s must be built on a valid shoreline"); result.message = markForTranslation("%(name)s must be built on a valid shoreline");
return result; // Fail return result; // Fail
} }
@ -242,22 +239,18 @@ BuildRestrictions.prototype.CheckPlacement = function()
let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName)); let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName));
// Check distance restriction // Check distance restriction
if (this.template.Distance) if (this.template.Distance) {
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cat = this.template.Distance.FromClass; var cat = this.template.Distance.FromClass;
var filter = function(id) var filter = function (id) {
{
var cmpIdentity = Engine.QueryInterface(id, IID_Identity); var cmpIdentity = Engine.QueryInterface(id, IID_Identity);
return cmpIdentity.GetClassesList().indexOf(cat) > -1; return cmpIdentity.GetClassesList().indexOf(cat) > -1;
}; };
if (this.template.Distance.MinDistance !== undefined) if (this.template.Distance.MinDistance !== undefined) {
{
let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template); let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template);
if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) {
{
let result = markForPluralTranslation( let result = markForPluralTranslation(
"%(name)s too close to a %(category)s, must be at least %(distance)s meter away", "%(name)s too close to a %(category)s, must be at least %(distance)s meter away",
"%(name)s too close to a %(category)s, must be at least %(distance)s meters away", "%(name)s too close to a %(category)s, must be at least %(distance)s meters away",
@ -274,11 +267,9 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; // Fail return result; // Fail
} }
} }
if (this.template.Distance.MaxDistance !== undefined) if (this.template.Distance.MaxDistance !== undefined) {
{
let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template); let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template);
if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) {
{
let result = markForPluralTranslation( let result = markForPluralTranslation(
"%(name)s too far from a %(category)s, must be within %(distance)s meter", "%(name)s too far from a %(category)s, must be within %(distance)s meter",
"%(name)s too far from a %(category)s, must be within %(distance)s meters", "%(name)s too far from a %(category)s, must be within %(distance)s meters",
@ -303,6 +294,11 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; return result;
}; };
BuildRestrictions.prototype.IsWoodEntity = function (entity) {
let cmpResource = Engine.QueryInterface(entity, IID_ResourceSupply);
return cmpResource && cmpResource.GetType().generic === "wood";
}
BuildRestrictions.prototype.GetCategory = function() BuildRestrictions.prototype.GetCategory = function()
{ {
return this.template.Category; return this.template.Category;

View File

@ -5,8 +5,7 @@ Foundation.prototype.Schema =
"<ref name='nonNegativeDecimal'/>" + "<ref name='nonNegativeDecimal'/>" +
"</element>"; "</element>";
Foundation.prototype.Init = function() Foundation.prototype.Init = function () {
{
// Foundations are initially 'uncommitted' and do not block unit movement at all // Foundations are initially 'uncommitted' and do not block unit movement at all
// (to prevent players exploiting free foundations to confuse enemy units). // (to prevent players exploiting free foundations to confuse enemy units).
// The first builder to reach the uncommitted foundation will tell friendly units // The first builder to reach the uncommitted foundation will tell friendly units
@ -21,6 +20,7 @@ Foundation.prototype.Init = function()
this.buildTimeModifier = +this.template.BuildTimeModifier; this.buildTimeModifier = +this.template.BuildTimeModifier;
this.previewEntity = INVALID_ENTITY; this.previewEntity = INVALID_ENTITY;
this.entsToDestroy;
}; };
Foundation.prototype.Serialize = function() Foundation.prototype.Serialize = function()
@ -101,9 +101,12 @@ Foundation.prototype.GetNumBuilders = function()
return this.builders.size; return this.builders.size;
}; };
Foundation.prototype.IsFinished = function() Foundation.prototype.IsFinished = function () {
{ if (this.GetBuildProgress() == 1.0 && this.entsToDestroy)
return (this.GetBuildProgress() == 1.0); for (let ent of this.entsToDestroy)
Engine.DestroyEntity(ent);
return this.GetBuildProgress() == 1.0;
}; };
Foundation.prototype.OnOwnershipChanged = function(msg) Foundation.prototype.OnOwnershipChanged = function(msg)
@ -246,22 +249,18 @@ Foundation.prototype.GetBuildTime = function()
/** /**
* @return {boolean} - Whether the foundation has been committed sucessfully. * @return {boolean} - Whether the foundation has been committed sucessfully.
*/ */
Foundation.prototype.Commit = function() Foundation.prototype.Commit = function () {
{
if (this.committed) if (this.committed)
return false; return false;
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true)) if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true)) {
{ this.entsToDestroy = cmpObstruction.GetEntitiesDeletedUponConstruction();
for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
Engine.DestroyEntity(ent);
let collisions = cmpObstruction.GetEntitiesBlockingConstruction(); let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
if (collisions.length) if (collisions.length) {
{ for (let ent of collisions) {
for (let ent of collisions)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI) if (cmpUnitAI)
cmpUnitAI.LeaveFoundation(this.entity); cmpUnitAI.LeaveFoundation(this.entity);
@ -273,10 +272,10 @@ Foundation.prototype.Commit = function()
// animation to indicate they're waiting for people to get // animation to indicate they're waiting for people to get
// out the way // out the way
return false;
} }
} }
// The obstruction always blocks new foundations/construction, // The obstruction always blocks new foundations/construction,
// but we've temporarily allowed units to walk all over it // but we've temporarily allowed units to walk all over it
// (via CCmpTemplateManager). Now we need to remove that temporary // (via CCmpTemplateManager). Now we need to remove that temporary

View File

@ -1,12 +0,0 @@
{
"type": "garrisonedUnits",
"affects": [ "Hero" ],
"modifications": [
{
"value": "Health/RegenRate",
"add": 6
}
],
"auraName": "Officer Accommodation",
"auraDescription": "Garrisoned Heroes +6 health regeneration rate."
}

View File

@ -1,14 +0,0 @@
{
"type": "range",
"radius": 35,
"affects": [ "Human" ],
"modifications": [
{
"value": "Health/RegenRate",
"add": 0.5
}
],
"auraName": "Regeneration",
"auraDescription": "Humans +0.5 health regeneration rate.",
"overlayIcon": "art/textures/ui/session/auras/heal.png"
}

View File

@ -1,15 +0,0 @@
{
"type": "range",
"radius": 35,
"affects": [ "Healer" ],
"affectedPlayers": [ "MutualAlly" ],
"modifications": [
{
"value": "Heal/Health",
"add": 2.0
}
],
"auraName": "Medicine Father",
"auraDescription": "Own and Allied Healers +2 heal health.",
"overlayIcon": "art/textures/ui/session/auras/heal.png"
}

View File

@ -1,31 +0,0 @@
{
"genericName": "Ostracism",
"description": "In ancient Athens, ostracism was the process by which any citizen, including political leaders, could be expelled from the city-state for 10 years.",
"cost": {
"food": 300,
"metal": 300
},
"requirements": {
"all": [
{ "tech": "phase_city" },
{ "civ": "athen" }
]
},
"requirementsTooltip": "Unlocked in City Phase.",
"icon": "patriotism.png",
"researchTime": 60,
"tooltip": "Citizen soldiers +5% health, but Heroes −40% health.",
"modifications": [
{
"value": "Health/Max",
"multiply": 1.05,
"affects": "CitizenSoldier"
},
{
"value": "Health/Max",
"multiply": 0.6,
"affects": "Hero"
}
],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View File

@ -1,29 +0,0 @@
{
"genericName": "Pheidian Workshop",
"description": "Pheidias was a Greek sculptor, painter and architect, who lived in the 5th century BC, and is commonly regarded as one of the greatest of all sculptors of Classical Greece: Phidias' Statue of Zeus at Olympia was one of the Seven Wonders of the Ancient World. Phidias designed the statues of the goddess Athena on the Athenian Acropolis, namely the Athena Parthenos inside the Parthenon and the Athena Promachos, a colossal bronze statue of Athena which stood between it and the Propylaea, a monumental gateway that served as the entrance to the Acropolis in Athens.",
"cost": {
"stone": 300
},
"requirements": {
"all": [
{ "tech": "phase_town" },
{ "civ": "athen" }
]
},
"requirementsTooltip": "Unlocked in Town Phase.",
"icon": "vestals.png",
"researchTime": 40,
"tooltip": "Temples and Wonder −50% stone cost and build time.",
"modifications": [
{
"value": "Cost/BuildTime",
"multiply": 0.5
},
{
"value": "Cost/Resources/stone",
"multiply": 0.5
}
],
"affects": [ "Temple", "Wonder" ],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View File

@ -11,8 +11,7 @@
{ "notciv": "brit" }, { "notciv": "brit" },
{ "notciv": "han" }, { "notciv": "han" },
{ "notciv": "maur" }, { "notciv": "maur" },
{ "notciv": "spart" }, { "notciv": "spart" }
{ "notciv": "cart" }
] ]
}, },
"requirementsTooltip": "Unlocked in City Phase.", "requirementsTooltip": "Unlocked in City Phase.",

View File

@ -2,7 +2,7 @@
<Entity> <Entity>
<DeathDamage> <DeathDamage>
<Shape>Circular</Shape> <Shape>Circular</Shape>
<Range>22</Range> <Range>25</Range>
<FriendlyFire>false</FriendlyFire> <FriendlyFire>false</FriendlyFire>
<Bonuses> <Bonuses>
<BonusShip> <BonusShip>

View File

@ -37,7 +37,6 @@
<Researcher> <Researcher>
<Technologies datatype="tokens"> <Technologies datatype="tokens">
long_walls long_walls
ostracism
</Technologies> </Technologies>
</Researcher> </Researcher>
<Sound> <Sound>

View File

@ -11,11 +11,6 @@
<Obstruction> <Obstruction>
<Static width="17.5" depth="27"/> <Static width="17.5" depth="27"/>
</Obstruction> </Obstruction>
<Trainer>
<Entities datatype="tokens">
units/{civ}/hero_hippocrates
</Entities>
</Trainer>
<VisualActor> <VisualActor>
<Actor>structures/athenians/temple.xml</Actor> <Actor>structures/athenians/temple.xml</Actor>
<FoundationActor>props/special/eyecandy/greek_temple_unfinished.xml</FoundationActor> <FoundationActor>props/special/eyecandy/greek_temple_unfinished.xml</FoundationActor>

View File

@ -106,7 +106,6 @@
hellenistic_metropolis hellenistic_metropolis
apophora apophora
roman_roads roman_roads
pheidian_workshop
unlock_spies unlock_spies
spy_counter spy_counter
</Technologies> </Technologies>

View File

@ -32,8 +32,7 @@
<BuildTime>18</BuildTime> <BuildTime>18</BuildTime>
<Resources> <Resources>
<wood>175</wood> <wood>175</wood>
<food>50</food> <food>75</food>
<metal>50</metal>
</Resources> </Resources>
</Cost> </Cost>
<Footprint> <Footprint>
@ -42,7 +41,7 @@
</Footprint> </Footprint>
<GarrisonHolder disable=""/> <GarrisonHolder disable=""/>
<Health> <Health>
<Max>800</Max> <Max>1000</Max>
<DamageVariants> <DamageVariants>
<lightdamage>1.0</lightdamage> <lightdamage>1.0</lightdamage>
</DamageVariants> </DamageVariants>
@ -56,7 +55,7 @@
<Resistance> <Resistance>
<Entity> <Entity>
<Damage> <Damage>
<Pierce>1</Pierce> <Pierce>2</Pierce>
</Damage> </Damage>
</Entity> </Entity>
</Resistance> </Resistance>

View File

@ -49,7 +49,7 @@
<Entity> <Entity>
<Damage> <Damage>
<Hack>7</Hack> <Hack>7</Hack>
<Pierce>35</Pierce> <Pierce>30</Pierce>
</Damage> </Damage>
</Entity> </Entity>
</Resistance> </Resistance>

View File

@ -41,6 +41,11 @@
<Height>5.0</Height> <Height>5.0</Height>
</Footprint> </Footprint>
<Resistance> <Resistance>
<Entity>
<Damage>
<Pierce op="add">3</Pierce>
</Damage>
</Entity>
<Foundation> <Foundation>
<Damage> <Damage>
<Hack>1</Hack> <Hack>1</Hack>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit_hero_healer">
<Auras datatype="tokens">
units/heroes/athen_hero_hippocrates_1
units/heroes/athen_hero_hippocrates_2
</Auras>
<Health>
<Max>600</Max>
</Health>
<Identity>
<Civ>athen</Civ>
<GenericName>Hippocrates</GenericName>
<SpecificName>Hippocrates</SpecificName>
<Icon>units/athen/hero_hippocrates.png</Icon>
<RequiredTechnology>phase_town</RequiredTechnology>
</Identity>
<VisualActor>
<Actor>units/athenians/hero_healer_hippocrates.xml</Actor>
</VisualActor>
</Entity>

View File

@ -3,10 +3,7 @@
<Identity> <Identity>
<Rank>Elite</Rank> <Rank>Elite</Rank>
</Identity> </Identity>
<Promotion> <Promotion disable=""/>
<RequiredXp>400</RequiredXp>
<Entity>units/athen/champion_infantry</Entity>
</Promotion>
<VisualActor> <VisualActor>
<Actor>units/athenians/infantry_spearman_e.xml</Actor> <Actor>units/athenians/infantry_spearman_e.xml</Actor>
</VisualActor> </VisualActor>

View File

@ -5,7 +5,6 @@
<GenericName>Sacred Band Cavalry</GenericName> <GenericName>Sacred Band Cavalry</GenericName>
<SpecificName>Sacred Band of Astarte</SpecificName> <SpecificName>Sacred Band of Astarte</SpecificName>
<Icon>units/cart_champion_cavalry.png</Icon> <Icon>units/cart_champion_cavalry.png</Icon>
<RequiredTechnology>phase_city</RequiredTechnology>
</Identity> </Identity>
<VisualActor> <VisualActor>
<Actor>units/carthaginians/cavalry_spearman_c_m.xml</Actor> <Actor>units/carthaginians/cavalry_spearman_c_m.xml</Actor>

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit_hero_infantry_archer"> <Entity parent="template_unit_hero_infantry_archer">
<Auras datatype="tokens"> <Auras datatype="tokens">
units/heroes/pers_hero_xerxes_i_1 units/heroes/pers_hero_xerxes_i
units/heroes/pers_hero_xerxes_i_2
</Auras> </Auras>
<Identity> <Identity>
<Civ>pers</Civ> <Civ>pers</Civ>

View File

@ -1,56 +0,0 @@
line-length = 99
[format]
line-ending = "lf"
[lint]
select = ["ALL"]
ignore = [
"ANN",
"BLE001",
"C90",
"COM812",
"D10",
"DTZ005",
"EM",
"FA",
"FIX",
"FBT",
"ISC001",
"N817",
"PERF203",
"PERF401",
"PLR0912",
"PLR0913",
"PLR0915",
"PLR2004",
"PLW2901",
"PT",
"PTH",
"RUF012",
"S101",
"S310",
"S314",
"S324",
"S320",
"S603",
"S607",
"T20",
"TD002",
"TD003",
"TRY002",
"TRY003",
"TRY004",
"TRY301",
"UP038",
"W505"
]
[lint.isort]
lines-after-imports = 2
[lint.pycodestyle]
max-doc-length = 72
[lint.pydocstyle]
convention = "pep257"

View File

@ -1,17 +1,16 @@
import json import json
from pathlib import Path from pathlib import Path
MOD_PATH = Path("community-mod") MOD_PATH = Path("community-mod")
def check_cwd_is_correct(): def check_cwd_is_correct():
try: cwd = Path().resolve()
if not Path(".git").exists(): try:
raise Exception("No .git in current folder.") if not Path(".git").exists():
with open(MOD_PATH / "mod.json") as f: raise Exception("No .git in current folder.")
mod = json.load(f) with open(MOD_PATH / "mod.json", "r") as f:
if mod["name"] != "community-mod": mod = json.load(f)
raise Exception("mod.json has incorrect name") if mod['name'] != "community-mod":
except Exception as e: raise Exception("mod.json has incorrect name")
raise Exception("scripts.py must be called from the community mod repository") from e except:
raise Exception("scripts.py must be called from the community mod repository")

View File

@ -1,42 +1,42 @@
import argparse import argparse
import json
import shutil import shutil
from pathlib import Path from pathlib import Path
from . import MOD_PATH, check_cwd_is_correct from . import MOD_PATH, check_cwd_is_correct
PUBLIC_PATH = Path("binaries/data/mods/public/") PUBLIC_PATH = Path("binaries/data/mods/public/")
DEFAULT_COPY = ["simulation/data", "simulation/templates"] DEFAULT_COPY = [
"simulation/data",
"simulation/templates"
]
def validate_path(path: str): def validate_path(path: str):
mod_path = Path(path) / PUBLIC_PATH mod_path = Path(path) / PUBLIC_PATH
try: try:
with open(mod_path / "mod.json", encoding="utf-8") as f: with open(mod_path / "mod.json", "r") as f:
mod = json.load(f) mod = json.load(f)
if mod["name"] != "0ad": if mod['name'] != "0ad":
raise Exception("mod.json has incorrect name") raise Exception("mod.json has incorrect name")
except Exception as e: except:
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.") from e raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.")
return mod_path return mod_path
def copy_0ad_files(path_0ad: Path, to_copy=DEFAULT_COPY): def copy_0ad_files(path_0ad: Path, to_copy = DEFAULT_COPY):
for path in to_copy: for path in to_copy:
shutil.copytree(path_0ad / path, MOD_PATH / path, dirs_exist_ok=False) shutil.copytree(path_0ad / path, MOD_PATH / path, dirs_exist_ok=False)
if __name__ == "__main__": if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Copy files from 0 A.D. to the community mod.") parser = argparse.ArgumentParser(description='Copy files from 0 A.D. to the community mod.')
parser.add_argument("-0ad", help="Path to the 0 A.D. folder") parser.add_argument('-0ad', help='Path to the 0 A.D. folder')
parser.add_argument("-p", "--path", nargs="*", help="Optionally, a list of paths to copy.") parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of paths to copy.')
args = parser.parse_args() args = parser.parse_args()
check_cwd_is_correct() check_cwd_is_correct()
path = validate_path(getattr(args, "0ad")) path = validate_path(getattr(args, '0ad'))
copy_0ad_files(path, args.folder or DEFAULT_COPY) copy_0ad_files(path, args.folder or DEFAULT_COPY)
else: else:
raise Exception("Must be called directly") raise Exception("Must be called directly")

View File

@ -1,13 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import logging from os import chdir
from pathlib import Path from pathlib import Path
from subprocess import CalledProcessError, run from subprocess import run, CalledProcessError
from sys import exit from sys import exit
from xml.etree import ElementTree as ET from xml.etree import ElementTree
from .scriptlib import SimulTemplateEntity, find_files from .scriptlib import SimulTemplateEntity, find_files
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,54 +15,37 @@ relaxng_schema = root / "scripts" / "entity.rng"
vfs_root = root vfs_root = root
mod = "community-mod" mod = "community-mod"
def main(): def main():
if not relaxng_schema.exists(): if not relaxng_schema.exists():
print(f"""Relax NG schema non existant. print(f"""Relax NG schema non existant.
Please create the file {relaxng_schema.relative_to(root)} Please create the file {relaxng_schema.relative_to(root)}
You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""") You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""")
exit(1) exit(1)
if run(["xmllint", "--version"], capture_output=True, check=False).returncode != 0: if run(['xmllint', '--version'], capture_output=True).returncode != 0:
print("xmllint not found in your PATH, please install it (usually in libxml2 package)") print("xmllint not found in your PATH, please install it (usually in libxml2 package)")
exit(2) exit(2)
parser = argparse.ArgumentParser(description="Validate templates") parser = argparse.ArgumentParser(description='Validate templates')
parser.add_argument( parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of templates to validate.')
"-p", "--path", nargs="*", help="Optionally, a list of templates to validate."
)
args = parser.parse_args() args = parser.parse_args()
simul_templates_path = Path("simulation/templates") simul_templates_path = Path('simulation/templates')
simul_template_entity = SimulTemplateEntity(vfs_root, logger) simul_template_entity = SimulTemplateEntity(vfs_root, logger)
count = 0 count = 0
failed = 0 failed = 0
templates = sorted([(Path(p), None) for p in args.path]) if args.path else sorted(find_files(vfs_root, [mod], 'simulation/templates', 'xml'))
templates = []
if args.path is not None:
templates = sorted([(Path(p), None) for p in args.path])
else:
templates = sorted(find_files(vfs_root, [mod], "simulation/templates", "xml"))
for fp, _ in templates: for fp, _ in templates:
if fp.stem.startswith("template_"): if fp.stem.startswith('template_'):
continue continue
path = fp.as_posix() path = fp.as_posix()
if path.startswith(("simulation/templates/mixins/", "simulation/templates/special/")): if path.startswith('simulation/templates/mixins/') or path.startswith("simulation/templates/special/"):
continue continue
print(f"# {fp}...") print(f"# {fp}...")
count += 1 count += 1
entity = simul_template_entity.load_inherited( entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod])
simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod] xmlcontent = ElementTree.tostring(entity, encoding='unicode')
)
xmlcontent = ET.tostring(entity, encoding="unicode")
try: try:
run( run(['xmllint', '--relaxng', str(relaxng_schema.resolve()), '-'], input=xmlcontent, capture_output=True, text=True, check=True)
["xmllint", "--relaxng", str(relaxng_schema.resolve()), "-"],
input=xmlcontent,
capture_output=True,
text=True,
check=True,
)
except CalledProcessError as e: except CalledProcessError as e:
failed += 1 failed += 1
print(e.stderr) print(e.stderr)
@ -71,5 +53,5 @@ You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""
print(f"\nTotal: {count}; failed: {failed}") print(f"\nTotal: {count}; failed: {failed}")
if __name__ == "__main__": if __name__ == '__main__':
main() main()

View File

@ -1,16 +1,13 @@
import json import json
from . import MOD_PATH, check_cwd_is_correct from . import MOD_PATH, check_cwd_is_correct
def get_version(): def get_version():
with open(MOD_PATH / "mod.json") as f: with open(MOD_PATH / "mod.json", "r") as f:
mod = json.load(f) mod = json.load(f)
print(mod["version"]) print(mod['version'])
if __name__ == '__main__':
if __name__ == "__main__": check_cwd_is_correct()
check_cwd_is_correct() get_version()
get_version()
else: else:
raise Exception("Must be called directly") raise Exception("Must be called directly")

14
scripts/make_pyromod.py Normal file
View File

@ -0,0 +1,14 @@
import shutil
from pathlib import Path
from . import MOD_PATH, check_cwd_is_correct
def make_pyromod():
shutil.make_archive(MOD_PATH, 'zip', MOD_PATH)
if __name__ == '__main__':
check_cwd_is_correct()
make_pyromod()
else:
raise Exception("Must be called directly")

View File

@ -1,127 +1,44 @@
import json import json
import os import os
import subprocess
import requests import requests
from . import MOD_PATH from . import MOD_PATH
api_key = os.getenv('MODIO_API_KEY')
def run_git_command(command):
"""Run a git command and return its output."""
result = subprocess.run(command, capture_output=True, text=True, check=False)
if result.returncode != 0:
raise Exception(f"Git command failed: {result.stderr}")
return result.stdout.strip()
def get_current_commit():
"""Get the current commit hash."""
return run_git_command(["git", "rev-parse", "HEAD"])
def get_commit_for_tag(tag):
"""Get the commit hash for a tag, or return None if the tag doesn't exist."""
try:
return run_git_command(["git", "rev-list", "-n", "1", tag])
except Exception as e:
print(f"Failed to get commit for tag {tag}: {e}")
return None
def get_previous_tag_in_series(tag):
"""Get the previous tag in the same minor version series (e.g., 0.26.X-1)."""
try:
version_array = tag.split(".")
# Extract the major.minor part of the tag (e.g., '0.26' from '0.26.11')
major_minor_version = ".".join(version_array[:2])
# List all tags, reverse sorted by version number
tags = run_git_command(["git", "tag", "--sort=-v:refname"]).splitlines()
# Filter tags that match the major.minor version
filtered_tags = [t for t in tags if t.startswith(major_minor_version)]
if tag in filtered_tags:
current_index = filtered_tags.index(tag)
if current_index + 1 < len(filtered_tags):
return filtered_tags[current_index + 1]
version_array[2] = str(int(version_array[2]) - 1)
manual_tag = ".".join(version_array)
if manual_tag not in filtered_tags:
return None
current_index = filtered_tags.index(manual_tag)
return filtered_tags[current_index]
except Exception as e:
raise Exception(f"Failed to get previous tag in series: {e}") from e
def get_changelog(current_commit, previous_tag):
"""Get the changelog between the current commit and the previous tag."""
try:
if previous_tag is not None:
return run_git_command(
["git", "log", f"{previous_tag}..{current_commit}", "--oneline"]
)
# No previous tag, so get the log from the start of the repository
return run_git_command(["git", "log", current_commit, "--oneline"])
except Exception as e:
raise Exception(f"Failed to generate changelog: {e}") from e
api_key = os.getenv("MODIO_API_KEY")
# This must be generated manually from the website's interface. # This must be generated manually from the website's interface.
oauth2_token = os.getenv("MODIO_OAUTH2_TOKEN") oauth2_token = os.getenv("MODIO_OAUTH2_TOKEN")
mod_file_path = os.getenv("MOD_FILE_PATH") mod_file_path = os.getenv("MOD_FILE_PATH")
mod_version = os.getenv("MOD_VERSION") mod_version = os.getenv("MOD_VERSION")
mod_name = os.getenv("MOD_NAME")
community_mod_id = 2144305 community_mod_id = 2144305
zeroad_id = 5 zeroad_id = 5
mod_json = None headers = {
with open(MOD_PATH / "mod.json") as f: 'Authorization': f'Bearer {oauth2_token}',
mod_json = json.load(f) 'Accept': 'application/json'
}
tag = mod_json["version"] r = requests.get(f'https://api.mod.io/v1/me/mods', params={
commit = get_commit_for_tag(tag) 'api_key': api_key
if not commit: }, headers = headers)
print(f"Tag {tag} does not exist. Using current commit as fallback.")
commit = get_current_commit()
previous_tag = get_previous_tag_in_series(tag)
changelog = get_changelog(commit, previous_tag)
print(changelog)
headers = {"Authorization": f"Bearer {oauth2_token}", "Accept": "application/json"}
r = requests.get(
"https://api.mod.io/v1/me/mods", timeout=5, params={"api_key": api_key}, headers=headers
)
print(r.json()) print(r.json())
with open(mod_file_path, "rb") as f: files = {'filedata': open(mod_file_path, 'rb')}
files = {"filedata": f}
signature = ""
with open(f"{mod_name}-{mod_version}.zip.minisign", encoding="utf-8") as f:
signature = f.read()
rq = requests.post( mod_json = json.load(open(MOD_PATH / 'mod.json', 'r'))
f"https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files",
files=files,
headers=headers,
timeout=500,
data={
"version": mod_version,
"active": True,
"changelog": changelog,
"metadata_blob": json.dumps(
{"dependencies": mod_json["dependencies"], "minisigs": [signature]}
),
},
)
print(rq.json()) signature = open('signature.minisign', 'r').read()
rq = requests.post(f'https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files', files=files, headers=headers, data={
'version': mod_version,
'active': True,
'metadata_blob' : json.dumps({
'dependencies': mod_json['dependencies'],
'minisigs': [signature]
})
})
print(rq.json())

View File

@ -1,9 +1,9 @@
from collections import Counter from collections import Counter
from decimal import Decimal from decimal import Decimal
from os.path import exists
from re import split from re import split
from xml.etree import ElementTree as ET from sys import stderr
from xml.etree import ElementTree
from os.path import exists
class SimulTemplateEntity: class SimulTemplateEntity:
def __init__(self, vfs_root, logger): def __init__(self, vfs_root, logger):
@ -12,11 +12,11 @@ class SimulTemplateEntity:
def get_file(self, base_path, vfs_path, mod): def get_file(self, base_path, vfs_path, mod):
default_path = self.vfs_root / mod / base_path default_path = self.vfs_root / mod / base_path
file = (default_path / "special" / "filter" / vfs_path).with_suffix(".xml") file = (default_path/ "special" / "filter" / vfs_path).with_suffix('.xml')
if not exists(file): if not exists(file):
file = (default_path / "mixins" / vfs_path).with_suffix(".xml") file = (default_path / "mixins" / vfs_path).with_suffix('.xml')
if not exists(file): if not exists(file):
file = (default_path / vfs_path).with_suffix(".xml") file = (default_path / vfs_path).with_suffix('.xml')
return file return file
def get_main_mod(self, base_path, vfs_path, mods): def get_main_mod(self, base_path, vfs_path, mods):
@ -33,110 +33,107 @@ class SimulTemplateEntity:
return main_mod return main_mod
def apply_layer(self, base_tag, tag): def apply_layer(self, base_tag, tag):
"""Apply tag layer to base_tag.""" """
if tag.get("datatype") == "tokens": apply tag layer to base_tag
base_tokens = split(r"\s+", base_tag.text or "") """
tokens = split(r"\s+", tag.text or "") if tag.get('datatype') == 'tokens':
base_tokens = split(r'\s+', base_tag.text or '')
tokens = split(r'\s+', tag.text or '')
final_tokens = base_tokens.copy() final_tokens = base_tokens.copy()
for token in tokens: for token in tokens:
if token.startswith("-"): if token.startswith('-'):
token_to_remove = token[1:] token_to_remove = token[1:]
if token_to_remove in final_tokens: if token_to_remove in final_tokens:
final_tokens.remove(token_to_remove) final_tokens.remove(token_to_remove)
elif token not in final_tokens: elif token not in final_tokens:
final_tokens.append(token) final_tokens.append(token)
base_tag.text = " ".join(final_tokens) base_tag.text = ' '.join(final_tokens)
base_tag.set("datatype", "tokens") base_tag.set("datatype", "tokens")
elif tag.get("op"): elif tag.get('op'):
op = tag.get("op") op = tag.get('op')
op1 = Decimal(base_tag.text or "0") op1 = Decimal(base_tag.text or '0')
op2 = Decimal(tag.text or "0") op2 = Decimal(tag.text or '0')
# Try converting to integers if possible, to pass validation. # Try converting to integers if possible, to pass validation.
if op == "add": if op == 'add':
base_tag.text = str(int(op1 + op2) if int(op1 + op2) == op1 + op2 else op1 + op2) base_tag.text = str(int(op1 + op2) if int(op1 + op2) == op1 + op2 else op1 + op2)
elif op == "mul": elif op == 'mul':
base_tag.text = str(int(op1 * op2) if int(op1 * op2) == op1 * op2 else op1 * op2) base_tag.text = str(int(op1 * op2) if int(op1 * op2) == op1 * op2 else op1 * op2)
elif op == "mul_round": elif op == 'mul_round':
base_tag.text = str(round(op1 * op2)) base_tag.text = str(round(op1 * op2))
else: else:
raise ValueError(f"Invalid operator '{op}'") raise ValueError(f"Invalid operator '{op}'")
else: else:
base_tag.text = tag.text base_tag.text = tag.text
for prop in tag.attrib: for prop in tag.attrib:
if prop not in ("disable", "replace", "parent", "merge"): if prop not in ('disable', 'replace', 'parent', 'merge'):
base_tag.set(prop, tag.get(prop)) base_tag.set(prop, tag.get(prop))
for child in tag: for child in tag:
base_child = base_tag.find(child.tag) base_child = base_tag.find(child.tag)
if "disable" in child.attrib: if 'disable' in child.attrib:
if base_child is not None: if base_child is not None:
base_tag.remove(base_child) base_tag.remove(base_child)
elif ("merge" not in child.attrib) or (base_child is not None): elif ('merge' not in child.attrib) or (base_child is not None):
if "replace" in child.attrib and base_child is not None: if 'replace' in child.attrib and base_child is not None:
base_tag.remove(base_child) base_tag.remove(base_child)
base_child = None base_child = None
if base_child is None: if base_child is None:
base_child = ET.Element(child.tag) base_child = ElementTree.Element(child.tag)
base_tag.append(base_child) base_tag.append(base_child)
self.apply_layer(base_child, child) self.apply_layer(base_child, child)
if "replace" in base_child.attrib: if 'replace' in base_child.attrib:
del base_child.attrib["replace"] del base_child.attrib['replace']
def load_inherited(self, base_path, vfs_path, mods): def load_inherited(self, base_path, vfs_path, mods):
entity = self._load_inherited(base_path, vfs_path, mods) entity = self._load_inherited(base_path, vfs_path, mods)
entity[:] = sorted(entity[:], key=lambda x: x.tag) entity[:] = sorted(entity[:], key=lambda x: x.tag)
return entity return entity
def _load_inherited(self, base_path, vfs_path, mods, base=None): def _load_inherited(self, base_path, vfs_path, mods, base = None):
"""vfs_path should be relative to base_path in a mod.""" """
if "|" in vfs_path: vfs_path should be relative to base_path in a mod
"""
if '|' in vfs_path:
paths = vfs_path.split("|", 1) paths = vfs_path.split("|", 1)
self._load_inherited(base_path, paths[1], mods, base) base = self._load_inherited(base_path, paths[1], mods, base);
return self._load_inherited(base_path, paths[0], mods, base) base = self._load_inherited(base_path, paths[0], mods, base);
return base
main_mod = self.get_main_mod(base_path, vfs_path, mods) main_mod = self.get_main_mod(base_path, vfs_path, mods)
fp = self.get_file(base_path, vfs_path, main_mod) fp = self.get_file(base_path, vfs_path, main_mod)
layer = ET.parse(fp).getroot() layer = ElementTree.parse(fp).getroot()
for el in layer.iter(): for el in layer.iter():
children = [x.tag for x in el] children = [x.tag for x in el]
duplicates = [x for x, c in Counter(children).items() if c > 1] duplicates = [x for x, c in Counter(children).items() if c > 1]
if duplicates: if duplicates:
for dup in duplicates: for dup in duplicates:
self.logger.warning( self.logger.warning(f"Duplicate child node '{dup}' in tag {el.tag} of {fp}")
"Duplicate child node '%s' in tag %s of %s", dup, el.tag, fp if layer.get('parent'):
) parent = self._load_inherited(base_path, layer.get('parent'), mods, base)
if layer.get("parent"):
parent = self._load_inherited(base_path, layer.get("parent"), mods, base)
self.apply_layer(parent, layer) self.apply_layer(parent, layer)
return parent return parent
else:
if not base: if not base:
return layer return layer
else:
self.apply_layer(base, layer) self.apply_layer(base, layer)
return base return base
def find_files(vfs_root, mods, vfs_path, *ext_list): def find_files(vfs_root, mods, vfs_path, *ext_list):
"""Return a list of 2-size tuples.
Each tuple contains:
- Path relative to the mod base
- full Path
""" """
full_exts = ["." + ext for ext in ext_list] returns a list of 2-size tuple with:
- Path relative to the mod base
- full Path
"""
full_exts = ['.' + ext for ext in ext_list]
def find_recursive(dp, base): def find_recursive(dp, base):
"""(relative Path, full Path) generator.""" """(relative Path, full Path) generator"""
if dp.is_dir(): if dp.is_dir():
if dp.name not in (".svn", ".git") and not dp.name.endswith("~"): if dp.name != '.svn' and dp.name != '.git' and not dp.name.endswith('~'):
for fp in dp.iterdir(): for fp in dp.iterdir():
yield from find_recursive(fp, base) yield from find_recursive(fp, base)
elif dp.suffix in full_exts: elif dp.suffix in full_exts:
relative_file_path = dp.relative_to(base) relative_file_path = dp.relative_to(base)
yield (relative_file_path, dp.resolve()) yield (relative_file_path, dp.resolve())
return [(rp, fp) for mod in mods for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)]
return [
(rp, fp)
for mod in mods
for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)
]