All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m18s
447 lines
12 KiB
TypeScript
447 lines
12 KiB
TypeScript
import { _decorator, Director, director, Label, Node, Overflow, RichText, tween, UITransform, Vec3 } from 'cc';
|
||
import { StaticTxt } from './StaticTxt';
|
||
const { ccclass, property } = _decorator;
|
||
|
||
export class Tools {
|
||
private static _instance: Tools;
|
||
|
||
ctx: any = null
|
||
|
||
public static get instance(): Tools {
|
||
if (!this._instance) {
|
||
this._instance = new Tools();
|
||
|
||
let canvas = document.createElement('canvas');
|
||
this._instance.ctx = canvas.getContext('2d')!;
|
||
}
|
||
|
||
return this._instance;
|
||
}
|
||
|
||
}
|
||
|
||
/**RichText元素*/
|
||
interface Segment {
|
||
type: "label" | "image";
|
||
value: string;
|
||
}
|
||
|
||
interface WrapRes {
|
||
result: string;
|
||
lastLineWidth: number;
|
||
}
|
||
|
||
interface LangTxt {
|
||
labels: Label[]; // Label 组件数组
|
||
richTexts: RichText[]; // RichText 组件数组
|
||
}
|
||
|
||
export let ICON_RATE: number[][] = [
|
||
[0, 0, 0], // scatter
|
||
[10, 25, 50], // 神灯
|
||
[2.5, 10, 25], // 魔毯
|
||
[2, 5, 15], // 刀
|
||
[1.5, 2, 12], // 戒指
|
||
[1, 1.5, 10], // 项链
|
||
[0.8, 1.2, 8], // 瓶子
|
||
[0.5, 1, 5], // 金币
|
||
[0.4, 0.9, 4], // 银币
|
||
[0.2, 0.75, 2], // 铜币
|
||
]
|
||
|
||
export function fixNum(num: number): string {
|
||
return (num / 10000).toFixed(2)
|
||
}
|
||
|
||
// 需要代码计算空格处理换行的婆罗米系文字
|
||
export function isBrahmic(lang: string) {
|
||
let arr = ["th", "my"]
|
||
return arr.indexOf(lang) != -1
|
||
}
|
||
|
||
export function getLabelsAndRichTexts(node: Node, langText: LangTxt): LangTxt {
|
||
|
||
|
||
const label = node.getComponent(Label);
|
||
const richText = node.getComponent(RichText);
|
||
|
||
const staticTxt = node.getComponent(StaticTxt)
|
||
if (label && staticTxt) {
|
||
langText.labels.push(label);
|
||
}
|
||
if (richText && staticTxt) {
|
||
langText.richTexts.push(richText);
|
||
}
|
||
// 遍历所有子节点
|
||
node.children.forEach(child => {
|
||
getLabelsAndRichTexts(child, langText);
|
||
});
|
||
|
||
return langText
|
||
}
|
||
|
||
/**拿到节点下所有需要翻译的, 挂载了StaticTxt脚本的 Label*/
|
||
export function getAllLabels(node: Node, result: Label[] = []): Label[] {
|
||
const label = node.getComponent(Label);
|
||
const staticTxt = node.getComponent(StaticTxt)
|
||
if (label && staticTxt) {
|
||
result.push(label);
|
||
}
|
||
|
||
// 遍历所有子节点
|
||
node.children.forEach(child => {
|
||
getAllLabels(child, result);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/**拿到节点下所有需要翻译的, 挂载了StaticTxt脚本的 Label*/
|
||
export function getAllRichTexts(node: Node, result: RichText[] = []): RichText[] {
|
||
const richText = node.getComponent(RichText);
|
||
const staticTxt = node.getComponent(StaticTxt)
|
||
if (richText && staticTxt) {
|
||
result.push(richText);
|
||
}
|
||
|
||
// 遍历所有子节点
|
||
node.children.forEach(child => {
|
||
getAllRichTexts(child, result);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
|
||
|
||
export function getTranslate(dict, key) {
|
||
let str = dict[key] ?? key
|
||
return str;
|
||
}
|
||
|
||
export function getTimezoneOffsetString(): string {
|
||
|
||
const pad = (n: number) => (n < 10 ? '0' + n : String(n));
|
||
|
||
const offset = -new Date().getTimezoneOffset(); // 单位是分钟,注意要取反
|
||
const sign = offset >= 0 ? "+" : "-";
|
||
const hours = pad(Math.floor(Math.abs(offset) / 60))
|
||
const minutes = pad(Math.abs(offset) % 60);
|
||
|
||
return `(GMT${sign}${hours}:${minutes})`;
|
||
}
|
||
|
||
export function updateLang(node: Node, dict, lang: string) {
|
||
|
||
let langText: LangTxt = { labels: [], richTexts: [] }
|
||
getLabelsAndRichTexts(node, langText)
|
||
|
||
langText.labels.forEach((label, i) => {
|
||
// console.log(`label.node.name: ${label.node.name}`)
|
||
|
||
const staticTxt = label.node.getComponent(StaticTxt)
|
||
const bResize = label.overflow == Overflow.RESIZE_HEIGHT
|
||
|
||
let key = staticTxt?.key
|
||
if (dict[key]) {
|
||
|
||
if (isBrahmic(lang) && bResize) {
|
||
label.string = wrapTextBySpace(label, dict[key])
|
||
} else {
|
||
label.string = dict[key]
|
||
}
|
||
|
||
if (label.node.activeInHierarchy) {
|
||
label.updateRenderData()
|
||
}
|
||
}
|
||
})
|
||
|
||
langText.richTexts.forEach((richText, i) => {
|
||
|
||
const staticTxt = richText.node.getComponent(StaticTxt)
|
||
richText.maxWidth = staticTxt.maxWidth
|
||
richText.string = wrapRichTextBySpace(richText, staticTxt.key, dict, lang)
|
||
})
|
||
|
||
return langText
|
||
}
|
||
|
||
export function wrapRichTextBySpace(richText: RichText, key: string, dict, lang): string {
|
||
|
||
// key like this: Label_Rules_6_14_2 <img src='dates' /> Label_Rules_6_14_3
|
||
const segmentArr: Segment[] = [];
|
||
|
||
//Label_[^<\s] 表示由Label开头,遇到<或空白(\s)则停止
|
||
//+表示前面的字符类可能出现多次
|
||
//|表示另一种组合,匹配其中任意一种
|
||
//<img[^>]+>表示由img开头,遇到>则停止,并在结尾再添加一个>
|
||
const regexp = /Label_[^<\s]+|<img[^>]+>/g;
|
||
|
||
|
||
// console.log('返回所有匹配的字符串', key.match(regexp))
|
||
// let matchArr = key.match(regexp)
|
||
// matchArr.forEach(match => {
|
||
// if (match.startsWith("Label_")) {
|
||
// let key = match.split("Label_")[1]
|
||
// if (dict[key]) {
|
||
// segmentArr.push({ type: "label", value: dict[key] });
|
||
// }
|
||
|
||
// } else {
|
||
// segmentArr.push({ type: "image", value: match }); // "<img src='dates' />"
|
||
// }
|
||
// });
|
||
|
||
// console.log('返回所有匹配的字符串', key.matchAll(regexp))
|
||
// let matchArr = [...key.matchAll(regexp)]
|
||
// matchArr.forEach(match => {
|
||
// if (match[0].startsWith("Label_")) {
|
||
// let key = match[0].split("Label_")[1]
|
||
// if (dict[key]) {
|
||
// segmentArr.push({ type: "label", value: dict[key] });
|
||
// }
|
||
|
||
// } else {
|
||
// segmentArr.push({ type: "image", value: match[0] }); // "<img src='dates' />"
|
||
// }
|
||
// });
|
||
let match: RegExpExecArray | null;
|
||
while ((match = regexp.exec(key)) !== null) {
|
||
if (match[0].startsWith("Label_")) {
|
||
|
||
let key = match[0].split("Label_")[1]
|
||
if (dict[key]) {
|
||
segmentArr.push({ type: "label", value: dict[key] });
|
||
}
|
||
|
||
} else {
|
||
segmentArr.push({ type: "image", value: match[0] }); // "<img src='dates' />"
|
||
}
|
||
}
|
||
|
||
// console.log("segmentArr:", segmentArr);
|
||
|
||
/*
|
||
segmentArr:
|
||
[
|
||
{
|
||
"type": "label",
|
||
"value": "Tap"
|
||
},
|
||
{
|
||
"type": "image",
|
||
"value": "<img src='dates' />"
|
||
},
|
||
{
|
||
"type": "label",
|
||
"value": "to select the dates of games to be shown in History."
|
||
}
|
||
]
|
||
*/
|
||
|
||
let wrapResult: WrapRes = { result: "", lastLineWidth: 0 }
|
||
segmentArr.forEach((part, i) => {
|
||
|
||
if (isBrahmic(lang)) {
|
||
if (part.type == "label") {
|
||
wrapResult = wrapTextByRichTextWidth(richText, part.value, wrapResult)
|
||
} else if (part.type == "image") {
|
||
|
||
let img = part.value.match(/src=['"]([^'"]+)['"]/)
|
||
const spriteFrame = richText.imageAtlas.getSpriteFrame(img[1]);
|
||
|
||
if (spriteFrame) {
|
||
const rect = spriteFrame.rect; // 原始图片矩形
|
||
// console.log("原始图片宽度:", rect.width);
|
||
|
||
// 图片宽度 + 最后一行宽度 比 最大宽度大,图片换行放到下一行去
|
||
if (wrapResult.lastLineWidth + rect.width > richText.maxWidth) {
|
||
wrapResult.result += "\n" + part.value
|
||
wrapResult.lastLineWidth = rect.width
|
||
} else {
|
||
// 放得下就放在最后一行
|
||
wrapResult.result += part.value
|
||
wrapResult.lastLineWidth += rect.width
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 非婆罗米系文字,引擎自动换行就能处理好
|
||
// 不需要手动计算
|
||
wrapResult.result += part.value
|
||
}
|
||
|
||
})
|
||
|
||
return wrapResult.result
|
||
}
|
||
|
||
|
||
// 富文本换行
|
||
export function wrapTextByRichTextWidth(richText: RichText, str: string, lastWrapResult: WrapRes = { result: "", lastLineWidth: 0 }): WrapRes {
|
||
const fontSize = richText.fontSize;
|
||
const fontFamily = richText.fontFamily ?? 'Arial'
|
||
const maxWidth = richText.maxWidth // richText 最大宽度
|
||
|
||
let wrapResult = lastWrapResult;
|
||
|
||
Tools.instance.ctx.font = `${fontSize}px ${fontFamily}`;
|
||
|
||
const words = str.split(' ')
|
||
let line = '';
|
||
let result = '';
|
||
|
||
let endIdx = words.length - 1
|
||
for (let i = 0; i < words.length; i++) {
|
||
let word = words[i]
|
||
|
||
console.log("word:", word)
|
||
|
||
let testLine = line + " " + word
|
||
|
||
const width = wrapResult.lastLineWidth + Tools.instance.ctx.measureText(testLine).width;
|
||
|
||
console.log("width = ", width, "maxWidth = ", maxWidth)
|
||
|
||
|
||
// 长度超了放到下一行去
|
||
if (width + 5 > maxWidth && line.length > 0) {
|
||
if (i == endIdx) {
|
||
|
||
console.log("最后一个单词长度超了")
|
||
|
||
|
||
result += line + '\n' + word;
|
||
wrapResult.lastLineWidth = Tools.instance.ctx.measureText(word).width;
|
||
} else {
|
||
|
||
console.log("单词长度超了,换行")
|
||
|
||
|
||
result += line + '\n';
|
||
line = word
|
||
wrapResult.lastLineWidth = 0
|
||
}
|
||
} else {
|
||
if (i == endIdx) {
|
||
|
||
console.log("最后一个单词,拼在屁股后面")
|
||
|
||
|
||
result += testLine;
|
||
wrapResult.lastLineWidth = Tools.instance.ctx.measureText(testLine).width;
|
||
|
||
} else {
|
||
|
||
console.log("拼在屁股后面")
|
||
|
||
line = testLine;
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("循环结束, ", result)
|
||
|
||
wrapResult.result += result
|
||
|
||
return wrapResult;
|
||
}
|
||
|
||
|
||
|
||
// 按单词换行
|
||
export function wrapTextBySpace(label: Label, str: string): string {
|
||
|
||
|
||
const fontSize = label.fontSize;
|
||
const fontFamily = label.fontFamily ?? 'Arial'
|
||
const maxWidth = label.node.getComponent(UITransform).width; // Label 最大宽度
|
||
|
||
const words = str.split(' ')
|
||
|
||
let line = '';
|
||
let result = '';
|
||
// const canvas = document.createElement('canvas');
|
||
// const ctx = canvas.getContext('2d')!;
|
||
Tools.instance.ctx.font = `${fontSize}px ${fontFamily}`;
|
||
|
||
let endIdx = words.length - 1
|
||
for (let i = 0; i < words.length; i++) {
|
||
let word = words[i]
|
||
|
||
// console.log("word = ", word)
|
||
|
||
let testLine = line + " " + word
|
||
|
||
const width = Tools.instance.ctx.measureText(testLine).width;
|
||
|
||
// 长度超了放到下一行去
|
||
if (width > maxWidth && line.length > 0) {
|
||
if (i == endIdx) {
|
||
result += line + '\n' + word;
|
||
} else {
|
||
result += line + '\n';
|
||
line = word
|
||
}
|
||
} else {
|
||
if (i == endIdx) {
|
||
result += testLine;
|
||
} else {
|
||
line = testLine;
|
||
}
|
||
}
|
||
}
|
||
|
||
// console.log("循环结束, ", result)
|
||
|
||
return result;
|
||
}
|
||
|
||
export function waitNextFrame(): Promise<void> {
|
||
return new Promise(resolve => {
|
||
director.once(Director.EVENT_BEFORE_UPDATE, () => resolve());
|
||
});
|
||
}
|
||
|
||
export function hideAllChildren(node: Node) {
|
||
node.children.forEach(child => {
|
||
child.active = false
|
||
})
|
||
}
|
||
|
||
export function showAllChildren(node: Node) {
|
||
node.children.forEach(child => {
|
||
child.active = true
|
||
})
|
||
}
|
||
|
||
export function showFromBottom(node: Node) {
|
||
if (!node) {
|
||
return
|
||
}
|
||
let height = node.getComponent(UITransform)?.height
|
||
if (height) {
|
||
height = 1920;
|
||
}
|
||
|
||
node.position = new Vec3(0, -height / 2)
|
||
tween(node).to(0.15, { position: new Vec3() }).start()
|
||
}
|
||
|
||
export function hideToBottom(node: Node, callFuc = null) {
|
||
if (!node) {
|
||
return
|
||
}
|
||
let height = node.getComponent(UITransform)?.height
|
||
if (height) {
|
||
height = 1920;
|
||
}
|
||
|
||
let endPos = new Vec3(0, -height)
|
||
tween(node).to(0.15, { position: endPos }).call(() => {
|
||
if (callFuc) {
|
||
callFuc()
|
||
}
|
||
}).start()
|
||
} |