-
-
Notifications
You must be signed in to change notification settings - Fork 78
Skills with a CELL pattern hit their pattern, not a single target #2289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3311f44
ed7e5fd
ea7d1f0
c22b0ad
986dbfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| using Microsoft.EntityFrameworkCore.Migrations; | ||
|
|
||
| #nullable disable | ||
|
|
||
| namespace NosCore.Database.Migrations | ||
| { | ||
| /// <inheritdoc /> | ||
| public partial class AddSkillCellPattern : Migration | ||
| { | ||
| /// <inheritdoc /> | ||
| protected override void Up(MigrationBuilder migrationBuilder) | ||
| { | ||
| migrationBuilder.AddColumn<string>( | ||
| name: "CellPattern", | ||
| table: "Skill", | ||
| type: "character varying(512)", | ||
| maxLength: 512, | ||
| nullable: true); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override void Down(MigrationBuilder migrationBuilder) | ||
| { | ||
| migrationBuilder.DropColumn( | ||
| name: "CellPattern", | ||
| table: "Skill"); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // __ _ __ __ ___ __ ___ ___ | ||
| // | \| |/__\ /' _/ / _//__\| _ \ __| | ||
| // | | ' | \/ |`._`.| \_| \/ | v / _| | ||
| // |_|\__|\__/ |___/ \__/\__/|_|_\___| | ||
| // | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Globalization; | ||
|
|
||
| namespace NosCore.GameObject.Services.BattleService; | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment is having some data that is just example not sure how useful those are
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All gone in 2f5177d — the four you named plus the file header and the two in the tests, so the whole PR is comment-free now. The two things that were genuinely non-obvious moved into the PR description instead: the CELL triples end on a Build clean, 1004 tests green. |
||
| public static class SkillCells | ||
| { | ||
| public static sbyte[]? Parse(string? pattern) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(pattern)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var parts = pattern.Split(','); | ||
|
|
||
| // Pairs, so an odd count is a broken row, not a pattern with a spare coordinate. | ||
| if (parts.Length == 0 || parts.Length % 2 != 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var cells = new sbyte[parts.Length]; | ||
| for (var i = 0; i < parts.Length; i++) | ||
| { | ||
| if (!sbyte.TryParse(parts[i], NumberStyles.Integer, CultureInfo.InvariantCulture, | ||
| out cells[i])) | ||
| { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| return cells; | ||
| } | ||
|
|
||
| public static HashSet<(short X, short Y)> Resolve(sbyte[] pattern, short casterX, | ||
| short casterY, short targetX, short targetY) | ||
| { | ||
| var cells = new HashSet<(short, short)>(pattern.Length / 2); | ||
|
|
||
| double dx = targetX - casterX; | ||
| double dy = targetY - casterY; | ||
| var len = Math.Sqrt((dx * dx) + (dy * dy)); | ||
|
|
||
| double ux = 0, uy = -1; | ||
| if (len > 0.0001) | ||
| { | ||
| ux = dx / len; | ||
| uy = dy / len; | ||
| } | ||
|
|
||
| foreach (var (cx, cy) in Pairs(pattern)) | ||
| { | ||
| double right = cx; | ||
| double forward = -cy; | ||
|
|
||
| var x = (right * -uy) + (forward * ux); | ||
| var y = (right * ux) + (forward * uy); | ||
|
|
||
| cells.Add(((short)(casterX + Math.Round(x, MidpointRounding.AwayFromZero)), | ||
| (short)(casterY + Math.Round(y, MidpointRounding.AwayFromZero)))); | ||
| } | ||
|
|
||
| return cells; | ||
| } | ||
|
|
||
| private static IEnumerable<(sbyte X, sbyte Y)> Pairs(sbyte[] pattern) | ||
| { | ||
| for (var i = 0; i + 1 < pattern.Length; i += 2) | ||
| { | ||
| yield return (pattern[i], pattern[i + 1]); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,6 +54,7 @@ public FluentParserBuilder<SkillDto> BuildParser(string folder) | |
| .Field(x => x.Type, chunk => Convert.ToByte(chunk["TYPE"][0][5])) | ||
| .Field(x => x.Element, chunk => Convert.ToByte(chunk["TYPE"][0][7])) | ||
| .Field(x => x.Combo, chunk => AddCombos(chunk)) | ||
| .Field(x => x.CellPattern, chunk => ReadCellPattern(chunk)) | ||
| .Field(x => x.CpCost, chunk => chunk["COST"][0][2] == "-1" ? (byte)0 : byte.Parse(chunk["COST"][0][2])) | ||
| .Field(x => x.Price, chunk => Convert.ToInt32(chunk["COST"][0][3])) | ||
| .Field(x => x.CastEffect, chunk => Convert.ToInt16(chunk["EFFECT"][0][3])) | ||
|
|
@@ -144,6 +145,48 @@ private List<BCardDto> AddBCards(Dictionary<string, string[][]> chunks) | |
| return list; | ||
| } | ||
|
|
||
| // CELL holds thirty cells at most; a longer pattern continues in the unused tail of | ||
| // COST. A continues flag on the last available triple means the row ran out, not that | ||
| // there is more, so the tail is read and may well be empty. | ||
| private static string? ReadCellPattern(Dictionary<string, string[][]> chunks) | ||
| { | ||
| if (!chunks.TryGetValue("CELL", out var cell) || cell.Length == 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var cells = new List<int>(); | ||
| var ranOut = ReadTriples(cell[0], 4, cells); | ||
|
|
||
| if (ranOut && chunks.TryGetValue("COST", out var cost) && cost.Length > 0) | ||
| { | ||
| ReadTriples(cost[0], 5, cells); | ||
| } | ||
|
|
||
| return cells.Count == 0 ? null : string.Join(",", cells); | ||
| } | ||
|
|
||
| // Field [0] is empty and [1] is the section name, so CELL's triples start at [4] and | ||
| // COST's tail at [5]. | ||
| private static bool ReadTriples(string[] fields, int start, List<int> into) | ||
| { | ||
| for (var i = start; i + 2 < fields.Length; i += 3) | ||
| { | ||
| if (!int.TryParse(fields[i], out var dx) | ||
| || !int.TryParse(fields[i + 1], out var dy) | ||
| || !int.TryParse(fields[i + 2], out var continues) | ||
| || continues == 0) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| into.Add(dx); | ||
| into.Add(dy); | ||
| } | ||
|
Comment on lines
+175
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Discard the complete pattern after an invalid triple. If a later Make the parse result distinguish an invalid field from a zero terminator. Return 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| return true; | ||
| } | ||
|
|
||
| // FCOMBO's first field is a switch (has a chain / has not), not a step. The row starts | ||
| // at index 2, so triplet j starts at 3 + j*3; counting from the switch shifted every | ||
| // step by one field and the chain never fired. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| // __ _ __ __ ___ __ ___ ___ | ||
| // | \| |/__\ /' _/ / _//__\| _ \ __| | ||
| // | | ' | \/ |`._`.| \_| \/ | v / _| | ||
| // |_|\__|\__/ |___/ \__/\__/|_|_\___| | ||
| // | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
| using NosCore.GameObject.Services.BattleService; | ||
|
|
||
| namespace NosCore.GameObject.Tests.Services.BattleService | ||
| { | ||
| [TestClass] | ||
| public class SkillCellsTests | ||
| { | ||
| private const string PiercingShot = "0,-8,0,-7,0,-6,0,-5,0,-4,0,-3,0,-2,0,-1"; | ||
|
|
||
| private static List<(sbyte dx, sbyte dy)> Pairs(sbyte[] flat) | ||
| { | ||
| var cells = new List<(sbyte, sbyte)>(); | ||
| for (var i = 0; i < flat.Length; i += 2) | ||
| { | ||
| cells.Add((flat[i], flat[i + 1])); | ||
| } | ||
|
|
||
| return cells; | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void AimingNorthLeavesThePatternAsWritten() | ||
| { | ||
| var pattern = SkillCells.Parse(PiercingShot)!; | ||
| var cells = SkillCells.Resolve(pattern, 50, 50, 50, 40); | ||
|
|
||
| Assert.AreEqual(8, cells.Count); | ||
| for (short dy = 1; dy <= 8; dy++) | ||
| { | ||
| Assert.IsTrue(cells.Contains((50, (short)(50 - dy))), $"missing (50,{50 - dy})"); | ||
| } | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void AimingSouthTurnsThePatternAround() | ||
| { | ||
| var pattern = SkillCells.Parse(PiercingShot)!; | ||
| var cells = SkillCells.Resolve(pattern, 50, 50, 50, 60); | ||
|
|
||
| Assert.AreEqual(8, cells.Count); | ||
| for (short dy = 1; dy <= 8; dy++) | ||
| { | ||
| Assert.IsTrue(cells.Contains((50, (short)(50 + dy))), $"missing (50,{50 + dy})"); | ||
| } | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void AimingEastPutsTheLineOnTheXAxis() | ||
| { | ||
| var pattern = SkillCells.Parse(PiercingShot)!; | ||
| var cells = SkillCells.Resolve(pattern, 50, 50, 60, 50); | ||
|
|
||
| Assert.AreEqual(8, cells.Count); | ||
| for (short dx = 1; dx <= 8; dx++) | ||
| { | ||
| Assert.IsTrue(cells.Contains(((short)(50 + dx), 50)), $"missing ({50 + dx},50)"); | ||
| } | ||
| } | ||
|
|
||
| // Diagonals round, so demand the quadrant and the reach rather than exact cells. | ||
| [TestMethod] | ||
| public void AimingDiagonallyPointsTheLineAtTheTarget() | ||
| { | ||
| var pattern = SkillCells.Parse(PiercingShot)!; | ||
| var cells = SkillCells.Resolve(pattern, 50, 50, 60, 40); | ||
|
|
||
| Assert.IsTrue(cells.All(c => c.X >= 50 && c.Y <= 50), | ||
| "a cell landed outside the target's quadrant"); | ||
|
|
||
| var furthest = cells.Max(c => System.Math.Max(System.Math.Abs(c.X - 50), | ||
| System.Math.Abs(c.Y - 50))); | ||
| Assert.AreEqual(6, furthest, | ||
| "eight diagonal steps reach six cells on each axis, not eight"); | ||
| } | ||
|
|
||
| // Without the zero-distance guard the normalisation divides by zero and every cell | ||
| // collapses onto the caster, silently. | ||
| [TestMethod] | ||
| public void CastingOnYourOwnCellKeepsTheWrittenOrientation() | ||
| { | ||
| var pattern = SkillCells.Parse(PiercingShot)!; | ||
| var cells = SkillCells.Resolve(pattern, 50, 50, 50, 50); | ||
|
|
||
| Assert.AreEqual(8, cells.Count); | ||
| Assert.IsTrue(cells.Contains((50, 42))); | ||
| } | ||
|
|
||
| // Most skills have no drawing: 1890 of the 1958. The column is null for them. | ||
| [TestMethod] | ||
| public void ASkillWithoutADrawingHasNoPattern() | ||
| { | ||
| Assert.IsNull(SkillCells.Parse(null)); | ||
| Assert.IsNull(SkillCells.Parse("")); | ||
| Assert.IsNull(SkillCells.Parse(" ")); | ||
| } | ||
|
|
||
| // A broken row must not stop a fight: the pattern decides who a skill hits, and throwing | ||
| // here would take down the cast instead of degrading it to a single target. | ||
| [TestMethod] | ||
| public void AMalformedColumnIsNoPatternRatherThanAnException() | ||
| { | ||
| Assert.IsNull(SkillCells.Parse("0,-1,0"), "an odd count is a broken row, not a cell"); | ||
| Assert.IsNull(SkillCells.Parse("0,-1,x,2")); | ||
| Assert.IsNull(SkillCells.Parse("0,-1,,2")); | ||
| Assert.IsNull(SkillCells.Parse("0,-1,200,2"), "200 does not fit an sbyte"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void AWellFormedColumnComesBackAsPairs() | ||
| { | ||
| var pattern = SkillCells.Parse(PiercingShot)!; | ||
| Assert.AreEqual(16, pattern.Length); | ||
|
|
||
| var cells = Pairs(pattern); | ||
| Assert.AreEqual(8, cells.Count); | ||
| Assert.IsTrue(cells.All(c => c.dx == 0), "the line is one cell wide"); | ||
| Assert.AreEqual(-8, cells.Min(c => c.dy)); | ||
| Assert.AreEqual(-1, cells.Max(c => c.dy)); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove UTF-8 BOMs from the new C# files.
src/NosCore.GameObject/Services/BattleService/SkillCells.cs#L1-L1: remove the UTF-8 BOM before the file header.test/NosCore.GameObject.Tests/Services/BattleService/SkillCellsTests.cs#L1-L1: remove the UTF-8 BOM before the file header.As per coding guidelines, “No UTF-8 BOM on
.csfiles.”📍 Affects 2 files
src/NosCore.GameObject/Services/BattleService/SkillCells.cs#L1-L1(this comment)test/NosCore.GameObject.Tests/Services/BattleService/SkillCellsTests.cs#L1-L1🤖 Prompt for AI Agents
Source: Coding guidelines