feat(civilization): add battle sound effects for unit combat

Add audio feedback for combat events by introducing unit-specific sound
effects that play during attack animations. Each unit in civilization-rules.json
now has a battleSound property mapping to appropriate audio assets. Adds 10
new MP3 files (arrows, cannon, catapult, gunfire-classic/modern, helicopter,
missile, submarine, sword, tank) and integrates playback in CivilizationMapView's
combat lunge animation.
This commit is contained in:
Brian Fertig 2026-07-17 13:03:26 -06:00
parent 8b5aa6064a
commit d6d3bafc7f
13 changed files with 50 additions and 41 deletions

BIN
assets/fx/battle-arrows.mp3 Normal file

Binary file not shown.

BIN
assets/fx/battle-cannon.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fx/battle-missle.mp3 Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/fx/battle-sword.mp3 Normal file

Binary file not shown.

BIN
assets/fx/battle-tank.mp3 Normal file

Binary file not shown.

View File

@ -108,52 +108,52 @@
{ "id": "caravan", "name": "Caravan", "domain": "land", "attack": 0, "defense": 1, "move": 1, "hp": 10, "fp": 1, "cost": 50, "prereq": "trade", "obsoletedBy": "freight", "flags": ["caravan", "noncombat"], "abbr": "CV", "frame": 3 },
{ "id": "freight", "name": "Freight", "domain": "land", "attack": 0, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 50, "prereq": "thecorporation", "obsoletedBy": null, "flags": ["caravan", "noncombat"], "abbr": "FR", "frame": 4 },
{ "id": "warriors", "name": "Warriors", "domain": "land", "attack": 1, "defense": 1, "move": 1, "hp": 10, "fp": 1, "cost": 10, "prereq": null, "obsoletedBy": "pikemen", "flags": [], "abbr": "WA", "frame": 5 },
{ "id": "phalanx", "name": "Phalanx", "domain": "land", "attack": 1, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 20, "prereq": "bronzeworking", "obsoletedBy": "pikemen", "flags": [], "abbr": "PH", "frame": 6 },
{ "id": "archers", "name": "Archers", "domain": "land", "attack": 3, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 30, "prereq": "warriorcode", "obsoletedBy": "musketeers", "flags": [], "abbr": "AR", "frame": 7 },
{ "id": "legion", "name": "Legion", "domain": "land", "attack": 4, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 40, "prereq": "ironworking", "obsoletedBy": "musketeers", "flags": [], "abbr": "LG", "frame": 8 },
{ "id": "pikemen", "name": "Pikemen", "domain": "land", "attack": 1, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 20, "prereq": "feudalism", "obsoletedBy": "musketeers", "flags": ["antimounted"], "abbr": "PK", "frame": 9 },
{ "id": "musketeers", "name": "Musketeers", "domain": "land", "attack": 3, "defense": 3, "move": 1, "hp": 20, "fp": 1, "cost": 30, "prereq": "gunpowder", "obsoletedBy": "riflemen", "flags": [], "abbr": "MK", "frame": 10 },
{ "id": "riflemen", "name": "Riflemen", "domain": "land", "attack": 5, "defense": 4, "move": 1, "hp": 20, "fp": 1, "cost": 40, "prereq": "conscription", "obsoletedBy": null, "flags": [], "abbr": "RF", "frame": 11 },
{ "id": "alpinetroops", "name": "Alpine Troops", "domain": "land", "attack": 5, "defense": 5, "move": 1, "hp": 20, "fp": 1, "cost": 50, "prereq": "tactics", "obsoletedBy": null, "flags": ["ignoreterrain"], "abbr": "AL", "frame": 12 },
{ "id": "partisans", "name": "Partisans", "domain": "land", "attack": 4, "defense": 4, "move": 1, "hp": 20, "fp": 1, "cost": 50, "prereq": "guerrillawarfare", "obsoletedBy": null, "flags": ["ignoreterrain"], "abbr": "PS", "frame": 13 },
{ "id": "marines", "name": "Marines", "domain": "land", "attack": 8, "defense": 5, "move": 1, "hp": 20, "fp": 1, "cost": 60, "prereq": "amphibiouswarfare", "obsoletedBy": null, "flags": ["amphibious"], "abbr": "MN", "frame": 14 },
{ "id": "paratroopers", "name": "Paratroopers", "domain": "land", "attack": 6, "defense": 4, "move": 1, "hp": 20, "fp": 1, "cost": 60, "prereq": "combinedarms", "obsoletedBy": null, "flags": ["paradrop"], "abbr": "PT", "frame": 15 },
{ "id": "mechinf", "name": "Mech. Inf.", "domain": "land", "attack": 6, "defense": 6, "move": 3, "hp": 30, "fp": 1, "cost": 50, "prereq": "laborunion", "obsoletedBy": null, "flags": [], "abbr": "MI", "frame": 16 },
{ "id": "warriors", "name": "Warriors", "domain": "land", "attack": 1, "defense": 1, "move": 1, "hp": 10, "fp": 1, "cost": 10, "prereq": null, "obsoletedBy": "pikemen", "flags": [], "battleSound": "sword", "abbr": "WA", "frame": 5 },
{ "id": "phalanx", "name": "Phalanx", "domain": "land", "attack": 1, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 20, "prereq": "bronzeworking", "obsoletedBy": "pikemen", "flags": [], "battleSound": "sword", "abbr": "PH", "frame": 6 },
{ "id": "archers", "name": "Archers", "domain": "land", "attack": 3, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 30, "prereq": "warriorcode", "obsoletedBy": "musketeers", "flags": [], "battleSound": "arrows", "abbr": "AR", "frame": 7 },
{ "id": "legion", "name": "Legion", "domain": "land", "attack": 4, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 40, "prereq": "ironworking", "obsoletedBy": "musketeers", "flags": [], "battleSound": "sword", "abbr": "LG", "frame": 8 },
{ "id": "pikemen", "name": "Pikemen", "domain": "land", "attack": 1, "defense": 2, "move": 1, "hp": 10, "fp": 1, "cost": 20, "prereq": "feudalism", "obsoletedBy": "musketeers", "flags": ["antimounted"], "battleSound": "sword", "abbr": "PK", "frame": 9 },
{ "id": "musketeers", "name": "Musketeers", "domain": "land", "attack": 3, "defense": 3, "move": 1, "hp": 20, "fp": 1, "cost": 30, "prereq": "gunpowder", "obsoletedBy": "riflemen", "flags": [], "battleSound": "gunfire-classic", "abbr": "MK", "frame": 10 },
{ "id": "riflemen", "name": "Riflemen", "domain": "land", "attack": 5, "defense": 4, "move": 1, "hp": 20, "fp": 1, "cost": 40, "prereq": "conscription", "obsoletedBy": null, "flags": [], "battleSound": "gunfire-classic", "abbr": "RF", "frame": 11 },
{ "id": "alpinetroops", "name": "Alpine Troops", "domain": "land", "attack": 5, "defense": 5, "move": 1, "hp": 20, "fp": 1, "cost": 50, "prereq": "tactics", "obsoletedBy": null, "flags": ["ignoreterrain"], "battleSound": "gunfire-modern", "abbr": "AL", "frame": 12 },
{ "id": "partisans", "name": "Partisans", "domain": "land", "attack": 4, "defense": 4, "move": 1, "hp": 20, "fp": 1, "cost": 50, "prereq": "guerrillawarfare", "obsoletedBy": null, "flags": ["ignoreterrain"], "battleSound": "gunfire-modern", "abbr": "PS", "frame": 13 },
{ "id": "marines", "name": "Marines", "domain": "land", "attack": 8, "defense": 5, "move": 1, "hp": 20, "fp": 1, "cost": 60, "prereq": "amphibiouswarfare", "obsoletedBy": null, "flags": ["amphibious"], "battleSound": "gunfire-modern", "abbr": "MN", "frame": 14 },
{ "id": "paratroopers", "name": "Paratroopers", "domain": "land", "attack": 6, "defense": 4, "move": 1, "hp": 20, "fp": 1, "cost": 60, "prereq": "combinedarms", "obsoletedBy": null, "flags": ["paradrop"], "battleSound": "gunfire-modern", "abbr": "PT", "frame": 15 },
{ "id": "mechinf", "name": "Mech. Inf.", "domain": "land", "attack": 6, "defense": 6, "move": 3, "hp": 30, "fp": 1, "cost": 50, "prereq": "laborunion", "obsoletedBy": null, "flags": [], "battleSound": "gunfire-modern", "abbr": "MI", "frame": 16 },
{ "id": "horsemen", "name": "Horsemen", "domain": "land", "attack": 2, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 20, "prereq": "horsebackriding", "obsoletedBy": "knights", "flags": ["mounted"], "abbr": "HS", "frame": 17 },
{ "id": "chariot", "name": "Chariot", "domain": "land", "attack": 3, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 30, "prereq": "thewheel", "obsoletedBy": "knights", "flags": ["mounted"], "abbr": "CH", "frame": 18 },
{ "id": "elephant", "name": "Elephant", "domain": "land", "attack": 4, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 40, "prereq": "polytheism", "obsoletedBy": "crusaders", "flags": ["mounted"], "abbr": "EL", "frame": 19 },
{ "id": "knights", "name": "Knights", "domain": "land", "attack": 4, "defense": 2, "move": 2, "hp": 10, "fp": 1, "cost": 40, "prereq": "chivalry", "obsoletedBy": "dragoons", "flags": ["mounted"], "abbr": "KN", "frame": 20 },
{ "id": "crusaders", "name": "Crusaders", "domain": "land", "attack": 5, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 40, "prereq": "monotheism", "obsoletedBy": "dragoons", "flags": ["mounted"], "abbr": "CS", "frame": 21 },
{ "id": "dragoons", "name": "Dragoons", "domain": "land", "attack": 5, "defense": 2, "move": 2, "hp": 20, "fp": 1, "cost": 50, "prereq": "leadership", "obsoletedBy": "cavalry", "flags": ["mounted"], "abbr": "DG", "frame": 22 },
{ "id": "cavalry", "name": "Cavalry", "domain": "land", "attack": 8, "defense": 3, "move": 2, "hp": 20, "fp": 1, "cost": 60, "prereq": "tactics", "obsoletedBy": "armor", "flags": ["mounted"], "abbr": "CY", "frame": 23 },
{ "id": "armor", "name": "Armor", "domain": "land", "attack": 10, "defense": 5, "move": 3, "hp": 30, "fp": 1, "cost": 80, "prereq": "mobilewarfare", "obsoletedBy": null, "flags": ["mounted"], "abbr": "AM", "frame": 24 },
{ "id": "horsemen", "name": "Horsemen", "domain": "land", "attack": 2, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 20, "prereq": "horsebackriding", "obsoletedBy": "knights", "flags": ["mounted"], "battleSound": "sword", "abbr": "HS", "frame": 17 },
{ "id": "chariot", "name": "Chariot", "domain": "land", "attack": 3, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 30, "prereq": "thewheel", "obsoletedBy": "knights", "flags": ["mounted"], "battleSound": "sword", "abbr": "CH", "frame": 18 },
{ "id": "elephant", "name": "Elephant", "domain": "land", "attack": 4, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 40, "prereq": "polytheism", "obsoletedBy": "crusaders", "flags": ["mounted"], "battleSound": "sword", "abbr": "EL", "frame": 19 },
{ "id": "knights", "name": "Knights", "domain": "land", "attack": 4, "defense": 2, "move": 2, "hp": 10, "fp": 1, "cost": 40, "prereq": "chivalry", "obsoletedBy": "dragoons", "flags": ["mounted"], "battleSound": "sword", "abbr": "KN", "frame": 20 },
{ "id": "crusaders", "name": "Crusaders", "domain": "land", "attack": 5, "defense": 1, "move": 2, "hp": 10, "fp": 1, "cost": 40, "prereq": "monotheism", "obsoletedBy": "dragoons", "flags": ["mounted"], "battleSound": "sword", "abbr": "CS", "frame": 21 },
{ "id": "dragoons", "name": "Dragoons", "domain": "land", "attack": 5, "defense": 2, "move": 2, "hp": 20, "fp": 1, "cost": 50, "prereq": "leadership", "obsoletedBy": "cavalry", "flags": ["mounted"], "battleSound": "gunfire-classic", "abbr": "DG", "frame": 22 },
{ "id": "cavalry", "name": "Cavalry", "domain": "land", "attack": 8, "defense": 3, "move": 2, "hp": 20, "fp": 1, "cost": 60, "prereq": "tactics", "obsoletedBy": "armor", "flags": ["mounted"], "battleSound": "gunfire-classic", "abbr": "CY", "frame": 23 },
{ "id": "armor", "name": "Armor", "domain": "land", "attack": 10, "defense": 5, "move": 3, "hp": 30, "fp": 1, "cost": 80, "prereq": "mobilewarfare", "obsoletedBy": null, "flags": ["mounted"], "battleSound": "tank", "abbr": "AM", "frame": 24 },
{ "id": "catapult", "name": "Catapult", "domain": "land", "attack": 6, "defense": 1, "move": 1, "hp": 10, "fp": 1, "cost": 40, "prereq": "mathematics", "obsoletedBy": "cannon", "flags": [], "abbr": "CP", "frame": 25 },
{ "id": "cannon", "name": "Cannon", "domain": "land", "attack": 8, "defense": 1, "move": 1, "hp": 20, "fp": 1, "cost": 40, "prereq": "metallurgy", "obsoletedBy": "artillery", "flags": [], "abbr": "CN", "frame": 26 },
{ "id": "artillery", "name": "Artillery", "domain": "land", "attack": 10, "defense": 1, "move": 1, "hp": 20, "fp": 2, "cost": 50, "prereq": "machinetools", "obsoletedBy": "howitzer", "flags": [], "abbr": "AT", "frame": 27 },
{ "id": "howitzer", "name": "Howitzer", "domain": "land", "attack": 12, "defense": 2, "move": 2, "hp": 30, "fp": 2, "cost": 70, "prereq": "robotics", "obsoletedBy": null, "flags": ["ignorewalls"], "abbr": "HW", "frame": 28 },
{ "id": "catapult", "name": "Catapult", "domain": "land", "attack": 6, "defense": 1, "move": 1, "hp": 10, "fp": 1, "cost": 40, "prereq": "mathematics", "obsoletedBy": "cannon", "flags": [], "battleSound": "catapult", "abbr": "CP", "frame": 25 },
{ "id": "cannon", "name": "Cannon", "domain": "land", "attack": 8, "defense": 1, "move": 1, "hp": 20, "fp": 1, "cost": 40, "prereq": "metallurgy", "obsoletedBy": "artillery", "flags": [], "battleSound": "cannon", "abbr": "CN", "frame": 26 },
{ "id": "artillery", "name": "Artillery", "domain": "land", "attack": 10, "defense": 1, "move": 1, "hp": 20, "fp": 2, "cost": 50, "prereq": "machinetools", "obsoletedBy": "howitzer", "flags": [], "battleSound": "cannon", "abbr": "AT", "frame": 27 },
{ "id": "howitzer", "name": "Howitzer", "domain": "land", "attack": 12, "defense": 2, "move": 2, "hp": 30, "fp": 2, "cost": 70, "prereq": "robotics", "obsoletedBy": null, "flags": ["ignorewalls"], "battleSound": "cannon", "abbr": "HW", "frame": 28 },
{ "id": "fighter", "name": "Fighter", "domain": "air", "attack": 4, "defense": 3, "move": 10, "hp": 20, "fp": 2, "cost": 60, "prereq": "flight", "obsoletedBy": "stealthfighter", "flags": ["fighter"], "abbr": "FT", "frame": 29 },
{ "id": "bomber", "name": "Bomber", "domain": "air", "attack": 12, "defense": 1, "move": 8, "hp": 20, "fp": 2, "cost": 120, "prereq": "advancedflight", "obsoletedBy": "stealthbomber", "flags": [], "abbr": "BM", "frame": 30 },
{ "id": "helicopter", "name": "Helicopter", "domain": "air", "attack": 10, "defense": 3, "move": 6, "hp": 20, "fp": 2, "cost": 100, "prereq": "combinedarms", "obsoletedBy": null, "flags": [], "abbr": "HC", "frame": 31 },
{ "id": "stealthfighter", "name": "Stealth Ftr.", "domain": "air", "attack": 8, "defense": 4, "move": 14, "hp": 20, "fp": 2, "cost": 80, "prereq": "stealth", "obsoletedBy": null, "flags": ["fighter"], "abbr": "SF", "frame": 32 },
{ "id": "stealthbomber", "name": "Stealth Bmb.", "domain": "air", "attack": 14, "defense": 5, "move": 12, "hp": 20, "fp": 2, "cost": 160, "prereq": "stealth", "obsoletedBy": null, "flags": [], "abbr": "SB", "frame": 33 },
{ "id": "cruisemsl", "name": "Cruise Msl.", "domain": "air", "attack": 18, "defense": 0, "move": 12, "hp": 10, "fp": 3, "cost": 60, "prereq": "rocketry", "obsoletedBy": null, "flags": ["missile"], "abbr": "CM", "frame": 34 },
{ "id": "nuclearmsl", "name": "Nuclear Msl.", "domain": "air", "attack": 99, "defense": 0, "move": 16, "hp": 10, "fp": 1, "cost": 160, "prereq": "nuclearfission", "obsoletedBy": null, "flags": ["missile", "nuke"], "abbr": "NM", "frame": 35 },
{ "id": "fighter", "name": "Fighter", "domain": "air", "attack": 4, "defense": 3, "move": 10, "hp": 20, "fp": 2, "cost": 60, "prereq": "flight", "obsoletedBy": "stealthfighter", "flags": ["fighter"], "battleSound": "gunfire-modern", "abbr": "FT", "frame": 29 },
{ "id": "bomber", "name": "Bomber", "domain": "air", "attack": 12, "defense": 1, "move": 8, "hp": 20, "fp": 2, "cost": 120, "prereq": "advancedflight", "obsoletedBy": "stealthbomber", "flags": [], "battleSound": "missle", "abbr": "BM", "frame": 30 },
{ "id": "helicopter", "name": "Helicopter", "domain": "air", "attack": 10, "defense": 3, "move": 6, "hp": 20, "fp": 2, "cost": 100, "prereq": "combinedarms", "obsoletedBy": null, "flags": [], "battleSound": "helicopter", "abbr": "HC", "frame": 31 },
{ "id": "stealthfighter", "name": "Stealth Ftr.", "domain": "air", "attack": 8, "defense": 4, "move": 14, "hp": 20, "fp": 2, "cost": 80, "prereq": "stealth", "obsoletedBy": null, "flags": ["fighter"], "battleSound": "missle", "abbr": "SF", "frame": 32 },
{ "id": "stealthbomber", "name": "Stealth Bmb.", "domain": "air", "attack": 14, "defense": 5, "move": 12, "hp": 20, "fp": 2, "cost": 160, "prereq": "stealth", "obsoletedBy": null, "flags": [], "battleSound": "missle", "abbr": "SB", "frame": 33 },
{ "id": "cruisemsl", "name": "Cruise Msl.", "domain": "air", "attack": 18, "defense": 0, "move": 12, "hp": 10, "fp": 3, "cost": 60, "prereq": "rocketry", "obsoletedBy": null, "flags": ["missile"], "battleSound": "missle", "abbr": "CM", "frame": 34 },
{ "id": "nuclearmsl", "name": "Nuclear Msl.", "domain": "air", "attack": 99, "defense": 0, "move": 16, "hp": 10, "fp": 1, "cost": 160, "prereq": "nuclearfission", "obsoletedBy": null, "flags": ["missile", "nuke"], "battleSound": "missle", "abbr": "NM", "frame": 35 },
{ "id": "trireme", "name": "Trireme", "domain": "sea", "attack": 1, "defense": 1, "move": 3, "hp": 10, "fp": 1, "cost": 40, "prereq": "mapmaking", "obsoletedBy": "caravel", "flags": ["coastal"], "cargo": 2, "abbr": "TR", "frame": 36 },
{ "id": "caravel", "name": "Caravel", "domain": "sea", "attack": 2, "defense": 1, "move": 3, "hp": 10, "fp": 1, "cost": 40, "prereq": "navigation", "obsoletedBy": "galleon", "flags": [], "cargo": 3, "abbr": "CL", "frame": 37 },
{ "id": "trireme", "name": "Trireme", "domain": "sea", "attack": 1, "defense": 1, "move": 3, "hp": 10, "fp": 1, "cost": 40, "prereq": "mapmaking", "obsoletedBy": "caravel", "flags": ["coastal"], "cargo": 2, "battleSound": "arrows", "abbr": "TR", "frame": 36 },
{ "id": "caravel", "name": "Caravel", "domain": "sea", "attack": 2, "defense": 1, "move": 3, "hp": 10, "fp": 1, "cost": 40, "prereq": "navigation", "obsoletedBy": "galleon", "flags": [], "cargo": 3, "battleSound": "cannon", "abbr": "CL", "frame": 37 },
{ "id": "galleon", "name": "Galleon", "domain": "sea", "attack": 0, "defense": 2, "move": 4, "hp": 20, "fp": 1, "cost": 40, "prereq": "magnetism", "obsoletedBy": "transport", "flags": [], "cargo": 4, "abbr": "GL", "frame": 38 },
{ "id": "frigate", "name": "Frigate", "domain": "sea", "attack": 4, "defense": 2, "move": 4, "hp": 20, "fp": 1, "cost": 50, "prereq": "magnetism", "obsoletedBy": "destroyer", "flags": [], "cargo": 2, "abbr": "FG", "frame": 39 },
{ "id": "ironclad", "name": "Ironclad", "domain": "sea", "attack": 4, "defense": 4, "move": 4, "hp": 30, "fp": 1, "cost": 60, "prereq": "steamengine", "obsoletedBy": "destroyer", "flags": [], "abbr": "IC", "frame": 40 },
{ "id": "destroyer", "name": "Destroyer", "domain": "sea", "attack": 4, "defense": 4, "move": 6, "hp": 30, "fp": 1, "cost": 60, "prereq": "combustion", "obsoletedBy": null, "flags": [], "abbr": "DS", "frame": 41 },
{ "id": "cruiser", "name": "Cruiser", "domain": "sea", "attack": 6, "defense": 6, "move": 5, "hp": 30, "fp": 2, "cost": 80, "prereq": "steel", "obsoletedBy": "aegiscruiser", "flags": [], "abbr": "CU", "frame": 42 },
{ "id": "aegiscruiser", "name": "AEGIS Cruiser", "domain": "sea", "attack": 8, "defense": 8, "move": 5, "hp": 30, "fp": 2, "cost": 100, "prereq": "rocketry", "obsoletedBy": null, "flags": ["aegis"], "abbr": "AE", "frame": 43 },
{ "id": "battleship", "name": "Battleship", "domain": "sea", "attack": 12, "defense": 12, "move": 4, "hp": 40, "fp": 2, "cost": 160, "prereq": "automobile", "obsoletedBy": null, "flags": [], "abbr": "BS", "frame": 44 },
{ "id": "submarine", "name": "Submarine", "domain": "sea", "attack": 10, "defense": 2, "move": 3, "hp": 30, "fp": 2, "cost": 60, "prereq": "combustion", "obsoletedBy": null, "flags": ["submarine"], "abbr": "SM", "frame": 45 },
{ "id": "carrier", "name": "Carrier", "domain": "sea", "attack": 1, "defense": 9, "move": 5, "hp": 40, "fp": 2, "cost": 160, "prereq": "advancedflight", "obsoletedBy": null, "flags": [], "cargoAir": 8, "abbr": "CR", "frame": 46 },
{ "id": "frigate", "name": "Frigate", "domain": "sea", "attack": 4, "defense": 2, "move": 4, "hp": 20, "fp": 1, "cost": 50, "prereq": "magnetism", "obsoletedBy": "destroyer", "flags": [], "cargo": 2, "battleSound": "cannon", "abbr": "FG", "frame": 39 },
{ "id": "ironclad", "name": "Ironclad", "domain": "sea", "attack": 4, "defense": 4, "move": 4, "hp": 30, "fp": 1, "cost": 60, "prereq": "steamengine", "obsoletedBy": "destroyer", "flags": [], "battleSound": "cannon", "abbr": "IC", "frame": 40 },
{ "id": "destroyer", "name": "Destroyer", "domain": "sea", "attack": 4, "defense": 4, "move": 6, "hp": 30, "fp": 1, "cost": 60, "prereq": "combustion", "obsoletedBy": null, "flags": [], "battleSound": "cannon", "abbr": "DS", "frame": 41 },
{ "id": "cruiser", "name": "Cruiser", "domain": "sea", "attack": 6, "defense": 6, "move": 5, "hp": 30, "fp": 2, "cost": 80, "prereq": "steel", "obsoletedBy": "aegiscruiser", "flags": [], "battleSound": "cannon", "abbr": "CU", "frame": 42 },
{ "id": "aegiscruiser", "name": "AEGIS Cruiser", "domain": "sea", "attack": 8, "defense": 8, "move": 5, "hp": 30, "fp": 2, "cost": 100, "prereq": "rocketry", "obsoletedBy": null, "flags": ["aegis"], "battleSound": "missle", "abbr": "AE", "frame": 43 },
{ "id": "battleship", "name": "Battleship", "domain": "sea", "attack": 12, "defense": 12, "move": 4, "hp": 40, "fp": 2, "cost": 160, "prereq": "automobile", "obsoletedBy": null, "flags": [], "battleSound": "cannon", "abbr": "BS", "frame": 44 },
{ "id": "submarine", "name": "Submarine", "domain": "sea", "attack": 10, "defense": 2, "move": 3, "hp": 30, "fp": 2, "cost": 60, "prereq": "combustion", "obsoletedBy": null, "flags": ["submarine"], "battleSound": "submarine", "abbr": "SM", "frame": 45 },
{ "id": "carrier", "name": "Carrier", "domain": "sea", "attack": 1, "defense": 9, "move": 5, "hp": 40, "fp": 2, "cost": 160, "prereq": "advancedflight", "obsoletedBy": null, "flags": [], "cargoAir": 8, "battleSound": "gunfire-modern", "abbr": "CR", "frame": 46 },
{ "id": "transport", "name": "Transport", "domain": "sea", "attack": 0, "defense": 3, "move": 5, "hp": 30, "fp": 1, "cost": 50, "prereq": "industrialization", "obsoletedBy": null, "flags": [], "cargo": 8, "abbr": "TP", "frame": 47 },
{ "id": "ssstructural", "name": "SS Structural", "domain": "project", "attack": 0, "defense": 0, "move": 0, "hp": 1, "fp": 1, "cost": 80, "prereq": "spaceflight", "obsoletedBy": null, "flags": ["spaceship"], "abbr": "S1", "frame": 48 },

View File

@ -158,6 +158,11 @@ export const MANIFEST = {
civilization: [
{ type: 'json', key: 'civilization-rules', path: 'data/civilization-rules.json' },
image('civilization-setup-bg', 'assets/images/civilization/background.png'),
// Battle sounds keyed by each unit's `battleSound` in civilization-rules.json
// (played on attack by CivilizationMapView.playCombatEvent).
...['arrows', 'cannon', 'catapult', 'gunfire-classic', 'gunfire-modern',
'helicopter', 'missle', 'submarine', 'sword', 'tank']
.map((n) => ({ type: 'audio', key: `sfx-battle-${n}`, path: `assets/fx/battle-${n}.mp3` })),
(scene) => sheetsFrom(scene, 'civilization-artwork',
['terrainSheet', 'resourceSheet', 'improvementSheet', 'unitSheet', 'iconSheet', 'citySheets']),
],

View File

@ -17,6 +17,7 @@ import {
computeVisible, isUnitVisibleTo, shieldGrassAt,
} from './CivilizationLogic.js';
import { Tooltip } from '../../ui/Tooltip.js';
import { playSound } from '../../ui/Sounds.js';
import { describeUnitStackTooltip, describeCityTooltip } from './CivilizationTooltips.js';
export const TILE_W = 128;
@ -914,6 +915,9 @@ export class CivilizationMapView {
const target = { x: this.isoX(e.x, e.y), y: this.isoY(e.x, e.y) + TILE_H / 2 };
const LUNGE_MS = 250;
const battleSound = this.rules.units[e.attackerType].battleSound;
if (battleSound) playSound(this.scene, `sfx-battle-${battleSound}`);
// Lunge onto the defender's tile...
this.scene.tweens.add({
targets: attackerGhost.container,