feat(naval): shipyard vessel construction, wind vector navigation, and cannon broadside volleys - #199
Conversation
…, and cannon broadside volleys
| * 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; | ||
|
|
There was a problem hiding this comment.
⚠️ 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 👍 / 👎
| if (!defender || defender.isSunk) { | ||
| return { damageDealt: 0, isTargetSunk: true, remainingTargetHp: 0, isOnCooldown: false, reason: "Target vessel is already sunken." }; | ||
| } |
There was a problem hiding this comment.
💡 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 👍 / 👎
| const bp = VESSEL_BLUEPRINTS[vesselClass]; | ||
| if (!bp) { | ||
| throw new Error(`Unsupported vessel class: ${String(vesselClass)}`); | ||
| } | ||
|
|
||
| const hullFactor = HULL_MULTIPLIERS[hullMaterial] ?? 1.0; |
There was a problem hiding this comment.
💡 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 👍 / 👎
| if (!vessel || vessel.isSunk) { | ||
| return { success: false, healedHp: 0, currentHp: 0, reason: "Cannot drydock repair a sunken vessel." }; | ||
| } |
There was a problem hiding this comment.
💡 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 👍 / 👎
Code Review
|
| Auto-apply | Compact |
|
|
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
Summary
Implements a naval shipyard, coastal war galleon, and ocean navigation engine for OpenAO MMORPG.
Features