Skip to content

Add basic bombard by land units#928

Open
ajhalme wants to merge 19 commits intoC7-Game:Developmentfrom
ajhalme:bombard
Open

Add basic bombard by land units#928
ajhalme wants to merge 19 commits intoC7-Game:Developmentfrom
ajhalme:bombard

Conversation

@ajhalme
Copy link
Copy Markdown
Contributor

@ajhalme ajhalme commented Apr 29, 2026

Basic bombard mechanic based on #927.

Changes

  • base-ruleset.json extension with bombardRange and rateofFire for all units
  • unit_bombard unit button enabled, including "B" shortcut key
  • Texture based crosshair & disallowed cursor (Godot native)
  • Mouse-based interactive target selection
  • BombardLayer to render bombard grid, cursor, target selection
  • BombardInfo, in the spirit of GotoInfo, to carry information about user inputs to BombardLayer
  • Fixed/re-enabled "effect" animations for the bombard explosion
  • Extend bombard(..) MapUnit logic with rate of fire (and hit notifications)
  • Bombard target tile collection enumerator in Tile, GetTilesWithinBombardRange(..) -- Rank-based enumerator ignores the "corners" that are needed for the bombard grid. (Think "Rank-2 City BFC" vs "complete 5x5 tile square".)

Extras

  • If a saved game is set up such that an opponent civ is about to attack, CreateGame will get stuck in an infinite loop, waiting for animations. This PR includes a workaround that disabled animations on CreateGame load.

Demo

artillery_oc3

@ajhalme
Copy link
Copy Markdown
Contributor Author

ajhalme commented Apr 29, 2026

Known issues (Update: Resolved!):

  • Bombard before declaring war
  • Bombing your own units
  • Lethal bombard

Copy link
Copy Markdown
Contributor

@stavrosfa stavrosfa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff, thanks!

I didn't playtest this yet, but I will and let you know if I have any more thoughts


public override async void process() {
MapUnit unit = EngineStorage.gameData.GetUnit(unitID);
Tile tile = EngineStorage.gameData.map.tileAt(tileX, tileY);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of passing the Tile in the constructor, extracting the X,Y coords, and the get the Tile itself from the coords? Why not use the Tile/TileId directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly just trying to follow the existing message patterns here.

This is the UI--Engine boundary, so having the message be easy to serialise is probably a good idea. If the message can be fully described with primitive values and then hydrated to the actual game object when processed, that's probably a win for portability, multiplayer, etc. Tile Id could work here as well, I haven't really looked at the usage patterns, but coordinates at least should be fairly stable in any game.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, which pattern are you referring to here? I am not seeing anything similar in the other messages.

Messages that can be serialized, is something that I haven't though of directly for this game.

The pattern so far seems to be to use DTOs instead of directly serializing an object, so I would guess that's how it would be for messages too, as they handle City, TilePath or MapUnit objects at the moment directly.

Anyway, I don't have a problem with this, if it were me, I would simplify it as I mentioned, but I don't mind this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of the engine messages in this file (MsgSetUnitPath, MsgMoveUnit, MsgSetFortification, ...) feature a constructor that takes a UnitId from a MapUnit. This id is then hydrated into the MapUnit inside process(..). Why do these messages bother with an id instead of the full MapUnit?

This is the pattern I'm building on:

UnitId --> MapUnit
TileId/Coords --> Tile

Any thoughts on engine message data fields vs full DTOs, @esbudylin ?

Comment thread C7Engine/C7GameData/Tile.cs Outdated
Comment thread C7/Map/BombardLayer.cs Outdated
this.folderPath = "Art/Animations/" + effectCategories[effect];
this.iniFileName = effectINIFileNames[effect];
this.action = MapUnit.AnimatedAction.DEATH;
this.effect = effect;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a comment on this particular PR, but since you had some friction with it now, does this whole system look ok to you (AnimationManager, C7Animation, AnimationTracker, etc)?
Let me know if you have any comments, I am trying to refactor this on the side, make it more versatile and extendible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current effect animation feels very glued-on on top of the unit animation scheme. Rethinking/refactoring the animation system as a whole makes sense.

The bomber plane bombardment animation might be a good starting point as it's a complex sequence. If you can support that, then everything else is easy.

I kinda like the approach of having the animation scheduling and progress-dependent rendering be separate parts. It's a bit much initially, but I can see how it can be really flexible. Maybe we need a central scheduling scheme and then a few different renderers. Could have simpler tile-based ones for unit and effect, and then a more complex renderer that can handle moving the render target on screen.

Comment thread C7/Map/UnitLayer.cs
@@ -162,13 +162,19 @@ private Vector2 GetFramePosition(MapUnit.Appearance appearance, C7Animation unit
}

public void drawEffectAnimFrame(LooseView looseView, C7Animation anim, float progress, Vector2 tileCenter) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question rather than critique per se, do you think the effects animations in general should live in the Unit Layer? What if we had a dedicated EffectsLayer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separate EffectLayer could work fine. The effects generally are unit-linked so they can in my view live alongside the units, but a separate layer could make it easier to, well, layer. The z-index hackery doesn't scale.

Comment thread C7Engine/EntryPoints/CreateGame.cs Outdated
false => throw new Exception($"The provided save does not contain a human player"),
};

EngineStorage.animationsEnabled = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I forgot to adress this in the previous review.

This looks inoccent enough, although I am pretty sure it's gonna have side effects. I need to investigate further.
If you have more detailed clues as to why the game gets stuck in an infinite loop, let me know.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tracked the issue down to the fight(..) method in MapUnit. The comment "Do combat rounds" (line 355) is at the start of a loop that never resolves with the attached save file. It's not actually looping, I think the animation system just isn't ready to receive the animation render requests for the AI civ's fight animation at game load.

Indeed, my save files load differently in OC3 and Civ3, as the same save file in the former starts with AI civs making their moves, while latter starts with human user's (previous?) turn ended.

Attached zipped save file reproduces the issue, getting stuck on await attacker.animateAsync(ATTACK1); in fight(..) (L358 in my codebase).

on_load_animation_bug.json.zip

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't get this fully. Does the saved game begin with the AI attacking? If so, it shouldn't be possible. It shouldn't be possible to save the game while someone else is moving in the first place.

If the game was saved at the very end of the round, where you have to press the button/Enter to finish the round, then loading it up begins the round without waiting for the human to trigger it, it's faulty behaviour.

Regardless, I don't think disabling all animations is a valid workaround, there is clearly something else going on here, and we should address this instead.

Also, as far as I understand, this is not bombard related, right? Any attack triggers this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this workaround. The load issue is not bombard-specific.

But yes, I observed the game loading with AI civ units making moves, including combat, which triggers this bug. I've been able to replicate this with both normal saves and ones I modified by giving the AI civs more units. (I was actually trying to replicate the city crashing issue you mentioned in issue 907 by having all the civs at war with each other: this mayhem led me to this issue initially.)

Anyway, I've created a new save file that reliably results in an all-black map for me:
on_load_animation_bug_2.json.zip

@ajhalme
Copy link
Copy Markdown
Contributor Author

ajhalme commented May 4, 2026

Added basic improvements bombardment and even city and population bombardment. Had a stab at introducing city walls logic as well.

Added some basic wiring for requiring war declaration, disallowing friendly fire, disallowing bombarding of in-range empty tiles, etc.

Merged in the recent changes, plus the not yet merged test file change.

Good to go - bombard has everything I wanted for now. Didn't explore how things break with naval vessels and air things, but that's for another PR, I reckon.

Comment thread C7Engine/C7GameData/City.cs
Comment thread C7Engine/C7GameData/MapUnit.cs Outdated
Comment thread C7Engine/C7GameData/Tile.cs
Comment thread C7Engine/EntryPoints/CreateGame.cs Outdated
false => throw new Exception($"The provided save does not contain a human player"),
};

EngineStorage.animationsEnabled = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't get this fully. Does the saved game begin with the AI attacking? If so, it shouldn't be possible. It shouldn't be possible to save the game while someone else is moving in the first place.

If the game was saved at the very end of the round, where you have to press the button/Enter to finish the round, then loading it up begins the round without waiting for the human to trigger it, it's faulty behaviour.

Regardless, I don't think disabling all animations is a valid workaround, there is clearly something else going on here, and we should address this instead.

Also, as far as I understand, this is not bombard related, right? Any attack triggers this


public override async void process() {
MapUnit unit = EngineStorage.gameData.GetUnit(unitID);
Tile tile = EngineStorage.gameData.map.tileAt(tileX, tileY);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, which pattern are you referring to here? I am not seeing anything similar in the other messages.

Messages that can be serialized, is something that I haven't though of directly for this game.

The pattern so far seems to be to use DTOs instead of directly serializing an object, so I would guess that's how it would be for messages too, as they handle City, TilePath or MapUnit objects at the moment directly.

Anyway, I don't have a problem with this, if it were me, I would simplify it as I mentioned, but I don't mind this.

Comment thread C7/Map/BombardLayer.cs
DrawSquare(looseView, left, top, right, bottom);
}

private void drawTargetBombardTile(LooseView looseView, Vector2 tileCenter) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's the same thin brown outline pattern for the target square as in here:

private void DrawOccupiedTileSquare(LooseView looseView, Vector2 tileCenter) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the mechanism looks similar - but just different enough that making one work like the other is a bit involved. Something to look into for consolidation/refactoring.

I'm also inclined to break up the monolithic MapView.cs file, bring LooseLayer closer to the usage. Maybe then we can have a layer utilities library where we can put some of these reusable pieces.

Comment thread C7Engine/C7GameData/MapUnit.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants