Files
MrMahoandClaude Opus 5.5 2502a0800e Fix teleport to places landing in the wrong spot (2.10.5)
Use the median of a place's parts and the dense group around it instead of
the bounds of everything (scattered/disabled children pulled the center into
the forest); only take top-level objects as the place root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:04:55 +02:00

149 lines
6.9 KiB
C#

using System.Collections.Generic;
using HutongGames.PlayMaker;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// Admin-Cheats für den eigenen Spieler (Geld, Bedürfnisse). Nur aus dem Admin-Menü erreichbar, also nur für
/// Server-Admins bzw. den P2P-Host. Geld wird über MoneySync automatisch an alle weitergegeben.
/// </summary>
public static class Cheats
{
// Globale PlayMaker-Variablen des Spielers: Wert, auf den "auffüllen" sie setzt
static readonly KeyValuePair<string, float>[] Needs =
{
new KeyValuePair<string, float>("PlayerHunger", 0f),
new KeyValuePair<string, float>("PlayerThirst", 0f),
new KeyValuePair<string, float>("PlayerStress", 0f),
new KeyValuePair<string, float>("PlayerUrine", 0f),
new KeyValuePair<string, float>("PlayerFatigue", 0f),
new KeyValuePair<string, float>("PlayerDirtiness", 0f),
};
public static string AddMoney(float amount)
{
FsmFloat money = FsmVariables.GlobalVariables.FindFsmFloat("PlayerMoney");
if (money == null) return "Money not found (only works in the game)";
money.Value += amount;
return "+" + amount.ToString("0") + " MK (now " + money.Value.ToString("0") + " MK)";
}
public static string FillNeeds()
{
int done = 0;
foreach (KeyValuePair<string, float> need in Needs)
{
FsmFloat v = FsmVariables.GlobalVariables.FindFsmFloat(need.Key);
if (v == null) continue;
v.Value = need.Value;
done++;
}
return done > 0 ? "Hunger, thirst, stress, fatigue, bladder and dirt reset" : "Player needs not found (only works in the game)";
}
/// <summary>Teleport-Ziele im Admin-Menü: Anzeigename und Objekt im Spiel.</summary>
public static readonly string[][] Places =
{
new[] { "Home", "YARD" }, new[] { "Shop (Teimo)", "STORE" }, new[] { "Repair shop", "REPAIRSHOP" },
new[] { "Inspection", "INSPECTION" }, new[] { "Peräjärvi", "PERAJARVI" }, new[] { "Landfill", "LANDFILL" },
new[] { "Dance hall", "DANCEHALL" }, new[] { "Cottage", "COTTAGE" }, new[] { "Cabin", "CABIN" },
new[] { "Rally", "RALLY" }, new[] { "Drag race", "DRAGRACE" }, new[] { "Soccer field", "SOCCER" },
new[] { "Kilju buyer", "KILJUGUY" }, new[] { "Theatre", "THEATRE" }, new[] { "Satsuma", "SATSUMA" },
};
/// <summary>
/// Wohin bei einem Teleport-Ziel? Zu einem Ort gehören oft weit verstreute Teile (Briefkasten, Wege, Strecken,
/// abgeschaltete Objekte) - deren gemeinsame Mitte liegt irgendwo im Wald. Deshalb: Median aller Teile (der Ort
/// selbst, Ausreißer zählen nicht), darum die dichte Gruppe (60 m), daneben ein Stück außerhalb auf den Boden
/// (nicht ins Dach oder in eine Wand).
/// </summary>
public static bool FindPlace(string objectName, out Vector3 position)
{
position = Vector3.zero;
GameObject root = FindRoot(objectName);
if (root == null) return false;
var centers = new List<Vector3>();
foreach (Renderer r in root.GetComponentsInChildren<Renderer>(true))
{
if (r == null || r is ParticleRenderer || r is ParticleSystemRenderer) continue;
Vector3 size = r.bounds.size;
if (size == Vector3.zero || size.x > 150f || size.z > 150f) continue; // Gelände-/Wasserflächen
centers.Add(r.bounds.center);
}
if (centers.Count == 0)
{
Vector3 ground0;
position = Ground(root.transform.position, root.transform.position.y + 50f, out ground0) ? ground0 : root.transform.position;
return true;
}
Vector3 median = new Vector3(Median(centers, 0), Median(centers, 1), Median(centers, 2));
Bounds cluster = new Bounds(median, Vector3.zero);
foreach (Vector3 c in centers)
if (new Vector2(c.x - median.x, c.z - median.z).sqrMagnitude < 60f * 60f) cluster.Encapsulate(c);
Vector3 center = cluster.center;
float ox = Mathf.Min(cluster.extents.x, 20f) + 4f, oz = Mathf.Min(cluster.extents.z, 20f) + 4f;
Vector3[] candidates =
{
center + new Vector3(ox, 0f, 0f), center + new Vector3(-ox, 0f, 0f),
center + new Vector3(0f, 0f, oz), center + new Vector3(0f, 0f, -oz), center,
};
foreach (Vector3 c in candidates)
{
Vector3 ground;
if (Ground(c, cluster.max.y + 30f, out ground)) { position = ground; return true; }
}
position = center + Vector3.up * 2f;
return true;
}
static float Median(List<Vector3> points, int axis)
{
var values = new List<float>(points.Count);
foreach (Vector3 p in points) values.Add(p[axis]);
values.Sort();
return values[values.Count / 2];
}
/// <summary>Wurzelobjekt eines Ortes (nur oberste Ebene - "YARD" o.ä. gibt es auch als Unterobjekt).</summary>
static GameObject FindRoot(string name)
{
GameObject found = null;
// auch ausgeblendete Objekte (das Spiel schaltet entfernte Orte ab); aktive bevorzugt
foreach (GameObject candidate in Resources.FindObjectsOfTypeAll<GameObject>())
{
if (candidate.name != name || candidate.transform.parent != null || candidate.hideFlags != HideFlags.None) continue;
if (candidate.activeInHierarchy) return candidate;
if (found == null) found = candidate;
}
return found;
}
/// <summary>Höchster fester Boden unter einem Punkt (ohne Trigger).</summary>
static bool Ground(Vector3 at, float top, out Vector3 ground)
{
ground = at;
bool found = false;
foreach (RaycastHit hit in Physics.RaycastAll(new Vector3(at.x, top, at.z), Vector3.down, 1000f))
{
if (hit.collider == null || hit.collider.isTrigger || hit.normal.y < 0.6f) continue;
if (!found || hit.point.y > ground.y) { ground = hit.point; found = true; }
}
return found;
}
/// <summary>Eigenen Spieler an eine Position setzen (etwas darüber, damit er nicht im Boden steckt).</summary>
public static bool Teleport(Transform player, Vector3 position)
{
if (player == null) return false;
player.position = position + Vector3.up * 0.3f;
Rigidbody rb = player.GetComponent<Rigidbody>();
if (rb != null && !rb.isKinematic) rb.velocity = Vector3.zero;
return true;
}
}
}