forked from xiangwang25/whale-town-front-v2
fix: restore full Godot auth in progressive web build
This commit is contained in:
@@ -47,6 +47,7 @@ var _active_pack_scene_name: String = ""
|
|||||||
var _pack_overlay: CanvasLayer
|
var _pack_overlay: CanvasLayer
|
||||||
var _pack_status_label: Label
|
var _pack_status_label: Label
|
||||||
var _pack_progress_bar: ProgressBar
|
var _pack_progress_bar: ProgressBar
|
||||||
|
var _web_pack_progress_last_emit_msec: int = 0
|
||||||
|
|
||||||
const PACK_MANIFEST_PATH: String = "/packs/manifest.json"
|
const PACK_MANIFEST_PATH: String = "/packs/manifest.json"
|
||||||
const PACK_CACHE_DIR: String = "user://scene-packs"
|
const PACK_CACHE_DIR: String = "user://scene-packs"
|
||||||
@@ -160,8 +161,21 @@ func _process(_delta: float) -> void:
|
|||||||
_pack_progress_bar.value = downloaded
|
_pack_progress_bar.value = downloaded
|
||||||
if is_instance_valid(_pack_status_label):
|
if is_instance_valid(_pack_status_label):
|
||||||
_pack_status_label.text = "正在加载场景 %s" % _format_download_progress(downloaded, total)
|
_pack_status_label.text = "正在加载场景 %s" % _format_download_progress(downloaded, total)
|
||||||
|
_notify_web_auth_pack_progress(downloaded, total)
|
||||||
scene_pack_progress.emit(_active_pack_scene_name, downloaded, total)
|
scene_pack_progress.emit(_active_pack_scene_name, downloaded, total)
|
||||||
|
|
||||||
|
func _notify_web_auth_pack_progress(downloaded: int, total: int) -> void:
|
||||||
|
if OS.get_name() != "Web" or _active_pack_scene_name != "auth":
|
||||||
|
return
|
||||||
|
var now := Time.get_ticks_msec()
|
||||||
|
if now - _web_pack_progress_last_emit_msec < 250:
|
||||||
|
return
|
||||||
|
_web_pack_progress_last_emit_msec = now
|
||||||
|
if not Engine.has_singleton("JavaScriptBridge"):
|
||||||
|
return
|
||||||
|
var bridge: Object = Engine.get_singleton("JavaScriptBridge")
|
||||||
|
bridge.eval("window.whaletownPackProgress && window.whaletownPackProgress(%d, %d)" % [downloaded, total], true)
|
||||||
|
|
||||||
func _ensure_scene_pack(scene_name: String) -> bool:
|
func _ensure_scene_pack(scene_name: String) -> bool:
|
||||||
var packKey := str(SCENE_PACK_KEYS.get(scene_name, ""))
|
var packKey := str(SCENE_PACK_KEYS.get(scene_name, ""))
|
||||||
if packKey.is_empty() or bool(_loaded_scene_packs.get(packKey, false)):
|
if packKey.is_empty() or bool(_loaded_scene_packs.get(packKey, false)):
|
||||||
@@ -181,10 +195,11 @@ func _ensure_scene_pack(scene_name: String) -> bool:
|
|||||||
if fileName.is_empty() or not fileName.ends_with(".pck"):
|
if fileName.is_empty() or not fileName.ends_with(".pck"):
|
||||||
_show_pack_error(scene_name, "场景资源清单格式错误")
|
_show_pack_error(scene_name, "场景资源清单格式错误")
|
||||||
return false
|
return false
|
||||||
|
var expectedSize := int(entry.get("size", 0))
|
||||||
var cacheDirAbsolute := ProjectSettings.globalize_path(PACK_CACHE_DIR)
|
var cacheDirAbsolute := ProjectSettings.globalize_path(PACK_CACHE_DIR)
|
||||||
DirAccess.make_dir_recursive_absolute(cacheDirAbsolute)
|
DirAccess.make_dir_recursive_absolute(cacheDirAbsolute)
|
||||||
var localPath := "%s/%s" % [PACK_CACHE_DIR, fileName]
|
var localPath := "%s/%s" % [PACK_CACHE_DIR, fileName]
|
||||||
if FileAccess.file_exists(localPath) and ProjectSettings.load_resource_pack(localPath, true):
|
if _is_scene_pack_file_valid(localPath, fileName, expectedSize) and ProjectSettings.load_resource_pack(localPath, true):
|
||||||
_loaded_scene_packs[packKey] = true
|
_loaded_scene_packs[packKey] = true
|
||||||
_hide_pack_overlay()
|
_hide_pack_overlay()
|
||||||
return true
|
return true
|
||||||
@@ -194,6 +209,10 @@ func _ensure_scene_pack(scene_name: String) -> bool:
|
|||||||
if packUrl.is_empty() or not await _download_pack(scene_name, packUrl, localPath):
|
if packUrl.is_empty() or not await _download_pack(scene_name, packUrl, localPath):
|
||||||
_show_pack_error(scene_name, "场景下载失败,请检查网络后重试")
|
_show_pack_error(scene_name, "场景下载失败,请检查网络后重试")
|
||||||
return false
|
return false
|
||||||
|
if not _is_scene_pack_file_valid(localPath, fileName, expectedSize):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(localPath))
|
||||||
|
_show_pack_error(scene_name, "场景资源下载不完整,请重新进入")
|
||||||
|
return false
|
||||||
if not ProjectSettings.load_resource_pack(localPath, true):
|
if not ProjectSettings.load_resource_pack(localPath, true):
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(localPath))
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(localPath))
|
||||||
_show_pack_error(scene_name, "场景资源损坏,请重新进入")
|
_show_pack_error(scene_name, "场景资源损坏,请重新进入")
|
||||||
@@ -202,6 +221,26 @@ func _ensure_scene_pack(scene_name: String) -> bool:
|
|||||||
_hide_pack_overlay()
|
_hide_pack_overlay()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
func _is_scene_pack_file_valid(local_path: String, file_name: String, expected_size: int) -> bool:
|
||||||
|
if not FileAccess.file_exists(local_path):
|
||||||
|
return false
|
||||||
|
var file := FileAccess.open(local_path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
return false
|
||||||
|
var actualSize := file.get_length()
|
||||||
|
file.close()
|
||||||
|
if expected_size > 0 and actualSize != expected_size:
|
||||||
|
return false
|
||||||
|
var stem := file_name.trim_suffix(".pck")
|
||||||
|
var separator := stem.rfind("-")
|
||||||
|
if separator < 0:
|
||||||
|
return true
|
||||||
|
var expectedHashPrefix := stem.substr(separator + 1).to_lower()
|
||||||
|
if expectedHashPrefix.length() != 12:
|
||||||
|
return true
|
||||||
|
var actualHash := FileAccess.get_sha256(local_path).to_lower()
|
||||||
|
return not actualHash.is_empty() and actualHash.begins_with(expectedHashPrefix)
|
||||||
|
|
||||||
func _download_pack_manifest() -> Dictionary:
|
func _download_pack_manifest() -> Dictionary:
|
||||||
var request := HTTPRequest.new()
|
var request := HTTPRequest.new()
|
||||||
request.timeout = 12.0
|
request.timeout = 12.0
|
||||||
|
|||||||
@@ -696,6 +696,7 @@ func _ready() -> void:
|
|||||||
_connect_signals()
|
_connect_signals()
|
||||||
_refresh_appearance_ui()
|
_refresh_appearance_ui()
|
||||||
_show_login()
|
_show_login()
|
||||||
|
_notify_web_shell_ready()
|
||||||
|
|
||||||
if _auth_manager != null and bool(_auth_manager.call("is_authenticated")) and _should_auto_resume_cached_session():
|
if _auth_manager != null and bool(_auth_manager.call("is_authenticated")) and _should_auto_resume_cached_session():
|
||||||
_resume_cached_session()
|
_resume_cached_session()
|
||||||
@@ -741,6 +742,12 @@ func _connect_signals() -> void:
|
|||||||
if _auth_manager.has_signal("browser_bootstrap_received"):
|
if _auth_manager.has_signal("browser_bootstrap_received"):
|
||||||
_auth_manager.connect("browser_bootstrap_received", _complete_browser_bootstrap)
|
_auth_manager.connect("browser_bootstrap_received", _complete_browser_bootstrap)
|
||||||
|
|
||||||
|
func _notify_web_shell_ready() -> void:
|
||||||
|
if OS.get_name() != "Web" or not Engine.has_singleton("JavaScriptBridge"):
|
||||||
|
return
|
||||||
|
var bridge: Object = Engine.get_singleton("JavaScriptBridge")
|
||||||
|
bridge.eval("window.whaletownGodotReady && window.whaletownGodotReady()", true)
|
||||||
|
|
||||||
func _complete_browser_bootstrap(kind: String) -> void:
|
func _complete_browser_bootstrap(kind: String) -> void:
|
||||||
if _is_submitting:
|
if _is_submitting:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,40 +1,9 @@
|
|||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
var _auth_manager: Node
|
|
||||||
var _scene_manager: Node
|
var _scene_manager: Node
|
||||||
var _handled: bool = false
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_auth_manager = get_node_or_null("/root/AuthManager")
|
|
||||||
_scene_manager = get_node_or_null("/root/SceneManager")
|
_scene_manager = get_node_or_null("/root/SceneManager")
|
||||||
if OS.get_name() != "Web":
|
if _scene_manager == null:
|
||||||
|
return
|
||||||
_scene_manager.call_deferred("change_scene", "auth", false)
|
_scene_manager.call_deferred("change_scene", "auth", false)
|
||||||
return
|
|
||||||
if _auth_manager == null:
|
|
||||||
return
|
|
||||||
if _auth_manager.has_signal("browser_bootstrap_received"):
|
|
||||||
_auth_manager.connect("browser_bootstrap_received", _on_browser_bootstrap_received)
|
|
||||||
var kind := str(_auth_manager.call("get_browser_bootstrap_kind")) if _auth_manager.has_method("get_browser_bootstrap_kind") else ""
|
|
||||||
if not kind.is_empty():
|
|
||||||
call_deferred("_on_browser_bootstrap_received", kind)
|
|
||||||
|
|
||||||
func _on_browser_bootstrap_received(kind: String) -> void:
|
|
||||||
if _handled or _scene_manager == null:
|
|
||||||
return
|
|
||||||
_handled = true
|
|
||||||
if kind == "register":
|
|
||||||
_scene_manager.call("change_scene", "auth", false)
|
|
||||||
return
|
|
||||||
if _auth_manager.has_method("consume_browser_bootstrap_kind"):
|
|
||||||
_auth_manager.call("consume_browser_bootstrap_kind")
|
|
||||||
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
|
||||||
if appearanceManager != null and appearanceManager.has_method("apply_account_profile"):
|
|
||||||
appearanceManager.call("apply_account_profile", _auth_manager.call("get_current_profile"))
|
|
||||||
var chatManager := get_node_or_null("/root/ChatManager")
|
|
||||||
var token := str(_auth_manager.call("get_access_token"))
|
|
||||||
if chatManager != null and not token.is_empty():
|
|
||||||
chatManager.call("set_game_token", token)
|
|
||||||
chatManager.call("connect_to_chat_server")
|
|
||||||
if _auth_manager.has_method("fetch_profile"):
|
|
||||||
_auth_manager.call("fetch_profile")
|
|
||||||
_scene_manager.call("change_scene", "square", false)
|
|
||||||
|
|||||||
@@ -13,9 +13,17 @@ fi
|
|||||||
|
|
||||||
mkdir -p "$WEB_DIR" "$PACK_DIR"
|
mkdir -p "$WEB_DIR" "$PACK_DIR"
|
||||||
find "$PACK_DIR" -maxdepth 1 -type f \( -name '*.pck' -o -name 'manifest.json' \) -delete
|
find "$PACK_DIR" -maxdepth 1 -type f \( -name '*.pck' -o -name 'manifest.json' \) -delete
|
||||||
|
find "$WEB_DIR" -maxdepth 1 -type f -name 'index-*.pck' -delete
|
||||||
|
|
||||||
"$GODOT_BIN" --headless --path "$PROJECT_DIR" --export-release "Web" "$WEB_DIR/index.html"
|
"$GODOT_BIN" --headless --path "$PROJECT_DIR" --export-release "Web" "$WEB_DIR/index.html"
|
||||||
|
|
||||||
|
core_pack_path="$WEB_DIR/index.pck"
|
||||||
|
core_hash=$(shasum -a 256 "$core_pack_path" | awk '{print substr($1, 1, 12)}')
|
||||||
|
core_pack_name="index-$core_hash.pck"
|
||||||
|
core_pack_size=$(stat -f '%z' "$core_pack_path")
|
||||||
|
mv "$core_pack_path" "$WEB_DIR/$core_pack_name"
|
||||||
|
perl -0pi -e "s/__WHALETOWN_CORE_PACK__/$core_pack_name/g; s/__WHALETOWN_CORE_PACK_SIZE__/$core_pack_size/g" "$WEB_DIR/index.html"
|
||||||
|
|
||||||
build_pack() {
|
build_pack() {
|
||||||
pack_key=$1
|
pack_key=$1
|
||||||
preset=$2
|
preset=$2
|
||||||
@@ -46,4 +54,4 @@ jq -n \
|
|||||||
cp "$PROJECT_DIR/web/auth-background.jpg" "$WEB_DIR/auth-background.jpg"
|
cp "$PROJECT_DIR/web/auth-background.jpg" "$WEB_DIR/auth-background.jpg"
|
||||||
|
|
||||||
echo "Progressive web build created in $WEB_DIR"
|
echo "Progressive web build created in $WEB_DIR"
|
||||||
du -h "$WEB_DIR/index.pck" "$WEB_DIR/index.wasm" "$WEB_DIR/auth-background.jpg" "$PACK_DIR"/*.pck
|
du -h "$WEB_DIR/$core_pack_name" "$WEB_DIR/index.wasm" "$WEB_DIR/auth-background.jpg" "$PACK_DIR"/*.pck
|
||||||
|
|||||||
@@ -9,197 +9,58 @@
|
|||||||
:root { color-scheme: light; font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; }
|
:root { color-scheme: light; font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #b9d9df; }
|
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #b9d9df; }
|
||||||
button, input { font: inherit; letter-spacing: 0; }
|
|
||||||
#canvas { position: fixed; inset: 0; display: block; width: 100%; height: 100%; border: 0; background: #0a2437; }
|
#canvas { position: fixed; inset: 0; display: block; width: 100%; height: 100%; border: 0; background: #0a2437; }
|
||||||
#auth-shell { position: fixed; inset: 0; display: grid; place-items: center; padding: 24px; background: #b9d9df url("auth-background.jpg") center / cover no-repeat; }
|
#loading-shell { position: fixed; inset: 0; z-index: 10; display: grid; place-items: end center; padding: 0 24px max(9vh, 34px); background: #b9d9df url("auth-background.jpg") center / cover no-repeat; transition: opacity .18s ease; }
|
||||||
#auth-shell::before { content: ""; position: absolute; inset: 0; background: rgba(222, 242, 244, .38); }
|
#loading-shell::before { content: ""; position: absolute; inset: 0; background: rgba(222, 242, 244, .22); }
|
||||||
.auth-panel { position: relative; width: min(440px, 100%); max-height: calc(100vh - 48px); overflow: auto; padding: 30px 32px 24px; border: 2px solid rgba(38, 112, 151, .68); border-radius: 8px; background: rgba(249, 253, 252, .96); box-shadow: 0 14px 38px rgba(15, 65, 89, .22); }
|
.loading { position: relative; width: min(420px, 88vw); color: #174f72; text-align: center; text-shadow: 0 1px 0 rgba(255, 255, 255, .9); }
|
||||||
.brand { display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 22px; color: #174f72; }
|
.status { min-height: 24px; margin: 0 0 10px; font-size: 15px; font-weight: 700; line-height: 1.5; letter-spacing: 0; }
|
||||||
.brand-mark { display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%; background: #4c9ac5; color: white; font-size: 27px; font-weight: 800; }
|
.track { height: 7px; overflow: hidden; border: 1px solid rgba(23, 79, 114, .28); border-radius: 4px; background: rgba(255, 255, 255, .78); box-shadow: 0 2px 8px rgba(15, 65, 89, .16); }
|
||||||
.brand h1 { margin: 0; font-size: 26px; line-height: 1.1; letter-spacing: 0; }
|
.bar { width: 0; height: 100%; background: #2b86b6; transition: width .2s ease; }
|
||||||
.tabs { display: grid; grid-template-columns: 1fr 1fr; margin-bottom: 22px; border-bottom: 1px solid #c8dbe2; }
|
.failure { display: none; margin-top: 10px; color: #8f2634; font-size: 13px; line-height: 1.5; }
|
||||||
.tab { min-height: 42px; border: 0; border-bottom: 3px solid transparent; background: transparent; color: #647b86; cursor: pointer; }
|
#loading-shell.done { opacity: 0; pointer-events: none; }
|
||||||
.tab[aria-selected="true"] { border-bottom-color: #257aa8; color: #15577b; font-weight: 700; }
|
|
||||||
.form { display: grid; gap: 15px; }
|
|
||||||
.form[hidden] { display: none; }
|
|
||||||
.field { display: grid; gap: 7px; }
|
|
||||||
.field label { color: #294b5b; font-size: 14px; font-weight: 650; }
|
|
||||||
.field input { width: 100%; min-height: 44px; padding: 9px 12px; border: 1px solid #a9c4cf; border-radius: 6px; outline: none; background: #fff; color: #102e3d; }
|
|
||||||
.field input:focus { border-color: #257aa8; box-shadow: 0 0 0 3px rgba(37, 122, 168, .14); }
|
|
||||||
.code-row { display: grid; grid-template-columns: 1fr 126px; gap: 8px; }
|
|
||||||
.primary, .secondary { min-height: 44px; border-radius: 6px; cursor: pointer; font-weight: 700; }
|
|
||||||
.primary { border: 1px solid #17638e; background: #257aa8; color: #fff; }
|
|
||||||
.secondary { border: 1px solid #84adbe; background: #e7f3f5; color: #205f7c; }
|
|
||||||
button:disabled { cursor: wait; opacity: .62; }
|
|
||||||
.status { min-height: 22px; margin: 13px 0 0; color: #4b6875; font-size: 13px; line-height: 1.5; text-align: center; }
|
|
||||||
.status.error { color: #9b3340; }
|
|
||||||
.load-track { height: 5px; margin-top: 12px; overflow: hidden; border-radius: 3px; background: #d5e5e9; }
|
|
||||||
.load-bar { width: 0; height: 100%; background: #3292b9; transition: width .2s ease; }
|
|
||||||
#failure { display: none; margin-top: 12px; color: #9b3340; font-size: 13px; text-align: center; }
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
#auth-shell { align-items: stretch; padding: 0; }
|
|
||||||
.auth-panel { width: 100%; max-height: 100%; margin-top: 18vh; padding: 24px 22px; border-width: 1px 0 0; border-radius: 8px 8px 0 0; }
|
|
||||||
.brand { margin-bottom: 16px; }
|
|
||||||
.brand h1 { font-size: 23px; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="canvas">当前浏览器不支持游戏画布。</canvas>
|
<canvas id="canvas">当前浏览器不支持游戏画布。</canvas>
|
||||||
<main id="auth-shell">
|
<div id="loading-shell" aria-live="polite">
|
||||||
<section class="auth-panel" aria-label="鲸鱼小镇账户入口">
|
<div class="loading">
|
||||||
<div class="brand"><span class="brand-mark">W</span><h1>鲸鱼小镇</h1></div>
|
<p class="status" id="status">正在加载小镇 0%</p>
|
||||||
<div class="tabs" role="tablist">
|
<div class="track" aria-hidden="true"><div class="bar" id="bar"></div></div>
|
||||||
<button class="tab" id="login-tab" type="button" role="tab" aria-selected="true">登录</button>
|
<div class="failure" id="failure" role="alert"></div>
|
||||||
<button class="tab" id="register-tab" type="button" role="tab" aria-selected="false">注册</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="form" id="login-form">
|
|
||||||
<div class="field"><label for="identifier">用户名 / 邮箱 / 手机号</label><input id="identifier" autocomplete="username" maxlength="100" required></div>
|
|
||||||
<div class="field"><label for="login-password">密码</label><input id="login-password" type="password" autocomplete="current-password" maxlength="128" required></div>
|
|
||||||
<button class="primary" type="submit">进入小镇</button>
|
|
||||||
</form>
|
|
||||||
<form class="form" id="register-form" hidden>
|
|
||||||
<div class="field"><label for="username">用户名</label><input id="username" autocomplete="username" maxlength="50" pattern="[A-Za-z0-9_]+" required></div>
|
|
||||||
<div class="field"><label for="email">邮箱</label><div class="code-row"><input id="email" type="email" autocomplete="email" required><button class="secondary" id="send-code" type="button">获取验证码</button></div></div>
|
|
||||||
<div class="field"><label for="code">邮箱验证码</label><input id="code" inputmode="numeric" autocomplete="one-time-code" minlength="6" maxlength="6" pattern="[0-9]{6}" required></div>
|
|
||||||
<div class="field"><label for="register-password">密码</label><input id="register-password" type="password" autocomplete="new-password" minlength="8" maxlength="128" required></div>
|
|
||||||
<div class="field"><label for="confirm-password">确认密码</label><input id="confirm-password" type="password" autocomplete="new-password" minlength="8" maxlength="128" required></div>
|
|
||||||
<button class="primary" type="submit">注册居民身份</button>
|
|
||||||
</form>
|
|
||||||
<p class="status" id="status" aria-live="polite">主程序正在后台加载 0%</p>
|
|
||||||
<div class="load-track" aria-hidden="true"><div class="load-bar" id="load-bar"></div></div>
|
|
||||||
<div id="failure" role="alert"></div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<noscript>当前浏览器未启用 JavaScript,无法运行鲸鱼小镇。</noscript>
|
<noscript>当前浏览器未启用 JavaScript,无法运行鲸鱼小镇。</noscript>
|
||||||
<script src="$GODOT_URL"></script>
|
<script src="$GODOT_URL"></script>
|
||||||
<script>
|
<script>
|
||||||
const GODOT_CONFIG = $GODOT_CONFIG;
|
const GODOT_CONFIG = $GODOT_CONFIG;
|
||||||
const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
|
const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
|
||||||
const AUTH_STORAGE_KEY = 'whaletown.auth.bootstrap';
|
const CORE_PACK_URL = '__WHALETOWN_CORE_PACK__';
|
||||||
const engine = new Engine(GODOT_CONFIG);
|
const CORE_PACK_SIZE = __WHALETOWN_CORE_PACK_SIZE__;
|
||||||
const shell = document.getElementById('auth-shell');
|
GODOT_CONFIG.mainPack = CORE_PACK_URL;
|
||||||
const canvas = document.getElementById('canvas');
|
GODOT_CONFIG.fileSizes[CORE_PACK_URL] = CORE_PACK_SIZE;
|
||||||
|
const shell = document.getElementById('loading-shell');
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
const loadBar = document.getElementById('load-bar');
|
const bar = document.getElementById('bar');
|
||||||
const failure = document.getElementById('failure');
|
const failure = document.getElementById('failure');
|
||||||
let engineReady = false;
|
const engine = new Engine(GODOT_CONFIG);
|
||||||
let authReady = false;
|
|
||||||
|
|
||||||
function selectTab(name) {
|
window.whaletownGodotReady = function () {
|
||||||
const login = name === 'login';
|
bar.style.width = '100%';
|
||||||
document.getElementById('login-tab').setAttribute('aria-selected', String(login));
|
shell.classList.add('done');
|
||||||
document.getElementById('register-tab').setAttribute('aria-selected', String(!login));
|
window.setTimeout(function () { shell.style.display = 'none'; }, 220);
|
||||||
document.getElementById('login-form').hidden = !login;
|
document.getElementById('canvas').focus();
|
||||||
document.getElementById('register-form').hidden = login;
|
};
|
||||||
setMessage(engineReady ? '主程序已就绪' : status.textContent, false);
|
window.whaletownPackProgress = function (downloaded, total) {
|
||||||
|
const downloadedMb = downloaded / 1048576;
|
||||||
|
if (total > 0) {
|
||||||
|
const totalMb = total / 1048576;
|
||||||
|
const percent = Math.min(100, Math.round(downloaded / total * 100));
|
||||||
|
bar.style.width = percent + '%';
|
||||||
|
status.textContent = '正在加载完整登录界面 ' + downloadedMb.toFixed(1) + ' / ' + totalMb.toFixed(1) + ' MB';
|
||||||
|
} else {
|
||||||
|
status.textContent = '正在加载完整登录界面 ' + downloadedMb.toFixed(1) + ' MB';
|
||||||
}
|
}
|
||||||
|
};
|
||||||
function setMessage(message, isError) {
|
|
||||||
status.textContent = message;
|
|
||||||
status.classList.toggle('error', Boolean(isError));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFormsDisabled(disabled) {
|
|
||||||
document.querySelectorAll('form input, form button').forEach((element) => { element.disabled = disabled; });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postJson(path, body) {
|
|
||||||
const response = await fetch('/api' + path, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
let payload = {};
|
|
||||||
try { payload = await response.json(); } catch (_) { throw new Error('服务器响应格式错误'); }
|
|
||||||
if (!response.ok || !payload.success) throw new Error(payload.message || '请求失败');
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
function canRecoverRegistration(error) {
|
|
||||||
const message = String(error && error.message ? error.message : error);
|
|
||||||
return error instanceof TypeError || message.includes('用户名已存在') || message.includes('用户名已被注册');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerWithRecovery(body) {
|
|
||||||
try {
|
|
||||||
return await postJson('/auth/register', body);
|
|
||||||
} catch (registerError) {
|
|
||||||
if (!canRecoverRegistration(registerError)) throw registerError;
|
|
||||||
try {
|
|
||||||
return await postJson('/auth/login', { identifier: body.username, password: body.password });
|
|
||||||
} catch (_) {
|
|
||||||
throw registerError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handOffAuth(kind, payload) {
|
|
||||||
sessionStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ kind, payload }));
|
|
||||||
authReady = true;
|
|
||||||
setFormsDisabled(true);
|
|
||||||
setMessage(engineReady ? '正在进入小镇...' : '登录成功,主程序仍在后台加载...', false);
|
|
||||||
showGameWhenReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showGameWhenReady() {
|
|
||||||
if (!engineReady || !authReady) return;
|
|
||||||
canvas.focus();
|
|
||||||
shell.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('login-tab').addEventListener('click', () => selectTab('login'));
|
|
||||||
document.getElementById('register-tab').addEventListener('click', () => selectTab('register'));
|
|
||||||
document.getElementById('login-form').addEventListener('submit', async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setFormsDisabled(true);
|
|
||||||
setMessage('正在登录...', false);
|
|
||||||
try {
|
|
||||||
const payload = await postJson('/auth/login', {
|
|
||||||
identifier: document.getElementById('identifier').value.trim(),
|
|
||||||
password: document.getElementById('login-password').value,
|
|
||||||
});
|
|
||||||
handOffAuth('login', payload);
|
|
||||||
} catch (error) {
|
|
||||||
setFormsDisabled(false);
|
|
||||||
setMessage(error.message || '登录失败', true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('send-code').addEventListener('click', async () => {
|
|
||||||
const email = document.getElementById('email').value.trim();
|
|
||||||
if (!email) { setMessage('请先输入邮箱', true); return; }
|
|
||||||
const button = document.getElementById('send-code');
|
|
||||||
button.disabled = true;
|
|
||||||
setMessage('正在发送验证码...', false);
|
|
||||||
try {
|
|
||||||
await postJson('/auth/send-email-verification', { email });
|
|
||||||
setMessage('验证码已发送,请查收邮件', false);
|
|
||||||
} catch (error) {
|
|
||||||
setMessage(error.message || '验证码发送失败', true);
|
|
||||||
} finally { button.disabled = false; }
|
|
||||||
});
|
|
||||||
document.getElementById('register-form').addEventListener('submit', async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const username = document.getElementById('username').value.trim();
|
|
||||||
const password = document.getElementById('register-password').value;
|
|
||||||
if (password !== document.getElementById('confirm-password').value) { setMessage('两次输入的密码不一致', true); return; }
|
|
||||||
if (!/[A-Za-z]/.test(password) || !/[0-9]/.test(password)) { setMessage('密码需要同时包含字母和数字', true); return; }
|
|
||||||
setFormsDisabled(true);
|
|
||||||
setMessage('正在注册...', false);
|
|
||||||
try {
|
|
||||||
const payload = await registerWithRecovery({
|
|
||||||
username,
|
|
||||||
nickname: username,
|
|
||||||
email: document.getElementById('email').value.trim(),
|
|
||||||
email_verification_code: document.getElementById('code').value.trim(),
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
handOffAuth('register', payload);
|
|
||||||
} catch (error) {
|
|
||||||
setFormsDisabled(false);
|
|
||||||
setMessage(error.message || '注册失败', true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const missing = Engine.getMissingFeatures({ threads: GODOT_THREADS_ENABLED });
|
const missing = Engine.getMissingFeatures({ threads: GODOT_THREADS_ENABLED });
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
@@ -210,15 +71,13 @@
|
|||||||
onProgress(current, total) {
|
onProgress(current, total) {
|
||||||
if (total <= 0) return;
|
if (total <= 0) return;
|
||||||
const percent = Math.min(100, Math.round(current / total * 100));
|
const percent = Math.min(100, Math.round(current / total * 100));
|
||||||
loadBar.style.width = percent + '%';
|
bar.style.width = percent + '%';
|
||||||
if (!authReady) setMessage('主程序正在后台加载 ' + percent + '%', false);
|
status.textContent = '正在加载小镇 ' + percent + '%';
|
||||||
},
|
},
|
||||||
}).then(() => {
|
}).then(function () {
|
||||||
engineReady = true;
|
status.textContent = '正在加载完整登录界面...';
|
||||||
loadBar.style.width = '100%';
|
bar.style.width = '0';
|
||||||
if (!authReady) setMessage('主程序已就绪', false);
|
}).catch(function (error) {
|
||||||
showGameWhenReady();
|
|
||||||
}).catch((error) => {
|
|
||||||
failure.style.display = 'block';
|
failure.style.display = 'block';
|
||||||
failure.textContent = error && error.message ? error.message : '主程序加载失败,请刷新重试';
|
failure.textContent = error && error.message ? error.message : '主程序加载失败,请刷新重试';
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user