mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-09-08 11:25:10 -05:00
Automatically wrap custom card names and calculate text scale on the client
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user