Automatically wrap custom card names and calculate text scale on the client

This commit is contained in:
Andrio Celos
2024-02-09 17:07:43 +11:00
parent 548a3116b2
commit 42dc0b462b
9 changed files with 319 additions and 274 deletions

View File

@@ -1,9 +1,9 @@
class Card {
number: number;
altNumber?: number | null;
name: string;
line1?: string | null;
line2?: string | null;
readonly name: string;
readonly line1: string | null;
readonly line2: string | null;
artFileName?: string | null;
imageUrl?: string;
textScale: number;
@@ -22,10 +22,49 @@ class Card {
static DEFAULT_INK_COLOUR_1: Colour = { r: 116, g: 96, b: 240 };
static DEFAULT_INK_COLOUR_2: Colour = { r: 224, g: 242, b: 104 };
constructor(number: number, name: string, textScale: number, inkColour1: Colour, inkColour2: Colour, rarity: Rarity, specialCost: number, grid: Space[][]) {
private static textScaleCalculationContext: OffscreenCanvasRenderingContext2D | null = null;
private static getTextScaleCalculationContext() {
if (this.textScaleCalculationContext == null) {
const canvas = new OffscreenCanvas(256, 256);
this.textScaleCalculationContext = canvas.getContext("2d")!;
this.textScaleCalculationContext.font = 'bold 72pt "Splatoon 1"';
}
return this.textScaleCalculationContext;
}
static wrapName(name: string): [line1: string | null, line2: string | null] {
// If the user has entered manual line breaks, use those instead of auto-wrapping.
const pos = name.indexOf('\n');
if (pos >= 0)
return [ name.substring(0, pos), name.substring(pos + 1) ];
const ctx = Card.getTextScaleCalculationContext();
const line1Width = ctx.measureText(name).width;
if (line1Width <= 700)
return [ null, null ];
// We're going to break the line.
let bestPos = 0; let bestWidth = Infinity;
for (const m of name.matchAll(/[-\s]/g)) {
const pos = m.index! + 1;
const width = Math.max(ctx.measureText(name.substring(0, m[0] == ' ' ? pos - 1 : pos)).width, ctx.measureText(name.substring(pos)).width);
if (width < bestWidth) {
bestPos = pos;
bestWidth = width;
}
}
return bestPos > 0
? [ name.substring(0, bestPos).trimEnd(), name.substring(bestPos) ]
: [ null, null ];
}
constructor(number: number, name: string, line1: string | null, line2: string | null, inkColour1: Colour, inkColour2: Colour, rarity: Rarity, specialCost: number, grid: Space[][]) {
this.number = number;
this.name = name;
this.textScale = textScale;
this.line1 = line1;
this.line2 = line2;
this.inkColour1 = inkColour1;
this.inkColour2 = inkColour2;
this.rarity = rarity;
@@ -62,14 +101,18 @@ class Card {
if (!hasSpecialSpace && this.specialCost > 3)
this.specialCost = 3;
}
const ctx = Card.getTextScaleCalculationContext();
const line1Width = ctx.measureText(line1 ?? name).width;
const line2Width = line2 != null ? ctx.measureText(line2).width : 0;
const width = Math.max(line1Width, line2Width);
this.textScale = width <= 700 ? 1 : 700 / width;
}
static fromJson(obj: any) {
if (cardDatabase.cards && cardDatabase.isValidCardNumber(obj.number)) return cardDatabase.get(obj.number);
const card = new Card(obj.number, obj.name, obj.textScale ?? 1, obj.inkColour1 ?? this.DEFAULT_INK_COLOUR_1, obj.inkColour2 ?? this.DEFAULT_INK_COLOUR_2, obj.rarity, obj.specialCost, obj.grid);
const card = new Card(obj.number, obj.name, obj.line1, obj.line2, obj.inkColour1 ?? this.DEFAULT_INK_COLOUR_1, obj.inkColour2 ?? this.DEFAULT_INK_COLOUR_2, obj.rarity, obj.specialCost, obj.grid);
card.altNumber = obj.altNumber ?? null;
card.line1 = obj.line1 ?? null;
card.line2 = obj.line2 ?? null;
card.artFileName = obj.artFileName ?? null;
card.imageUrl = obj.imageUrl ?? null;
card.isVariantOf = obj.isVariantOf ?? null;

View File

@@ -104,7 +104,7 @@ class CardDisplay implements ICardElement {
text1.setAttribute('fill', 'url("#freshGradient")');
break;
}
if (card.line1 && card.line2) {
if (card.line1 != null && card.line2 != null) {
const tspan1 = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan1.setAttribute('y', '122');
tspan1.appendChild(document.createTextNode(card.line1));

View File

@@ -429,12 +429,16 @@ function deckExportJsonReplacer(key: string, value: any) {
case 'imageUrl':
case 'specialCost':
case 'size':
case 'textScale':
case 'isVariantOf':
case 'minX':
case 'minY':
case 'maxX':
case 'maxY':
return undefined;
case 'line1':
case 'line2':
return value ?? undefined; // Omit null values.
default:
return value;
}

View File

@@ -93,6 +93,7 @@ function openGalleryCardView(card: Card) {
galleryCardEditorSubmitButton.hidden = true;
galleryCardEditorCancelButton.innerText = 'Close';
galleryCardEditorName.value = card.line2 == null ? card.name : `${card.line1}\n${card.line2}`;
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
galleryCardEditorGridButtons[y][x].dataset.state = card.grid[y][x].toString();
@@ -284,22 +285,22 @@ galleryCardEditorSpecialCostDefaultBox.addEventListener('change', () => {
galleryCardEditorEditButton.addEventListener('click', () => startEditingCustomCard());
galleryNewCustomCardButton.addEventListener('click', () => {
const card = new Card(UNSAVED_CUSTOM_CARD_INDEX, 'New card', 1, Card.DEFAULT_INK_COLOUR_1, Card.DEFAULT_INK_COLOUR_2, Rarity.Common, 1, Array.from({ length: 8 }, () => [ 0, 0, 0, 0, 0, 0, 0, 0]) );
const card = new Card(UNSAVED_CUSTOM_CARD_INDEX, 'New card', 'New card', null, Card.DEFAULT_INK_COLOUR_1, Card.DEFAULT_INK_COLOUR_2, Rarity.Common, 1, Array.from({ length: 8 }, () => [ 0, 0, 0, 0, 0, 0, 0, 0]) );
openGalleryCardView(card);
startEditingCustomCard();
});
galleryCardEditorSubmitButton.addEventListener('click', () => {
const card = galleryCardDisplay!.card;
card.grid = Array.from(galleryCardEditorGridButtons, r => Array.from(r, b => parseInt(b.dataset.state!)));
card.name = galleryCardEditorName.value;
card.size = customCardSize;
card.specialCost = customCardSpecialCost;
if (card.number == UNSAVED_CUSTOM_CARD_INDEX) {
card.number = CUSTOM_CARD_START - cardDatabase.customCards.length;
const isNew = galleryCardDisplay!.card.number == UNSAVED_CUSTOM_CARD_INDEX;
const number = isNew ? CUSTOM_CARD_START - cardDatabase.customCards.length : galleryCardDisplay!.card.number;
const lines = Card.wrapName(galleryCardEditorName.value);
const card = new Card(number, galleryCardEditorName.value.replaceAll('\n', ' '), lines[0], lines[1], Card.DEFAULT_INK_COLOUR_1, Card.DEFAULT_INK_COLOUR_2,
Rarity.Common, customCardSpecialCost, Array.from(galleryCardEditorGridButtons, r => Array.from(r, b => parseInt(b.dataset.state!))));
if (isNew) {
cardDatabase.customCards.push(card);
addCardToGallery(card);
} else {
cardDatabase.customCards[CUSTOM_CARD_START - number] = card;
updateCardInGallery(card);
}
cardDatabase.customCardsModified = true;

View File

@@ -127,10 +127,13 @@ class ReplayLoader {
if (version >= 4) {
const numCustomCards = this.read7BitEncodedInt();
for (let i = 0; i < numCustomCards; i++) {
const name = this.readString();
const rarity = <Rarity> this.readUint8();
const line1 = this.readString();
const line2 = this.readString();
const name = line2 != '' ? `${line1} ${line2}` : line1;
const b = this.readUint8();
const rarity = <Rarity> b & 0x7F;
const wordWrap = (b & 0x80) != 0;
const specialCost = this.readUint8();
const textScale = this.readFloat();
const inkColour1 = this.readColour();
const inkColour2 = this.readColour();
const grid = [ ];
@@ -145,7 +148,7 @@ class ReplayLoader {
}
grid.push(row);
}
const card = new Card(RECEIVED_CUSTOM_CARD_START - i, name, textScale, inkColour1, inkColour2, rarity, specialCost, grid);
const card = new Card(RECEIVED_CUSTOM_CARD_START - i, name, line1, line2 == '' ? null : line2, inkColour1, inkColour2, rarity, specialCost, grid);
customCards.push(card);
}
}
@@ -232,11 +235,6 @@ class ReplayLoader {
this.pos += 2;
return v;
}
private readFloat() {
const v = this.dataView.getFloat32(this.pos, true);
this.pos += 4;
return v;
}
private readColour(): Colour { return { r: this.readUint8(), g: this.readUint8(), b: this.readUint8() }; }
private readString(length?: number) {
length ??= this.read7BitEncodedInt();

View File

@@ -11,34 +11,33 @@ public class Card {
public int Size { get; }
public int? IsVariantOf { get; init; }
public string? Line1 { get; init; }
public string Line1 { get; init; }
public string? Line2 { get; init; }
public string? ArtFileName { get; init; }
public float TextScale { get; init; }
public Colour? InkColour1 { get; init; }
public Colour? InkColour2 { get; init; }
[JsonProperty]
private readonly Space[,] grid;
internal Card(int number, string name, Rarity rarity, float textScale, string? artFileName, Space[,] grid) : this(number, null, name, rarity, null, textScale, artFileName, grid) { }
internal Card(int number, int? altNumber, string name, Rarity rarity, float textScale, string? artFileName, Space[,] grid) : this(number, altNumber, name, rarity, null, textScale, artFileName, grid) { }
internal Card(int number, string name, Rarity rarity, int? specialCost, float textScale, string? artFileName, Space[,] grid) : this(number, null, name, rarity, specialCost, textScale, artFileName, grid) { }
internal Card(int number, int? altNumber, string name, Rarity rarity, int? specialCost, float textScale, string? artFileName, Space[,] grid) {
internal Card(int number, string name, Rarity rarity, string? artFileName, Space[,] grid) : this(number, null, name, rarity, null, artFileName, grid) { }
internal Card(int number, int? altNumber, string name, Rarity rarity, string? artFileName, Space[,] grid) : this(number, altNumber, name, rarity, null, artFileName, grid) { }
internal Card(int number, string name, Rarity rarity, int? specialCost, string? artFileName, Space[,] grid) : this(number, null, name, rarity, specialCost, artFileName, grid) { }
internal Card(int number, int? altNumber, string name, Rarity rarity, int? specialCost, string? artFileName, Space[,] grid) {
this.Number = number;
this.AltNumber = altNumber;
this.Rarity = rarity;
this.TextScale = textScale;
this.ArtFileName = artFileName;
this.grid = grid ?? throw new ArgumentNullException(nameof(grid));
var pos = (name ?? throw new ArgumentNullException(nameof(name))).IndexOf('\n');
if (pos < 0)
if (pos < 0) {
this.Name = name;
else {
this.Line1 = name;
} else {
this.Name = name[pos - 1] == '-' ? name.Remove(pos, 1) : name.Replace('\n', ' ');
this.Line1 = name[0..pos];
this.Line2 = name[(pos + 1)..];
this.Line2 = name[(pos + 1)..];
}
var size = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ public class PlayerData(int playerIndex, Card[]? hand, Deck? deck, Move? move, L
public PlayerData(int playerIndex, Player player) : this(playerIndex, player.Hand, player.CurrentGameData.Deck, player.Move, player.CardsUsed, player.StageSelectionPrompt) { }
}
public record UserCustomCard(string Name, string? Line1, string? Line2, float TextScale, Colour InkColour1, Colour InkColour2, Rarity Rarity, Space[,] Grid) {
public record UserCustomCard(string Name, string? Line1, string? Line2, Colour InkColour1, Colour InkColour2, Rarity Rarity, Space[,] Grid) {
public bool CheckGrid(out bool hasSpecialSpace, out int size) {
size = 0;
hasSpecialSpace = false;
@@ -61,5 +61,5 @@ public record UserCustomCard(string Name, string? Line1, string? Line2, float Te
return true;
}
public Card ToCard(int number, int altNumber, int? specialCost) => new(number, altNumber, this.Name, this.Rarity, specialCost, this.TextScale, null, this.Grid) { InkColour1 = this.InkColour1, InkColour2 = this.InkColour2 };
public Card ToCard(int number, int altNumber, int? specialCost) => new(number, altNumber, this.Line2 != null ? $"{this.Line1}\n{this.Line2}" : this.Name, this.Rarity, specialCost, null, this.Grid) { InkColour1 = this.InkColour1, InkColour2 = this.InkColour2 };
}

View File

@@ -672,10 +672,10 @@ public class Game(int maxPlayers) {
// Custom cards
writer.Write7BitEncodedInt(this.customCards.Count);
foreach (var card in this.customCards) {
writer.Write(card.Name);
writer.Write(card.Line1 ?? card.Name);
writer.Write(card.Line2 ?? "");
writer.Write((byte) card.Rarity);
writer.Write((byte) card.SpecialCost);
writer.Write(card.TextScale);
writer.Write((byte) card.InkColour1.GetValueOrDefault().R);
writer.Write((byte) card.InkColour1.GetValueOrDefault().G);
writer.Write((byte) card.InkColour1.GetValueOrDefault().B);