feat(all): complete map fog unlock, outfit toggle F8, gacha draws, monster combat, cutscenes catalog, and arcade minigame

This commit is contained in:
CAtlas0784 committed 2026-09-18 00:54:40 +07:00
1 parent 3853760c34
commit 75a1d91df0
9 files changed
+242 -29

No files matched your search

@@ -137,6 +137,10 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
await WriteJsonAsync(ctx, await PlayCutsceneAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else if (method == "POST" && path == "/api/timeline/play")
await WriteJsonAsync(ctx, await PlayTimelineAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else if (method == "POST" && path == "/api/minigame/launch")
await WriteJsonAsync(ctx, await LaunchMinigameAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else if (method == "GET" && path == "/api/cutscene/catalog")
await WriteJsonAsync(ctx, CutsceneCatalog(), token);
else if (method == "POST" && path == "/api/npc/spawn")
await WriteJsonAsync(ctx, await NpcSpawnAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else if (method == "GET" && path == "/api/scenes")
@@ -1241,6 +1245,41 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
return new { ok = true };
}
private static object CutsceneCatalog()
{
return new
{
ok = true,
items = new[]
{
new { id = "TL_SEYM_010_Q010_S01", name = "Seymour Story Quest: Chapter 1 S01 (เนื้อเรื่องหลัก)" },
new { id = "SwitchChar_tafei_01", name = "Taffy Cinematic Arrival (ทาฟี่ โดดจากตึก)" },
new { id = "SwitchChar_dila_03", name = "Dila Combat Cinematic (ดิลา แอกชันสตรีท)" },
new { id = "SwitchChar_saimo_05", name = "Seymour Street Arrival (เซย์มัวร์ สไตล์คูล)" },
new { id = "SwitchChar_lixi_04", name = "Richie Motorcycle Rush (ริชชี่ ซิ่งมอเตอร์ไซค์)" },
new { id = "SwitchChar_nanzhujue_02", name = "Male MC Arrival A (พระเอก สลับตัว A)" },
new { id = "SwitchChar_nanzhujue_03", name = "Male MC Arrival B (พระเอก สลับตัว B)" },
new { id = "SwitchChar_common_01", name = "Metropolis Skyline Overview (มุมกล้องเมือง 1)" },
new { id = "SwitchChar_common_04", name = "Street Parkour Traverse (มุมกล้องฟรีรันนิง 4)" },
new { id = "SwitchChar_common_10", name = "Night Neon Lights (มุมกล้องวิวกลางคืน 10)" },
new { id = "SwitchChar_common_27", name = "Dynamic Roof Drop (แอกชันดรอป 27)" },
}
};
}
private async Task<object> LaunchMinigameAsync(string json, CancellationToken token)
{
var session = hub.Current;
if (session is null)
return new { ok = false, error = "no live game session (is the client in the world?)" };
var cmd = "CMD:LAUNCH_MINIGAME:FIGHTER";
await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token);
session.Log.Info("[DEBUG-API] launch arcade minigame fighter dispatched");
return new { ok = true, game = "FIGHTER" };
}
private static async Task<string> ReadBodyAsync(HttpListenerRequest request, CancellationToken token)
{
if (!request.HasEntityBody)
+50 -3
View File
@@ -235,10 +235,41 @@
<div class="card" style="border-color:#ab47bc">
<h2 style="color:#ce93d8">👗 Outfit / Appearance (สลับชุด / ซ่อนเสื้อผ้า)</h2>
<div>
<button style="background:#8e24aa;color:white" onclick="toggleClothes()">Toggle Clothes / ซ่อน-แสดงชุด</button>
<div style="display:flex;gap:8px;align-items:center">
<button style="background:#8e24aa;color:white" onclick="toggleClothes()">Toggle Clothes / ซ่อน-แสดงชุด (F8)</button>
<span class="badge" style="background:#4a148c;color:#e1bee7">กดปุ่ม F8 ในเกมได้</span>
</div>
<div class="muted" style="margin-top:6px">สลับการแสดงผลเสื้อผ้า (SkinnedMeshRenderer) ของตัวละครที่กำลังเล่นอยู่ โดยยังคงแสดงโมเดลบอดี้เดิมไว้</div>
<div class="muted" style="margin-top:6px">สลับการแสดงผลเสื้อผ้าและ Dynamic Bone ของชุดชั้นนอกอย่างหมดจด โดยยังคงโมเดลบอดี้เดิมไว้ 100% ไร้ชิ้นส่วนลอยค้าง</div>
</div>
<div class="card" style="border-color:#00bcd4">
<h2 style="color:#4dd0e1">🎬 3D Cinematics & Story Timelines (ฉากคัทซีนเนื้อเรื่อง)</h2>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<select id="cutsceneSelect" style="flex:1;min-width:260px;background:#263238;color:#eee;border:1px solid #37474f;border-radius:4px;padding:6px">
<option value="TL_SEYM_010_Q010_S01">Seymour Story Quest: Chapter 1 S01 (เนื้อเรื่องหลัก)</option>
<option value="SwitchChar_tafei_01">Taffy Cinematic Arrival (ทาฟี่ โดดจากตึก)</option>
<option value="SwitchChar_dila_03">Dila Combat Cinematic (ดิลา แอกชันสตรีท)</option>
<option value="SwitchChar_saimo_05">Seymour Street Arrival (เซย์มัวร์ สไตล์คูล)</option>
<option value="SwitchChar_lixi_04">Richie Motorcycle Rush (ริชชี่ ซิ่งมอเตอร์ไซค์)</option>
<option value="SwitchChar_nanzhujue_02">Male MC Arrival A (พระเอก สลับตัว A)</option>
<option value="SwitchChar_nanzhujue_03">Male MC Arrival B (พระเอก สลับตัว B)</option>
<option value="SwitchChar_common_01">Metropolis Skyline Overview (มุมกล้องเมือง 1)</option>
<option value="SwitchChar_common_04">Street Parkour Traverse (มุมกล้องฟรีรันนิง 4)</option>
<option value="SwitchChar_common_10">Night Neon Lights (มุมกล้องวิวกลางคืน 10)</option>
<option value="SwitchChar_common_27">Dynamic Roof Drop (แอกชันดรอป 27)</option>
</select>
<button style="background:#00838f;color:white" onclick="playSelectedCutscene()">Play Cinematic / เล่นคัทซีน</button>
</div>
<div class="muted" style="margin-top:6px">สั่งเล่นฉาก Cinematic Timeline 3D ภายใน Engine ของเกมทันทีแบบเรียลไทม์</div>
</div>
<div class="card" style="border-color:#ff9800">
<h2 style="color:#ffb74d">🕹️ Arcade Minigame (ตู้เกมต่อสู้อาร์เคด)</h2>
<div style="display:flex;gap:8px;align-items:center">
<button style="background:#e65100;color:white" onclick="launchArcadeMinigame()">Launch Arcade Fighter / เปิดตู้เกมต่อสู้</button>
<span class="badge" style="background:#bf360c;color:#ffccbc">L18 Street Fighter</span>
</div>
<div class="muted" style="margin-top:6px">เปิดระบบมินิเกมตู้เกม Arcade Fighter ในฉาก</div>
</div>
<div class="card" style="border-color:#e53935">
@@ -661,6 +692,22 @@ async function toggleClothes() {
log('toggle clothes: ' + JSON.stringify(r));
}
async function playSelectedCutscene() {
const sel = document.getElementById('cutsceneSelect');
const name = sel ? sel.value : 'TL_SEYM_010_Q010_S01';
const r = await api('/api/timeline/play', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
log('play cutscene: ' + JSON.stringify(r));
}
async function launchArcadeMinigame() {
const r = await api('/api/minigame/launch', { method: 'POST' });
log('launch arcade minigame: ' + JSON.stringify(r));
}
async function setTimePreset(hour, minute) {
const slider = document.getElementById('timeslider');
if (slider) {
@@ -161,11 +161,19 @@ internal static class RuntimePayloadFactory
root.InfoAchievement.SceneFogMaps = new Dictionary<uint, Auto.SceneFogMap>
{
[1] = new Auto.SceneFogMap { All = true },
[101] = new Auto.SceneFogMap { All = true },
[102] = new Auto.SceneFogMap { All = true },
[103] = new Auto.SceneFogMap { All = true },
[1001] = new Auto.SceneFogMap { All = true },
[10001] = new Auto.SceneFogMap { All = true }
[10001] = new Auto.SceneFogMap { All = true },
[20001125] = new Auto.SceneFogMap { All = true }, // SouDaChe / Parkour
[20001222] = new Auto.SceneFogMap { All = true }, // Xinqi Main City
[20001223] = new Auto.SceneFogMap { All = true }, // Chongxiao Longqi Village
[23300888] = new Auto.SceneFogMap { All = true }, // Main City Raid
[23300999] = new Auto.SceneFogMap { All = true }, // Chongxiao Raid
};
// Unlock all Map POIs
root.InfoAchievement.SceneFogMapPoiIds = Enumerable.Range(1, 1000).Select(i => (uint)i).ToList();
root.InfoAchievement.SceneFogMapPoiIds = Enumerable.Range(1, 5000).Select(i => (uint)i).ToList();
return root;
}
@@ -17,30 +17,64 @@ internal sealed partial class GameRouter
conn.Session.Log.Info($"[GACHA] AskDrawGacha: poolId={poolId} count={count} autoEx={args.isAutoExchange}");
var fiveStarSpirits = new uint[] { 15020992, 15020991, 15020997, 15021024, 15021025 };
var fourStarSpirits = new uint[] { 15020989, 15020990, 15021016, 15021017, 15021020, 15021021, 15021022, 15021023 };
var topWeapons = new uint[] { 98003001, 98003023, 98003131, 98003184, 98003185 };
var sync = new SyncGachaDrawInfo
{
isGrandPrizeWithAllFillers = false,
drawDetails = new List<GachaDrawDetail>()
};
var records = new List<GachaDrawRecord>();
var rng = Random.Shared;
for (uint i = 0; i < count; i++)
{
bool isGuaranteedFiveStar = (count >= 10 && i == count - 1) || (rng.Next(100) < 15);
bool isFourStar = !isGuaranteedFiveStar && (rng.Next(100) < 45);
uint prizeId;
if (isGuaranteedFiveStar)
{
prizeId = fiveStarSpirits[rng.Next(fiveStarSpirits.Length)];
}
else if (isFourStar)
{
prizeId = fourStarSpirits[rng.Next(fourStarSpirits.Length)];
}
else
{
prizeId = topWeapons[rng.Next(topWeapons.Length)];
}
sync.drawDetails.Add(new GachaDrawDetail
{
PoolContentId = poolId,
IsGrandPrize = (i == count - 1),
PoolContentId = prizeId,
IsGrandPrize = isGuaranteedFiveStar,
IsConverted = false,
IsNew = true
});
records.Add(new GachaDrawRecord
{
GachaId = poolId,
PoolContentId = prizeId,
DropId = prizeId,
DrawTimeUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
});
}
await conn.NotifyAsync(MethodId.SyncGachaDrawInfo, sync);
var gachaInfos = new PlayerGachaInfos();
gachaInfos.PoolInfos[poolId] = new PlayerGachaPoolInfo
var poolInfo = new PlayerGachaPoolInfo
{
DrawCount = count
DrawCount = count,
DrawRecords = records
};
gachaInfos.PoolInfos[poolId] = poolInfo;
gachaInfos.GroupInfos[poolId] = new PlayerGachaGroupInfo
{
TotalDrawCount = count
@@ -175,14 +175,15 @@ internal sealed partial class GameRouter
AdminNpcSpawnItem item, NpcCatalogEntry4229938 npc,
PoiActionCatalogEntry4229938? poi, uint mainActionId)
{
bool isMonster = npc.Category == "monster" || npc.Camp is 0 or 26;
return new GameMethods.NpcAgentSyncClientInfo4229938
{
stimIDList = new List<int>(),
CanBeExaminedByPolice = true,
CanBeExaminedByPolice = !isMonster,
indoorList = new(),
roomIds = new List<int>(),
SpoonAgentId = (int)item.NpcFormworkId,
treeName = "PedBase",
treeName = isMonster ? "" : "PedBase",
petPerformData = "",
spawnEffectId = new List<uint>(),
randomModelCfgId = 0,
@@ -197,8 +198,8 @@ internal sealed partial class GameRouter
AgentDataSetsActivityCfgId = 0,
InteractId = npc.InteractSettingId,
AISetting = npc.AiSettingId,
beHitType = 1, // Civilian hit reaction without combat damage.
agentStimType = 0,
beHitType = (byte)(isMonster ? 0 : 1), // 0 = Combat hit reaction & damage, 1 = Civilian non-combat
agentStimType = (byte)(isMonster ? 1 : 0), // 1 = Aggressive combat stimulation
forbidStimulateType = 0,
isAttackInSafeMode = true,
};
@@ -368,8 +368,9 @@ internal sealed partial class GameRouter
await ctx.NotifyAsync(MethodId.SyncAllActivities, activitiesSync);
// Unlock all scene fog of war on the client across all city/world scenes
foreach (var fogSceneId in new uint[] { 1, 1001, 10001, state.ActiveRaidId })
foreach (var fogSceneId in new uint[] { 1, 101, 102, 103, 1001, 10001, 20001125, 20001222, 20001223, 23300888, 23300999, (uint)state.SceneId, state.ActiveRaidId })
{
if (fogSceneId == 0) continue;
await ctx.NotifyAsync(MethodId.SyncSceneFogMapAllUnlock, new SceneMethods.SyncSceneFogMapAllUnlockInfo
{
SceneId = fogSceneId,