fix: TextContent 的各种标签

This commit is contained in:
unanmed 2025-02-25 23:06:50 +08:00
parent 72f02726ba
commit d0dae40a5a
3 changed files with 191 additions and 145 deletions

View File

@ -90,6 +90,7 @@
"56": "Method '$1' has been deprecated. Consider using '$2' instead.", "56": "Method '$1' has been deprecated. Consider using '$2' instead.",
"57": "Repeated UI controller on item '$1', new controller will not work.", "57": "Repeated UI controller on item '$1', new controller will not work.",
"58": "Fail to set ellipse round rect, since length of 'ellipse' property should only be 2, 4, 6 or 8. delivered: $1", "58": "Fail to set ellipse round rect, since length of 'ellipse' property should only be 2, 4, 6 or 8. delivered: $1",
"59": "Unknown icon '$1' in parsing text content.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance." "1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
} }

View File

@ -131,24 +131,30 @@ export const TextContent = defineComponent<
const ctx = canvas.ctx; const ctx = canvas.ctx;
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
renderable.forEach(v => { renderable.forEach(v => {
if (v.type === TextContentType.Text) { switch (v.type) {
ctx.fillStyle = v.fillStyle; case TextContentType.Text: {
ctx.strokeStyle = v.strokeStyle; if (v.text.length === 0) return;
ctx.font = v.font; ctx.fillStyle = v.fillStyle;
const text = v.text.slice(0, v.pointer); ctx.strokeStyle = v.strokeStyle;
ctx.font = v.font;
const text = v.text.slice(0, v.pointer);
if (props.fill ?? true) { if (props.fill ?? true) {
ctx.fillText(text, v.x, v.y); ctx.fillText(text, v.x, v.y);
}
if (props.stroke) {
ctx.strokeText(text, v.x, v.y);
}
break;
} }
if (props.stroke) { case TextContentType.Icon: {
ctx.strokeText(text, v.x, v.y); const { renderable: r, x: dx, y: dy, width, height } = v;
const render = r.render;
const [x, y, w, h] = render[0];
const icon = r.autotile ? r.image[0] : r.image;
ctx.drawImage(icon, x, y, w, h, dx, dy, width, height);
break;
} }
} else {
const r = v.renderable;
const render = r.render;
const [x, y, w, h] = render[0];
const icon = r.autotile ? r.image[0] : r.image;
ctx.drawImage(icon, x, y, w, h, v.x, v.y, v.width, v.height);
} }
}); });
}; };

View File

@ -135,7 +135,16 @@ export interface TyperIconRenderable {
renderable: RenderableData | AutotileRenderable; renderable: RenderableData | AutotileRenderable;
} }
export type TyperRenderable = TyperTextRenderable | TyperIconRenderable; export interface TyperWaitRenderable {
type: TextContentType.Wait;
wait: number;
waited: number;
}
export type TyperRenderable =
| TyperTextRenderable
| TyperIconRenderable
| TyperWaitRenderable;
interface TextContentTyperEvent { interface TextContentTyperEvent {
typeStart: []; typeStart: [];
@ -271,49 +280,64 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
private createTyperData(index: number, line: number) { private createTyperData(index: number, line: number) {
const renderable = this.renderObject.data[index]; const renderable = this.renderObject.data[index];
if (!renderable) return false; if (!renderable) return false;
if (renderable.type === TextContentType.Text) { switch (renderable.type) {
if (line < 0 || line > renderable.splitLines.length) { case TextContentType.Text: {
return false; if (line < 0 || line > renderable.splitLines.length) {
return false;
}
const start = renderable.splitLines[line - 1] ?? -1;
const end =
renderable.splitLines[line] ?? renderable.text.length - 1;
const data: TyperTextRenderable = {
type: TextContentType.Text,
x: this.x,
y: this.y,
text: renderable.text.slice(start + 1, end + 1),
font: renderable.font,
fillStyle: renderable.fillStyle,
strokeStyle: this.config.strokeStyle,
pointer: 0
};
this.processingData = data;
this.renderData.push(data);
return true;
} }
const start = renderable.splitLines[line - 1] ?? 0; case TextContentType.Icon: {
const end = renderable.splitLines[line] ?? renderable.text.length; const tex = texture.getRenderable(renderable.icon!);
const data: TyperTextRenderable = { if (!tex) return false;
type: TextContentType.Text, const { render } = tex;
x: this.x, const [, , width, height] = render[0];
y: this.y, const aspect = width / height;
text: renderable.text.slice(start, end), let iconWidth = 0;
font: renderable.font, if (aspect < 1) {
fillStyle: renderable.fillStyle, // 这时候应该把高度限定在当前字体大小
strokeStyle: this.config.strokeStyle, iconWidth = width * (renderable.fontSize / height);
pointer: 0 } else {
}; iconWidth = renderable.fontSize;
this.processingData = data; }
this.renderData.push(data); const data: TyperIconRenderable = {
return true; type: TextContentType.Icon,
} else { x: this.x,
const tex = texture.getRenderable(renderable.icon!); y: this.y,
if (!tex) return false; width: iconWidth,
const { render } = tex; height: iconWidth / aspect,
const [, , width, height] = render[0]; renderable: tex
const aspect = width / height; };
let iconWidth = 0; this.processingData = data;
if (aspect < 1) { this.renderData.push(data);
// 这时候应该把高度限定在当前字体大小 return true;
iconWidth = width * (renderable.fontSize / height); }
} else { case TextContentType.Wait: {
iconWidth = renderable.fontSize; const data: TyperWaitRenderable = {
type: TextContentType.Wait,
wait: renderable.wait!,
waited: 0
};
this.processingData = data;
this.renderData.push(data);
return true;
} }
const data: TyperIconRenderable = {
type: TextContentType.Icon,
x: this.x,
y: this.y,
width: iconWidth,
height: iconWidth / aspect,
renderable: tex
};
this.processingData = data;
this.renderData.push(data);
return true;
} }
} }
@ -339,62 +363,64 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
return true; return true;
} }
const lineHeight = this.renderObject.lineHeights[this.nowLine]; const lineHeight = this.renderObject.lineHeights[this.nowLine];
if (now.type === TextContentType.Text) { switch (now.type) {
const restChars = now.text.length - now.pointer; case TextContentType.Text: {
if (restChars <= rest) { const restChars = now.text.length - now.pointer;
// 当前这段 renderable 打字完成后,刚好结束或还有内容 if (restChars <= rest) {
rest -= restChars; // 当前这段 renderable 打字完成后,刚好结束或还有内容
now.pointer = now.text.length; rest -= restChars;
if (this.dataLine === renderable.splitLines.length) { now.pointer = now.text.length;
// 如果是最后一行 if (this.dataLine === renderable.splitLines.length) {
if (isNil(renderable.lastLineWidth)) { // 如果是最后一行
const ctx = this.parser.testCanvas.ctx; if (isNil(renderable.lastLineWidth)) {
ctx.font = now.font; const ctx = this.parser.testCanvas.ctx;
const metrics = ctx.measureText(now.text); ctx.font = now.font;
renderable.lastLineWidth = metrics.width; const metrics = ctx.measureText(now.text);
renderable.lastLineWidth = metrics.width;
}
this.x += renderable.lastLineWidth;
this.dataLine = 0;
this.pointer++;
} else {
// 不是最后一行,那么换行
this.x = 0;
this.y += lineHeight + this.config.lineHeight;
this.dataLine++;
this.nowLine++;
} }
this.x += renderable.lastLineWidth;
this.dataLine = 0;
this.pointer++;
const success = this.createTyperData(
this.pointer,
this.dataLine
);
if (!success) return true;
} else { } else {
// 不是最后一行,那么换行 now.pointer += rest;
return false;
}
break;
}
case TextContentType.Icon: {
rest--;
this.pointer++;
if (renderable.splitLines[0] === 0) {
// 如果图标换行
this.x = 0; this.x = 0;
this.y += lineHeight + this.config.lineHeight; this.y += lineHeight + this.config.lineHeight;
this.dataLine++;
this.nowLine++; this.nowLine++;
const success = this.createTyperData( this.dataLine = 0;
this.pointer, now.x = 0;
this.dataLine now.y = this.y;
); } else {
if (!success) return true; this.x += now.width;
} }
} else { break;
now.pointer += rest;
return false;
} }
} else { case TextContentType.Wait: {
rest--; now.waited += num;
this.pointer++; if (now.waited > now.wait) {
if (renderable.splitLines[0] === 0) { // 等待结束
// 如果图标换行 this.pointer++;
this.x = 0; }
this.y += lineHeight + this.config.lineHeight; break;
this.nowLine++;
this.dataLine = 0;
now.x = 0;
now.y = this.y;
} }
const success = this.createTyperData(
this.pointer,
this.dataLine
);
if (!success) return true;
} }
const success = this.createTyperData(this.pointer, this.dataLine);
if (!success) return true;
} }
return false; return false;
} }
@ -563,7 +589,7 @@ export class TextContentParser {
const end = this.indexParam(start); const end = this.indexParam(start);
if (end === -1) { if (end === -1) {
// 标签结束 // 标签结束
return ['', start]; return ['', start - 1];
} else { } else {
// 标签开始 // 标签开始
return [this.text.slice(start + 1, end), end]; return [this.text.slice(start + 1, end), end];
@ -634,43 +660,43 @@ export class TextContentParser {
} }
private parseFillStyle(pointer: number) { private parseFillStyle(pointer: number) {
const [param, end] = this.getChildableTagParam(pointer + 1); const [param, end] = this.getChildableTagParam(pointer + 2);
if (!param) { if (!param) {
// 参数为空或没有参数,视为标签结束 // 参数为空或没有参数,视为标签结束
const color = this.fillStyleStack.pop(); const color = this.fillStyleStack.pop();
if (!color) { if (!color) {
logger.warn(54, '\\r', pointer.toString()); logger.warn(54, '\\r', pointer.toString());
return pointer; return end;
} }
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fillStyle = color; this.status.fillStyle = color;
return pointer; return end;
} else { } else {
// 标签开始 // 标签开始
this.fillStyleStack.push(this.status.fillStyle); this.fillStyleStack.push(this.status.fillStyle);
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fillStyle = param; this.status.fillStyle = param;
return end; return end;
} }
} }
private parseFontSize(pointer: number) { private parseFontSize(pointer: number) {
const [param, end] = this.getChildableTagParam(pointer + 1); const [param, end] = this.getChildableTagParam(pointer + 2);
if (!param) { if (!param) {
// 参数为空或没有参数,视为标签结束 // 参数为空或没有参数,视为标签结束
const size = this.fontSizeStack.pop(); const size = this.fontSizeStack.pop();
if (!size) { if (!size) {
logger.warn(54, '\\c', pointer.toString()); logger.warn(54, '\\c', pointer.toString());
return pointer; return end;
} }
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontSize = size; this.status.fontSize = size;
this.font = this.buildFont(); this.font = this.buildFont();
return pointer; return end;
} else { } else {
// 标签开始 // 标签开始
this.fontSizeStack.push(this.status.fontSize); this.fontSizeStack.push(this.status.fontSize);
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontSize = parseFloat(param); this.status.fontSize = parseFloat(param);
this.font = this.buildFont(); this.font = this.buildFont();
return end; return end;
@ -678,22 +704,22 @@ export class TextContentParser {
} }
private parseFontFamily(pointer: number) { private parseFontFamily(pointer: number) {
const [param, end] = this.getChildableTagParam(pointer + 1); const [param, end] = this.getChildableTagParam(pointer + 2);
if (!param) { if (!param) {
// 参数为空或没有参数,视为标签结束 // 参数为空或没有参数,视为标签结束
const font = this.fontFamilyStack.pop(); const font = this.fontFamilyStack.pop();
if (!font) { if (!font) {
logger.warn(54, '\\g', pointer.toString()); logger.warn(54, '\\g', pointer.toString());
return pointer; return end;
} }
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontFamily = font; this.status.fontFamily = font;
this.font = this.buildFont(); this.font = this.buildFont();
return pointer; return end;
} else { } else {
// 标签开始 // 标签开始
this.fontFamilyStack.push(this.status.fontFamily); this.fontFamilyStack.push(this.status.fontFamily);
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontFamily = param; this.status.fontFamily = param;
this.font = this.buildFont(); this.font = this.buildFont();
return end; return end;
@ -701,20 +727,20 @@ export class TextContentParser {
} }
private parseFontWeight() { private parseFontWeight() {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontWeight = this.status.fontWeight > 500 ? 500 : 700; this.status.fontWeight = this.status.fontWeight > 500 ? 500 : 700;
this.font = this.buildFont(); this.font = this.buildFont();
} }
private parseFontItalic() { private parseFontItalic() {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontItalic = !this.status.fontItalic; this.status.fontItalic = !this.status.fontItalic;
this.font = this.buildFont(); this.font = this.buildFont();
} }
private parseWait(pointer: number) { private parseWait(pointer: number) {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
const [param, end] = this.getTagParam(pointer); const [param, end] = this.getTagParam(pointer + 2);
if (!param) { if (!param) {
logger.warn(55, '\\z'); logger.warn(55, '\\z');
return pointer; return pointer;
@ -725,10 +751,10 @@ export class TextContentParser {
} }
private parseIcon(pointer: number) { private parseIcon(pointer: number) {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
const [param, end] = this.getTagParam(pointer); const [param, end] = this.getTagParam(pointer + 2);
if (!param) { if (!param) {
logger.warn(55, '\\z'); logger.warn(55, '\\i');
return pointer; return pointer;
} }
if (/^\d+$/.test(param)) { if (/^\d+$/.test(param)) {
@ -741,6 +767,10 @@ export class TextContentParser {
this.addIconRenderable(num as AllNumbers); this.addIconRenderable(num as AllNumbers);
} else { } else {
const num = texture.idNumberMap[param as AllIds]; const num = texture.idNumberMap[param as AllIds];
if (num === void 0) {
logger.warn(59, param);
return end;
}
this.addIconRenderable(num); this.addIconRenderable(num);
} }
} }
@ -844,9 +874,11 @@ export class TextContentParser {
break; break;
case 'd': case 'd':
this.parseFontWeight(); this.parseFontWeight();
pointer++;
break; break;
case 'e': case 'e':
this.parseFontItalic(); this.parseFontItalic();
pointer++;
break; break;
case 'z': case 'z':
pointer = this.parseWait(pointer); pointer = this.parseWait(pointer);
@ -863,7 +895,7 @@ export class TextContentParser {
// 表达式 // 表达式
pointer++; pointer++;
inExpression = true; inExpression = true;
expStart = pointer; expStart = pointer + 1;
continue; continue;
} }
@ -902,10 +934,14 @@ export class TextContentParser {
// 如果大于猜测,那么算长度 // 如果大于猜测,那么算长度
const data = this.renderable[this.nowRenderable]; const data = this.renderable[this.nowRenderable];
const ctx = this.testCanvas.ctx; const ctx = this.testCanvas.ctx;
ctx.font = this.font; ctx.font = data.font;
const metrics = ctx.measureText( const metrics = ctx.measureText(
data.text.slice(this.lineStart, pointer + 1) data.text.slice(this.lineStart, pointer + 1)
); );
const height = this.getHeight(metrics);
if (height > this.lineHeight) {
this.lineHeight = height;
}
if (metrics.width < rest) { if (metrics.width < rest) {
// 实际宽度小于剩余宽度时,将猜测增益乘以剩余总宽度与当前宽度的比值的若干倍 // 实际宽度小于剩余宽度时,将猜测增益乘以剩余总宽度与当前宽度的比值的若干倍
this.guessGain *= (rest / metrics.width) * (1.1 + 1 / length); this.guessGain *= (rest / metrics.width) * (1.1 + 1 / length);
@ -950,7 +986,7 @@ export class TextContentParser {
const data = this.renderable[index]; const data = this.renderable[index];
const { wordBreak } = data; const { wordBreak } = data;
const ctx = this.testCanvas.ctx; const ctx = this.testCanvas.ctx;
ctx.font = this.font; ctx.font = data.font;
while (true) { while (true) {
const mid = Math.floor((start + end) / 2); const mid = Math.floor((start + end) / 2);
if (mid === start) { if (mid === start) {
@ -961,9 +997,10 @@ export class TextContentParser {
} }
const text = data.text.slice( const text = data.text.slice(
wordBreak[this.bsStart], wordBreak[this.bsStart],
wordBreak[mid] wordBreak[mid] + 1
); );
const metrics = ctx.measureText(text); const metrics = ctx.measureText(text);
height = this.getHeight(metrics);
if (metrics.width > width) { if (metrics.width > width) {
end = mid; end = mid;
} else if (metrics.width === width) { } else if (metrics.width === width) {
@ -974,21 +1011,19 @@ export class TextContentParser {
} else { } else {
start = mid; start = mid;
} }
height = this.getHeight(metrics);
} }
} }
/** /**
* *
*/ */
private checkRestLine(width: number, guess: number) { private checkRestLine(width: number, guess: number, pointer: number) {
if (this.wordBreak.length === 0) return true; if (this.wordBreak.length === 0) return true;
const last = this.nowRenderable - 1; if (pointer === -1) {
if (last === -1) { return this.checkLineWidth(width, guess, 0);
const now = this.renderable[this.nowRenderable];
return this.checkLineWidth(width, guess, now.text.length);
} }
const data = this.renderable[last]; const isLast = this.renderable.length - 1 === pointer;
const data = this.renderable[pointer];
const rest = width - this.lineWidth; const rest = width - this.lineWidth;
if (data.type === TextContentType.Text) { if (data.type === TextContentType.Text) {
const wordBreak = data.wordBreak; const wordBreak = data.wordBreak;
@ -996,7 +1031,7 @@ export class TextContentParser {
const lastIndex = isNil(lastLine) ? 0 : lastLine; const lastIndex = isNil(lastLine) ? 0 : lastLine;
const restText = data.text.slice(lastIndex); const restText = data.text.slice(lastIndex);
const ctx = this.testCanvas.ctx; const ctx = this.testCanvas.ctx;
ctx.font = this.font; ctx.font = data.font;
const metrics = ctx.measureText(restText); const metrics = ctx.measureText(restText);
// 如果剩余内容不能构成完整的行 // 如果剩余内容不能构成完整的行
if (metrics.width < rest) { if (metrics.width < rest) {
@ -1014,12 +1049,12 @@ export class TextContentParser {
this.bsEnd = lastBreak; this.bsEnd = lastBreak;
let maxWidth = rest; let maxWidth = rest;
while (true) { while (true) {
const index = this.bsLineWidth(maxWidth, last); const index = this.bsLineWidth(maxWidth, pointer);
data.splitLines.push(this.wordBreak[index]); data.splitLines.push(this.wordBreak[index]);
this.lineHeights.push(this.lineHeight); this.lineHeights.push(this.lineHeight);
this.bsStart = index; this.bsStart = index;
const text = data.text.slice(this.wordBreak[index]); const text = data.text.slice(this.wordBreak[index]);
if (text.length < guess / 4) { if (!isLast && text.length < guess / 4) {
// 如果剩余文字很少,几乎不可能会单独成一行时,直接结束循环 // 如果剩余文字很少,几乎不可能会单独成一行时,直接结束循环
this.lastBreakIndex = index; this.lastBreakIndex = index;
break; break;
@ -1046,9 +1081,15 @@ export class TextContentParser {
} else { } else {
iconWidth = this.status.fontSize; iconWidth = this.status.fontSize;
} }
this.lineWidth += iconWidth;
const iconHeight = iconWidth / aspect;
if (iconHeight > this.lineHeight) {
this.lineHeight = iconHeight;
}
if (iconWidth > rest) { if (iconWidth > rest) {
data.splitLines.push(0); data.splitLines.push(0);
this.lineHeights.push(this.lineHeight); this.lineHeights.push(this.lineHeight);
this.lineWidth = 0;
return true; return true;
} else { } else {
return false; return false;
@ -1076,8 +1117,6 @@ export class TextContentParser {
const allBreak = this.wordBreakRule === WordBreak.All; const allBreak = this.wordBreakRule === WordBreak.All;
// debugger;
for (let i = 0; i < this.renderable.length; i++) { for (let i = 0; i < this.renderable.length; i++) {
const data = this.renderable[i]; const data = this.renderable[i];
const { wordBreak, fontSize } = data; const { wordBreak, fontSize } = data;
@ -1086,7 +1125,7 @@ export class TextContentParser {
this.wordBreak = wordBreak; this.wordBreak = wordBreak;
if (data.type === TextContentType.Icon) { if (data.type === TextContentType.Icon) {
this.checkRestLine(width, guess); this.checkRestLine(width, guess, i - 1);
continue; continue;
} else if (data.type === TextContentType.Wait) { } else if (data.type === TextContentType.Wait) {
continue; continue;
@ -1128,7 +1167,7 @@ export class TextContentParser {
} }
} }
this.checkRestLine(width, guess); this.checkRestLine(width, guess, i);
} }
return { return {