Skip to content

feat(naval): shipyard vessel construction, wind vector navigation, and cannon broadside volleys - #199

Open
angelTomo9 wants to merge 1 commit into
Bitcoindefi:mainfrom
angelTomo9:feat-naval-shipyard-1787860927148
Open

feat(naval): shipyard vessel construction, wind vector navigation, and cannon broadside volleys#199
angelTomo9 wants to merge 1 commit into
Bitcoindefi:mainfrom
angelTomo9:feat-naval-shipyard-1787860927148

Conversation

@angelTomo9

Copy link
Copy Markdown

Summary

Implements a naval shipyard, coastal war galleon, and ocean navigation engine for OpenAO MMORPG.

Features

  • Shipyard vessel construction across multiple classes (Fishing Boat, Cargo Clipper, War Galleon, Ghost Frigate)
  • Hull material durability scaling (Oak, Reinforced Ironwood, Cursed Ghostwood)
  • Wind-vector trigonometric sailing speed calculations with cargo hold weight penalties
  • Multi-cannon broadside volleys with reload cooldowns and armor mitigation
  • Drydock repair facilities for damaged vessels
  • Full unit test coverage under Vitest

Comment on lines +117 to +131
* Computes the effective sailing speed in knots based on ship heading and wind vector angle.
* @param headingDegrees Vessel heading (0 to 360)
* @param windDegrees Wind origin direction (0 to 360)
*/
public static calculateSailingSpeed(
vessel: ConstructedVessel,
headingDegrees: number,
windDegrees: number
): number {
if (!vessel || vessel.isSunk) return 0;

const bp = VESSEL_BLUEPRINTS[vessel.vesselClass];
const heading = Number.isFinite(headingDegrees) ? ((headingDegrees % 360) + 360) % 360 : 0;
const wind = Number.isFinite(windDegrees) ? ((windDegrees % 360) + 360) % 360 : 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Wind efficiency inverted vs. documented "wind origin" semantics

The docstring states windDegrees is the "Wind origin direction" (the direction the wind blows FROM). With that convention, a ship whose heading equals the wind-origin bearing is sailing directly INTO the wind (a headwind), yet the code gives that case angleDiff = 0 and the maximum 1.25x tailwind boost, while sailing away from the wind origin (angleDiff = 180) yields the 0.35x headwind penalty. The physics is reversed relative to the documented input. Either flip the efficiency curve (use (1 - Math.cos(rad)) / 2) or fix the docstring to say windDegrees is the wind's travel/heading-to direction.

Invert the cosine term so a wind originating from directly ahead is a headwind (0.35x) and one from astern is a tailwind (1.25x), matching the documented "wind origin" semantics.:

// windDegrees = direction wind blows FROM; tailwind when it opposes heading.
const rad = (angleDiff * Math.PI) / 180;
const windEfficiency = 0.35 + 0.90 * ((1 - Math.cos(rad)) / 2);
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines +160 to +162
if (!defender || defender.isSunk) {
return { damageDealt: 0, isTargetSunk: true, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is already sunken." };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Bug: Null defender reported as sunk (isTargetSunk: true)

When defender is null/undefined, the volley returns isTargetSunk: true and remainingTargetHp: 0, conflating "invalid/missing target" with "target destroyed." A caller that treats isTargetSunk as a kill confirmation could wrongly credit a sink. Only set isTargetSunk: true when the defender actually exists and is sunk; for a missing defender report isTargetSunk: false with the descriptive reason.

Fix:

if (!defender) {
    return { damageDealt: 0, isTargetSunk: false, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is invalid." };
}
if (defender.isSunk) {
    return { damageDealt: 0, isTargetSunk: true, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is already sunken." };
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines +95 to +100
const bp = VESSEL_BLUEPRINTS[vesselClass];
if (!bp) {
throw new Error(`Unsupported vessel class: ${String(vesselClass)}`);
}

const hullFactor = HULL_MULTIPLIERS[hullMaterial] ?? 1.0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: Inconsistent handling of invalid enum inputs

constructVessel throws on an unknown vesselClass but silently falls back to a 1.0 multiplier (?? 1.0) for an unknown hullMaterial. This asymmetry means a typo'd/invalid hull material is accepted and produces an OAK-equivalent ship with no signal to the caller. Either validate both (throw on unknown hull) or document that unknown hulls default to 1.0, so behavior is predictable.

Fix:

const hullFactor = HULL_MULTIPLIERS[hullMaterial];
if (hullFactor === undefined) {
    throw new Error(`Unsupported hull material: ${String(hullMaterial)}`);
}
const totalMaxHp = Math.floor(bp.baseMaxHp * hullFactor);
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines +208 to +210
if (!vessel || vessel.isSunk) {
return { success: false, healedHp: 0, currentHp: 0, reason: "Cannot drydock repair a sunken vessel." };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: repairVessel returns currentHp: 0 for sunk vessel

When rejecting a repair on a sunk vessel, the function returns currentHp: 0 regardless of the vessel's actual currentHp. This is misleading for callers that read currentHp from the result. Return the vessel's real currentHp on the failure path.

Fix:

if (!vessel || vessel.isSunk) {
    return { success: false, healedHp: 0, currentHp: vessel?.currentHp ?? 0, reason: "Cannot drydock repair a sunken vessel." };
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 4 findings

Implements naval shipyard construction, wind-based sailing mechanics, and cannon volleys for the OpenAO MMORPG. Wind efficiency calculation is inverted relative to the documented "wind origin" semantics—a ship sailing into the wind receives a tailwind boost instead of a headwind penalty. Additionally, null defenders are incorrectly reported as sunk, hull material validation is inconsistent with vessel class validation, and repairVessel returns misleading HP values on failed repairs. These issues should be resolved before merge.

⚠️ Bug: Wind efficiency inverted vs. documented "wind origin" semantics

📄 api/src/lib/navalShipyardTradeVessel.ts:117-131

The docstring states windDegrees is the "Wind origin direction" (the direction the wind blows FROM). With that convention, a ship whose heading equals the wind-origin bearing is sailing directly INTO the wind (a headwind), yet the code gives that case angleDiff = 0 and the maximum 1.25x tailwind boost, while sailing away from the wind origin (angleDiff = 180) yields the 0.35x headwind penalty. The physics is reversed relative to the documented input. Either flip the efficiency curve (use (1 - Math.cos(rad)) / 2) or fix the docstring to say windDegrees is the wind's travel/heading-to direction.

Invert the cosine term so a wind originating from directly ahead is a headwind (0.35x) and one from astern is a tailwind (1.25x), matching the documented "wind origin" semantics.
// windDegrees = direction wind blows FROM; tailwind when it opposes heading.
const rad = (angleDiff * Math.PI) / 180;
const windEfficiency = 0.35 + 0.90 * ((1 - Math.cos(rad)) / 2);
💡 Bug: Null defender reported as sunk (isTargetSunk: true)

📄 api/src/lib/navalShipyardTradeVessel.ts:160-162

When defender is null/undefined, the volley returns isTargetSunk: true and remainingTargetHp: 0, conflating "invalid/missing target" with "target destroyed." A caller that treats isTargetSunk as a kill confirmation could wrongly credit a sink. Only set isTargetSunk: true when the defender actually exists and is sunk; for a missing defender report isTargetSunk: false with the descriptive reason.

Fix
if (!defender) {
    return { damageDealt: 0, isTargetSunk: false, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is invalid." };
}
if (defender.isSunk) {
    return { damageDealt: 0, isTargetSunk: true, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is already sunken." };
}
💡 Quality: Inconsistent handling of invalid enum inputs

📄 api/src/lib/navalShipyardTradeVessel.ts:95-100

constructVessel throws on an unknown vesselClass but silently falls back to a 1.0 multiplier (?? 1.0) for an unknown hullMaterial. This asymmetry means a typo'd/invalid hull material is accepted and produces an OAK-equivalent ship with no signal to the caller. Either validate both (throw on unknown hull) or document that unknown hulls default to 1.0, so behavior is predictable.

Fix
const hullFactor = HULL_MULTIPLIERS[hullMaterial];
if (hullFactor === undefined) {
    throw new Error(`Unsupported hull material: ${String(hullMaterial)}`);
}
const totalMaxHp = Math.floor(bp.baseMaxHp * hullFactor);
💡 Quality: repairVessel returns currentHp: 0 for sunk vessel

📄 api/src/lib/navalShipyardTradeVessel.ts:208-210

When rejecting a repair on a sunk vessel, the function returns currentHp: 0 regardless of the vessel's actual currentHp. This is misleading for callers that read currentHp from the result. Return the vessel's real currentHp on the failure path.

Fix
if (!vessel || vessel.isSunk) {
    return { success: false, healedHp: 0, currentHp: vessel?.currentHp ?? 0, reason: "Cannot drydock repair a sunken vessel." };
}
🤖 Prompt for agents
Code Review: Implements naval shipyard construction, wind-based sailing mechanics, and cannon volleys for the OpenAO MMORPG. Wind efficiency calculation is inverted relative to the documented "wind origin" semantics—a ship sailing into the wind receives a tailwind boost instead of a headwind penalty. Additionally, null defenders are incorrectly reported as sunk, hull material validation is inconsistent with vessel class validation, and `repairVessel` returns misleading HP values on failed repairs. These issues should be resolved before merge.

1. ⚠️ Bug: Wind efficiency inverted vs. documented "wind origin" semantics
   Files: api/src/lib/navalShipyardTradeVessel.ts:117-131

   The docstring states `windDegrees` is the "Wind origin direction" (the direction the wind blows FROM). With that convention, a ship whose heading equals the wind-origin bearing is sailing directly INTO the wind (a headwind), yet the code gives that case `angleDiff = 0` and the maximum 1.25x tailwind boost, while sailing away from the wind origin (angleDiff = 180) yields the 0.35x headwind penalty. The physics is reversed relative to the documented input. Either flip the efficiency curve (use `(1 - Math.cos(rad)) / 2`) or fix the docstring to say `windDegrees` is the wind's travel/heading-to direction.

   Fix (Invert the cosine term so a wind originating from directly ahead is a headwind (0.35x) and one from astern is a tailwind (1.25x), matching the documented "wind origin" semantics.):
   // windDegrees = direction wind blows FROM; tailwind when it opposes heading.
   const rad = (angleDiff * Math.PI) / 180;
   const windEfficiency = 0.35 + 0.90 * ((1 - Math.cos(rad)) / 2);

2. 💡 Bug: Null defender reported as sunk (isTargetSunk: true)
   Files: api/src/lib/navalShipyardTradeVessel.ts:160-162

   When `defender` is null/undefined, the volley returns `isTargetSunk: true` and `remainingTargetHp: 0`, conflating "invalid/missing target" with "target destroyed." A caller that treats `isTargetSunk` as a kill confirmation could wrongly credit a sink. Only set `isTargetSunk: true` when the defender actually exists and is sunk; for a missing defender report `isTargetSunk: false` with the descriptive reason.

   Fix:
   if (!defender) {
       return { damageDealt: 0, isTargetSunk: false, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is invalid." };
   }
   if (defender.isSunk) {
       return { damageDealt: 0, isTargetSunk: true, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is already sunken." };
   }

3. 💡 Quality: Inconsistent handling of invalid enum inputs
   Files: api/src/lib/navalShipyardTradeVessel.ts:95-100

   `constructVessel` throws on an unknown `vesselClass` but silently falls back to a 1.0 multiplier (`?? 1.0`) for an unknown `hullMaterial`. This asymmetry means a typo'd/invalid hull material is accepted and produces an OAK-equivalent ship with no signal to the caller. Either validate both (throw on unknown hull) or document that unknown hulls default to 1.0, so behavior is predictable.

   Fix:
   const hullFactor = HULL_MULTIPLIERS[hullMaterial];
   if (hullFactor === undefined) {
       throw new Error(`Unsupported hull material: ${String(hullMaterial)}`);
   }
   const totalMaxHp = Math.floor(bp.baseMaxHp * hullFactor);

4. 💡 Quality: repairVessel returns currentHp: 0 for sunk vessel
   Files: api/src/lib/navalShipyardTradeVessel.ts:208-210

   When rejecting a repair on a sunk vessel, the function returns `currentHp: 0` regardless of the vessel's actual `currentHp`. This is misleading for callers that read `currentHp` from the result. Return the vessel's real `currentHp` on the failure path.

   Fix:
   if (!vessel || vessel.isSunk) {
       return { success: false, healedHp: 0, currentHp: vessel?.currentHp ?? 0, reason: "Cannot drydock repair a sunken vessel." };
   }

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 5 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

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.

1 participant