Add formal enemies, remaining formal partners and synthesized UI SFX
- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting. - Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn): Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes. - Formal partners for all ten pets in the arena and portraits, including new VineSprite, ThunderLeopard and TideWhale. - Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands, configurable Unity run_script timeout, companion static stage. - Also includes concurrent work from another tool in this working tree (FlameLion, enemy combat FX, challenge mode, home explorer, server updates). - Resources registered in Doc/ResDesc_*.md; verified on isolated :18081. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
using System.Collections;
|
||||
using HotUpdate.Gameplay;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace HotUpdate.UI
|
||||
{
|
||||
/// <summary>Walks a placeholder explorer over the camp painting and speaks only after a tap.</summary>
|
||||
public sealed class HomeExplorerPresenter : MonoBehaviour
|
||||
{
|
||||
const int Layer = 31;
|
||||
static readonly Color Key = new Color(0.07f, 0.02f, 0.09f, 1f);
|
||||
static readonly string[] Lines =
|
||||
{
|
||||
"喷泉今天的水声很好听。",
|
||||
"芽芽他们在营地里等你。",
|
||||
"想再去弹珠台转转吗?",
|
||||
"这条石路是我每天散步的地方。",
|
||||
"花开得正好,要不要一起看看?",
|
||||
"下一关的路上好像有新朋友。",
|
||||
"训练之后,大家都会更有精神。",
|
||||
"我在这儿看着营地,你去冒险吧。"
|
||||
};
|
||||
|
||||
UiPrefabLoader _loader;
|
||||
GameObject _stage;
|
||||
Camera _camera;
|
||||
Transform _actor;
|
||||
RectTransform _button;
|
||||
RectTransform _bubble;
|
||||
TextMeshProUGUI _line;
|
||||
float _hideAt;
|
||||
int _last = -1;
|
||||
bool _hooked;
|
||||
Vector3 _wander;
|
||||
|
||||
public bool Ready => _actor != null;
|
||||
public bool Talking => _bubble != null && _bubble.gameObject.activeSelf;
|
||||
public string Line => _line != null ? _line.text : "";
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (_camera != null) _camera.enabled = true;
|
||||
else StartCoroutine(Boot());
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (_camera != null) _camera.enabled = false;
|
||||
if (_bubble != null) _bubble.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_button != null) _button.GetComponent<Button>().onClick.RemoveListener(Speak);
|
||||
if (_stage != null) Destroy(_stage);
|
||||
if (_camera != null && _camera.targetTexture != null)
|
||||
{
|
||||
_camera.targetTexture.Release();
|
||||
Destroy(_camera.targetTexture);
|
||||
}
|
||||
_loader?.Dispose();
|
||||
}
|
||||
|
||||
IEnumerator Boot()
|
||||
{
|
||||
var root = transform.parent;
|
||||
_button = root.Find("Btn_Explorer") as RectTransform;
|
||||
_bubble = root.Find("Panel_ExplorerTalk") as RectTransform;
|
||||
_line = root.Find("Panel_ExplorerTalk/Txt_ExplorerTalk").GetComponent<TextMeshProUGUI>();
|
||||
_bubble.gameObject.SetActive(false);
|
||||
yield return null;
|
||||
|
||||
GameObject prefab = null;
|
||||
_loader = new UiPrefabLoader(null, "Assets/Bundles/Characters/Explorer");
|
||||
yield return _loader.LoadAsync("ExplorerSimple", loaded => prefab = loaded);
|
||||
if (prefab == null || _stage != null) yield break;
|
||||
|
||||
_stage = new GameObject("HomeExplorerStage");
|
||||
_stage.transform.position = new Vector3(40f, 0f, 0f);
|
||||
_actor = Instantiate(prefab, _stage.transform).transform;
|
||||
_actor.localPosition = Vector3.zero;
|
||||
_actor.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
SetLayer(_actor);
|
||||
|
||||
var lightGo = new GameObject("Light");
|
||||
lightGo.transform.SetParent(_stage.transform, false);
|
||||
var light = lightGo.AddComponent<Light>();
|
||||
light.type = LightType.Directional;
|
||||
light.intensity = 1.15f;
|
||||
light.cullingMask = 1 << Layer;
|
||||
light.transform.rotation = Quaternion.Euler(38f, -28f, 0f);
|
||||
SetLayer(lightGo.transform);
|
||||
|
||||
var camGo = new GameObject("Camera");
|
||||
camGo.transform.SetParent(_stage.transform, false);
|
||||
_camera = camGo.AddComponent<Camera>();
|
||||
_camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
_camera.backgroundColor = Key;
|
||||
_camera.cullingMask = 1 << Layer;
|
||||
_camera.orthographic = true;
|
||||
_camera.orthographicSize = 1.45f;
|
||||
_camera.nearClipPlane = 0.05f;
|
||||
_camera.farClipPlane = 30f;
|
||||
_camera.allowHDR = false;
|
||||
_camera.depth = -50;
|
||||
_camera.transform.localPosition = new Vector3(0.15f, 1.7f, -3.6f);
|
||||
_camera.transform.LookAt(_stage.transform.TransformPoint(0f, 0.7f, 0f));
|
||||
var rt = new RenderTexture(512, 512, 16, RenderTextureFormat.ARGB32);
|
||||
rt.Create();
|
||||
_camera.targetTexture = rt;
|
||||
|
||||
var raw = gameObject.GetComponent<RawImage>() ?? gameObject.AddComponent<RawImage>();
|
||||
raw.texture = rt;
|
||||
raw.raycastTarget = false;
|
||||
raw.color = Color.white;
|
||||
var shader = Shader.Find("AIGame/HomeExplorerCutout");
|
||||
if (shader != null)
|
||||
{
|
||||
var material = new Material(shader);
|
||||
material.SetColor("_Key", Key);
|
||||
raw.material = material;
|
||||
}
|
||||
_bubble.gameObject.SetActive(false);
|
||||
if (!_hooked && _button != null)
|
||||
{
|
||||
_button.GetComponent<Button>().onClick.AddListener(Speak);
|
||||
_hooked = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_actor == null) return;
|
||||
float t = Time.time * 0.45f;
|
||||
Vector3 next = new Vector3(Mathf.Sin(t) * 1.05f, 0f, Mathf.Sin(t * 0.5f) * 0.28f);
|
||||
Vector3 dir = next - _wander;
|
||||
_wander = next;
|
||||
_actor.localPosition = next;
|
||||
if (dir.sqrMagnitude > 0.000001f)
|
||||
_actor.localRotation = Quaternion.LookRotation(new Vector3(dir.x, 0f, dir.z)) * Quaternion.Euler(0f, 180f, 0f);
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (_actor == null || _camera == null || _button == null) return;
|
||||
var stage = (RectTransform)transform;
|
||||
Vector3 view = _camera.WorldToViewportPoint(_actor.position + Vector3.up * 0.75f);
|
||||
if (view.z < 0f) return;
|
||||
Vector3 world = stage.TransformPoint(new Vector3((view.x - 0.5f) * stage.rect.width, (view.y - 0.5f) * stage.rect.height, 0f));
|
||||
_button.position = world;
|
||||
if (_bubble.gameObject.activeSelf)
|
||||
{
|
||||
_bubble.position = _button.position + new Vector3(0f, 150f * _button.lossyScale.y, 0f);
|
||||
if (Time.unscaledTime >= _hideAt) _bubble.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Speak()
|
||||
{
|
||||
if (_line == null || _bubble == null) return;
|
||||
int index = Random.Range(0, Lines.Length);
|
||||
if (Lines.Length > 1 && index == _last) index = (index + 1) % Lines.Length;
|
||||
_last = index;
|
||||
_line.text = Lines[index];
|
||||
_bubble.gameObject.SetActive(true);
|
||||
_hideAt = Time.unscaledTime + 3.5f;
|
||||
}
|
||||
|
||||
static void SetLayer(Transform root)
|
||||
{
|
||||
root.gameObject.layer = Layer;
|
||||
foreach (Transform child in root) SetLayer(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27a0e40b2f8c1e049adc17487bfe0501
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -65,7 +65,7 @@ namespace HotUpdate.UI
|
||||
SetText("Txt_Diamond", "0");
|
||||
SetText("Txt_Pinballs", state.balls.ToString());
|
||||
SetText("Txt_TaskCaption", "教学目标");
|
||||
SetText("Txt_Task", state.goalClaimed ? "教学目标已完成" : state.cleared ? "领取首通目标奖励" : state.shots == 0 ? "发射第一枚弹珠" : state.level < 2 ? "共享训练至 Lv.2" : "完成教学关卡 1-1");
|
||||
SetText("Txt_Task", GuideTask(state));
|
||||
SetText("Txt_AdventureHint", state.cleared ? "首通已完成 · 可重复挑战" : "首通另获糖果 40 · 不消耗体力");
|
||||
if (config != null && state.schema >= 2)
|
||||
{
|
||||
@@ -100,6 +100,16 @@ namespace HotUpdate.UI
|
||||
}
|
||||
}
|
||||
|
||||
public static string GuideTask(Gameplay.PrototypeSave state)
|
||||
{
|
||||
if (state.goalClaimed) return "教学目标已完成";
|
||||
if (state.cleared) return "领取首通目标奖励";
|
||||
if (state.shots == 0) return "发射第一枚弹珠";
|
||||
if (!state.captureTutorialDone && state.tickets > 0) return "用捕捉券获得芽芽";
|
||||
if (state.level < 2) return "用糖果做一次共享训练";
|
||||
return "布阵并完成教学关 1-1";
|
||||
}
|
||||
|
||||
void SetText(string name, string value)
|
||||
{
|
||||
var label = FindTmp(name);
|
||||
|
||||
Reference in New Issue
Block a user