下载 App

理性值跨局持久化

07/272 浏览

1. 数据常量(RationalityData.lua)

lua
M.INITIAL_VALUE = 100 -- 新玩家初始理性值 M.MAX_VALUE = 100 -- 理论上限 M.REMEDY_MAX_VALUE = 90 -- 药剂恢复封顶 M.REMEDY_REQUIRED_MAX_VALUE = 89 -- 药剂可用阈值(理性≤89) M.REMEDY_COST = 3 -- 药剂价格(宝石) M.REMEDY_RECOVERY = 10 -- 药剂恢复量

2. 事件与持久化 Key(Shared.lua)

lua
-- 远程事件 SHOP_RESULT = "BullBear_ShopResult", RATIONALITY_REMEDY_RESULT = "BullBear_RationalityRemedyResult", -- serverCloud 持久化 key ACCOUNT_GEMS_KEY = "friendship_total_gems", REMEDY_CHARGES_KEY = "friendship_remedy_charges", RATIONALITY_KEY = "friendship_rationality_value",

3. 服务端内存缓存(Server.lua 全局 S 表)

lua
S.userIdRationalityCache_ = {} -- userId → 理性值 S.userIdAccountGemsCache_ = {} -- userId → 账户宝石 S.userIdRemedyChargesCache_ = {} -- userId → 背包药剂数

4. 核心增量写入(ServerRationality.lua ApplyValue)

lua
local function ApplyValue(p, value, reason) local oldValue = p.rationality or Data.INITIAL_VALUE local newValue = Data.Clamp(value) p.rationality = newValue -- ... tier/weather/revision 更新 ... -- 1. 内存缓存(WASM刷新不丢失,服务端进程持续) if p.userId then S.userIdRationalityCache_[p.userId] = newValue end -- 2. 云端增量写入(serverCloud:Add,和宝石/药剂一致的可靠方式) if p.userId and serverCloud and reason ~= "cloud_load" and reason ~= "cache_load" then local cloudBase = p.rationalityCloudValue or 0 local delta = newValue - cloudBase if delta ~= 0 then serverCloud:Add(p.userId, Shared.CONFIG.RATIONALITY_KEY, delta, { ok = function(_, iscores) local synced = newValue if iscores and iscores[Shared.CONFIG.RATIONALITY_KEY] then synced = iscores[Shared.CONFIG.RATIONALITY_KEY] end p.rationalityCloudValue = synced end, }) p.rationalityCloudValue = newValue end end return newValue ~= oldValue end

5. 两层读取(Server.lua LoadRationality)

lua
function Server.LoadRationality(slot) local p = S.players_[slot] local userId = p.userId local key = Shared.CONFIG.RATIONALITY_KEY -- 第1层:内存缓存命中 → 零延迟同步发送 local cached = S.userIdRationalityCache_[userId] if cached ~= nil then p.rationalityCloudValue = cached Server.RationalityApplyValue(slot, cached, "cache_load") Server.SendPlayerInfoToConnection(p.connection) end -- 第2层:异步 serverCloud:Get serverCloud:Get(userId, key, { ok = function(scores, iscores) local cloudRaw = (iscores and iscores[key]) or 0 local value = cloudRaw > 0 and cloudRaw or RationalityData.INITIAL_VALUE p.rationalityCloudValue = cloudRaw if cached == nil then -- 无缓存:以云端值为准 S.userIdRationalityCache_[userId] = value Server.RationalityApplyValue(slot, value, "cloud_load") -- 新玩家(cloudRaw=0)首次写入100 if cloudRaw == 0 then serverCloud:Add(userId, key, RationalityData.INITIAL_VALUE, {}) end else -- 缓存命中:云端过期则 Add(delta) 回写 if cloudRaw ~= cached then serverCloud:Add(userId, key, cached - cloudRaw, {}) end end end, }) end

6. 购买药剂(ServerRationality.lua HandleBuyRemedy)

lua
-- 单线程内存权威扣费 p.accountGems = (p.accountGems or 0) - cost p.remedyCharges = (p.remedyCharges or 0) + 1 -- 更新内存缓存 if p.userId then S.userIdAccountGemsCache_[p.userId] = p.accountGems S.userIdRemedyChargesCache_[p.userId] = p.remedyCharges end -- 异步持久化两个 key(serverCloud:Add 增量) serverCloud:Add(userId, ACCOUNT_GEMS_KEY, -cost, { ... }) serverCloud:Add(userId, REMEDY_CHARGES_KEY, 1, { ... })

7. 使用药剂(ServerRationality.lua HandleRationalityRemedy)

lua
-- 检查背包 > 0、阶段/局次限制、理性 ≤ 89 local newValue = math.min(current + Data.REMEDY_RECOVERY, Data.REMEDY_MAX_VALUE) p.remedyCharges = p.remedyCharges - 1 -- ApplyValue 内部自动写内存缓存 + serverCloud:Add(delta) ApplyValue(p, newValue, "remedy") -- 更新药剂缓存 S.userIdRemedyChargesCache_[p.userId] = p.remedyCharges serverCloud:Add(p.userId, REMEDY_CHARGES_KEY, -1, { ... })

8. 关键设计经验

原则说明
内存缓存优先S.userIdXxxCache_[userId] 在每次变化时立即更新,WASM刷新不丢失
增量写入(Add 替代 Set)整数持久化用 serverCloud:Add(key, delta),原子累加,比 Set 覆盖可靠
cloudValue 追踪基准p.rationalityCloudValue 记录上次同步到云端的值,计算 delta 避免重复累加
cloud_load/cache_load 不回写从云端/缓存加载时 reason 标记,ApplyValue 跳过云端写入避免循环
连接时不重置ClientConnected 不再调用 RationalityResetPlayer,等 LoadRationality 加载正确值
人类玩家跨局保留ResetPlayer(p, force) 检查 p.userId,有 userId 时跳过重置(AI 仍重置)
服务端进程不重启WASM 刷新只重启客户端,服务端进程持续运行,内存缓存跨刷新存活
【更新】增加了一些任务截图
【更新】增加了一些任务
我从故事便签墙中,看到了大多数玩家都在和野兽、疾病博弈中倒下 我这两天补充了一些任务,有关合成物品的为主 在此感谢大伙儿的积极反馈
1 赞
4 回复
01:39
【新游】友谊的小船……望官方首推截图
【新游】友谊的小船……望官方首推
Tap平台有大量的‌投资模拟类游戏,谁不想在‌市场中当常胜将军。 但现实中是残酷的,希望这个游戏能让玩家先锻炼好心态,再去博弈 游戏简介: 该游戏是一款以投资市场为背景的心理博弈桌游,核心设计围绕人性弱点(贪婪、恐惧、从众、侥幸、冲动等) 游戏通过大量映射现实市场中的事件,将投资心理陷阱(幸存者偏差、囚徒困境、羊群效应、赌徒谬误、沉没成本、从众心理、社会懈怠、过度自信、锚定效应、确认偏误等)
1 赞
大厅校准模式
一、整体架构 校准系统由四层组成: 层 文件 职责 状态容器 Client.lua 保存 lobbyCalibrationMode_/Drag_/Slots_/RestorePending_/SaveStatus_、路径常量、管理员标记 拖拽 + UI ClientUILobby.lua 指针事件包装、归一化坐标、实时预览、设置面板 序列化 ClientStorage.lua 本地 pref 读写
官方
UI按键动画设计
一、本次反复失败的根因(三个坑) 现象 根因 按钮完全不可见(空白/小白点) 入场动画把 scale/alpha 初始设 0,靠 Update 驱动 Tween 补到 1;但该模板页面级 Update 时序不可靠,补间不推进 → 永远停在 0 背景/按钮颜色反转 用了 nvgRGBA(r,g,b,255) 整数版,出现通道映射异常 矩形在、文字消失 文字色≈填充色;或 DrawStrokedTex
官方
WASM 崩溃
nvgCreateImage 在 WASM 环境下无法正确加载图片文件(中文路径/资源未打包进虚拟文件系统),返回看似正数但实际无效的纹理句柄。Lua 中 0/-1 都是 truthy,if 判断通过后把无效句柄传给 nvgImagePattern,C 层访问空指针 → WASM 内存越界。 彻底解决方案:纯矢量绘制,零文件 I/O 完全放弃图片加载,用 NanoVG 基础图元(圆/椭圆/描边)绘
官方
闪跳问题
下面是这轮三个问题(音量滑块、拖动闪跳、选角页闪跳/白屏)的可复用经验与对应关键代码。代码均为磁盘最终落地版本。 经验一:拖动控件别在回调里重建整棵树 滑块/开关这类"边拖边回调"的控件,onChange 里只能原地改值,绝不能触发 BuildXxxUI() 整树重建——否则正在拖的滑块会被销毁,拖动中断。音量百分比标签也是同理,用闭包持有引用做原地 SetText。 scripts/bullbe
官方
拖到合成目标才留位
错误原因 我在上一轮加回收功能时,为了"修位置回正"加了 else 分支:当牌既没拖到回收区、也没合成目标时,强制把牌弹回原位。这破坏了 Stacklands 的基本交互——拖到空地就该留在那里。 -- ❌ 错误版本(多余的 else 导致拖到空地也弹回) if inRecycle and not card.locked then ...回收溶解... elseif self._mergeTar
官方
【牌】兴登堡凶兆
这张卡的主题很适合《友谊的小船》,但按原始数值直接落地会有一个关键问题:右牌严格优于左牌,四人局会趋向全员选右,博弈不成立。 先评估原方案 选择 未扎堆 同侧≥3人 结论 左:追高 -15万 -40万 始终比右牌更痛 右:抄底 -10万 -25万 任意人数下都更优 无论同侧人数是多少,右牌都少亏: 未扎堆:右牌少亏 5万 扎堆后:右牌少亏 15万 所以理性玩家没有选择左牌的理由;最终通常会四人全选
官方
牌面文字反复重新布局和渲染
调查结论 问题已查清:牌面文字本身没有异常,也不是 NanoVG 重复绘制。真正原因是每名玩家或 AI 投币后,客户端都会销毁并重新创建整个战斗 UI。 左右牌文字、牌框、顶部头像、底部状态栏都在同一棵 UI 树中。因此本来只需要更新底部投币槽,却连带让红框内的牌面文字反复重新布局和渲染,表现为“闪烁几下”。 直接证据 Client.BuildPlayingUI() 每次先销毁现有根节点: scr
官方
卡牌:市场调研
skill_market_research 当前仅完成了: 加入随机技能池; 可以在技能节点被选中; 有新绘制图标; 有 Tooltip 设计说明; 能作为技能徽章显示。 但“选择后显示实时人数,并允许一次改选”的玩法逻辑尚未接入。 代码证据 1. Tooltip 明确标记为“设计中” scripts/bullbear/Client.lua:830 当前内容仍是: 技能 · 侦察 + 改选 · 设
官方