8000 GitHub - 54145a/battle-of-pens: 弹笔开源代码
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

54145a/battle-of-pens

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 

Repository files navigation

battle-of-pens

永远的“弹笔”

此仓库为神岛地图“弹笔”开源代码,按MIT协议提供。该地图目前已不再更新。

本仓库更新日期:2025年5月25日

server

index.js

/*
world.onTick(()=>{world.say(`世界人数:${world.querySelectorAll("player").length}`)})
world.onPlayerJoin(p=>p.entity.player.canFly = 1)
*/
require("./main.js");
/*挂机括号(  )
todo{
    
    足球赛
    重构比赛系统
    笔筒 不采纳
    MVP得到更多小橡皮 已完成
    ui封装与缓存 已完成
    观战自由视角(Daniel林子) 不采纳
    订书机(云里雾里) 已有模型 已完成
    音乐火影RISE(L-342) 不采纳
    取消打开盲盒(wccwcc) 已完成
    单笔模式(某只咕咕小鸽子) 不采纳
    PVP(诗仙李白打老虎) 不采纳
    小橡皮固定时间(鼠窝中的笨鼠) 已完成
    萝卜刀(云里雾里) 不采纳
    戒尺(鼠窝中的笨鼠) 已有模型 已完成
    黑板擦(ZiganotosaurusSpinosaurusTyrar)
    量角器(洒脱的小蓝t1z4) 已完成
    召唤班主任技能(名字真难) 不采纳
    转笔技能(洒脱的小蓝t1z4) 不采纳
    生化模式{
        教室门口传来了一阵脚步声……
        笔,不是玩具,是文具!
        我的办公室里有一个袋子用来统一保管你们的玩具!
        学生,就应该干学生该干的事!
        xxx被没收
    }(一只果蒋吖) 不采纳
    右键蓄力技能 已改为长按蓄力,已完成
    夹子(越打力度越小)(洒脱的小蓝t1z4) 不采纳
    神龙摆尾技能(机灵的神射手) 不采纳
    盲盒空盒(U.S.S尛黍諟) 已完成
    美工刀(叛腻der泥) 不采纳
    涂卡笔(mzaa但是暂退) 不采纳
    水笔(一颗红小豆) 不采纳
}
转移测试:
storage.getDataStorage("main").set("13033456",{items:["重重的橡皮"],boxes:[["超级文具盲盒",null]],joinTimes:10,totalScore: 54145,smallErasers: 54145,bestScore: 145})
145:寒假更新{
    pen.js 凑活
    match 实现了一半,放弃
    ui和匹配机制 凑活,鸽了吧
    用item实现的blindbox 鸽
}
145:{
段位 已完成
}
145:游乐场化{
引导报名 已完成
}
脚本喵:签到
万嘉麒:签到
145:todo{
测试新文具稀有度显示 已完成
修复神秘的盲盒不关闭bug
测试一遍游戏
}
万嘉麒:e,那个时候我代码写的太繁琐了
145:没事,我也看不懂大多数商业大陆代码。对话框背景改成透明了。
万嘉麒:地图一会黑屏选择框,一会屏幕一亮。我认为选择框背景色可以不调
万嘉麒:你的代码非常好,虽然我看不懂大部分代码。这个原因可能是我的能力不行或我的编程涉及面不够广
145:广告词{
    万物皆可弹4来啦!这是令人兴奋的万物皆可弹系列第四个地图!我们打破常规,引领创新的风潮!
    这个地图的独特之处在于,它并不依赖华丽的模型和复杂的代码,而只运用了纯色方块和不到1000行的代码,呈现出独特而奇妙的弹笔世界。我们和你一样在平板上敲代码一敲一卡,苦苦等待着编辑器对移动端创作的支持。我们没有使用官方未公开的api接口,如http和rtc,我们唯一使用的未公开api是借鉴了社区用户“145a”的gui代码。
    我们并非遥不可及的大佬,而是与你一样,怀揣梦想的普通岛民。我们热衷于探索、创新和分享。我们与你一起在首页地图中寻找灵感,梦想着成为地图制作的大佬。
    在社区论坛中,我们与你一同浏览、借鉴开源代码,分享我们的发现,共同解决其中的bug。我们希望通过这种方式,与您共同探索、学习和成长。
    在创作端,我们与你并肩作战,消掉一个又一个todo,为地图注入新的生命力。我们坚信,你的每一次游玩、点赞、分享、收藏、评论、issue,甚至是模型和代码的贡献,都对我们至关重要。
    为了回馈您的支持,我们承诺以下几点:首先,我们将永远不会推出氪金;其次,我们将免费向创作的你开放地图内广告位,与你共享成功的喜悦;最后,我们会向你开源所有代码,让你能够深入探索和继续创新。
    让我们携手共进,共同创造更多精彩!万物皆可弹4期待您的参与,让我们一起在这个奇妙的地图中探险、创新和成长!
}
万嘉麒:签到
145:模型任务:粉笔、毛笔
145:地图开始内测
简介{
万物皆可弹4全新启航啦!
这是全岛第五个弹笔地图,万物皆可弹系列的第四个作品。
}
145:第二阶段计划 已完成{
    1.(60/15)种文具 已完成
    2.总分排行榜 已完成
    3.单局排行榜 已完成
    4.盲盒提示(先把盲盒拆出来) 已完成
    5.Squads 已完成{
        先做一个队伍系统
        1.队伍属性 已完成
        2.npc索敌规则 已完成
        3.击杀判定规则 已完成
        4.击杀播报、计分规则 已完成
        5.队伍颜色 已完成
        6.分队 已完成
    }
    6.盲盒系统优化 已完成{
        1.BlindBoxConfigData类 已完成
        2.盲盒开启时间 已完成
    }
}
145:第一阶段已结束,现在地图完全适配Arena。
145:刺客,能帮我修个bug吗,我尽力了。就是main.js报名之后会加入游戏,创造笔实体后笔不知道为什么突然被destroy了。我查过了所有的destroy(),没找到问题在哪
145:遇到了难修的bug
145:爆肝一上午,gui初具雏形
145:有人吗
145:可恶的闪退
145:吉吉什么时候修一下bug
145:我试试
万嘉麒:其实我对这个一点都不懂...你完全可以自己写的...
145:还要更简单些,我之前写过一个简单的显示,可以用Promise封装一下,就像原生的对话框那样使用,还要一个全屏的背景 https://shequ.codemao.cn/mobile/community/549165
万嘉麒:经过两坤时的修改测试,GUI文本框函数已构建完成,详情请见文件“GUI测试专用”中的“dingyiGUI”函数以及文件第一行使用函数参照数组
145:主要是这个要展示大量内容,就是相当于重写对话框,这个新的对话框要全屏展示
万嘉麒:GUI函数经过一坤时修改测试有进展,勿动【2023/7/24/21:33:19】
万嘉麒:...关键我不知道GUI的输入框和选择框怎么写...
145:有人吗 有人做任务吗 ……
145:还有什么比修改自己一年前的破代码更加痛苦的?啊啊啊啊啊啊啊啊啊啊
145:我要让代码易扩展度和易维护度对标瓶盖人,我要挑战首页!!
145:没事慢慢来(有时间)
万嘉麒:只是告知...这是我搜到的,将要修改的内容
145:别写在index.js
万嘉麒:收到,GUI不会用...
145:代码规范{
    命名按照这个
    https://juejin.cn/post/7076259548872310814
    所有变量可修改可访问,易于调试
    //todo 待测试
    //tofix 待修复的bug
    //totest 待测试(自己写的totest可以让别人消)
    //patch 补丁,不需要时再移除
    //toremove 在每次大整理之后统一移除的无用代码
}
145:代码任务来了[
    一位大佬刚刚研究出来了一种GUI新技术 https://shequ.codemao.cn/community/552330
    代码任务是写一个GUI系统(写在main.js合适的位置(我也不知道哪里合适))
    需要提供三个快速显示GUI对话框的函数(跟官方对话框一样都是异步
    能兼容各种屏幕大小(手表除外)
    可以指定标题、选项、content、
    (文字、选项、输入(输入框))
    最后我来包装一下
]
万嘉麒:每天晚上我都会来,代码可以做什么任务呢
145:快来做任务啊!发布前一周会踢掉贡献最少的人,留下五个人{
    1.各式各样的笔模型(可以用Vobi)
    2.修正带模型(现在这个不行)
    3.地图宣传片(这个发布前10天开始做,有人领吗)(有jscode我领!!!!!!我会剪视频)
}
145:地图重启,重构代码!!!!{
    1.数据库 已完成
    2.大拆分计划(把所有代码拆成函数和多个文件 进行中
    (3).换模型(自制的模型)
    4.把不清楚的变量命名都改了,修改不合适的位置,按规范修改全部代码 进行中
    4.目标是确保所有数据都可访问可修改 进行中

以下内容暂缓,优先执行大拆分

    5.补丁系统(简)
    6.弹笔方向控制(待定)
    6.更换场景(建筑) 已完成
    7.笔的技能(待定)
    8.实时排行榜(待定)
    9.交易
    9.gui(如果可以) 已完成
    10.参加创作节(ye)
}
*/

main.js

/*main.js*/
require("./ui.js");
require("./145player.js");
require("./145match.js");
const { PenTeamType, PenConfigData } = require("./pen.js");

console.log("main.js开始运行");

const ADMIN_LIST = ["145a", "TQ.YFT.MOSS", "万嘉麒"];
let END_COUNDDOWN_TIME = 10;
let MIN_PLAYER_NUM = 10;
let MAX_ENEMY_BOT_NUM = 5;
let PEN_CLICK_COOL_DOWN_TIME = 1000;
const SPAWN_POS = new GameVector3(64, 10, 64);
const GameModeConfig = {

}
const GameModeType = {
    FFA: "自由混战赛",
    TDM: "团队淘汰赛",
    SIEGE: "无尽挑战赛"
};
const PEN_ENHANCE_SCALE_CONFIG = {
    "大大的": {
        scale: 1.5
    },
    "小小的": {
        scale: 0.8
    },
    "重重的": {
        mass: 1.5
    },
    "轻轻的": {
        mass: 0.8
    }
}
const PEN_CONFIG = {
    "铅笔": new PenConfigData("谁能借个削笔刀qwq,,", 4, 0.2, 0.3, 0),
    "蜡笔": new PenConfigData("不粘手()", 4, 0.2, 0.2, 0),
    "尺子": new PenConfigData("还带这么玩的?", 4, 0.2, 0.3, 1),
    "自动铅笔": new PenConfigData("借根铅芯呗)", 6, 0.2, 0.1, 1),
    "圆规": new PenConfigData("小心,它很尖。", 8, 0.1, 0.3, 3, 2),
    "剪刀": new PenConfigData("不带这么玩的!", 8, 0.1, 0.12, 3, 3),
    "橡皮": new PenConfigData("Q弹。", 6, 0.3, 0.2, 3),
    "马克笔": new PenConfigData("又轻又大。", 4, 0.1, 0.2, 1),
    "修正带": new PenConfigData("小心,别摔坏了。", 4, 0.1, 0.2, 1, 2),
    "口香糖": new PenConfigData("小又粘。", 4, 0.3, 0.1, 2, 2),
    "钢笔": new PenConfigData("好沉啊……", 8, 0.1, 0.2, 3),
    "订书机": new PenConfigData("没钉了:(", 7, 0.2, 0.05, 3),
    "圆珠笔": new PenConfigData("又细又黑。", 6, 0.2, 0.2, 2),
    "胶带": new PenConfigData("轻,但是上面有点胶。", 2, 0.3, 0.06, 1),
    "量角器": new PenConfigData("还带这么玩的?!", 4, 0.2, 0.4, 2, 2)
}
Object.keys(PEN_CONFIG).forEach((pen) => {
    PEN_CONFIG[pen].mesh = `mesh/${pen}.vb`;
    Object.keys(PEN_ENHANCE_SCALE_CONFIG).forEach((enchancement) => {
        PEN_CONFIG[enchancement + pen] = PEN_CONFIG[pen].toEnhanced(PEN_ENHANCE_SCALE_CONFIG[enchancement]);
    });
});
class BossConfigData extends PenConfigData {
    constructor(scale) {
        return super("Boss", 12, 0.5, scale, 1);
    }
}
const BOSS_CONFIG = {
    "水壶": new BossConfigData(0.2),
    "笔盒": new BossConfigData(0.4),
    "字典": new BossConfigData(0.4),
    "笔记本": new BossConfigData(0.5),
    "瓶子": new BossConfigData(0.4),
    "作业本": new BossConfigData(0.2),
    "戒尺": new BossConfigData(0.1)
};
Object.keys(BOSS_CONFIG).forEach((boss) => {
    BOSS_CONFIG[boss].mesh = `mesh/${boss}.vb`;
});
class BlindboxConfigData {
    constructor(stationeryNum, openingTimeNeedHour) {
        this.stationeryNum = stationeryNum;
        this.openingTimeNeed = openingTimeNeedHour * 60 * 60 * 1000;
    }
}
const BLINDBOX_CONFIG = {
    "普通文具盲盒": new BlindboxConfigData(1, 1),
    "幸运文具盲盒": new BlindboxConfigData(2, 2),
    "巨型文具盲盒": new BlindboxConfigData(4, 8),
    "超级文具盲盒": new BlindboxConfigData(8, 48)
};
let SCORE_PER_LEVEL = 10;
const LEVEL_DISPLAY_NAME = [
    "新手上桌",
    "江湖新秀",
    "技巧高手",
    "运笔大师",
    "控笔专家",
    "桌上传奇"
];
class CameraViewTypeData {
    constructor(setup) {
        this.setup = setup;
    }
}
const CameraViewType = {
    "第三人称跟随": new CameraViewTypeData((entity) => {
        entity.player.cameraMode = "follow";
        entity.player.cameraDistance = 50 * entity.screenAspectRatio;
        entity.player.swapInputDirection = false;
        entity.player.reverseInputDirection = GameInputDirection.NONE;
        entity.player.cameraFreezedAxis = GameCameraFreezedAxis.NONE;
        entity.player.freezedForwardDirection = null;
    }),
    "俯视": new CameraViewTypeData((entity) => {
        entity.player.cameraMode = GameCameraMode.RELATIVE;
        entity.player.cameraPosition = new GameVector3(-0.01, 70 * entity.screenAspectRatio, 0);
        entity.player.swapInputDirection = true;
        entity.player.reverseInputDirection = GameInputDirection.HORIZONTAL;
        entity.player.cameraFreezedAxis = GameCameraFreezedAxis.Y;
        entity.player.freezedForwardDirection = new GameVector3(0, 0, 1);
    })
}
function displayTimeMsNum(num) {
    const hourMs = 1000 * 60 * 60;
    const minMs = 1000 * 60;
    const secondMs = 1000;
    const hour = Math.floor(num / hourMs);
    const min = Math.floor(num % hourMs / minMs);
    const second = Math.floor(num % hourMs % minMs / secondMs);
    return `${hour}${min}${second}秒`;
}

function randomPos() {
    return new GameVector3(10 + Math.round(Math.random() * 107), 5, 10 + Math.round(Math.random() * 107));
}
function randomPen() {
    const s = Object.keys(PEN_CONFIG)[Math.floor(Math.random() * Object.keys(PEN_CONFIG).length)];
    let index = PEN_CONFIG[s].rarity + 1;
    if (1 / index < Math.random()) return randomPen();
    return s;
}

let endCountDownStarted = null;
let endCountdownStartSecond = null;
let gameModeVoteList = [];
let seigeCurrentWave = 0;

let gameMode = null;
let topContent = "弹笔";
const broadcastContent = [];
let bestScoreRank = "加载中,请稍后再试";
let totalScoreRank = "加载中,请稍后再试";

world.maxFog = 1;
world.fogHeightFalloff = 1;
world.fogHeightOffset = -5;
world.useOBB = true;
world.addCollisionFilter("player", "*");

function broadcast(content) {
    //world.say(content);
    broadcastContent.unshift(content);
    sleep(10000).then(() => broadcastContent.pop());
}

function setupPlayer(entity) {
    //转移和修复代码
    if (entity.stationery.length === 0 && entity.items?.length > 0) {
        try {
            entity.stationery = [...entity.items];
            entity.items = {};
        } catch (e) {
            entity.textDialog("数据库故障", `${e}\n请务必在评论区向我们反馈这些信息`);
            entity.stationery = [randomPen(), randomPen()];
            entity.items = {};
        }
    }
    entity.stationery = Array.from(new Set(entity.stationery));

    entity.isInGame = false;
    entity.pen = null;
    entity.watchGlobalPos = new GameVector3(64, 165, 64);
    entity.isCharging = false;
    entity.action0PressTime = null;
    Object.defineProperties(entity, {
        level: {
            get() {
                return LEVEL_DISPLAY_NAME[Math.floor(this.totalScore / SCORE_PER_LEVEL)] ?? LEVEL_DISPLAY_NAME[LEVEL_DISPLAY_NAME.length - 1];
            }
        },
        isControllingPen: {
            get() {
                return this.isInGame && this.pen;
            }
        },
        action0PressTimePassed: {
            get() {
                return Date.now() - this.action0PressTime;
            }
        },
        clickCoolDownTimeLeft: {
            get() {
                return Math.max(0, PEN_CLICK_COOL_DOWN_TIME - (Date.now() - entity.pen.lastClickTime));
            }
        },
        chargeScale: {
            get() {
                return (Math.sqrt(Math.sqrt(this.action0PressTimePassed)) / 2).toFixed(2);
            }
        },
        hasBlindboxOpening: {
            get() {
                return this.blindboxOpening.type !== null;
            }
        },
        blindboxOpeningTimeNeed: {
            get() {
                return BLINDBOX_CONFIG[this.blindboxOpening.type].openingTimeNeed
            }
        },
        blindboxOpeningTimePassed: {
            get() {
                return Date.now() - this.blindboxOpening.startTime;
            }
        },
        blindboxOpeningTimeLeft: {
            get() {
                return this.blindboxOpeningTimeNeed - this.blindboxOpeningTimePassed;
            }
        },
        blindboxOpeningCanOpen: {
            get() {
                return this.blindboxOpeningTimeLeft <= 0;
            }
        },
        speedupBlindboxPrice: {
            get() {
                return Math.max(1, Math.floor(this.blindboxOpeningTimeLeft / 1000 / 60 / 60));
            }
        },
        blindboxDisplay: {
            get() {
                if (!this.hasBlindboxOpening) {
                    return `待打开 ${entity.blindboxes.length} 盲盒`;
                } else if (this.blindboxOpeningCanOpen) {
                    return `${this.blindboxOpening.type} 已打开`;
                } else {
                    return `${this.blindboxOpening.type} 打开中\n剩余${displayTimeMsNum(this.blindboxOpeningTimeLeft)}`;
                }
            }
        },
        menuContent: {
            get() {
                return `[${this.level}] ${this.player.name}\n最高单局分数: ${this.bestScore}\n累计分数: ${this.totalScore}\n${this.blindboxDisplay}\n小橡皮: ${this.smallErasers}`;
            }
        }
    });
    entity.getBlindbox = function (BoxLv = 0) {
        this.blindboxes.push(Object.keys(BLINDBOX_CONFIG)[BoxLv]);
    };
    entity.joinGame = async function () {
        this.isInGame = true;
        this.player.cancelDialogs();
        for (let n = 0; n < 3; n++)
            await world.nextTick();
        this.position.copy(randomPos());
        this.pen = createPen(this.usePenType, `${this.player.name}${this.usePenType}`, this.position.clone());
        this.pen.owner = this;
        this.player.cameraEntity = this.pen;
        CameraViewType[this.cameraView].setup(this);
    };
    entity.exitGame = async function () {
        this.isInGame = false;
        if (this.isCharging) {
            this.isCharging = false;
        }
        const score = this.pen.currentRoundScore;
        let extra = `本局分数为${score}`;
        this.totalScore += score;
        if (score > 0) {
            let boxLv = Math.min(Math.floor(Math.random() * score), Object.keys(BLINDBOX_CONFIG).length);
            if (boxLv > 0) {
                boxLv -= 1;
                extra += `,你获得了${Object.keys(BLINDBOX_CONFIG)[boxLv]}\n${this.viewAd()}`;
                this.getBlindbox(boxLv);
            }
        }
        this.pen.currentRoundScore = 0;
        if (!this.pen.destroyed)
            this.pen.destroy();
        this.player.cancelDialogs();
        if (score > this.bestScore) {
            extra += "\n恭喜你创造了新纪录!";
            this.bestScore = score;
        }
        await this.textDialog(
            `比赛结束`,
            `${!game.ended ? `${this.pen.id}${this.pen.attacker?.id ?? "你"} 弹下了桌面。` : "恭喜你成为了最后的赢家!"}\n${extra}`
        );
        this.player.cameraEntity = this;
        this.pen = null;
        await this.menu();
    };
    entity.chooseCameraView = async function () {
        await this.selectDialog(
            "选择视角",
            "请选择你希望使用的视角",
            Object.keys(CameraViewType)
        );
        if (!this.dialogResult) return;
        this.cameraView = this.dialogResult.value;
    };
    entity.choosePenType = async function () {
        await this.selectDialog(
            "选择文具",
            `请选择你要使用的文具`,
            this.stationery
        );
        if (!this.dialogResult) return;
        this.usePenType = this.dialogResult.value;
    };
    entity.signUpGame = async function () {
        await this.selectDialog(
            "报名参加比赛",
            `请选择你想参加的比赛模式`,
            Object.values(GameModeType)
        );
        if (!this.dialogResult) return;
        const voteValue = this.dialogResult.value;
        while (!this.usePenType) {
            await this.choosePenType();
        }
        while (!this.cameraView) {
            await this.chooseCameraView();
        }
        while (true) {
            await this.selectDialog(
                "报名参加游戏",
                `游戏模式: ${voteValue}\n文具: ${this.usePenType}\n视角: ${this.cameraView}\n开始时将扣除1分数`,
                ["确认报名", "选择文具", "选择视角"]
            );
            if (!this.dialogResult) return;
            switch (this.dialogResult.value) {
                case "确认报名":
                    gameModeVoteList.push(voteValue);
                    this.addTag("joinGame");
                    return;
                case "选择文具":
                    await this.choosePenType();
                    break;
                case "选择视角":
                    await this.chooseCameraView();
                    break;
            }
        }
    }
    entity.stationeryHandbook = async function () {
        while (true) {
            await this.selectDialog(
                `我的文具`,
                `文具收集进度 ${this.stationery.length}/${Object.keys(PEN_CONFIG).length}`,
                this.stationery
            );
            if (!this.dialogResult || typeof this.dialogResult === "string") break;
            const config = PEN_CONFIG[this.dialogResult.value];
            await this.textDialog(
                this.dialogResult.value,
                config.toString()
            );
        }
    };
    entity.openBlindbox = async function () {
        await this.selectDialog(
            "打开盲盒",
            `选择一个盲盒开始打开`,
            this.blindboxes
        );
        if (!this.dialogResult) return;
        const type = this.dialogResult.value;
        const index = this.dialogResult.index;
        const config = BLINDBOX_CONFIG[type];
        this.blindboxOpening.type = type;
        this.blindboxOpening.startTime = Date.now();
        this.blindboxes.splice(index, 1);
    };
    entity.getStationeryFromBlindbox = async function () {
        let content = "";
        const config = BLINDBOX_CONFIG[this.blindboxOpening.type];
        for (let l = 0; l < config.stationeryNum; l++) {
            let p = randomPen();
            if (Math.random() > 0.3) {
                if (this.stationery.includes(p)) {
                    content += `你开出了 ${p} ,但已拥有,转化为1小橡皮。`;
                    this.smallErasers += 1;
                } else {
                    content += `恭喜!你开出了 ${p} 。`;
                    this.stationery.unshift(p);
                }
            } else {
                content += `盲盒的这一格没有任何文具。`;
            }
        }
        const type = this.blindboxOpening.type;
        this.blindboxOpening.type = null;
        await this.textDialog(type, content);
    };
    entity.speedupBlindboxOpening = async function () {
        this.smallErasers -= this.speedupBlindboxPrice;
        this.getStationeryFromBlindbox();
    };
    entity.displayLeaderbroad = async function () {
        while (true) {
            await this.selectDialog("排行榜", "查看排行榜", ["累计分数排行榜", "最高单局分数排行榜"]);
            if (!this.dialogResult) break;
            switch (this.dialogResult.value) {
                case "累计分数排行榜":
                    await this.textDialog("累计分数排行榜", totalScoreRank);
                    break;
                case "最高单局分数排行榜":
                    await this.textDialog("最高单局分数排行榜", bestScoreRank);
                    break;
            }
        }
    };
    entity.moreOptions = async function () {
        while (true) {
            await this.selectDialog(
                "更多",
                void 0,
                ["捐助", "关于地图", "个人数据管理"]
            );
            if (!this.dialogResult) break;
            switch (this.dialogResult.value) {
                case "捐助":
                    this.player.openMarketplace([283435159560112, 0]);
                    break;
                case "个人数据管理":
                    await this.manageData();
                    break;
                case "关于地图":
                    this.player.link("https://dao3.fun/exp/experience/detail/100029019", { isNewTab: true, isConfirm: false });
                    break;
            }
        }
    };
    entity.menu = async function () {
        let currentSeedupBlindboxPrice = 0;
        const exit = this.watchGlobal(this.watchGlobalPos);
        this.position.copy(SPAWN_POS);
        do {
            if (this.hasTag("joinGame")) {
                await this.textDialog(
                    void 0,
                    `你已报名参加比赛,请等待下轮比赛开始。\n${this.viewTip()}`
                );
            } else {
                const opt = ["开始", `文具(${this.stationery.length})`, "帮助", "排行榜", "更多"];
                if (this.hasBlindboxOpening) {
                    if (this.blindboxOpeningCanOpen) {
                        opt.splice(1, 0, "获取盲盒中的文具");
                    } else {
                        currentSeedupBlindboxPrice = this.speedupBlindboxPrice;
                        opt.splice(1, 0, "取消打开盲盒");
                        if (this.smallErasers >= currentSeedupBlindboxPrice) {
                            opt.splice(1, 0, `加速打开盲盒(${currentSeedupBlindboxPrice}小橡皮)`);
                        }
                    }
                } else if (this.blindboxes.length > 0) {
                    opt.splice(1, 0, "打开盲盒");
                }
                await this.selectDialog(
                    void 0,
                    void 0,
                    opt
                );
            }
            if (!this.dialogResult?.value) {
                if (entity.isInGame) break;
                else continue;
            }
            switch (this.dialogResult.value) {
                case "帮助":
                    await this.viewHelp();
                    break;
                case "排行榜":
                    await this.displayLeaderbroad()
                    break;
                case "更多":
                    await this.moreOptions();
                    break;
                case "开始":
                    await this.signUpGame();
                    break;
                case `文具(${this.stationery.length})`:
                    await this.stationeryHandbook();
                    break;
                case "打开盲盒":
                    await this.openBlindbox();
                    break;
                case "获取盲盒中的文具":
                    await this.getStationeryFromBlindbox();
                    break;
                case `加速打开盲盒(${currentSeedupBlindboxPrice}小橡皮)`:
                    await this.speedupBlindboxOpening();
                    break;
                case "取消打开盲盒":
                    this.blindboxes.push(this.blindboxOpening.type);
                    this.blindboxOpening.type = null;
                    break;
            }
        } while (!this.isInGame)
        exit();
    };
}

function createPen(penType, id, position) {
    const config = PEN_CONFIG[penType];
    const pen = world.createPen(config, id, position);
    return pen;
}
/*
async function autoOrientateAllPen() {
    for (const pen of world.querySelectorAll(".pen")) {
        if (!pen.velocity.exactEquals(new GameVector3(0, 0, 0))) {
            pen.meshOrientation.copy(pen.meshOrientation.rotateY(Math.PI / 20 * pen.rotateDirection));
        }
        await sleep(1);
    }
}
*/
function buildCarpet(voxName) {
    for (let x = 0; x < 128; x++) {
        for (let z = 0; z < 128; z++) {
            voxels.setVoxel(x, 0, z, voxName);
        }
    }
}
let onRegisterToken = remoteChannel.onServerEvent(async ({ entity, args }) => {
    if (args.name !== "register") return;
    await sleep(1000);
    await world.setup145player(entity);
    setupPlayer(entity);
    entity.id = entity.player.name;
    if (!entity.player.userId) {
        entity.player.kick();
        return;
    }
    entity.player.spectator = true;
    entity.player.invisible = true;
    entity.player.showName = false;
    const exit = entity.watchGlobal(entity.watchGlobalPos);
    entity.position.copy(SPAWN_POS);
    entity.addTag("playing");
    if (entity.joinTimes === 0) {
        await entity.textDialog(
            "新手引导",
            `欢迎来到 弹笔`,
        );
        do {
            await entity.selectDialog("新手引导",
                "请选择你的初始笔\n你可以通过盲盒获得更多笔",
                Object.entries(PEN_CONFIG).filter(v => v[1].rarity === 0).map(v => v[0])
            );
        } while (!entity.dialogResult);
        entity.stationery.push(entity.dialogResult.value);
    }
    entity.joinTimes += 1;
    exit();
    await entity.menu();
});
let onPlayerLeaveToken = world.onPlayerLeave(async ({ entity }) => {
    if (entity.pen) {
        entity.pen.attacker = entity;
        entity.pen.destroy();
    }
});
let onChatToken = world.onChat(async ({ entity, message }) => {
    //broadcast(`${entity.player.name}: ${message}`);
    switch (message) {
        case "管理个人数据":
            await entity.manageData();
            break;
        case "$":
            if (!ADMIN_LIST.includes(entity.player.name)) return;
            entity.player.cancelDialogs();
            await entity.inputDialog("调试", `欢迎你,管理员 ${entity.player.name}`, void 0, "执行");
            if (!entity.dialogResult) break;
            const code = entity.dialogResult;
            let result;
            try {
                result = eval(code);
            } catch (e) {
                result = e;
            } finally {
                await entity.textDialog("调试", result);
            }
            break;
    }
});
let onEntityContactToken = world.onEntityContact(async ({ entity, other }) => {
    if (!(entity.hasTag("pen") && other.hasTag("pen"))) return;
    entity.attacker = other;
});
let onPressToken = world.onPress(({ entity, button }) => {
    if (button === "action0") {
        if (entity.isInGame && entity.clickCoolDownTimeLeft === 0) {
            entity.action0PressTime = Date.now();
            entity.isCharging = true;
        }
    }
    if (button === "action1") {
        if (entity.isInGame) {
            entity.isCharging = false;
        }
    }
});
let onRealeaseToken = world.onRelease(async ({ entity, button, raycast }) => {
    /*if (button === "action1") {
    }*/
    if (button === "action0") {
        if (entity.isInGame && entity.isCharging) {
            entity.pen.click(entity.player.facingDirection, entity.chargeScale);
            entity.isCharging = false;
        }
    }
});
const gameZone = world.addZone({
    selector: ".pen",
    bounds: {
        lo: new GameVector3(-100, -20, -100),
        hi: new GameVector3(228, 30, 228)
    },
});
let gameZoneOnLeaveToken = gameZone.onLeave(async ({ entity }) => {
    entity.destroy();
});
let onEntityDestroyToken = world.onEntityDestroy(({ entity }) => {
    if (!entity.hasTag("pen")) return;
    if (!game.ended) {
        let extra = "";
        if (gameMode !== GameModeType.SIEGE) {
            if (entity.team && entity.attacker.team) {
                let scoreTeam = null;
                if (entity.attacker.team !== entity.team) {
                    scoreTea
17AE
m = entity.attacker.team;
                }
                world.querySelectorAll(".pen").filter(p => scoreTeam ? (p.team === scoreTeam) : (p.team !== entity.team)).forEach(e => e.currentRoundScore += 1);
                extra = `${scoreTeam ?? "敌方"}全体+1分`;
            } else {
                const scoreGet = entity.currentRoundScore + 1;
                entity.attacker.currentRoundScore += scoreGet;
                extra = `+${scoreGet}分共${entity.attacker.currentRoundScore}分`;
            }
        }
        broadcast(`${entity.attacker.team ?? ""} ${entity.attacker.id} 击败 ${entity.team ?? ""} ${entity.id} ${extra}`);
    }
    if (!entity.owner || entity.owner.destroyed) return;
    entity.owner.exitGame();
});
let onTickToken = world.onTick(async ({ tick }) => {
    world.querySelectorAll(".uiClient").forEach(entity => {
        entity.uiDiaplay.left = entity.isControllingPen ? `${entity.pen.id}\n本局分数 ${entity.pen.currentRoundScore}` : entity.menuContent;
        entity.uiDiaplay.top = [topContent, ...broadcastContent.slice(0, 2)].join("\n");
        entity.uiDiaplay.bottom = entity.isControllingPen ? (entity.isCharging ? `蓄力 ${entity.chargeScale}×` : `冷却 ${(entity.clickCoolDownTimeLeft / 1000).toFixed(2)}秒`) : "";
    });
});
function createBoss(name) {
    name ??= Object.keys(BOSS_CONFIG)[Math.floor(Object.keys(BOSS_CONFIG).length * Math.random())];
    buildCarpet("red");
    sleep(5000).then(async () => {
        buildCarpet("black");
    });
    const boss = world.createPen(
        BOSS_CONFIG[name],
        `[boss]${name} `,
        randomPos().add(new GameVector3(0, 2, 0)),
        true
    );
    boss.addTag("npc");
    return boss;
}
function createNPC() {
    const penName = randomPen();
    const npc = createPen(penName, `npc 的 ${penName}`, randomPos());
    npc.target = null;
    npc.addTag("npc");
    return npc;
}
async function refreshRank() {
    totalScoreRank = await storage.userDataStorage.getRank("totalScore", "累计分数", 100, false);
    bestScoreRank = await storage.userDataStorage.getRank("bestScore", "最佳单局分数", 100, false);
}
function seigeWave() {
    const spawnNum = MAX_ENEMY_BOT_NUM - world.querySelectorAll(".npc").length;
    if (spawnNum < MAX_ENEMY_BOT_NUM / 2) return;
    seigeCurrentWave += 1;
    topContent = `${gameMode}${seigeCurrentWave}波`;
    for (let i = 0; i < spawnNum; i++) {
        const npc = i === 0 && Math.random() > 0.5 ? createBoss() : createNPC();
        npc.joinTeam(PenTeamType.RED);
    }
    if (seigeCurrentWave > 1) {
        for (const pen of world.querySelectorAll(".pen")) {
            pen.currentRoundScore += 1;
        }
    }
}
async function startGame() {
    broadcast("新一局比赛即将开始");
    buildCarpet("black");
    world.fogColor = new GameRGBColor(1, 0, 0);
    await sleep(10000);
    if (gameModeVoteList.length === 0) {
        gameMode = GameModeType.FFA;
    } else {
        let gameModeList = Object.values(GameModeType);
        let gameModeVoteResult = gameModeVoteList.reduce(
            (p, v) => {
                p[v] += 1;
                return p;
            },
            gameModeList.reduce(
                (p, v) => {
                    p[v] = 0;
                    return p;
                },
                {}
            )
        );
        gameModeList.sort((a, b) => gameModeVoteResult[b] - gameModeVoteResult[a]);
        gameMode = gameModeList[0];
    }
    gameModeVoteList = [];
    endCountDownStarted = false;
    endCountdownStartSecond = null;
    broadcast(`${gameMode}开始`);
    if (gameMode !== GameModeType.SIEGE) {
        for (let l = 0; l < (MIN_PLAYER_NUM - world.querySelectorAll(".joinGame").length); l++) {
            let npc = await createNPC();
        }
    }
    world.querySelectorAll(".joinGame").forEach(async entity => {
        if (entity.totalScore > 0) entity.totalScore -= 1;
        await entity.joinGame();
        if (gameMode === GameModeType.SIEGE)
            entity.pen.joinTeam(PenTeamType.BLUE);
        entity.removeTag("joinGame");
    });
    await sleep(1000);
    if (gameMode === GameModeType.TDM) {
        world.querySelectorAll(".pen").forEach(e => {
            e.joinRandomTeam();
        });
    }
}
async function endGame() {
    let players = world.querySelectorAll(".pen");
    players.sort((a, b) => {
        return b.currentRoundScore - a.currentRoundScore;
    });
    let extra = "";
    switch (gameMode) {
        case GameModeType.FFA:
            if (players.length == 0) break;
            const mvpScore = players[0].currentRoundScore;
            extra = `MVP ${players[0].id} ${mvpScore}分`;
            if (players[0].owner) {
                players[0].owner.smallErasers += mvpScore;
                extra += ` 获得${mvpScore}小橡皮`;
            }
            break;
        case GameModeType.SIEGE:
            extra = `最终波数:${seigeCurrentWave}`;
            break;
    }
    topContent = `${gameMode}结束 ${extra}`;
    buildCarpet("white");
    world.fogColor = new GameRGBColor(1, 1, 1);
    refreshRank();
    players.forEach(p => p.destroy());
    if (gameMode === GameModeType.SIEGE) {
        seigeCurrentWave = 0;
    }
}
function canEndGame(second) {
    return (
        world.querySelectorAll(".pen").length < 2
        ||
        world.querySelectorAll(".pen").map(v => v.team).every((v, i, a) => v && v === a[0])
        ||
        (endCountDownStarted && second - endCountdownStartSecond >= END_COUNDDOWN_TIME && gameMode !== GameModeType.SIEGE)
    );
}
const game = world.createMatch(
    startGame,
    async (second) => {
        if (gameMode !== GameModeType.SIEGE) {
            const pens = world.querySelectorAll(".pen");
            if (pens.length < 3) {
                if (!endCountDownStarted) {
                    endCountDownStarted = true;
                    endCountdownStartSecond = second;
                }
                topContent = `${gameMode}决赛剩余${END_COUNDDOWN_TIME - (second - endCountdownStartSecond)}秒`;
            } else {
                topContent = `${gameMode}剩余${pens.length}选手`;
            }
        }
        if (gameMode === GameModeType.FFA && second === 20) {
            const boss = createBoss();
            broadcast(`${boss.id} 出现`);
        }
        if (gameMode === GameModeType.SIEGE) {
            seigeWave();
        }
        if (second % 3 === 0) {
            const a = Math.floor(3000 / world.querySelectorAll(".npc").length);
            if (second > 0) {
                for (const npc of world.querySelectorAll(".npc")) {
                    await npc.autoClick();
                    await sleep(a);
                }
            }
        }
    },
    canEndGame,
    endGame
);
async function main() {
    buildCarpet("black");
    world.fogColor = new GameRGBColor(0, 0, 0);
    while (true) {
        await sleep(5000);
        await game.play();
        await sleep(5000);
    }
}
main();
//请勿删除最后一行

pen.js

/*
*/
const PenTeamType = {
    RED: "红队",
    BLUE: "蓝队"
};
const PenColor = {
    RED: new GameRGBAColor(1, 0.1, 0.1, 1),
    BLUE: new GameRGBAColor(0.1, 0.1, 1, 1)
};
const PenRarityType = ["普通", "稀有", "罕见", "史诗", "传奇"];
class PenConfigData {
    constructor(description, mass, friction, scale, rarity, scaleY = 1) {
        this.description = description;
        this.mass = mass;
        this.friction = friction;
        this.scale = scale;
        this.rarity = rarity;
        this.scaleY = scaleY;
        this.mesh = null;
    }
    toEnhanced(scaleConfig) {
        const result = new PenConfigData(this.description, this.mass, this.friction, this.scale, this.rarity + 1, this.scaleY);
        Object.keys(scaleConfig).forEach((k) => {
            result[k] = result[k] * scaleConfig[k];
        });
        result.mesh = this.mesh;
        return result;
    }
    toString() {
        return `“${this.description}”\n重量: ${this.mass}\n摩擦力: ${this.friction}\n稀有度: ${PenRarityType[this.rarity]} `
    }
}
world.createPen = function (config, id, position, isboss = false) {
    let pen = world.createEntity({
        id: id,
        mesh: config.mesh,
        meshScale: new GameVector3(config.scale, config.scale * config.scaleY, config.scale),
        meshOrientation: new GameQuaternion(0, 0, 0, 1).rotateY(Math.random() * Math.PI * 2),
        meshEmissive: 1,
        position: position,
        mass: config.mass,
        friction: config.friction,
        collides: true,
        restitution: 1
    });
    pen.addTag("pen");
    pen.showId = true;
    //pen.interactHint = pen.id;
    pen.customName = pen.id;
    pen.target = null;
    //pen.interactRadius = 300;
    //pen.interactColor = new GameRGBColor(0.3, 0.9, 1);
    //pen.enableInteract = true;
    pen.showEntityName = true;
    pen.nameRadius = 300;
    pen.target = null;
    pen.currentRoundScore = 0;
    pen.lastClickTime = 0;
    pen.team = null;
    pen.attacker = {
        id: pen.id,
        team: null,
        currentRoundScore: 0
    };
    if (isboss) {
        pen.addTag("boss");
    }
    pen.joinTeam = function (teamName) {
        this.team = this.attacker.team = teamName;
        switch (teamName) {
            case PenTeamType.RED:
                this.meshColor.copy(PenColor.RED);
                break;
            case PenTeamType.BLUE:
                this.meshColor.copy(PenColor.BLUE);
                break;
        }
    }
    pen.joinRandomTeam = function () {
        const redNum = world.querySelectorAll(".pen").filter(e => e.team === PenTeamType.RED).length;
        const blueNum = world.querySelectorAll(".pen").filter(e => e.team === PenTeamType.BLUE).length;
        if (redNum > blueNum) {
            this.joinTeam(PenTeamType.BLUE);
        }
        if (blueNum > redNum) {
            this.joinTeam(PenTeamType.RED);
        }
        if (blueNum === redNum) {
            this.joinTeam(Math.random() > 0.5 ? PenTeamType.RED : PenTeamType.BLUE);
        }
    }
    pen.click = function (direction, force = 3/*, rotateDirection = Math.round(Math.random() + 0.5) * (Math.random() > 0.5 ? 1 : -1)*/) {
        this.lastClickTime = Date.now();
        direction.y = 0;
        this.velocity.addEq(direction.scale(1 / direction.mag()).scale(force));
        //this.rotateDirection = rotateDirection;
    };
    pen.autoClick = async function () {
        if (!this.target || this.target.destroyed || this.target.position.distance(this.position) > 50) {
            let players = world.querySelectorAll(".pen").filter(p => p !== this && (!p.team || p.team !== this.team));
            this.target = players.sort((a, b) => {
                return a.position.distance(this.position) - b.position.distance(this.position)
            })[0];
            if (!this.target) return;
        }
        let direction;
        if (this.position.x > 122 || this.position.x < 5 || this.position.z > 115 || this.position.z < 12) {
            direction = new GameVector3(64, this.position.y, 64).sub(this.position);
        } else {
            direction = this.target.position.sub(this.position);
        }
        await this.click(direction, Math.ceil(Math.random() * 3 + 1));
    };

    return pen;
}
module.exports = { PenTeamType, PenRarityType, PenConfigData };
//请勿删除最后一行

145player.js

/** 
 * !Info {Module} -来自145a
 * 145player 4.0
 * 145player是145lab的一部分
 */
"use strict";
const { DEFAULT_USER_DATA, TIPS, ADS } = require("./145playerConfig.js");
require("./145storage.js");
/*config*/
const HELP = TIPS.join("\n");
/*storage*/
storage.userDataStorage = storage.get145DataStorage("main", DEFAULT_USER_DATA);
/*setup*/
world.setup145player = async function (entity) {
    /*dialog*/
    entity.dialogResult = null;
    entity.basicDialog = async function (params) {
        Object.assign(params, {
            titleTextColor: new GameRGBAColor(1, 1, 1, 1),
            titleBackgroundColor: new GameRGBAColor(0, 0, 0, 0.2),
            contentBackgroundColor: new GameRGBAColor(0, 0, 0, 0.2)
        });
        return this.dialogResult = await this.player.dialog(params);//await gui.dialog(this, params.title, params.content, params.options, gui.DialogPositionType.FULL_SCREEN);
    };
    entity.textDialog = async function (title, content) {
        return await this.basicDialog({
            type: "text",
            title: title,
            content: content
        });
    };
    entity.selectDialog = async function (title, content, options) {
        return await this.basicDialog({
            type: "select",
            title: title,
            content: content,
            options: options
        });
    };
    entity.inputDialog = async function (title, content, placeholder, confirmText) {
        return this.dialogResult = await this.player.dialog({
            type: "input",
            title: title,
            content: content,
            placeholder: placeholder,
            console: confirmText
        });
    };
    entity.cancelDialog = async function () {
        this.player.cancelDialogs();
    };
    /*debug*/
    entity.debugBridgeRun = function (input) {
        let output;
        try {
            output = eval(input);
        } catch (e) {
            output = e;
        }
        return output
    };
    entity.debugBridgeConsole = async function () {
        while (true) {
            await this.inputDialog("145lab", "145lab Debug Bridge", "命令", "执行");
            if (!this.dialogResult) break;
            await this.textDialog("145lab", this.debugBridgeRun(this.dialogResult));
        }
    };
    /*tips*/
    entity.viewTip = function () {
        return "Tips:" + TIPS[Math.floor(Math.random() * TIPS.length)];
    };
    /*ad*/
    entity.viewAd = function () {
        return `[广告]${ADS[Math.floor(Math.random() * ADS.length)]}`;
    };
    /*help*/
    entity.viewHelp = async function () {
        await this.textDialog("帮助", HELP);
    };
    /*camera*/
    GamePlayer.prototype.enable
    entity.watchGlobal = function (cameraPos) {
        const previousMode = this.player.cameraMode;
        const previousPosition = this.player.cameraPosition.clone();
        const previousTarget = this.player.cameraTarget.clone();
        const previousUp = this.player.cameraUp.clone();
        const previousDisableInputDirection = this.player.disableInputDirection;
        const previousEnableAction0 = this.player.enableAction0;
        const previousEnableAction1 = this.player.enableAction1;
        this.player.cameraMode = "fixed";
        this.player.cameraPosition.copy(cameraPos);
        this.player.cameraTarget.copy(cameraPos.sub(new GameVector3(0, 100, 0)));
        this.player.cameraUp.set(1, 0, 0);
        this.player.disableInputDirection = GameInputDirection.BOTH;
        this.player.enableAction0 = false;
        this.player.enableAction1 = false;
        return () => {
            this.player.cameraMode = previousMode;
            this.player.cameraTarget.copy(previousTarget);
            this.player.cameraPosition.copy(previousPosition);
            this.player.cameraUp.copy(previousUp);
            this.player.disableInputDirection = previousDisableInputDirection;
            this.player.enableAction0 = previousEnableAction0;
            this.player.enableAction1 = previousEnableAction1;
        };
    };
    /*items*/
    Object.defineProperties(entity, {
        displayItems: {
            enumerable: true,
            get() {
                return Object.entries(entity.items).map(([k, v]) => `${k} * ${v}`);
            }
        },
        totalItemsNum: {
            enumerable: true,
            get() {
                return Object.values(entity.items).reduce((p, v) => p + v, 0);
            }
        }
    });
    entity.addItem = function (name, num = 1) {
        if (entity.items[name]) {
            entity.items[name] += num;
        } else {
            entity.items[name] = num;
        }
        if (entity.items[name] < 1) {
            delete entity.items[name];
        }
    };
    /*storage*/
    entity.manageData = async function () {
        await this.selectDialog("管理145lab储存的个人数据", `userId:${this.player.userId}\n已储存的数据:${JSON.stringify((await storage.userDataStorage.get(this.player.userId)).value)}`, ["删除个人数据"]);
        switch (this.dialogResult?.value) {
            case "删除个人数据":
                await this.selectDialog("删除个人数据", "是否要删除您的个人数据?此操作不可逆", ["删除个人数据"]);
                if (!this.dialogResult) break;
                entity.player.kick();
                storage.userDataStorage.remove(entity.player.userId);
                break;
        }
    };
    await storage.userDataStorage.setupUser(entity);
};
//请勿删除最后一行

145playerConfig.js

//config
const DEFAULT_USER_DATA = {
    joinTimes: 0,//默认进入次数统计
    items: {},//默认背包
    stationery: [],
    totalScore: 0,
    smallErasers: 0,
    blindboxes: [],
    blindboxOpening: {
        type: null,
        startTime: 0
    },
    bestScore: 0,
    cameraView: "",
    usePenType: ""
};
const TIPS = [
    "欢迎来到 弹笔",
    "在 dao3.fun 搜索“弹笔”可以快速找到这张地图",
    "掉下平台边缘淘汰",
    "使用手机等较小屏幕的设备时,建议横屏并全屏",
    "电脑建议选择第三人称视角",
    "使用第三人称视角时,调整面对方向确定方向",
    "移动端推荐横屏并选择俯视视角",
    "使用俯视视角时,左下角摇杆(WASD)确定方向",
    "长按A键(鼠标左键)蓄力,B键(鼠标右键)取消蓄力",
    "比赛结束时概率获得盲盒,得分越高概率越大",
    "开启盲盒可以解锁更多文具",
    "如果你有疑问,请将你的问题写在地图评论区"
];
const ADS = [
    "把繁琐交给145lab,专注于地图本身",
    "在dao3.fun搜索“静谧之森”"
];
module.exports = { DEFAULT_USER_DATA, TIPS, ADS };
//请勿删除最后一行

145storage.js

/**
 * !Info {Module} -来自145a
 * 145storage 4.0
 * 145storage是145lab的一部分
 */
/**
 * 缓冲时间
 * @type {number}
 */
const REQUEST_MIN_INTERVAL = 1000;
class DeepProxy {
    /**
     * @param {object} target
     * @param {object} handler
     */
    constructor(target, handler) {
        const deepHandler = Object.assign({}, handler);
        deepHandler.get = (target, property) => {
            const value = Reflect.get(target, property);
            if (typeof (value) === "object" && value !== null) {
                return new DeepProxy(value, handler);
            } else {
                if (handler.get) {
                    return handler.get(target, property);
                } else {
                    return value;
                }
            }
        };
        return new Proxy(target, deepHandler);
    };
}
/**
 * @param {object} host
 * @param {string} key
 * @param {any} defaultValue
 * @param {function(JSONValue)} updater
 */
storage.setupCloudVariable = async function (host, key, defaultValue, updater) {
    const cacheKey = `_${key}_cache_`;
    const objectCacheKey = `_${key}_object_cache_`;
    const isWaitingKey = `_${key}_isWaiting_`;
    const scheduleUpdateKey = `_${key}_scheduleUpdate_`;
    host[cacheKey] = typeof defaultValue === "object"?Object.assign({},defaultValue):defaultValue;
    host[isWaitingKey] = false;
    host[scheduleUpdateKey] = async function () {
        if (host[isWaitingKey]) return;
        host[isWaitingKey] = true;
        await sleep(REQUEST_MIN_INTERVAL);
        host[isWaitingKey] = false;
        updater(host[cacheKey]);
    }
    Object.defineProperty(host, key, {
        get() {
            return host[cacheKey];
        },
        set(value) {
            host[cacheKey] = value;
            host[scheduleUpdateKey]();
            return true;
        }
    });
    if (typeof defaultValue === "object" && defaultValue !== null) {
        host[objectCacheKey] = defaultValue;
        host[cacheKey] = new DeepProxy(host[objectCacheKey], {
            set(target, property, value) {
                Reflect.set(target, property, value);
                host[scheduleUpdateKey]();
                return true;
            }
        });
    }
}
/**
 * @param {String} name
 * @param {JSONValue} defaultUserData 用户默认属性和数据类型,其中name自带
 * @returns {GameDataStorage}
 */
storage.get145DataStorage = function (name, defaultUserData) {
    let host = storage.getDataStorage(name);
    Object.assign(defaultUserData, {
        name: "游客"
    });
    /**
     * @param {GamePlayerEntity} user
     */
    host.setupUser = async function (user) {
        if (!user || !user.isPlayer) throw `setupUser的参数不对`;
        if (!user.player.userId) return;
        let dataStorage = this;
        const data = await dataStorage.get(user.player.userId);
        if (!data) {
            await dataStorage.set(user.player.userId, defaultUserData);
            return await dataStorage.setupUser(user);
        }
        user._isPreparingUpdate_ = false;
        user._storageUpdateCache_ = {};
        Object.keys(defaultUserData).forEach(key => {
            if (data.value[key] === null || typeof data.value[key] !== typeof defaultUserData[key]) {
                data.value[key] = defaultUserData[key];
            }
            user._storageUpdateCache_[key] = data.value[key];
            storage.setupCloudVariable(user, key, data.value[key], async (value) => {
                user._storageUpdateCache_[key] = value;
                if (user._isPreparingUpdate_) return;
                user._isPreparingUpdate_ = true;
                await sleep(REQUEST_MIN_INTERVAL);
                dataStorage.set(user.player.userId, user._storageUpdateCache_);
                user._isPreparingUpdate_ = false;
            });
        });
        user.name = user.player.name;
    }
    /**
     * @param {string} name
     * @param {string} displayName
     * @param {number} num
     * @param {boolean} ascending
     * @returns {string}
     */
    host.getRank = async function (name, displayName, num = 100, ascending) {
        const data = (await this.list({
            cursor: 0,
            pageSize: num,
            constraintTarget: name,
            ascending: ascending
        })).getCurrentPage().map(v => ({
            key: v.key,
            value: v.value
        }));
        let result = `用户名 ${displayName}\n` + data.map((v) => `${v.value.name} ${v.value[name]}`).join("\n");
        return result;
    }
    return host;
}
//请勿删除最后一行

145match.js

/**
 * !Info {Module} -来自145a
 * 145match 1.0
 * 145match是145lab的一部分
 */
"use strict";
class Match {
    /**
     * @param {function} onStart
     * @param {function(number)} onSecond
     * @param {function():boolean} canEnd
     * @param {function} onEnd
     */
    constructor(onStart, onSecond, canEnd, onEnd) {
        this.ended = null;
        this.timeSecond = 0;
        this.onStart = onStart;
        this.onSecond = onSecond;
        this.canEnd = canEnd;
        this.onEnd = onEnd;
    }
    async play(...params) {
        this.ended = false;
        await this.onStart(...params);
        this.timeSecond = 0
        do {
            this.timeSecond += 1;
            this.onSecond(this.timeSecond);
            await sleep(999);
        } while (!this.canEnd(this.timeSecond))
        this.ended = true;
        await this.onEnd(this.timeSecond);
        this.timeSecond = 0;
    }
}
world.createMatch = function (...params) {
    return new Match(...params);
}
//请勿删除最后一行

ui.js

/**
 * !Info {Module} -来自145a
 * ui模板 0.5
 * ui模板是145lab的一部分
 * ui.js
 */
let onRegisterToken = remoteChannel.onServerEvent(async ({ entity, args }) => {
    switch (args.name) {
        case "register":
            entity.uiDiaplayCache = {};
            Object.assign(entity, args.data);
            entity.screenAspectRatio = entity.screenHeight / entity.screenWidth;
            entity.uiDiaplay = new Proxy(entity.uiDiaplayCache, {
                set(target, property, value) {
                    if (Reflect.get(target, property) !== value) {
                        let data = {};
                        Reflect.set(data, property, value);
                        Reflect.set(target, property, value);
                        remoteChannel.sendClientEvent(entity, {
                            name: "display",
                            data: data
                        });
                    }
                    return true;
                }
            });
            entity.addTag("uiClient");
            break;
            case "resize":
                        Object.assign(entity, args.data);
                        entity.screenAspectRatio = entity.screenHeight / entity.screenWidth;
            break;
    }
});
//请勿删除最后一行

client

clientIndex.js

/**
 * !Info {Project} -来自145a
 * ui模板 0.5
 * ui模板是145lab的一部分
 * clientIndex.js
 */
console.clear();

let DEFAULT_TOP_DISPLAY_CONTENT = "正在连接服务器……"
let getBasicFontSize = () => Math.max(15, Math.floor(screenWidth / 60));
let basicFontSize = getBasicFontSize();

function setupTextColor(node) {
    node.textColor.copy({ r: 255, g: 255, b: 255 });
    node.backgroundColor.copy({ r: 0, g: 0, b: 0 });
    node.backgroundOpacity = 0.2;
}

const permanent = UiScreen.create();
/*permanent.position.offset.copy({x:0,y:0});
permanent.size.offset.copy({x:0,y:0});
permanent.parent = UiScreen.create();*/

const leftDisplay = UiText.create();
leftDisplay.name = "left";
leftDisplay.autoResize = "XY";
leftDisplay.textXAlignment = "Left";
leftDisplay.textYAlignment = "Center";
setupTextColor(leftDisplay);
leftDisplay.textContent = "";
leftDisplay.parent = permanent;

const topDisplay = UiText.create();
topDisplay.name = "top";
topDisplay.anchor.x = 0.5;
topDisplay.autoResize = "XY";
topDisplay.textXAlignment = "Center";
topDisplay.textYAlignment = "Bottom";
setupTextColor(topDisplay);
topDisplay.textContent = DEFAULT_TOP_DISPLAY_CONTENT;
topDisplay.parent = permanent

const bottomDisplay = UiText.create();
bottomDisplay.name = "bottom";
bottomDisplay.anchor.copy({ x: 0.5, y: 1 });
bottomDisplay.autoResize = "XY";
bottomDisplay.textXAlignment = "Center";
bottomDisplay.textYAlignment = "Bottom";
setupTextColor(bottomDisplay);
bottomDisplay.textContent = "";
bottomDisplay.parent = permanent;
function resize() {
    basicFontSize = getBasicFontSize();

    leftDisplay.position.offset.copy({ x: screenWidth / 100, y: 16 });
    leftDisplay.size.offset.copy({ x: 0, y: 0 });

    topDisplay.position.offset.copy({ x: screenWidth / 2, y: screenHeight / 20 });
    topDisplay.size.offset.copy({ x: screenWidth / 3, y: 0 });

    bottomDisplay.position.offset.copy({ x: screenWidth / 2, y: screenHeight - screenHeight / 20 });
    bottomDisplay.size.offset.copy({ x: 0, y: 0 });
    leftDisplay.textFontSize = basicFontSize;
    topDisplay.textFontSize = basicFontSize;
    bottomDisplay.textFontSize = basicFontSize;

}
sleep(20000).then(() => {
    if (topDisplay.textContent !== DEFAULT_TOP_DISPLAY_CONTENT) return;
    topDisplay.textContent = "服务器连接超时,请重试";
});

input.uiEvents.on("pointerup", (args) => {
    if (args?.target?.name) {
        remoteChannel.sendServerEvent({
            name: "click",
            data: args.target.name
        });
    }
});
remoteChannel.events.on("client", (args) => {
    switch (args.name) {
        case "display":
            for (const name of Object.keys(args.data)) {
                permanent.findChildByName(name).textContent = args.data[name];
            }
            break;
    }
});
remoteChannel.sendServerEvent({
    name: "register",
    data: {
        screenHeight: screenHeight,
        screenWidth: screenWidth
    }
});
screen.events.add("resize", ({ screenWidth, screenHeight }) => {
    resize();
    remoteChannel.sendServerEvent({
        name: "resize",
        data: {
            screenHeight: screenHeight,
            screenWidth: screenWidth
        }
    });
});
resize();
//请勿删除最后一行
0