| """ |
| unity_tools.py |
| ============== |
| |
| Complete tool registry for the Unity Agent — a code-generation assistant that |
| can scaffold a GTA 6-level open-world game inside Unity 2022+. |
| |
| Every tool in this module is a self-contained Python class that, when its |
| ``run()`` method is invoked, emits production-quality, compilable C# Unity |
| scripts as a string. The agent orchestrator loops over the requested tools, |
| collects the generated source, and writes the files into a Unity project |
| scaffold (see :class:`SetupUnityProjectTool` and |
| :class:`GenerateCompleteGameTool`). |
| |
| The registry covers every major gameplay system required by a modern open |
| world title: |
| |
| * Locomotion & cameras (player, FPS, third-person) |
| * Vehicles & driving physics |
| * Procedural world generation (city, terrain, roads, buildings, water) |
| * Combat (weapons, health, ragdolls, explosions) |
| * AI (NPCs, pedestrians, traffic, police/wanted) |
| * Systems (economy, quests, missions, factions, skills, crafting, loot) |
| * Presentation (UI, minimap, radio, phone, cutscenes, photo mode) |
| * Persistence (save system, inventory, garages, properties) |
| * Atmosphere (day/night, weather, particles, audio, CCTV) |
| * Networking (multiplayer netcode) |
| |
| All generated C# targets Unity 2022.3 LTS APIs and uses idiomatic patterns: |
| ``[SerializeField]`` private fields, ``[Header]`` grouping, ``Awake``/``Start``/ |
| ``Update``/``FixedUpdate``/``LateUpdate`` lifecycle hooks, ``Rigidbody`` physics, |
| ``NavMeshAgent`` navigation, the legacy ``Input`` class with optional new Input |
| System hooks, ``Canvas``-based UI, ``AudioSource`` 3D audio, and |
| ``ParticleSystem`` effects. |
| |
| The module exposes :data:`TOOL_REGISTRY` (a list of instantiated tools) and |
| the convenience function :func:`get_tool` for lookup by name. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| import textwrap |
| from typing import Any, Dict, List, Optional |
|
|
|
|
| |
| |
| |
|
|
|
|
| class BaseUnityTool: |
| """Base class for every Unity code-generation tool. |
| |
| Subclasses set the class attributes ``name``, ``description`` and |
| ``input_schema`` and override :meth:`run` to return generated C# source |
| code. The ``run`` method receives validated keyword arguments matching |
| ``input_schema`` and must return a string (typically the contents of one |
| or more ``.cs`` files joined by separators). |
| """ |
|
|
| name: str = "base_tool" |
| description: str = "Base Unity tool — override me." |
| input_schema: Dict[str, Any] = {} |
|
|
| def run(self, **kwargs: Any) -> str: |
| """Generate and return C# source code for this tool.""" |
| raise NotImplementedError |
|
|
| def get_schema(self) -> Dict[str, Any]: |
| """Return the JSON-schema-style descriptor for this tool.""" |
| return { |
| "name": self.name, |
| "description": self.description, |
| "input_schema": self.input_schema, |
| } |
|
|
| |
| @staticmethod |
| def _wrap(code: str) -> str: |
| """Dedent a triple-quoted code block for clean emission.""" |
| return textwrap.dedent(code).strip() + "\n" |
|
|
|
|
| TOOL_REGISTRY: List[BaseUnityTool] = [] |
|
|
|
|
| def register_tool(cls: type) -> type: |
| """Class decorator that instantiates a tool and adds it to the registry.""" |
| TOOL_REGISTRY.append(cls()) |
| return cls |
|
|
|
|
| def get_tool(name: str) -> Optional[BaseUnityTool]: |
| """Look up a registered tool by its ``name`` attribute.""" |
| for tool in TOOL_REGISTRY: |
| if tool.name == name: |
| return tool |
| return None |
|
|
|
|
| def list_tools() -> List[Dict[str, Any]]: |
| """Return schema descriptors for every registered tool.""" |
| return [tool.get_schema() for tool in TOOL_REGISTRY] |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreatePlayerControllerTool(BaseUnityTool): |
| """Generate a complete third-person player controller. |
| |
| Produces a ``PlayerController.cs`` MonoBehaviour implementing WASD |
| movement relative to the camera, sprint, crouch, jump, melee attack, |
| and a camera-aware facing direction. Uses a ``CharacterController`` |
| for collision, gravity integration in ``Update``, and root-motion-free |
| animation hooks (``IsWalking``, ``IsSprinting``, ``IsGrounded``, |
| ``Speed``) for an Animator. |
| """ |
|
|
| name = "create_player_controller" |
| description = ( |
| "Generate a full third-person player controller with WASD, sprint, " |
| "crouch, jump, melee and animation hooks." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "PlayerController"}, |
| "move_speed": {"type": "number", "default": 5.0}, |
| "sprint_multiplier": {"type": "number", "default": 2.0}, |
| "crouch_multiplier": {"type": "number", "default": 0.5}, |
| "jump_height": {"type": "number", "default": 2.0}, |
| "gravity": {"type": "number", "default": -19.62}, |
| "rotation_speed": {"type": "number", "default": 10.0}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "PlayerController", |
| move_speed: float = 5.0, |
| sprint_multiplier: float = 2.0, |
| crouch_multiplier: float = 0.5, |
| jump_height: float = 2.0, |
| gravity: float = -19.62, |
| rotation_speed: float = 10.0, |
| **_: Any, |
| ) -> str: |
| return self._wrap(f''' |
| using System; |
| using UnityEngine; |
| |
| namespace OpenWorld.Player |
| {{ |
| /// <summary> |
| /// Third-person character controller. Movement is camera-relative and |
| /// supports walking, sprinting, crouching, jumping and a simple melee |
| /// attack. Animation parameters (IsWalking, IsSprinting, IsGrounded, |
| /// Speed, Attack) are pushed every frame for an Animator to consume. |
| /// </summary> |
| [RequireComponent(typeof(CharacterController))] |
| [RequireComponent(typeof(Animator))] |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Movement")] |
| [SerializeField] private float moveSpeed = {move_speed}f; |
| [SerializeField] private float sprintMultiplier = {sprint_multiplier}f; |
| [SerializeField] private float crouchMultiplier = {crouch_multiplier}f; |
| [SerializeField] private float rotationSpeed = {rotation_speed}f; |
| [SerializeField] private float jumpHeight = {jump_height}f; |
| [SerializeField] private float gravity = {gravity}f; |
| |
| [Header("Ground Check")] |
| [SerializeField] private Transform groundCheck; |
| [SerializeField] private float groundDistance = 0.4f; |
| [SerializeField] private LayerMask groundMask; |
| |
| [Header("Camera")] |
| [SerializeField] private Transform cameraTransform; |
| |
| [Header("Combat")] |
| [SerializeField] private float meleeRange = 2.0f; |
| [SerializeField] private float meleeDamage = 25.0f; |
| [SerializeField] private float meleeCooldown = 0.8f; |
| |
| private CharacterController _controller; |
| private Animator _animator; |
| private Vector3 _velocity; |
| private bool _isGrounded; |
| private bool _isCrouching; |
| private float _meleeTimer; |
| private float _turnSmoothVelocity; |
| |
| private static readonly int IsWalkingHash = Animator.StringToHash("IsWalking"); |
| private static readonly int IsSprintingHash = Animator.StringToHash("IsSprinting"); |
| private static readonly int IsGroundedHash = Animator.StringToHash("IsGrounded"); |
| private static readonly int SpeedHash = Animator.StringToHash("Speed"); |
| private static readonly int AttackHash = Animator.StringToHash("Attack"); |
| |
| private void Awake() |
| {{ |
| _controller = GetComponent<CharacterController>(); |
| _animator = GetComponent<Animator>(); |
| if (cameraTransform == null && Camera.main != null) |
| cameraTransform = Camera.main.transform; |
| }} |
| |
| private void Update() |
| {{ |
| GroundCheck(); |
| HandleMovement(); |
| HandleJump(); |
| HandleCrouch(); |
| HandleMelee(); |
| ApplyGravity(); |
| UpdateAnimator(); |
| }} |
| |
| private void GroundCheck() |
| {{ |
| if (groundCheck != null) |
| _isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask); |
| else |
| _isGrounded = _controller.isGrounded; |
| }} |
| |
| private void HandleMovement() |
| {{ |
| float horizontal = Input.GetAxis("Horizontal"); |
| float vertical = Input.GetAxis("Vertical"); |
| Vector3 direction = new Vector3(horizontal, 0f, vertical).normalized; |
| |
| float targetSpeed = moveSpeed; |
| if (_isCrouching) targetSpeed *= crouchMultiplier; |
| else if (Input.GetKey(KeyCode.LeftShift)) targetSpeed *= sprintMultiplier; |
| |
| if (direction.magnitude >= 0.1f) |
| {{ |
| float targetAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg; |
| if (cameraTransform != null) |
| targetAngle += cameraTransform.eulerAngles.y; |
| float angle = Mathf.SmoothDampAngle( |
| transform.eulerAngles.y, targetAngle, ref _turnSmoothVelocity, |
| 1f / rotationSpeed); |
| transform.rotation = Quaternion.Euler(0f, angle, 0f); |
| |
| Vector3 moveDir = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward; |
| _controller.Move(moveDir.normalized * targetSpeed * Time.deltaTime); |
| }} |
| }} |
| |
| private void HandleJump() |
| {{ |
| if (Input.GetButtonDown("Jump") && _isGrounded && !_isCrouching) |
| _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); |
| }} |
| |
| private void HandleCrouch() |
| {{ |
| if (Input.GetKeyDown(KeyCode.C) && _isGrounded) |
| {{ |
| _isCrouching = !_isCrouching; |
| _controller.height = _isCrouching ? 1.2f : 2.0f; |
| _controller.center = new Vector3(0f, _controller.height / 2f, 0f); |
| }} |
| }} |
| |
| private void HandleMelee() |
| {{ |
| _meleeTimer -= Time.deltaTime; |
| if (Input.GetMouseButtonDown(0) && _meleeTimer <= 0f) |
| {{ |
| _meleeTimer = meleeCooldown; |
| _animator.SetTrigger(AttackHash); |
| if (Physics.Raycast(transform.position + Vector3.up, transform.forward, |
| out RaycastHit hit, meleeRange)) |
| {{ |
| var dmg = hit.collider.GetComponent<IDamageable>(); |
| dmg?.TakeDamage(meleeDamage, transform.position); |
| }} |
| }} |
| }} |
| |
| private void ApplyGravity() |
| {{ |
| if (_isGrounded && _velocity.y < 0f) |
| _velocity.y = -2f; |
| _velocity.y += gravity * Time.deltaTime; |
| _controller.Move(_velocity * Time.deltaTime); |
| }} |
| |
| private void UpdateAnimator() |
| {{ |
| float horizontal = Input.GetAxis("Horizontal"); |
| float vertical = Input.GetAxis("Vertical"); |
| float speed = new Vector2(horizontal, vertical).sqrMagnitude; |
| _animator.SetBool(IsWalkingHash, speed > 0.01f); |
| _animator.SetBool(IsSprintingHash, Input.GetKey(KeyCode.LeftShift) && speed > 0.01f); |
| _animator.SetBool(IsGroundedHash, _isGrounded); |
| _animator.SetFloat(SpeedHash, speed); |
| }} |
| |
| private void OnDrawGizmosSelected() |
| {{ |
| if (groundCheck != null) |
| {{ |
| Gizmos.color = Color.red; |
| Gizmos.DrawWireSphere(groundCheck.position, groundDistance); |
| }} |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateVehicleControllerTool(BaseUnityTool): |
| """Generate a vehicle controller with arcade-realistic car physics.""" |
|
|
| name = "create_vehicle_controller" |
| description = ( |
| "Generate a full car controller with acceleration/steering curves, " |
| "brakes, handbrake, gears, engine sound, lights and damage." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "VehicleController"}, |
| "max_torque": {"type": "number", "default": 1500.0}, |
| "max_brake": {"type": "number", "default": 3000.0}, |
| "max_steer": {"type": "number", "default": 30.0}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "VehicleController", |
| max_torque: float = 1500.0, |
| max_brake: float = 3000.0, |
| max_steer: float = 30.0, |
| **_: Any, |
| ) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Vehicles |
| {{ |
| /// <summary> |
| /// Arcade-realistic four-wheel car controller. WheelColliders provide |
| /// traction; a single Rigidbody carries mass and inertia. Includes gear |
| /// ratios, engine audio pitch, head/tail/brake/reverse lights and a |
| /// simple impact-damage model. |
| /// </summary> |
| [RequireComponent(typeof(Rigidbody))] |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Wheel Colliders")] |
| [SerializeField] private WheelCollider frontLeft; |
| [SerializeField] private WheelCollider frontRight; |
| [SerializeField] private WheelCollider rearLeft; |
| [SerializeField] private WheelCollider rearRight; |
| |
| [Header("Wheel Meshes")] |
| [SerializeField] private Transform frontLeftMesh; |
| [SerializeField] private Transform frontRightMesh; |
| [SerializeField] private Transform rearLeftMesh; |
| [SerializeField] private Transform rearRightMesh; |
| |
| [Header("Engine")] |
| [SerializeField] private float maxMotorTorque = {max_torque}f; |
| [SerializeField] private float maxBrakeTorque = {max_brake}f; |
| [SerializeField] private float maxSteerAngle = {max_steer}f; |
| [SerializeField] private float[] gearRatios = {{ 3.6f, 2.5f, 1.8f, 1.3f, 1.0f, 0.8f }}; |
| [SerializeField] private float reverseRatio = -3.2f; |
| [SerializeField] private float downforce = 50f; |
| |
| [Header("Audio")] |
| [SerializeField] private AudioSource engineAudio; |
| [SerializeField] private float minPitch = 0.4f; |
| [SerializeField] private float maxPitch = 2.2f; |
| |
| [Header("Lights")] |
| [SerializeField] private Light[] headLights; |
| [SerializeField] private Light[] brakeLights; |
| [SerializeField] private Light[] reverseLights; |
| [SerializeField] private Material brakeMat; |
| [SerializeField] private Color brakeOn = Color.red; |
| [SerializeField] private Color brakeOff = new Color(0.3f, 0f, 0f); |
| |
| [Header("Damage")] |
| [SerializeField] private float maxHealth = 100f; |
| [SerializeField] private ParticleSystem smokePrefab; |
| [SerializeField] private ParticleSystem firePrefab; |
| |
| private Rigidbody _rb; |
| private int _currentGear; |
| private float _currentSpeed; |
| private float _health; |
| private bool _braking; |
| private bool _reversing; |
| |
| public float SpeedKph => _currentSpeed * 3.6f; |
| public float HealthPercent => Mathf.Clamp01(_health / maxHealth); |
| public int CurrentGear => _reversing ? -1 : _currentGear + 1; |
| |
| private void Awake() |
| {{ |
| _rb = GetComponent<Rigidbody>(); |
| _health = maxHealth; |
| if (engineAudio != null) engineAudio.loop = true; |
| }} |
| |
| private void FixedUpdate() |
| {{ |
| Steer(); |
| Drive(); |
| Handbrake(); |
| UpdateWheels(); |
| ApplyDownforce(); |
| AutoShift(); |
| }} |
| |
| private void Update() |
| {{ |
| UpdateLights(); |
| UpdateAudio(); |
| }} |
| |
| private void Steer() |
| {{ |
| float steerInput = Input.GetAxis("Horizontal"); |
| float speedFactor = Mathf.Clamp01(_currentSpeed / 30f); |
| float steer = maxSteerAngle * steerInput * (1f - speedFactor * 0.4f); |
| frontLeft.steerAngle = steer; |
| frontRight.steerAngle = steer; |
| }} |
| |
| private void Drive() |
| {{ |
| float throttle = Input.GetAxis("Vertical"); |
| _braking = Mathf.Abs(throttle) < 0.05f; |
| _reversing = throttle < -0.05f && _currentSpeed < 2f; |
| |
| float ratio = _reversing ? reverseRatio : gearRatios[_currentGear]; |
| float torque = throttle * maxMotorTorque * Mathf.Abs(ratio); |
| |
| foreach (var wc in new[] {{ rearLeft, rearRight }}) |
| {{ |
| wc.motorTorque = _braking ? 0f : torque; |
| wc.brakeTorque = _braking ? maxBrakeTorque * 0.1f : 0f; |
| }} |
| foreach (var wc in new[] {{ frontLeft, frontRight }}) |
| wc.brakeTorque = _braking ? maxBrakeTorque * 0.1f : 0f; |
| |
| _currentSpeed = _rb.velocity.magnitude; |
| }} |
| |
| private void Handbrake() |
| {{ |
| if (Input.GetKey(KeyCode.Space)) |
| {{ |
| rearLeft.brakeTorque = maxBrakeTorque; |
| rearRight.brakeTorque = maxBrakeTorque; |
| rearLeft.motorTorque = 0f; |
| rearRight.motorTorque = 0f; |
| }} |
| }} |
| |
| private void UpdateWheels() |
| {{ |
| UpdateWheelPose(frontLeft, frontLeftMesh); |
| UpdateWheelPose(frontRight, frontRightMesh); |
| UpdateWheelPose(rearLeft, rearLeftMesh); |
| UpdateWheelPose(rearRight, rearRightMesh); |
| }} |
| |
| private static void UpdateWheelPose(WheelCollider collider, Transform mesh) |
| {{ |
| if (mesh == null) return; |
| collider.GetWorldPose(out Vector3 pos, out Quaternion rot); |
| mesh.SetPositionAndRotation(pos, rot); |
| }} |
| |
| private void ApplyDownforce() |
| {{ |
| _rb.AddForce(-transform.up * downforce * _rb.velocity.magnitude); |
| }} |
| |
| private void AutoShift() |
| {{ |
| float speed = SpeedKph; |
| int target = Mathf.Clamp(Mathf.FloorToInt(speed / 30f), 0, gearRatios.Length - 1); |
| if (target != _currentGear) _currentGear = target; |
| }} |
| |
| private void UpdateLights() |
| {{ |
| bool brakeOnNow = _braking || Input.GetKey(KeyCode.Space); |
| if (brakeMat != null) |
| brakeMat.SetColor("_EmissionColor", brakeOnNow ? brakeOn : brakeOff); |
| foreach (var l in brakeLights) if (l != null) l.enabled = brakeOnNow; |
| foreach (var l in reverseLights) if (l != null) l.enabled = _reversing; |
| }} |
| |
| private void UpdateAudio() |
| {{ |
| if (engineAudio == null) return; |
| float t = Mathf.Clamp01(_currentSpeed / 60f); |
| float gearFactor = (_currentGear + 1f) / gearRatios.Length; |
| engineAudio.pitch = Mathf.Lerp(minPitch, maxPitch, t * gearFactor + 0.2f); |
| }} |
| |
| private void OnCollisionEnter(Collision collision) |
| {{ |
| float impact = collision.relativeVelocity.magnitude; |
| if (impact > 8f) |
| {{ |
| _health -= impact * 0.5f; |
| if (_health < maxHealth * 0.3f && smokePrefab != null && !smokePrefab.isPlaying) |
| smokePrefab.Play(); |
| if (_health <= 0f && firePrefab != null) firePrefab.Play(); |
| }} |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateProceduralCityTool(BaseUnityTool): |
| """Generate a procedural grid-city builder with multiple districts.""" |
|
|
| name = "create_procedural_city" |
| description = ( |
| "Generate a procedural grid city with 200+ buildings, districts " |
| "(downtown, residential, industrial), variable heights and window " |
| "patterns per district." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "ProceduralCity"}, |
| "grid_size": {"type": "integer", "default": 16}, |
| "block_size": {"type": "number", "default": 40.0}, |
| "road_width": {"type": "number", "default": 12.0}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "ProceduralCity", |
| grid_size: int = 16, |
| block_size: float = 40.0, |
| road_width: float = 12.0, |
| **_: Any, |
| ) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.World |
| {{ |
| /// <summary> |
| /// Procedurally generates a grid-based city. The grid is partitioned |
| /// into districts by region; each district defines building height |
| /// ranges, footprint sizes and window texture tints. Roads separate |
| /// every block. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Grid")] |
| [SerializeField] private int gridSize = {grid_size}; |
| [SerializeField] private float blockSize = {block_size}f; |
| [SerializeField] private float roadWidth = {road_width}f; |
| [SerializeField] private int seed = 1337; |
| |
| [Header("Prefabs")] |
| [SerializeField] private GameObject roadPrefab; |
| [SerializeField] private GameObject sidewalkPrefab; |
| [SerializeField] private Material[] windowMaterials; |
| |
| [Header("Districts")] |
| [SerializeField] private District[] districts; |
| |
| private System.Random _rng; |
| |
| [Serializable] |
| public struct District |
| {{ |
| public string name; |
| public Vector2 heightRange; |
| public Vector2 footprintRange; |
| public Color windowTint; |
| public float density; |
| }} |
| |
| private void Awake() |
| {{ |
| _rng = new System.Random(seed); |
| Generate(); |
| }} |
| |
| public void Generate() |
| {{ |
| ClearChildren(); |
| for (int x = 0; x < gridSize; x++) |
| for (int z = 0; z < gridSize; z++) |
| {{ |
| Vector3 center = new Vector3( |
| x * (blockSize + roadWidth), 0f, z * (blockSize + roadWidth)); |
| District d = PickDistrict(x, z); |
| BuildBlock(center, d); |
| BuildRoads(center); |
| }} |
| }} |
| |
| private District PickDistrict(int x, int z) |
| {{ |
| // Downtown near centre, residential mid, industrial edges. |
| float dx = (x - gridSize / 2f) / gridSize; |
| float dz = (z - gridSize / 2f) / gridSize; |
| float dist = Mathf.Sqrt(dx * dx + dz * dz); |
| int idx = dist < 0.25f ? 0 : dist < 0.45f ? 1 : 2; |
| return districts[Mathf.Clamp(idx, 0, districts.Length - 1)]; |
| }} |
| |
| private void BuildBlock(Vector3 center, District d) |
| {{ |
| int count = Mathf.FloorToInt(blockSize / 12f * d.density); |
| for (int i = 0; i < count; i++) |
| {{ |
| float fx = (float)_rng.NextDouble(); |
| float fz = (float)_rng.NextDouble(); |
| Vector3 pos = center + new Vector3( |
| (fx - 0.5f) * (blockSize - 6f), 0f, |
| (fz - 0.5f) * (blockSize - 6f)); |
| float height = Mathf.Lerp(d.heightRange.x, d.heightRange.y, |
| (float)_rng.NextDouble()); |
| float footprint = Mathf.Lerp(d.footprintRange.x, d.footprintRange.y, |
| (float)_rng.NextDouble()); |
| GameObject b = GameObject.CreatePrimitive(PrimitiveType.Cube); |
| b.transform.SetParent(transform); |
| b.transform.position = pos + Vector3.up * (height * 0.5f); |
| b.transform.localScale = new Vector3(footprint, height, footprint); |
| ApplyWindowMaterial(b, d, height); |
| }} |
| }} |
| |
| private void ApplyWindowMaterial(GameObject building, District d, float height) |
| {{ |
| Renderer r = building.GetComponent<Renderer>(); |
| if (r == null || windowMaterials == null || windowMaterials.Length == 0) return; |
| Material m = windowMaterials[_rng.Next(windowMaterials.Length)]; |
| Material inst = new Material(m); |
| inst.SetColor("_EmissionColor", d.windowTint * 0.5f); |
| inst.mainTextureScale = new Vector2( |
| Mathf.Max(1, Mathf.FloorToInt(height / 3f)), |
| Mathf.Max(1, Mathf.FloorToInt(height / 3f))); |
| r.sharedMaterial = inst; |
| }} |
| |
| private void BuildRoads(Vector3 center) |
| {{ |
| if (roadPrefab == null) return; |
| Vector3 horiz = center + Vector3.left * (blockSize + roadWidth) * 0.5f; |
| Vector3 vert = center + Vector3.back * (blockSize + roadWidth) * 0.5f; |
| Instantiate(roadPrefab, horiz, Quaternion.Euler(0f, 90f, 0f), transform) |
| .transform.localScale = new Vector3(blockSize, 1f, roadWidth); |
| Instantiate(roadPrefab, vert, Quaternion.identity, transform) |
| .transform.localScale = new Vector3(roadWidth, 1f, blockSize); |
| }} |
| |
| private void ClearChildren() |
| {{ |
| for (int i = transform.childCount - 1; i >= 0; i--) |
| DestroyImmediate(transform.GetChild(i).gameObject); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateThirdPersonCameraTool(BaseUnityTool): |
| """Generate a smooth follow third-person camera with collision & zoom.""" |
|
|
| name = "create_third_person_camera" |
| description = ( |
| "Generate a third-person camera with smooth follow, collision, " |
| "zoom and aim mode." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "ThirdPersonCamera"}, |
| "distance": {"type": "number", "default": 6.0}, |
| "height": {"type": "number", "default": 2.5}, |
| "sensitivity": {"type": "number", "default": 3.0}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "ThirdPersonCamera", |
| distance: float = 6.0, |
| height: float = 2.5, |
| sensitivity: float = 3.0, |
| **_: Any, |
| ) -> str: |
| return self._wrap(f''' |
| using UnityEngine; |
| |
| namespace OpenWorld.Camera |
| {{ |
| /// <summary> |
| /// Smooth third-person follow camera with mouse orbit, scroll-wheel |
| /// zoom, camera collision via SphereCast and an aim mode that pulls |
| /// the camera over the shoulder. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Target")] |
| [SerializeField] private Transform target; |
| [SerializeField] private Vector3 targetOffset = new Vector3(0f, {height}f, 0f); |
| |
| [Header("Orbit")] |
| [SerializeField] private float distance = {distance}f; |
| [SerializeField] private float minDistance = 2f; |
| [SerializeField] private float maxDistance = 12f; |
| [SerializeField] private float sensitivity = {sensitivity}f; |
| [SerializeField] private float pitchMin = -40f; |
| [SerializeField] private float pitchMax = 80f; |
| [SerializeField] private float smoothTime = 0.12f; |
| |
| [Header("Collision")] |
| [SerializeField] private float collisionRadius = 0.3f; |
| [SerializeField] private LayerMask collisionMask = ~0; |
| |
| [Header("Aim Mode")] |
| [SerializeField] private Vector3 aimOffset = new Vector3(0.7f, 1.4f, 0f); |
| [SerializeField] private float aimDistance = 2.5f; |
| |
| private float _yaw; |
| private float _pitch = 15f; |
| private float _currentDistance; |
| private Vector3 _smoothVel; |
| |
| private void Start() |
| {{ |
| _currentDistance = distance; |
| if (target != null) _yaw = target.eulerAngles.y; |
| Cursor.lockState = CursorLockMode.Locked; |
| }} |
| |
| private void LateUpdate() |
| {{ |
| if (target == null) return; |
| ReadInput(); |
| UpdateOrbit(); |
| HandleCollision(); |
| transform.position = Vector3.SmoothDamp( |
| transform.position, DesiredPosition(), ref _smoothVel, smoothTime); |
| transform.LookAt(target.position + targetOffset); |
| }} |
| |
| private void ReadInput() |
| {{ |
| _yaw += Input.GetAxis("Mouse X") * sensitivity; |
| _pitch -= Input.GetAxis("Mouse Y") * sensitivity; |
| _pitch = Mathf.Clamp(_pitch, pitchMin, pitchMax); |
| float scroll = Input.GetAxis("Mouse ScrollWheel"); |
| distance = Mathf.Clamp(distance - scroll * 4f, minDistance, maxDistance); |
| }} |
| |
| private void UpdateOrbit() |
| {{ |
| if (Input.GetMouseButton(1)) |
| {{ |
| target.rotation = Quaternion.Euler(0f, _yaw, 0f); |
| }} |
| }} |
| |
| private Vector3 DesiredPosition() |
| {{ |
| bool aim = Input.GetMouseButton(1); |
| float dist = aim ? aimDistance : _currentDistance; |
| Vector3 offset = aim ? aimOffset : targetOffset; |
| Quaternion rot = Quaternion.Euler(_pitch, _yaw, 0f); |
| return target.position + offset - (rot * Vector3.forward * dist); |
| }} |
| |
| private void HandleCollision() |
| {{ |
| Vector3 origin = target.position + targetOffset; |
| Vector3 dir = transform.position - origin; |
| float dist = dir.magnitude; |
| dir.Normalize(); |
| if (Physics.SphereCast(origin, collisionRadius, dir, |
| out RaycastHit hit, dist, collisionMask)) |
| _currentDistance = Mathf.Min(distance, hit.distance); |
| else |
| _currentDistance = Mathf.Lerp(_currentDistance, distance, 0.1f); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateFpsControllerTool(BaseUnityTool): |
| """Generate an FPS controller with mouse-look, lean and crouch.""" |
|
|
| name = "create_fps_controller" |
| description = ( |
| "Generate an FPS controller with mouse look, WASD, jump, crouch " |
| "and lean mechanics." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "FpsController"}, |
| "move_speed": {"type": "number", "default": 5.0}, |
| "look_sensitivity": {"type": "number", "default": 2.0}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "FpsController", |
| move_speed: float = 5.0, |
| look_sensitivity: float = 2.0, |
| **_: Any, |
| ) -> str: |
| return self._wrap(f''' |
| using UnityEngine; |
| |
| namespace OpenWorld.Player |
| {{ |
| /// <summary> |
| /// First-person controller. Mouse look drives a yaw on the body and |
| /// pitch on the camera. WASD moves on the horizontal plane, jump uses |
| /// an impulse, crouch shrinks the capsule, and Q/E lean the camera. |
| /// </summary> |
| [RequireComponent(typeof(CharacterController))] |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Movement")] |
| [SerializeField] private float walkSpeed = {move_speed}f; |
| [SerializeField] private float sprintSpeed = 8f; |
| [SerializeField] private float crouchSpeed = 2.5f; |
| [SerializeField] private float jumpHeight = 1.4f; |
| [SerializeField] private float gravity = -19.62f; |
| |
| [Header("Look")] |
| [SerializeField] private Transform cameraTransform; |
| [SerializeField] private float lookSensitivity = {look_sensitivity}f; |
| [SerializeField] private float pitchLimit = 85f; |
| |
| [Header("Lean")] |
| [SerializeField] private float leanAngle = 25f; |
| [SerializeField] private float leanSpeed = 8f; |
| [SerializeField] private float leanOffset = 0.6f; |
| |
| [Header("Crouch")] |
| [SerializeField] private float standHeight = 2f; |
| [SerializeField] private float crouchHeight = 1.2f; |
| |
| private CharacterController _cc; |
| private float _pitch; |
| private float _targetLean; |
| private float _currentLean; |
| private Vector3 _velocity; |
| private bool _isCrouching; |
| |
| private void Awake() |
| {{ |
| _cc = GetComponent<CharacterController>(); |
| if (cameraTransform == null && Camera.main != null) |
| cameraTransform = Camera.main.transform; |
| Cursor.lockState = CursorLockMode.Locked; |
| }} |
| |
| private void Update() |
| {{ |
| Look(); |
| Move(); |
| Crouch(); |
| Lean(); |
| Jump(); |
| ApplyGravity(); |
| }} |
| |
| private void Look() |
| {{ |
| float mx = Input.GetAxis("Mouse X") * lookSensitivity; |
| float my = Input.GetAxis("Mouse Y") * lookSensitivity; |
| transform.Rotate(Vector3.up * mx); |
| _pitch = Mathf.Clamp(_pitch - my, -pitchLimit, pitchLimit); |
| cameraTransform.localEulerAngles = new Vector3(_pitch, 0f, _currentLean); |
| }} |
| |
| private void Move() |
| {{ |
| float h = Input.GetAxis("Horizontal"); |
| float v = Input.GetAxis("Vertical"); |
| Vector3 move = (transform.right * h + transform.forward * v); |
| float speed = _isCrouching ? crouchSpeed : |
| (Input.GetKey(KeyCode.LeftShift) ? sprintSpeed : walkSpeed); |
| _cc.Move(move.normalized * speed * Time.deltaTime); |
| }} |
| |
| private void Crouch() |
| {{ |
| if (Input.GetKeyDown(KeyCode.LeftControl)) |
| {{ |
| _isCrouching = !_isCrouching; |
| _cc.height = _isCrouching ? crouchHeight : standHeight; |
| _cc.center = new Vector3(0f, _cc.height * 0.5f, 0f); |
| }} |
| }} |
| |
| private void Lean() |
| {{ |
| if (Input.GetKey(KeyCode.Q)) _targetLean = leanAngle; |
| else if (Input.GetKey(KeyCode.E)) _targetLean = -leanAngle; |
| else _targetLean = 0f; |
| _currentLean = Mathf.Lerp(_currentLean, _targetLean, leanSpeed * Time.deltaTime); |
| Vector3 camPos = cameraTransform.localPosition; |
| camPos.x = Mathf.Lerp(camPos.x, (_targetLean / leanAngle) * leanOffset, |
| leanSpeed * Time.deltaTime); |
| cameraTransform.localPosition = camPos; |
| }} |
| |
| private void Jump() |
| {{ |
| if (Input.GetButtonDown("Jump") && _cc.isGrounded) |
| _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); |
| }} |
| |
| private void ApplyGravity() |
| {{ |
| if (_cc.isGrounded && _velocity.y < 0f) _velocity.y = -2f; |
| _velocity.y += gravity * Time.deltaTime; |
| _cc.Move(_velocity * Time.deltaTime); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateDayNightCycleTool(BaseUnityTool): |
| """Generate a 24-hour day/night cycle with sky gradients and lighting.""" |
|
|
| name = "create_day_night_cycle" |
| description = ( |
| "Generate a 24-hour sun arc, sky-color gradient, automatic street " |
| "lights and crowd-density changes." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "DayNightCycle"}, |
| "day_length_minutes": {"type": "number", "default": 24.0}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "DayNightCycle", |
| day_length_minutes: float = 24.0, |
| **_: Any, |
| ) -> str: |
| return self._wrap(f''' |
| using UnityEngine; |
| |
| namespace OpenWorld.World |
| {{ |
| /// <summary> |
| /// Drives a continuous 24-hour cycle. The directional sun rotates around |
| /// the world; the ambient/sky color is sampled from a gradient; street |
| /// lights toggle automatically at dusk/dawn; a callback notifies other |
| /// systems (e.g. crowd density) of the current hour. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Time")] |
| [SerializeField] private float dayLengthMinutes = {day_length_minutes}f; |
| [SerializeField] [Range(0f, 24f)] private float startHour = 8f; |
| public float CurrentHour {{ get; private set; }} |
| |
| [Header("Sun")] |
| [SerializeField] private Light sun; |
| [SerializeField] private float sunIntensityDay = 1.2f; |
| [SerializeField] private float sunIntensityNight = 0.05f; |
| [SerializeField] private Color sunColorDay = new Color(1f, 0.96f, 0.86f); |
| [SerializeField] private Color sunColorSunset = new Color(1f, 0.55f, 0.25f); |
| |
| [Header("Sky")] |
| [SerializeField] private Gradient skyColor; |
| [SerializeField] private Gradient ambientColor; |
| [SerializeField] private Material skyboxMaterial; |
| |
| [Header("Lights")] |
| [SerializeField] private Light[] streetLights; |
| [SerializeField] private float lightOnHour = 18.5f; |
| [SerializeField] private float lightOffHour = 6.5f; |
| |
| public event System.Action<float> OnHourChanged; |
| |
| private float _hoursPerSecond; |
| |
| private void Awake() |
| {{ |
| _hoursPerSecond = 24f / (dayLengthMinutes * 60f); |
| CurrentHour = startHour; |
| if (skyColor == null) BuildDefaultGradient(); |
| }} |
| |
| private void Update() |
| {{ |
| CurrentHour += _hoursPerSecond * Time.deltaTime; |
| if (CurrentHour >= 24f) CurrentHour -= 24f; |
| UpdateSun(); |
| UpdateSky(); |
| UpdateStreetLights(); |
| OnHourChanged?.Invoke(CurrentHour); |
| }} |
| |
| private void UpdateSun() |
| {{ |
| float sunAngle = (CurrentHour / 24f) * 360f - 90f; |
| if (sun != null) |
| {{ |
| sun.transform.rotation = Quaternion.Euler(sunAngle, 170f, 0f); |
| float dayFactor = Mathf.Clamp01(Mathf.Sin((CurrentHour / 24f) * Mathf.PI * 2f - Mathf.PI / 2f) * 0.5f + 0.5f); |
| sun.intensity = Mathf.Lerp(sunIntensityNight, sunIntensityDay, dayFactor); |
| sun.color = (CurrentHour < 7f || CurrentHour > 17f) ? sunColorSunset : sunColorDay; |
| }} |
| }} |
| |
| private void UpdateSky() |
| {{ |
| float t = CurrentHour / 24f; |
| Color sky = skyColor.Evaluate(t); |
| Color ambient = ambientColor.Evaluate(t); |
| RenderSettings.skybox?.SetColor("_Tint", sky); |
| RenderSettings.ambientLight = ambient; |
| if (skyboxMaterial != null) skyboxMaterial.SetColor("_Tint", sky); |
| }} |
| |
| private void UpdateStreetLights() |
| {{ |
| bool on = CurrentHour >= lightOnHour || CurrentHour <= lightOffHour; |
| if (streetLights == null) return; |
| foreach (var l in streetLights) if (l != null) l.enabled = on; |
| }} |
| |
| private void BuildDefaultGradient() |
| {{ |
| skyColor = new Gradient(); |
| var keys = new GradientColorKey[] |
| {{ |
| new GradientColorKey(new Color(0.02f, 0.03f, 0.08f), 0.00f), // midnight |
| new GradientColorKey(new Color(0.5f, 0.3f, 0.4f), 0.22f), // dawn |
| new GradientColorKey(new Color(0.53f, 0.81f, 0.98f), 0.35f), // morning |
| new GradientColorKey(new Color(0.53f, 0.81f, 0.98f), 0.65f), // afternoon |
| new GradientColorKey(new Color(1.0f, 0.45f, 0.20f), 0.78f), // sunset |
| new GradientColorKey(new Color(0.02f, 0.03f, 0.08f), 1.00f), // night |
| }}; |
| var alpha = new GradientAlphaKey[] {{ new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }}; |
| skyColor.SetKeys(keys, alpha); |
| ambientColor = new Gradient(); |
| var akeys = new GradientColorKey[] |
| {{ |
| new GradientColorKey(new Color(0.05f, 0.05f, 0.1f), 0f), |
| new GradientColorKey(new Color(0.4f, 0.4f, 0.5f), 0.3f), |
| new GradientColorKey(new Color(0.6f, 0.6f, 0.6f), 0.5f), |
| new GradientColorKey(new Color(0.4f, 0.35f, 0.3f), 0.78f), |
| new GradientColorKey(new Color(0.05f, 0.05f, 0.1f), 1f), |
| }}; |
| ambientColor.SetKeys(akeys, alpha); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateAiNpcTool(BaseUnityTool): |
| """Generate an AI NPC with NavMesh wandering, fleeing and chasing.""" |
|
|
| name = "create_ai_npc" |
| description = ( |
| "Generate an AI NPC with NavMesh wandering, fleeing, chasing, " |
| "ragdoll death and voice lines." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "AiNpc"}, |
| "wander_radius": {"type": "number", "default": 30.0}, |
| "detect_range": {"type": "number", "default": 15.0}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "AiNpc", |
| wander_radius: float = 30.0, |
| detect_range: float = 15.0, |
| **_: Any, |
| ) -> str: |
| return self._wrap(f''' |
| using System; |
| using UnityEngine; |
| using UnityEngine.AI; |
| |
| namespace OpenWorld.AI |
| {{ |
| /// <summary> |
| /// Generic NPC driven by a NavMeshAgent. Behaviour switches between |
| /// Wander, Flee and Chase states based on a threat Transform. On death |
| /// the agent is disabled and a ragdoll is enabled. Periodic voice lines |
| /// are played from an AudioClip pool. |
| /// </summary> |
| [RequireComponent(typeof(NavMeshAgent))] |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum State {{ Wander, Flee, Chase, Dead }} |
| |
| [Header("Movement")] |
| [SerializeField] private float wanderRadius = {wander_radius}f; |
| [SerializeField] private float detectRange = {detect_range}f; |
| [SerializeField] private float fleeRange = 25f; |
| [SerializeField] private float chaseSpeed = 5.5f; |
| [SerializeField] private float wanderSpeed = 1.6f; |
| [SerializeField] private float repathInterval = 3f; |
| |
| [Header("Health")] |
| [SerializeField] private float maxHealth = 100f; |
| |
| [Header("Audio")] |
| [SerializeField] private AudioClip[] voiceLines; |
| [SerializeField] private float voiceInterval = 12f; |
| [SerializeField] private AudioSource audioSource; |
| |
| [Header("Ragdoll")] |
| [SerializeField] private Rigidbody[] ragdollBodies; |
| [SerializeField] private Collider[] ragdollColliders; |
| [SerializeField] private Animator animator; |
| |
| [Header("Threat")] |
| [SerializeField] private Transform player; |
| |
| private NavMeshAgent _agent; |
| private State _state = State.Wander; |
| private float _health; |
| private float _repathTimer; |
| private float _voiceTimer; |
| |
| public float Health => _health; |
| public State CurrentState => _state; |
| |
| private void Awake() |
| {{ |
| _agent = GetComponent<NavMeshAgent>(); |
| _health = maxHealth; |
| SetRagdoll(false); |
| }} |
| |
| private void Update() |
| {{ |
| switch (_state) |
| {{ |
| case State.Wander: Wander(); break; |
| case State.Flee: Flee(); break; |
| case State.Chase: Chase(); break; |
| case State.Dead: return; |
| }} |
| UpdateVoice(); |
| }} |
| |
| private void Wander() |
| {{ |
| _agent.speed = wanderSpeed; |
| _repathTimer -= Time.deltaTime; |
| if (_repathTimer <= 0f || _agent.remainingDistance < 0.5f) |
| {{ |
| _repathTimer = repathInterval; |
| _agent.SetDestination(RandomNavPoint(transform.position, wanderRadius)); |
| }} |
| if (animator != null) animator.SetFloat("Speed", _agent.velocity.magnitude); |
| if (ThreatDistance() < detectRange && IsThreatHostile()) |
| _state = State.Chase; |
| }} |
| |
| private void Flee() |
| {{ |
| _agent.speed = chaseSpeed; |
| if (player == null) {{ _state = State.Wander; return; }} |
| Vector3 away = (transform.position - player.position).normalized * fleeRange; |
| _agent.SetDestination(transform.position + away); |
| if (ThreatDistance() > fleeRange) _state = State.Wander; |
| }} |
| |
| private void Chase() |
| {{ |
| if (player == null) {{ _state = State.Wander; return; }} |
| _agent.speed = chaseSpeed; |
| _agent.SetDestination(player.position); |
| if (ThreatDistance() > detectRange * 1.5f) _state = State.Wander; |
| }} |
| |
| private float ThreatDistance() |
| => player == null ? Mathf.Infinity : Vector3.Distance(transform.position, player.position); |
| |
| protected virtual bool IsThreatHostile() => true; |
| |
| private static Vector3 RandomNavPoint(Vector3 origin, float radius) |
| {{ |
| for (int i = 0; i < 20; i++) |
| {{ |
| Vector3 rnd = origin + UnityEngine.Random.insideUnitSphere * radius; |
| rnd.y = origin.y; |
| if (NavMesh.SamplePosition(rnd, out NavMeshHit hit, radius, NavMesh.AllAreas)) |
| return hit.position; |
| }} |
| return origin; |
| }} |
| |
| private void UpdateVoice() |
| {{ |
| if (audioSource == null || voiceLines == null || voiceLines.Length == 0) return; |
| _voiceTimer -= Time.deltaTime; |
| if (_voiceTimer <= 0f) |
| {{ |
| _voiceTimer = voiceInterval; |
| audioSource.PlayOneShot(voiceLines[UnityEngine.Random.Range(0, voiceLines.Length)]); |
| }} |
| }} |
| |
| public void TakeDamage(float dmg, Vector3 from) |
| {{ |
| if (_state == State.Dead) return; |
| _health -= dmg; |
| if (_health <= 0f) Die(); |
| else _state = State.Flee; |
| }} |
| |
| private void Die() |
| {{ |
| _state = State.Dead; |
| _agent.isStopped = true; |
| _agent.enabled = false; |
| if (animator != null) animator.enabled = false; |
| SetRagdoll(true); |
| foreach (var rb in ragdollBodies) |
| if (rb != null) rb.AddForce((transform.forward + Vector3.up) * 3f, ForceMode.Impulse); |
| }} |
| |
| private void SetRagdoll(bool enabled) |
| {{ |
| foreach (var rb in ragdollBodies) if (rb != null) rb.isKinematic = !enabled; |
| foreach (var col in ragdollColliders) if (col != null) col.enabled = enabled; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreatePickupSystemTool(BaseUnityTool): |
| """Generate a pickup system for weapons, health, money and ammo.""" |
|
|
| name = "create_pickup_system" |
| description = ( |
| "Generate pickups for weapons, health, money and ammo with glow " |
| "and rotation." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "Pickup"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "Pickup", **_: Any) -> str: |
| return self._wrap(f''' |
| using UnityEngine; |
| |
| namespace OpenWorld.Items |
| {{ |
| /// <summary> |
| /// Generic pickup. Rotates and bobs in place, emits a glow, and on |
| /// trigger applies an effect (health, money, ammo or weapon) to the |
| /// player via the IPickupReceiver interface. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum PickupType {{ Health, Money, Ammo, Weapon, Armor }} |
| |
| [Header("Config")] |
| [SerializeField] private PickupType type = PickupType.Health; |
| [SerializeField] private int amount = 25; |
| [SerializeField] private string weaponId = "pistol"; |
| [SerializeField] private bool respawn = true; |
| [SerializeField] private float respawnDelay = 30f; |
| |
| [Header("Visual")] |
| [SerializeField] private float rotateSpeed = 90f; |
| [SerializeField] private float bobHeight = 0.2f; |
| [SerializeField] private float bobSpeed = 2f; |
| [SerializeField] private Light glowLight; |
| [SerializeField] private ParticleSystem glowParticles; |
| [SerializeField] private AudioClip collectSound; |
| |
| private Vector3 _startPos; |
| private AudioSource _audio; |
| |
| private void Awake() |
| {{ |
| _startPos = transform.position; |
| _audio = GetComponent<AudioSource>(); |
| if (glowLight != null) |
| {{ |
| glowLight.color = type switch |
| {{ |
| PickupType.Health => Color.green, |
| PickupType.Money => Color.yellow, |
| PickupType.Ammo => Color.cyan, |
| PickupType.Weapon => Color.red, |
| PickupType.Armor => Color.blue, |
| _ => Color.white, |
| }}; |
| }} |
| }} |
| |
| private void Update() |
| {{ |
| transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime); |
| float y = Mathf.Sin(Time.time * bobSpeed) * bobHeight; |
| transform.position = _startPos + Vector3.up * y; |
| }} |
| |
| private void OnTriggerEnter(Collider other) |
| {{ |
| if (!other.CompareTag("Player")) return; |
| var receiver = other.GetComponent<IPickupReceiver>(); |
| if (receiver == null || !receiver.CanReceive(type)) return; |
| Apply(receiver); |
| if (collectSound != null && _audio != null) |
| _audio.PlayOneShot(collectSound); |
| if (glowParticles != null) glowParticles.Play(); |
| if (respawn) Invoke(nameof(Reactivate), respawnDelay); |
| gameObject.SetActive(false); |
| }} |
| |
| private void Apply(IPickupReceiver r) |
| {{ |
| switch (type) |
| {{ |
| case PickupType.Health: r.AddHealth(amount); break; |
| case PickupType.Money: r.AddMoney(amount); break; |
| case PickupType.Ammo: r.AddAmmo(amount); break; |
| case PickupType.Armor: r.AddArmor(amount); break; |
| case PickupType.Weapon: r.AddWeapon(weaponId, amount); break; |
| }} |
| }} |
| |
| private void Reactivate() |
| {{ |
| gameObject.SetActive(true); |
| if (glowParticles != null) glowParticles.Stop(); |
| }} |
| }} |
| |
| public interface IPickupReceiver |
| {{ |
| bool CanReceive(CreatePickupSystemTool.PickupType type); |
| void AddHealth(int amount); |
| void AddMoney(int amount); |
| void AddAmmo(int amount); |
| void AddArmor(int amount); |
| void AddWeapon(string weaponId, int ammo); |
| }} |
| }} |
| ''').replace("CreatePickupSystemTool.PickupType", "Pickup.PickupType") |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateHealthSystemTool(BaseUnityTool): |
| """Generate a health system with regen, armor, damage types and respawn.""" |
|
|
| name = "create_health_system" |
| description = ( |
| "Generate a health system with health regen, armor, damage types, " |
| "death screen and respawn." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "HealthSystem"}, |
| "max_health": {"type": "number", "default": 100.0}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "HealthSystem", max_health: float = 100.0, **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using UnityEngine; |
| |
| namespace OpenWorld.Player |
| {{ |
| /// <summary> |
| /// Player health & armor manager. Tracks health, armor, damage type |
| /// resistances, regen-after-delay, death state and respawn flow. |
| /// Exposes the IDamageable interface used by weapons and NPCs. |
| /// </summary> |
| [DisallowMultipleComponent] |
| public class {class_name} : MonoBehaviour, IDamageable |
| {{ |
| public enum DamageType {{ Bullet, Fire, Explosion, Fall, Melee, Drown }} |
| |
| [Header("Health")] |
| [SerializeField] private float maxHealth = {max_health}f; |
| [SerializeField] private float maxArmor = 100f; |
| |
| [Header("Regen")] |
| [SerializeField] private float regenDelay = 5f; |
| [SerializeField] private float regenRate = 8f; |
| [SerializeField] private float regenThreshold = 30f; |
| |
| [Header("Resistances")] |
| [SerializeField] private float armorAbsorb = 0.5f; |
| [SerializeField] private float fallThreshold = 8f; |
| |
| [Header("Respawn")] |
| [SerializeField] private Transform respawnPoint; |
| [SerializeField] private float respawnDelay = 4f; |
| [SerializeField] private GameObject deathScreen; |
| |
| public float Health {{ get; private set; }} |
| public float Armor {{ get; private set; }} |
| public bool IsDead {{ get; private set; }} |
| |
| public event Action<float, float> OnHealthChanged; // health, armor |
| public event Action OnDeath; |
| public event Action OnRespawn; |
| |
| private float _regenTimer; |
| private float _respawnTimer; |
| |
| private void Awake() |
| {{ |
| Health = maxHealth; |
| Armor = 0f; |
| OnHealthChanged?.Invoke(Health, Armor); |
| }} |
| |
| private void Update() |
| {{ |
| if (IsDead) |
| {{ |
| _respawnTimer -= Time.deltaTime; |
| if (_respawnTimer <= 0f) Respawn(); |
| return; |
| }} |
| TryRegen(); |
| }} |
| |
| private void TryRegen() |
| {{ |
| if (Health >= maxHealth) return; |
| _regenTimer -= Time.deltaTime; |
| if (_regenTimer > 0f) return; |
| Health = Mathf.Clamp(Health + regenRate * Time.deltaTime, 0f, maxHealth); |
| OnHealthChanged?.Invoke(Health, Armor); |
| }} |
| |
| public void TakeDamage(float amount, Vector3 source) |
| {{ |
| TakeDamage(amount, DamageType.Bullet, source); |
| }} |
| |
| public void TakeDamage(float amount, DamageType type, Vector3 source) |
| {{ |
| if (IsDead) return; |
| float dmg = ApplyResistances(amount, type); |
| if (Armor > 0f) |
| {{ |
| float absorbed = Mathf.Min(Armor, dmg * armorAbsorb); |
| Armor -= absorbed; |
| dmg -= absorbed; |
| }} |
| Health = Mathf.Max(0f, Health - dmg); |
| _regenTimer = regenDelay; |
| OnHealthChanged?.Invoke(Health, Armor); |
| if (Health <= 0f) Die(); |
| }} |
| |
| protected virtual float ApplyResistances(float amount, DamageType type) |
| {{ |
| return type switch |
| {{ |
| DamageType.Fire => amount * 0.7f, |
| DamageType.Explosion => amount * 1.2f, |
| DamageType.Melee => amount * 0.8f, |
| _ => amount, |
| }}; |
| }} |
| |
| public void Heal(float amount) |
| {{ |
| Health = Mathf.Clamp(Health + amount, 0f, maxHealth); |
| OnHealthChanged?.Invoke(Health, Armor); |
| }} |
| |
| public void AddArmor(float amount) |
| {{ |
| Armor = Mathf.Clamp(Armor + amount, 0f, maxArmor); |
| OnHealthChanged?.Invoke(Health, Armor); |
| }} |
| |
| private void Die() |
| {{ |
| IsDead = true; |
| _respawnTimer = respawnDelay; |
| if (deathScreen != null) deathScreen.SetActive(true); |
| OnDeath?.Invoke(); |
| }} |
| |
| private void Respawn() |
| {{ |
| IsDead = false; |
| Health = maxHealth; |
| Armor = 0f; |
| if (respawnPoint != null) transform.position = respawnPoint.position; |
| if (deathScreen != null) deathScreen.SetActive(false); |
| OnHealthChanged?.Invoke(Health, Armor); |
| OnRespawn?.Invoke(); |
| }} |
| |
| private void OnCollisionEnter(Collision c) |
| {{ |
| if (c.relativeVelocity.y < -fallThreshold) |
| TakeDamage(Mathf.Abs(c.relativeVelocity.y) * 4f, DamageType.Fall, transform.position); |
| }} |
| }} |
| |
| public interface IDamageable |
| {{ |
| void TakeDamage(float amount, Vector3 source); |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateWeaponSystemTool(BaseUnityTool): |
| """Generate a weapon system supporting multiple weapon archetypes.""" |
|
|
| name = "create_weapon_system" |
| description = ( |
| "Generate a weapon system supporting pistol, rifle, shotgun, SMG " |
| "and rocket launcher with muzzle flash, recoil, spread, reload " |
| "and ammo." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "WeaponSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "WeaponSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Combat |
| {{ |
| /// <summary> |
| /// Weapon controller supporting multiple archetypes (Pistol, Rifle, |
| /// Shotgun, SMG, RocketLauncher). Each weapon defines fire rate, |
| /// damage, spread, mag size, reload time, recoil curve and audio. |
| /// Ammo is tracked per-weapon with a shared reserve. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public enum WeaponType {{ Pistol, Rifle, Shotgun, SMG, RocketLauncher }} |
| |
| [Serializable] |
| public class WeaponData |
| {{ |
| public string id = "pistol"; |
| public WeaponType type = WeaponType.Pistol; |
| public float damage = 25f; |
| public float fireRate = 6f; // shots/sec |
| public int magSize = 12; |
| public int reserveAmmo = 96; |
| public float reloadTime = 1.5f; |
| public float spread = 1.5f; |
| public float range = 100f; |
| public float recoil = 1.2f; |
| public int pellets = 1; |
| public AudioClip fireSound; |
| public AudioClip reloadSound; |
| public GameObject muzzleFlashPrefab; |
| public GameObject projectilePrefab; // for rockets |
| }} |
| |
| [Header("Setup")] |
| [SerializeField] private List<WeaponData> weapons = new List<WeaponData>(); |
| [SerializeField] private Camera cam; |
| [SerializeField] private Transform muzzlePoint; |
| [SerializeField] private LayerMask hitMask = ~0; |
| [SerializeField] private AudioSource audioSource; |
| |
| private int _currentIndex; |
| private WeaponData _current; |
| private int _magAmmo; |
| private float _nextFireTime; |
| private float _reloadEndTime; |
| private bool _reloading; |
| |
| public WeaponData CurrentWeapon => _current; |
| public int CurrentMagAmmo => _magAmmo; |
| public bool IsReloading => _reloading; |
| |
| public event Action<int, int, string> OnWeaponChanged; // mag, reserve, id |
| |
| private void Awake() |
| {{ |
| if (cam == null) cam = Camera.main; |
| if (weapons.Count > 0) SelectWeapon(0); |
| }} |
| |
| private void Update() |
| {{ |
| if (weapons.Count == 0) return; |
| HandleSwitch(); |
| if (_reloading) |
| {{ |
| if (Time.time >= _reloadEndTime) FinishReload(); |
| return; |
| }} |
| if (Input.GetKeyDown(KeyCode.R)) StartReload(); |
| if (Input.GetMouseButton(0) && Time.time >= _nextFireTime) Fire(); |
| }} |
| |
| private void HandleSwitch() |
| {{ |
| if (Input.GetKeyDown(KeyCode.Alpha1)) SelectWeapon(0); |
| if (Input.GetKeyDown(KeyCode.Alpha2)) SelectWeapon(1); |
| if (Input.GetKeyDown(KeyCode.Alpha3)) SelectWeapon(2); |
| if (Input.GetKeyDown(KeyCode.Alpha4)) SelectWeapon(3); |
| if (Input.GetKeyDown(KeyCode.Alpha5)) SelectWeapon(4); |
| }} |
| |
| public void SelectWeapon(int index) |
| {{ |
| if (index < 0 || index >= weapons.Count) return; |
| _currentIndex = index; |
| _current = weapons[index]; |
| _magAmmo = _current.magSize; |
| _reloading = false; |
| OnWeaponChanged?.Invoke(_magAmmo, _current.reserveAmmo, _current.id); |
| }} |
| |
| private void Fire() |
| {{ |
| if (_magAmmo <= 0) {{ StartReload(); return; }} |
| _magAmmo--; |
| _nextFireTime = Time.time + 1f / _current.fireRate; |
| PlayFireEffects(); |
| ApplyRecoil(); |
| for (int i = 0; i < _current.pellets; i++) FireRay(); |
| if (_current.type == WeaponType.RocketLauncher) FireProjectile(); |
| OnWeaponChanged?.Invoke(_magAmmo, _current.reserveAmmo, _current.id); |
| }} |
| |
| private void FireRay() |
| {{ |
| Vector3 dir = cam.transform.forward; |
| dir = Quaternion.Euler( |
| UnityEngine.Random.Range(-_current.spread, _current.spread), |
| UnityEngine.Random.Range(-_current.spread, _current.spread), |
| 0f) * dir; |
| if (Physics.Raycast(cam.transform.position, dir, out RaycastHit hit, |
| _current.range, hitMask)) |
| {{ |
| var dmg = hit.collider.GetComponent<IDamageable>(); |
| dmg?.TakeDamage(_current.damage, hit.point); |
| SpawnImpact(hit); |
| }} |
| }} |
| |
| private void FireProjectile() |
| {{ |
| if (_current.projectilePrefab == null || muzzlePoint == null) return; |
| Instantiate(_current.projectilePrefab, muzzlePoint.position, |
| Quaternion.LookRotation(cam.transform.forward)); |
| }} |
| |
| private void PlayFireEffects() |
| {{ |
| if (audioSource != null && _current.fireSound != null) |
| audioSource.PlayOneShot(_current.fireSound); |
| if (_current.muzzleFlashPrefab != null && muzzlePoint != null) |
| {{ |
| GameObject flash = Instantiate(_current.muzzleFlashPrefab, muzzlePoint); |
| Destroy(flash, 0.08f); |
| }} |
| }} |
| |
| private void ApplyRecoil() |
| {{ |
| // Camera recoil handled by camera controller via event. |
| transform.localPosition -= Vector3.forward * _current.recoil * 0.05f; |
| }} |
| |
| private void SpawnImpact(RaycastHit hit) |
| {{ |
| // Subclass or pool impact effects. |
| }} |
| |
| private void StartReload() |
| {{ |
| if (_reloading || _magAmmo == _current.magSize || _current.reserveAmmo <= 0) return; |
| _reloading = true; |
| _reloadEndTime = Time.time + _current.reloadTime; |
| if (audioSource != null && _current.reloadSound != null) |
| audioSource.PlayOneShot(_current.reloadSound); |
| }} |
| |
| private void FinishReload() |
| {{ |
| _reloading = false; |
| int needed = _current.magSize - _magAmmo; |
| int taken = Mathf.Min(needed, _current.reserveAmmo); |
| _magAmmo += taken; |
| _current.reserveAmmo -= taken; |
| OnWeaponChanged?.Invoke(_magAmmo, _current.reserveAmmo, _current.id); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateAudioManagerTool(BaseUnityTool): |
| """Generate a 3D audio manager with music, SFX and radio stations.""" |
|
|
| name = "create_audio_manager" |
| description = ( |
| "Generate a 3D audio manager with spatial SFX, engine sounds, " |
| "gunshots, footsteps, radio stations and ambient beds." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "AudioManager"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "AudioManager", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Audio |
| {{ |
| /// <summary> |
| /// Central audio service. Maintains pools for SFX, a music bus, an |
| /// ambient bed and multiple radio stations. Spatial 3D sources are |
| /// created on demand for one-shot SFX; looping engine sounds are |
| /// tracked per-vehicle. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Header("Buses")] |
| [SerializeField] private AudioSource sfxPrefab; |
| [SerializeField] private AudioSource musicSource; |
| [SerializeField] private AudioSource ambientSource; |
| [SerializeField] private int sfxPoolSize = 32; |
| |
| [Header("Radio Stations")] |
| [SerializeField] private RadioStation[] stations; |
| [SerializeField] private AudioSource radioSource; |
| |
| [Serializable] |
| public class RadioStation |
| {{ |
| public string name; |
| public AudioClip[] songs; |
| [NonSerialized] public int currentIndex; |
| }} |
| |
| private readonly List<AudioSource> _pool = new List<AudioSource>(); |
| private readonly Dictionary<int, AudioSource> _looping = new Dictionary<int, AudioSource>(); |
| private int _currentStation = -1; |
| |
| private void Awake() |
| {{ |
| Instance = this; |
| for (int i = 0; i < sfxPoolSize; i++) |
| {{ |
| AudioSource src = Instantiate(sfxPrefab, transform); |
| src.gameObject.SetActive(false); |
| _pool.Add(src); |
| }} |
| }} |
| |
| public void PlaySfx(AudioClip clip, Vector3 position, float volume = 1f, float pitch = 1f) |
| {{ |
| AudioSource src = GetFreeSource(); |
| if (src == null) return; |
| src.transform.position = position; |
| src.clip = clip; |
| src.volume = volume; |
| src.pitch = pitch; |
| src.spatialBlend = 1f; |
| src.gameObject.SetActive(true); |
| src.Play(); |
| }} |
| |
| public int PlayLooping(AudioClip clip, Vector3 position, float volume = 1f) |
| {{ |
| AudioSource src = GetFreeSource(); |
| if (src == null) return -1; |
| src.transform.position = position; |
| src.clip = clip; |
| src.volume = volume; |
| src.loop = true; |
| src.spatialBlend = 1f; |
| src.gameObject.SetActive(true); |
| src.Play(); |
| int id = src.GetInstanceID(); |
| _looping[id] = src; |
| return id; |
| }} |
| |
| public void StopLooping(int id) |
| {{ |
| if (_looping.TryGetValue(id, out AudioSource src)) |
| {{ |
| src.Stop(); |
| src.loop = false; |
| src.gameObject.SetActive(false); |
| _looping.Remove(id); |
| }} |
| }} |
| |
| public void UpdateLoopingPosition(int id, Vector3 pos) |
| {{ |
| if (_looping.TryGetValue(id, out AudioSource src)) |
| src.transform.position = pos; |
| }} |
| |
| public void PlayMusic(AudioClip clip) |
| {{ |
| if (musicSource == null) return; |
| musicSource.clip = clip; |
| musicSource.Play(); |
| }} |
| |
| public void SetAmbient(AudioClip clip) |
| {{ |
| if (ambientSource == null) return; |
| ambientSource.clip = clip; |
| ambientSource.loop = true; |
| ambientSource.Play(); |
| }} |
| |
| public void PlayRadio(int stationIndex) |
| {{ |
| if (radioSource == null || stations == null || stationIndex < 0 || stationIndex >= stations.Length) return; |
| _currentStation = stationIndex; |
| PlayCurrentSong(); |
| }} |
| |
| public void NextSong() |
| {{ |
| if (_currentStation < 0) return; |
| stations[_currentStation].currentIndex = |
| (stations[_currentStation].currentIndex + 1) % stations[_currentStation].songs.Length; |
| PlayCurrentSong(); |
| }} |
| |
| public void StopRadio() |
| {{ |
| _currentStation = -1; |
| if (radioSource != null) radioSource.Stop(); |
| }} |
| |
| private void PlayCurrentSong() |
| {{ |
| var s = stations[_currentStation]; |
| radioSource.clip = s.songs[s.currentIndex]; |
| radioSource.Play(); |
| }} |
| |
| private AudioSource GetFreeSource() |
| {{ |
| foreach (var src in _pool) if (!src.isPlaying) return src; |
| return null; |
| }} |
| |
| private void Update() |
| {{ |
| if (_currentStation >= 0 && radioSource != null && !radioSource.isPlaying) |
| NextSong(); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateUiManagerTool(BaseUnityTool): |
| """Generate a HUD UI manager with health bar, minimap, money, wanted.""" |
|
|
| name = "create_ui_manager" |
| description = ( |
| "Generate a UI manager with health bar, minimap, money counter, " |
| "wanted level stars, weapon wheel and phone." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "UiManager"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "UiManager", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| using UnityEngine.UI; |
| |
| namespace OpenWorld.UI |
| {{ |
| /// <summary> |
| /// Central HUD manager. Subscribes to gameplay events and updates |
| /// health/armor bars, money counter, wanted-level stars, weapon label |
| /// and the in-game phone. Weapon wheel is shown on tab hold. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Health")] |
| [SerializeField] private Image healthBar; |
| [SerializeField] private Image armorBar; |
| [SerializeField] private Text healthText; |
| |
| [Header("Money")] |
| [SerializeField] private Text moneyText; |
| |
| [Header("Wanted")] |
| [SerializeField] private Image[] wantedStars; |
| [SerializeField] private Color starOff = new Color(0.2f, 0.2f, 0.2f, 0.4f); |
| [SerializeField] private Color starOn = Color.yellow; |
| |
| [Header("Weapon")] |
| [SerializeField] private Text weaponNameText; |
| [SerializeField] private Text ammoText; |
| [SerializeField] private GameObject weaponWheel; |
| [SerializeField] private Transform wheelCenter; |
| [SerializeField] private float wheelRadius = 120f; |
| |
| [Header("Phone")] |
| [SerializeField] private GameObject phoneRoot; |
| [SerializeField] private CanvasGroup phoneCanvas; |
| |
| [Header("Minimap")] |
| [SerializeField] private RawImage minimapImage; |
| |
| private int _money; |
| private int _wanted; |
| |
| public int Money |
| {{ |
| get => _money; |
| set {{ _money = Mathf.Max(0, value); moneyText.text = "$" + _money.ToString("N0"); }} |
| }} |
| |
| public int Wanted |
| {{ |
| get => _wanted; |
| set |
| {{ |
| _wanted = Mathf.Clamp(value, 0, wantedStars.Length); |
| for (int i = 0; i < wantedStars.Length; i++) |
| wantedStars[i].color = i < _wanted ? starOn : starOff; |
| }} |
| }} |
| |
| private void Awake() |
| {{ |
| if (weaponWheel != null) weaponWheel.SetActive(false); |
| if (phoneRoot != null) phoneRoot.SetActive(false); |
| }} |
| |
| private void Update() |
| {{ |
| HandleWeaponWheel(); |
| HandlePhone(); |
| }} |
| |
| public void SetHealth(float current, float max) |
| {{ |
| if (healthBar != null) healthBar.fillAmount = current / max; |
| if (healthText != null) healthText.text = Mathf.CeilToInt(current) + " / " + Mathf.CeilToInt(max); |
| }} |
| |
| public void SetArmor(float current, float max) |
| {{ |
| if (armorBar != null) armorBar.fillAmount = max > 0 ? current / max : 0f; |
| }} |
| |
| public void SetWeapon(string name, int mag, int reserve) |
| {{ |
| if (weaponNameText != null) weaponNameText.text = name; |
| if (ammoText != null) ammoText.text = mag + " / " + reserve; |
| }} |
| |
| private void HandleWeaponWheel() |
| {{ |
| if (weaponWheel == null) return; |
| bool show = Input.GetKey(KeyCode.Tab); |
| weaponWheel.SetActive(show); |
| if (show && wheelCenter != null) |
| {{ |
| Vector2 mouse = Input.mousePosition; |
| Vector2 center = RectTransformUtility.WorldToScreenPoint(null, wheelCenter.position); |
| Vector2 dir = (mouse - center).normalized; |
| float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; |
| int slot = Mathf.RoundToInt(((angle + 360f) % 360f) / (360f / 8f)) % 8; |
| if (Input.GetMouseButtonUp(0)) SelectWheelSlot(slot); |
| }} |
| }} |
| |
| private void SelectWheelSlot(int slot) |
| {{ |
| // Hook into WeaponSystem.SelectWeapon(slot). |
| Debug.Log("Selected weapon slot " + slot); |
| }} |
| |
| private void HandlePhone() |
| {{ |
| if (phoneRoot == null) return; |
| if (Input.GetKeyDown(KeyCode.P)) |
| {{ |
| bool open = !phoneRoot.activeSelf; |
| phoneRoot.SetActive(open); |
| if (open) Time.timeScale = 0.3f; |
| else Time.timeScale = 1f; |
| }} |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateTerrainGeneratorTool(BaseUnityTool): |
| """Generate a heightmap terrain with roads, rivers and elevation.""" |
|
|
| name = "create_terrain_generator" |
| description = ( |
| "Generate a heightmap terrain generator with roads, rivers and " |
| "elevation noise." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "TerrainGenerator"}, |
| "size": {"type": "integer", "default": 1024}, |
| "height": {"type": "number", "default": 600.0}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "TerrainGenerator", size: int = 1024, height: float = 600.0, **_: Any) -> str: |
| return self._wrap(f''' |
| using UnityEngine; |
| |
| namespace OpenWorld.World |
| {{ |
| /// <summary> |
| /// Procedural terrain generator. Builds a TerrainData from layered |
| /// Perlin noise (continental + hills + detail), carves a river spline |
| /// and stamps a road network onto the heightmap. Splat prototypes are |
| /// assigned by elevation/slope. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Dimensions")] |
| [SerializeField] private int size = {size}; |
| [SerializeField] private float worldHeight = {height}f; |
| [SerializeField] private int resolution = 513; |
| |
| [Header("Noise")] |
| [SerializeField] private float continentScale = 0.003f; |
| [SerializeField] private float hillsScale = 0.01f; |
| [SerializeField] private float detailScale = 0.05f; |
| [SerializeField] private float continentWeight = 0.7f; |
| [SerializeField] private float hillsWeight = 0.25f; |
| [SerializeField] private float detailWeight = 0.05f; |
| [SerializeField] private int seed = 1; |
| |
| [Header("River")] |
| [SerializeField] private AnimationCurve riverFalloff; |
| [SerializeField] private float riverWidth = 30f; |
| [SerializeField] private float riverDepth = 12f; |
| |
| [Header("Textures")] |
| [SerializeField] private Texture2D grassTexture; |
| [SerializeField] private Texture2D rockTexture; |
| [SerializeField] private Texture2D sandTexture; |
| |
| public Terrain Generated {{ get; private set; }} |
| |
| public void Generate() |
| {{ |
| TerrainData data = new TerrainData(); |
| data.heightmapResolution = resolution; |
| data.size = new Vector3(size, worldHeight, size); |
| data.baseMapResolution = 1024; |
| data.SetDetailResolution(1024, 8); |
| float[,] heights = new float[resolution, resolution]; |
| BuildHeights(heights); |
| CarveRiver(heights); |
| data.SetHeights(0, 0, heights); |
| ApplyTextures(data, heights); |
| Terrain terrain = Terrain.CreateTerrainGameObject(data); |
| terrain.transform.SetParent(transform); |
| Generated = terrain; |
| }} |
| |
| private void BuildHeights(float[,] heights) |
| {{ |
| float offsetX = seed * 137.13f; |
| float offsetZ = seed * 71.7f; |
| for (int z = 0; z < resolution; z++) |
| for (int x = 0; x < resolution; x++) |
| {{ |
| float c = Mathf.PerlinNoise(x * continentScale + offsetX, z * continentScale + offsetZ); |
| float h = Mathf.PerlinNoise(x * hillsScale + offsetX, z * hillsScale + offsetZ); |
| float d = Mathf.PerlinNoise(x * detailScale + offsetX, z * detailScale + offsetZ); |
| heights[z, x] = c * continentWeight + h * hillsWeight + d * detailWeight; |
| }} |
| }} |
| |
| private void CarveRiver(float[,] heights) |
| {{ |
| // River meanders diagonally across the map. |
| for (int z = 0; z < resolution; z++) |
| {{ |
| float t = z / (float)resolution; |
| int xc = Mathf.RoundToInt((Mathf.Sin(t * Mathf.PI * 4f) * 0.15f + 0.5f) * resolution); |
| for (int x = xc - (int)riverWidth; x < xc + (int)riverWidth; x++) |
| {{ |
| if (x < 0 || x >= resolution) continue; |
| float dist = Mathf.Abs(x - xc) / riverWidth; |
| float carve = riverFalloff != null ? riverFalloff.Evaluate(dist) : 1f - dist; |
| heights[z, x] = Mathf.Max(0f, heights[z, x] - carve * riverDepth / worldHeight); |
| }} |
| }} |
| }} |
| |
| private void ApplyTextures(TerrainData data, float[,] heights) |
| {{ |
| if (grassTexture == null) return; |
| var proto = new[] |
| {{ |
| new TerrainLayer {{ diffuseTexture = grassTexture, tileSize = new Vector2(8, 8) }}, |
| new TerrainLayer {{ diffuseTexture = rockTexture, tileSize = new Vector2(8, 8) }}, |
| new TerrainLayer {{ diffuseTexture = sandTexture, tileSize = new Vector2(8, 8) }}, |
| }}; |
| data.terrainLayers = proto; |
| float[,,] map = new float[resolution, resolution, proto.Length]; |
| for (int z = 0; z < resolution; z++) |
| for (int x = 0; x < resolution; x++) |
| {{ |
| float h = heights[z, x]; |
| if (h < 0.05f) {{ map[z, x, 2] = 1f; }} // sand near water |
| else if (h > 0.6f) {{ map[z, x, 1] = 1f; }} // rock up high |
| else {{ map[z, x, 0] = 1f; }} // grass |
| }} |
| data.SetAlphamaps(0, 0, map); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateWaterShaderTool(BaseUnityTool): |
| """Generate an animated water surface shader and controller.""" |
|
|
| name = "create_water_shader" |
| description = "Generate an animated water shader with reflections and waves." |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "shader_name": {"type": "string", "default": "OpenWorld/Water"}, |
| }, |
| } |
|
|
| def run(self, shader_name: str = "OpenWorld/Water", **_: Any) -> str: |
| shader_code = self._wrap(f''' |
| Shader "{shader_name}" |
| {{ |
| Properties |
| {{ |
| _MainColor ("Main Color", Color) = (0.1, 0.4, 0.7, 0.8) |
| _DeepColor ("Deep Color", Color) = (0.02, 0.1, 0.25, 1.0) |
| _WaveSpeed ("Wave Speed", Float) = 1.0 |
| _WaveHeight ("Wave Height", Float) = 0.2 |
| _WaveTiling ("Wave Tiling", Float) = 4.0 |
| _Glossiness ("Smoothness", Range(0,1)) = 0.9 |
| _FresnelPower ("Fresnel Power", Float) = 3.0 |
| }} |
| SubShader |
| {{ |
| Tags {{ "RenderType"="Transparent" "Queue"="Transparent" }} |
| LOD 200 |
| Blend SrcAlpha OneMinusSrcAlpha |
| |
| CGPROGRAM |
| #pragma surface surf Standard alpha vertex:vert |
| #pragma target 3.0 |
| |
| struct Input |
| {{ |
| float2 uv_MainTex; |
| float3 worldPos; |
| float3 viewDir; |
| }}; |
| |
| fixed4 _MainColor; |
| fixed4 _DeepColor; |
| float _WaveSpeed; |
| float _WaveHeight; |
| float _WaveTiling; |
| half _Glossiness; |
| float _FresnelPower; |
| |
| float Wave(float2 p, float t) |
| {{ |
| return sin(p.x * _WaveTiling + t * _WaveSpeed) * 0.5 + |
| sin(p.y * _WaveTiling * 1.3 + t * _WaveSpeed * 0.8) * 0.5; |
| }} |
| |
| void vert(inout appdata_full v) |
| {{ |
| float t = _Time.y; |
| float w = Wave(v.texcoord.xy * 10.0, t); |
| v.vertex.y += w * _WaveHeight; |
| v.normal = normalize(float3(-cos(v.texcoord.x * 10.0 * _WaveTiling + t * _WaveSpeed) * _WaveHeight * _WaveTiling, 1.0, 0.0)); |
| }} |
| |
| void surf(Input IN, inout SurfaceOutputStandard o) |
| {{ |
| float t = _Time.y; |
| float w = Wave(IN.uv_MainTex * 10.0, t); |
| fixed4 c = lerp(_DeepColor, _MainColor, w * 0.5 + 0.5); |
| float fres = pow(1.0 - saturate(dot(normalize(IN.viewDir), float3(0,1,0))), _FresnelPower); |
| o.Albedo = c.rgb; |
| o.Alpha = c.a + fres * 0.3; |
| o.Metallic = 0.0; |
| o.Smoothness = _Glossiness; |
| o.Emission = fres * 0.2; |
| }} |
| ENDCG |
| }} |
| FallBack "Transparent/VertexLit" |
| }} |
| ''') |
| controller = self._wrap(''' |
| using UnityEngine; |
| |
| namespace OpenWorld.World |
| { |
| /// <summary> |
| /// Drives the animated water material — adjusts wave height by weather |
| /// and toggles reflection probe refresh on demand. |
| /// </summary> |
| [ExecuteAlways] |
| public class WaterController : MonoBehaviour |
| { |
| [SerializeField] private Material waterMaterial; |
| [SerializeField] private float calmHeight = 0.15f; |
| [SerializeField] private float stormHeight = 0.6f; |
| [SerializeField] private float transitionSpeed = 0.5f; |
| |
| private float _currentHeight = 0.15f; |
| private float _targetHeight = 0.15f; |
| |
| public float StormFactor |
| { |
| set => _targetHeight = Mathf.Lerp(calmHeight, stormHeight, Mathf.Clamp01(value)); |
| } |
| |
| private void Update() |
| { |
| _currentHeight = Mathf.Lerp(_currentHeight, _targetHeight, transitionSpeed * Time.deltaTime); |
| if (waterMaterial != null) |
| waterMaterial.SetFloat("_WaveHeight", _currentHeight); |
| } |
| } |
| } |
| ''') |
| return f"// === SHADER ===\n{shader_code}\n// === CONTROLLER ===\n{controller}" |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateParticleEffectsTool(BaseUnityTool): |
| """Generate a particle effect factory for explosions, blood, smoke.""" |
|
|
| name = "create_particle_effects" |
| description = ( |
| "Generate a particle effect factory: explosions, blood, smoke, " |
| "sparks, muzzle flash, tire smoke." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "ParticleEffectFactory"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "ParticleEffectFactory", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.FX |
| {{ |
| /// <summary> |
| /// Pool-backed factory for one-shot particle effects. Configure prefabs |
| /// per effect type and call <see cref="Spawn"/> at world position. The |
| /// factory pools instances to avoid runtime GC. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum EffectType {{ Explosion, Blood, Smoke, Sparks, MuzzleFlash, TireSmoke, Debris, Fire }} |
| |
| [System.Serializable] |
| public class EffectEntry |
| {{ |
| public EffectType type; |
| public ParticleSystem prefab; |
| public int prewarmCount = 4; |
| }} |
| |
| [SerializeField] private EffectEntry[] entries; |
| private readonly Dictionary<EffectType, Queue<ParticleSystem>> _pools = |
| new Dictionary<EffectType, Queue<ParticleSystem>>(); |
| private readonly Dictionary<EffectType, ParticleSystem> _prefabs = |
| new Dictionary<EffectType, ParticleSystem>(); |
| |
| public static {class_name} Instance {{ get; private set; }} |
| |
| private void Awake() |
| {{ |
| Instance = this; |
| foreach (var e in entries) |
| {{ |
| _prefabs[e.type] = e.prefab; |
| _pools[e.type] = new Queue<ParticleSystem>(); |
| for (int i = 0; i < e.prewarmCount; i++) |
| _pools[e.type].Enqueue(CreateInstance(e.type)); |
| }} |
| }} |
| |
| public void Spawn(EffectType type, Vector3 pos, Quaternion rot = default) |
| {{ |
| if (!_pools.TryGetValue(type, out var pool) || pool.Count == 0) |
| pool.Enqueue(CreateInstance(type)); |
| ParticleSystem ps = pool.Dequeue(); |
| ps.transform.SetPositionAndRotation(pos, rot == default ? Quaternion.identity : rot); |
| ps.gameObject.SetActive(true); |
| ps.Play(); |
| StartCoroutine(ReturnAfter(ps, pool, ps.main.duration)); |
| }} |
| |
| private System.Collections.IEnumerator ReturnAfter(ParticleSystem ps, Queue<ParticleSystem> pool, float delay) |
| {{ |
| yield return new WaitForSeconds(delay + 0.5f); |
| ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); |
| ps.gameObject.SetActive(false); |
| pool.Enqueue(ps); |
| }} |
| |
| private ParticleSystem CreateInstance(EffectType type) |
| {{ |
| if (!_prefabs.TryGetValue(type, out var prefab)) return null; |
| ParticleSystem ps = Instantiate(prefab, transform); |
| ps.gameObject.SetActive(false); |
| return ps; |
| }} |
| |
| public void SpawnExplosion(Vector3 pos, float radius) |
| {{ |
| Spawn(EffectType.Explosion, pos); |
| Spawn(EffectType.Fire, pos); |
| Spawn(EffectType.Debris, pos); |
| Collider[] hits = Physics.OverlapSphere(pos, radius); |
| foreach (var h in hits) |
| {{ |
| var dmg = h.GetComponent<IDamageable>(); |
| dmg?.TakeDamage(80f * (1f - Vector3.Distance(pos, h.transform.position) / radius), pos); |
| if (h.TryGetComponent<Rigidbody>(out var rb)) |
| rb.AddExplosionForce(800f, pos, radius); |
| }} |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateSaveSystemTool(BaseUnityTool): |
| """Generate a JSON save/load system with player, vehicles and inventory.""" |
|
|
| name = "create_save_system" |
| description = ( |
| "Generate a JSON save/load system storing position, health, money, " |
| "weapons and vehicles." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "SaveSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "SaveSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using System.IO; |
| using UnityEngine; |
| |
| namespace OpenWorld.Persistence |
| {{ |
| /// <summary> |
| /// JSON-based save/load. Serializes player state, money, weapons, |
| /// inventory and a list of parked vehicles to Application.persistentDataPath. |
| /// Slot-based (0-9). Exposes events for UI hooks. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Serializable] |
| public class WeaponSave |
| {{ |
| public string id; |
| public int magAmmo; |
| public int reserveAmmo; |
| }} |
| |
| [Serializable] |
| public class VehicleSave |
| {{ |
| public string model; |
| public float x, y, z, qx, qy, qz, qw; |
| public float health; |
| public string color; |
| }} |
| |
| [Serializable] |
| public class GameSave |
| {{ |
| public int slot; |
| public string scene; |
| public float px, py, pz; |
| public float health, armor; |
| public int money; |
| public List<WeaponSave> weapons = new List<WeaponSave>(); |
| public List<VehicleSave> vehicles = new List<VehicleSave>(); |
| public long timestampTicks; |
| }} |
| |
| public event Action<int> OnSaveComplete; |
| public event Action<int> OnLoadComplete; |
| |
| private void Awake() {{ Instance = this; }} |
| |
| public void Save(int slot, GameSave data) |
| {{ |
| data.slot = slot; |
| data.timestampTicks = DateTime.UtcNow.Ticks; |
| string json = JsonUtility.ToJson(data, true); |
| string path = PathFor(slot); |
| File.WriteAllText(path, json); |
| OnSaveComplete?.Invoke(slot); |
| }} |
| |
| public GameSave Load(int slot) |
| {{ |
| string path = PathFor(slot); |
| if (!File.Exists(path)) return null; |
| string json = File.ReadAllText(path); |
| GameSave data = JsonUtility.FromJson<GameSave>(json); |
| OnLoadComplete?.Invoke(slot); |
| return data; |
| }} |
| |
| public bool HasSave(int slot) => File.Exists(PathFor(slot)); |
| |
| public void Delete(int slot) |
| {{ |
| string path = PathFor(slot); |
| if (File.Exists(path)) File.Delete(path); |
| }} |
| |
| private static string PathFor(int slot) |
| => Path.Combine(Application.persistentDataPath, $"save_{{slot}}.json"); |
| |
| public static Vector3 Vec(float x, float y, float z) => new Vector3(x, y, z); |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateInventorySystemTool(BaseUnityTool): |
| """Generate a grid inventory with drag-drop, weight and quick slots.""" |
|
|
| name = "create_inventory_system" |
| description = ( |
| "Generate a grid inventory system with drag-drop, weight, and " |
| "quick slots." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "InventorySystem"}, |
| "grid_width": {"type": "integer", "default": 8}, |
| "grid_height": {"type": "integer", "default": 6}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "InventorySystem", grid_width: int = 8, grid_height: int = 6, **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Inventory |
| {{ |
| /// <summary> |
| /// Grid-based inventory. Items occupy a rectangular footprint on the |
| /// grid; weight and stack counts are enforced. Up to 8 quick-slots |
| /// can be bound for hotkey use. The UI layer reads this data to render |
| /// drag/drop icons. |
| /// </summary> |
| [Serializable] |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public class Item |
| {{ |
| public string id; |
| public string displayName; |
| public int width = 1; |
| public int height = 1; |
| public int stack = 1; |
| public int maxStack = 99; |
| public float weight = 0.1f; |
| public Sprite icon; |
| public bool consumable; |
| }} |
| |
| [Header("Grid")] |
| [SerializeField] private int gridWidth = {grid_width}; |
| [SerializeField] private int gridHeight = {grid_height}; |
| [SerializeField] private float maxWeight = 80f; |
| |
| [Header("Quick Slots")] |
| [SerializeField] private int quickSlotCount = 8; |
| |
| private readonly List<Item> _items = new List<Item>(); |
| private readonly Item[] _quickSlots; |
| |
| public float CurrentWeight {{ get; private set; }} |
| public IReadOnlyList<Item> Items => _items; |
| public event Action OnChanged; |
| |
| public {class_name}() |
| {{ |
| _quickSlots = new Item[quickSlotCount]; |
| }} |
| |
| public bool Add(Item item) |
| {{ |
| if (CurrentWeight + item.weight > maxWeight) return false; |
| Item existing = _items.Find(i => i.id == item.id && i.stack < i.maxStack); |
| if (existing != null && existing.width == item.width && existing.height == item.height) |
| {{ |
| int can = Mathf.Min(item.stack, existing.maxStack - existing.stack); |
| existing.stack += can; |
| item.stack -= can; |
| }} |
| if (item.stack > 0) |
| {{ |
| if (_items.Count >= gridWidth * gridHeight) return false; |
| _items.Add(item); |
| }} |
| CurrentWeight += item.weight * item.stack; |
| OnChanged?.Invoke(); |
| return true; |
| }} |
| |
| public bool Remove(string id, int count = 1) |
| {{ |
| Item item = _items.Find(i => i.id == id); |
| if (item == null || item.stack < count) return false; |
| item.stack -= count; |
| CurrentWeight -= item.weight * count; |
| if (item.stack <= 0) _items.Remove(item); |
| OnChanged?.Invoke(); |
| return true; |
| }} |
| |
| public bool AssignQuickSlot(int slot, Item item) |
| {{ |
| if (slot < 0 || slot >= _quickSlots.Length) return false; |
| _quickSlots[slot] = item; |
| OnChanged?.Invoke(); |
| return true; |
| }} |
| |
| public Item GetQuickSlot(int slot) |
| => (slot >= 0 && slot < _quickSlots.Length) ? _quickSlots[slot] : null; |
| |
| public void UseQuickSlot(int slot) |
| {{ |
| Item item = GetQuickSlot(slot); |
| if (item == null || !item.consumable) return; |
| Remove(item.id, 1); |
| }} |
| |
| public int Count(string id) |
| {{ |
| int total = 0; |
| foreach (var i in _items) if (i.id == id) total += i.stack; |
| return total; |
| }} |
| |
| public void Sort() |
| {{ |
| _items.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.Ordinal)); |
| OnChanged?.Invoke(); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateQuestSystemTool(BaseUnityTool): |
| """Generate a quest system with tracking, objectives and rewards.""" |
|
|
| name = "create_quest_system" |
| description = ( |
| "Generate a quest system with mission tracking, objectives, " |
| "rewards and mission markers." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "QuestSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "QuestSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Quests |
| {{ |
| /// <summary> |
| /// Quest/mission tracker. Quests contain ordered objectives; each |
| /// objective has a type (Reach, Kill, Collect, Talk). Completion |
| /// awards money/XP and triggers the next objective. Mission markers |
| /// are exposed via events for the minimap. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum ObjectiveType {{ Reach, Kill, Collect, Talk, Escort, Survive }} |
| |
| [Serializable] |
| public class Objective |
| {{ |
| public string id; |
| public string description; |
| public ObjectiveType type; |
| public string targetId; |
| public int requiredCount = 1; |
| public int currentCount; |
| public Vector3 markerPosition; |
| public bool optional; |
| [NonSerialized] public bool complete; |
| }} |
| |
| [Serializable] |
| public class Quest |
| {{ |
| public string id; |
| public string title; |
| public string giverId; |
| public List<Objective> objectives = new List<Objective>(); |
| public int moneyReward; |
| public int xpReward; |
| public bool accepted; |
| public bool completed; |
| public bool failed; |
| }} |
| |
| [SerializeField] private List<Quest> allQuests = new List<Quest>(); |
| private readonly List<Quest> _active = new List<Quest>(); |
| |
| public IReadOnlyList<Quest> ActiveQuests => _active; |
| |
| public event Action<Quest> OnQuestAccepted; |
| public event Action<Quest, Objective> OnObjectiveCompleted; |
| public event Action<Quest> OnQuestCompleted; |
| public event Action<Vector3> OnMarkerUpdated; |
| |
| public void Accept(string questId) |
| {{ |
| Quest q = allQuests.Find(x => x.id == questId); |
| if (q == null || q.accepted) return; |
| q.accepted = true; |
| _active.Add(q); |
| OnQuestAccepted?.Invoke(q); |
| UpdateMarker(q); |
| }} |
| |
| public void Progress(string objectiveId, int amount = 1, string targetId = null) |
| {{ |
| foreach (var q in _active) |
| {{ |
| if (q.completed) continue; |
| foreach (var o in q.objectives) |
| {{ |
| if (o.complete || o.id != objectiveId) continue; |
| if (!string.IsNullOrEmpty(targetId) && o.targetId != targetId) continue; |
| o.currentCount = Mathf.Min(o.requiredCount, o.currentCount + amount); |
| if (o.currentCount >= o.requiredCount) |
| {{ |
| o.complete = true; |
| OnObjectiveCompleted?.Invoke(q, o); |
| if (AllObjectivesComplete(q)) CompleteQuest(q); |
| else UpdateMarker(q); |
| }} |
| }} |
| }} |
| }} |
| |
| private bool AllObjectivesComplete(Quest q) |
| {{ |
| foreach (var o in q.objectives) if (!o.optional && !o.complete) return false; |
| return true; |
| }} |
| |
| private void CompleteQuest(Quest q) |
| {{ |
| q.completed = true; |
| _active.Remove(q); |
| OnQuestCompleted?.Invoke(q); |
| // Rewards dispatched by EconomySystem/SkillTree listeners. |
| }} |
| |
| private void UpdateMarker(Quest q) |
| {{ |
| foreach (var o in q.objectives) |
| if (!o.complete) {{ OnMarkerUpdated?.Invoke(o.markerPosition); return; }} |
| }} |
| |
| public void FailQuest(string questId) |
| {{ |
| Quest q = _active.Find(x => x.id == questId); |
| if (q == null) return; |
| q.failed = true; |
| _active.Remove(q); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateBuildingGeneratorTool(BaseUnityTool): |
| """Generate a building generator with interiors, stairs and furniture.""" |
|
|
| name = "create_building_generator" |
| description = ( |
| "Generate a building generator with interiors, stairs, elevators, " |
| "rooms and furniture." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "BuildingGenerator"}, |
| "max_floors": {"type": "integer", "default": 8}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "BuildingGenerator", max_floors: int = 8, **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.World |
| {{ |
| /// <summary> |
| /// Generates an enterable building. Floors are stacked with a central |
| /// stairwell and an elevator shaft; rooms are subdivided per floor and |
| /// optionally furnished from a prefab set. Exteriors are windowed. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Footprint")] |
| [SerializeField] private Vector2Int footprint = new Vector2Int(20, 20); |
| [SerializeField] private float floorHeight = 3.5f; |
| [SerializeField] private int floors = {max_floors}; |
| |
| [Header("Prefabs")] |
| [SerializeField] private GameObject floorPrefab; |
| [SerializeField] private GameObject wallPrefab; |
| [SerializeField] private GameObject stairPrefab; |
| [SerializeField] private GameObject elevatorPrefab; |
| [SerializeField] private GameObject[] furniturePrefabs; |
| [SerializeField] private Material exteriorMaterial; |
| [SerializeField] private Material interiorMaterial; |
| |
| private readonly List<GameObject> _parts = new List<GameObject>(); |
| |
| public void Generate() |
| {{ |
| Clear(); |
| for (int f = 0; f < floors; f++) |
| {{ |
| Vector3 basePos = new Vector3(0, f * floorHeight, 0); |
| BuildFloor(basePos, f); |
| BuildStairwell(basePos, f); |
| BuildElevator(basePos, f); |
| SubdivideRooms(basePos, f); |
| }} |
| BuildExterior(); |
| }} |
| |
| private void BuildFloor(Vector3 basePos, int floor) |
| {{ |
| GameObject slab = Instantiate(floorPrefab, basePos, Quaternion.identity, transform); |
| slab.transform.localScale = new Vector3(footprint.x, 0.2f, footprint.y); |
| _parts.Add(slab); |
| }} |
| |
| private void BuildStairwell(Vector3 basePos, int floor) |
| {{ |
| Vector3 stairPos = basePos + new Vector3(-footprint.x * 0.4f, 0, 0); |
| GameObject stair = Instantiate(stairPrefab, stairPos, Quaternion.identity, transform); |
| _parts.Add(stair); |
| }} |
| |
| private void BuildElevator(Vector3 basePos, int floor) |
| {{ |
| Vector3 elevPos = basePos + new Vector3(footprint.x * 0.4f, 0, 0); |
| GameObject elev = Instantiate(elevatorPrefab, elevPos, Quaternion.identity, transform); |
| _parts.Add(elev); |
| }} |
| |
| private void SubdivideRooms(Vector3 basePos, int floor) |
| {{ |
| int roomsX = 2, roomsZ = 2; |
| for (int rx = 0; rx < roomsX; rx++) |
| for (int rz = 0; rz < roomsZ; rz++) |
| {{ |
| if (furniturePrefabs == null || furniturePrefabs.Length == 0) continue; |
| Vector3 roomCenter = basePos + new Vector3( |
| (rx - 0.5f) * footprint.x * 0.4f, 0, (rz - 0.5f) * footprint.y * 0.4f); |
| GameObject furn = furniturePrefabs[Random.Range(0, furniturePrefabs.Length)]; |
| Instantiate(furn, roomCenter, Quaternion.Euler(0, Random.Range(0, 360f), 0), transform); |
| }} |
| }} |
| |
| private void BuildExterior() |
| {{ |
| for (int f = 0; f < floors; f++) |
| {{ |
| Vector3 basePos = new Vector3(0, f * floorHeight + floorHeight * 0.5f, 0); |
| GameObject wall = Instantiate(wallPrefab, basePos, Quaternion.identity, transform); |
| wall.transform.localScale = new Vector3(footprint.x, floorHeight, 0.2f); |
| if (exteriorMaterial != null) wall.GetComponent<Renderer>().sharedMaterial = exteriorMaterial; |
| _parts.Add(wall); |
| }} |
| }} |
| |
| private void Clear() |
| {{ |
| foreach (var p in _parts) if (p != null) DestroyImmediate(p); |
| _parts.Clear(); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateRoadNetworkTool(BaseUnityTool): |
| """Generate a road network with intersections, traffic lights and lanes.""" |
|
|
| name = "create_road_network" |
| description = ( |
| "Generate a road network with highways, streets, intersections, " |
| "traffic lights, lane markings and sidewalks." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "RoadNetwork"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "RoadNetwork", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.World |
| {{ |
| /// <summary> |
| /// Builds a graph of road nodes and segments, instantiating meshes for |
| /// asphalt, lane markings, sidewalks and traffic lights at intersections. |
| /// Lane data is exposed for the traffic system to follow. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum RoadClass {{ Highway, Arterial, Street, Alley }} |
| |
| [System.Serializable] |
| public class RoadNode |
| {{ |
| public Vector3 position; |
| public bool isIntersection; |
| public TrafficLight light; |
| }} |
| |
| [System.Serializable] |
| public class RoadSegment |
| {{ |
| public RoadNode a, b; |
| public RoadClass roadClass = RoadClass.Street; |
| public int laneCount = 2; |
| public float width = 8f; |
| public LineRenderer markings; |
| }} |
| |
| [System.Serializable] |
| public class TrafficLight |
| {{ |
| public enum Phase {{ Red, Yellow, Green }} |
| public Phase phase = Phase.Red; |
| public float timer; |
| public float redDuration = 8f; |
| public float yellowDuration = 2f; |
| public float greenDuration = 8f; |
| public Light redLight, yellowLight, greenLight; |
| }} |
| |
| [Header("Prefabs")] |
| [SerializeField] private GameObject asphaltPrefab; |
| [SerializeField] private GameObject sidewalkPrefab; |
| [SerializeField] private GameObject trafficLightPrefab; |
| [SerializeField] private Material markingMaterial; |
| |
| [Header("Nodes")] |
| [SerializeField] private List<RoadNode> nodes = new List<RoadNode>(); |
| [SerializeField] private List<RoadSegment> segments = new List<RoadSegment>(); |
| |
| public IReadOnlyList<RoadSegment> Segments => segments; |
| |
| public void BuildNetwork() |
| {{ |
| foreach (var seg in segments) BuildSegment(seg); |
| foreach (var node in nodes) |
| if (node.isIntersection) BuildIntersection(node); |
| }} |
| |
| private void BuildSegment(RoadSegment seg) |
| {{ |
| Vector3 dir = seg.b.position - seg.a.position; |
| float length = dir.magnitude; |
| GameObject road = Instantiate(asphaltPrefab, seg.a.position + dir * 0.5f, |
| Quaternion.LookRotation(dir), transform); |
| road.transform.localScale = new Vector3(seg.width, 1f, length); |
| BuildMarkings(seg); |
| BuildSidewalks(seg, dir, length); |
| }} |
| |
| private void BuildMarkings(RoadSegment seg) |
| {{ |
| GameObject marks = new GameObject("Markings"); |
| marks.transform.SetParent(transform); |
| var lr = marks.AddComponent<LineRenderer>(); |
| lr.positionCount = 2; |
| lr.SetPosition(0, seg.a.position + Vector3.up * 0.02f); |
| lr.SetPosition(1, seg.b.position + Vector3.up * 0.02f); |
| lr.startWidth = 0.2f; |
| lr.endWidth = 0.2f; |
| lr.material = markingMaterial; |
| lr.numCornerVertices = 4; |
| lr.numCapVertices = 4; |
| seg.markings = lr; |
| }} |
| |
| private void BuildSidewalks(RoadSegment seg, Vector3 dir, float length) |
| {{ |
| Vector3 normal = Vector3.Cross(Vector3.up, dir).normalized; |
| Vector3 offset = normal * (seg.width * 0.5f + 1.5f); |
| Vector3 center = seg.a.position + dir * 0.5f; |
| Instantiate(sidewalkPrefab, center + offset, Quaternion.LookRotation(dir), transform) |
| .transform.localScale = new Vector3(2f, 0.2f, length); |
| Instantiate(sidewalkPrefab, center - offset, Quaternion.LookRotation(dir), transform) |
| .transform.localScale = new Vector3(2f, 0.2f, length); |
| }} |
| |
| private void BuildIntersection(RoadNode node) |
| {{ |
| Instantiate(asphaltPrefab, node.position, Quaternion.identity, transform) |
| .transform.localScale = new Vector3(12f, 1f, 12f); |
| if (trafficLightPrefab != null) |
| {{ |
| var tlObj = Instantiate(trafficLightPrefab, node.position + Vector3.up * 4f, |
| Quaternion.identity, transform); |
| node.light = tlObj.GetComponent<TrafficLight>(); |
| }} |
| }} |
| |
| private void Update() |
| {{ |
| foreach (var node in nodes) |
| if (node.isIntersection && node.light != null) |
| UpdateTrafficLight(node.light); |
| }} |
| |
| private void UpdateTrafficLight(TrafficLight tl) |
| {{ |
| tl.timer -= Time.deltaTime; |
| if (tl.timer > 0f) return; |
| switch (tl.phase) |
| {{ |
| case TrafficLight.Phase.Red: tl.phase = TrafficLight.Phase.Green; tl.timer = tl.greenDuration; break; |
| case TrafficLight.Phase.Green: tl.phase = TrafficLight.Phase.Yellow; tl.timer = tl.yellowDuration; break; |
| case TrafficLight.Phase.Yellow: tl.phase = TrafficLight.Phase.Red; tl.timer = tl.redDuration; break; |
| }} |
| if (tl.redLight != null) tl.redLight.enabled = tl.phase == TrafficLight.Phase.Red; |
| if (tl.yellowLight != null) tl.yellowLight.enabled = tl.phase == TrafficLight.Phase.Yellow; |
| if (tl.greenLight != null) tl.greenLight.enabled = tl.phase == TrafficLight.Phase.Green; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateWantedSystemTool(BaseUnityTool): |
| """Generate a police wanted system with escalation and roadblocks.""" |
|
|
| name = "create_wanted_system" |
| description = ( |
| "Generate a police wanted system with 1-5 stars, police cars, " |
| "helicopters, roadblocks and escalation." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "WantedSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "WantedSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.AI |
| {{ |
| /// <summary> |
| /// Police wanted-level system (1-5 stars). Crimes add heat; heat decays |
| /// when the player is out of sight. Higher levels spawn additional |
| /// units: cars at 2+, helicopters at 3+, roadblocks at 4+, SWAT at 5. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Header("Heat")] |
| [SerializeField] private float maxHeat = 100f; |
| [SerializeField] private float heatDecayPerSecond = 4f; |
| [SerializeField] private float[] starThresholds = {{ 0f, 15f, 35f, 55f, 80f }}; |
| |
| [Header("Spawning")] |
| [SerializeField] private GameObject policeCarPrefab; |
| [SerializeField] private GameObject helicopterPrefab; |
| [SerializeField] private GameObject roadblockPrefab; |
| [SerializeField] private GameObject swatPrefab; |
| [SerializeField] private Transform player; |
| [SerializeField] private float spawnRadius = 80f; |
| [SerializeField] private float spawnInterval = 6f; |
| [SerializeField] private int maxUnits = 16; |
| |
| public int Stars {{ get; private set; }} |
| public float Heat {{ get; private set; }} |
| public bool Hidden {{ get; set; }} |
| |
| public event Action<int> OnStarsChanged; |
| |
| private readonly List<GameObject> _activeUnits = new List<GameObject>(); |
| private float _spawnTimer; |
| |
| private void Awake() {{ Instance = this; }} |
| |
| private void Update() |
| {{ |
| DecayHeat(); |
| UpdateStars(); |
| ManageSpawns(); |
| }} |
| |
| public void ReportCrime(float heat) |
| {{ |
| Heat = Mathf.Min(maxHeat, Heat + heat); |
| Hidden = false; |
| }} |
| |
| private void DecayHeat() |
| {{ |
| if (Hidden && Stars > 0) |
| Heat = Mathf.Max(0f, Heat - heatDecayPerSecond * Time.deltaTime); |
| }} |
| |
| private void UpdateStars() |
| {{ |
| int newStars = 0; |
| for (int i = 0; i < starThresholds.Length; i++) |
| if (Heat >= starThresholds[i]) newStars = i + 1; |
| if (newStars == Stars) return; |
| Stars = newStars; |
| OnStarsChanged?.Invoke(Stars); |
| }} |
| |
| private void ManageSpawns() |
| {{ |
| if (Stars == 0) {{ DespawnAll(); return; }} |
| _spawnTimer -= Time.deltaTime; |
| if (_spawnTimer > 0f) return; |
| _spawnTimer = spawnInterval; |
| int desired = Mathf.Min(maxUnits, Stars * 2); |
| while (_activeUnits.Count < desired) SpawnUnit(); |
| }} |
| |
| private void SpawnUnit() |
| {{ |
| Vector3 pos = player.position + UnityEngine.Random.insideUnitSphere * spawnRadius; |
| pos.y = player.position.y; |
| GameObject prefab = Stars switch |
| {{ |
| 5 => swatPrefab, |
| 4 => roadblockPrefab, |
| 3 => helicopterPrefab, |
| _ => policeCarPrefab, |
| }}; |
| if (prefab == null) return; |
| _activeUnits.Add(Instantiate(prefab, pos, Quaternion.identity)); |
| }} |
| |
| private void DespawnAll() |
| {{ |
| foreach (var u in _activeUnits) if (u != null) Destroy(u); |
| _activeUnits.Clear(); |
| }} |
| |
| public void UnitDestroyed(GameObject unit) |
| {{ |
| if (_activeUnits.Remove(unit)) ReportCrime(5f); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateTrafficSystemTool(BaseUnityTool): |
| """Generate AI traffic cars that follow lanes and obey lights.""" |
|
|
| name = "create_traffic_system" |
| description = ( |
| "Generate an AI traffic system: cars on lanes, traffic light " |
| "obedience, lane following and accidents." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "TrafficSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "TrafficSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| using UnityEngine.AI; |
| |
| namespace OpenWorld.AI |
| {{ |
| /// <summary> |
| /// Spawns AI traffic vehicles that follow NavMesh paths along roads, |
| /// stop at red lights and react to collisions. Each vehicle is a |
| /// simplified kinematic car: it accelerates toward a target waypoint |
| /// and brakes for obstacles ahead via a sphere cast. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Spawn")] |
| [SerializeField] private GameObject[] vehiclePrefabs; |
| [SerializeField] private Transform[] spawnPoints; |
| [SerializeField] private int maxVehicles = 30; |
| [SerializeField] private float spawnInterval = 3f; |
| [SerializeField] private Transform player; |
| |
| [Header("Behaviour")] |
| [SerializeField] private float cruiseSpeed = 12f; |
| [SerializeField] private float detectionRange = 12f; |
| [SerializeField] private LayerMask obstacleMask = ~0; |
| |
| private readonly List<TrafficVehicle> _vehicles = new List<TrafficVehicle>(); |
| private float _spawnTimer; |
| |
| private void Update() |
| {{ |
| _spawnTimer -= Time.deltaTime; |
| if (_spawnTimer <= 0f) |
| {{ |
| _spawnTimer = spawnInterval; |
| if (_vehicles.Count < maxVehicles) SpawnVehicle(); |
| }} |
| for (int i = _vehicles.Count - 1; i >= 0; i--) |
| {{ |
| _vehicles[i].Tick(); |
| if (_vehicles[i].Expired) {{ Destroy(_vehicles[i].gameObject); _vehicles.RemoveAt(i); }} |
| }} |
| }} |
| |
| private void SpawnVehicle() |
| {{ |
| if (vehiclePrefabs == null || vehiclePrefabs.Length == 0 || spawnPoints.Length == 0) return; |
| Transform sp = spawnPoints[Random.Range(0, spawnPoints.Length)]; |
| GameObject go = Instantiate(vehiclePrefabs[Random.Range(0, vehiclePrefabs.Length)], |
| sp.position, sp.rotation); |
| var v = go.AddComponent<TrafficVehicle>(); |
| v.Initialize(this, cruiseSpeed, detectionRange, obstacleMask, player); |
| _vehicles.Add(v); |
| }} |
| |
| public bool IsRedLightAhead(Vector3 pos, Vector3 forward) |
| {{ |
| if (Physics.SphereCast(pos, 1f, forward, out RaycastHit hit, 6f, obstacleMask)) |
| return hit.collider.CompareTag("TrafficLight_Red"); |
| return false; |
| }} |
| }} |
| |
| public class TrafficVehicle : MonoBehaviour |
| {{ |
| private TrafficSystem _system; |
| private float _speed; |
| private float _cruise; |
| private float _range; |
| private LayerMask _mask; |
| private Transform _player; |
| private NavMeshAgent _agent; |
| private Vector3 _target; |
| private float _life = 120f; |
| |
| public bool Expired {{ get; private set; }} |
| |
| public void Initialize(TrafficSystem sys, float cruise, float range, LayerMask mask, Transform player) |
| {{ |
| _system = sys; _cruise = cruise; _range = range; _mask = mask; _player = player; |
| _agent = gameObject.AddComponent<NavMeshAgent>(); |
| _agent.speed = cruise; |
| _agent.acceleration = 8f; |
| _agent.angularSpeed = 180f; |
| PickNewDestination(); |
| }} |
| |
| private void PickNewDestination() |
| {{ |
| Vector3 rnd = transform.position + Random.insideUnitSphere * 60f; |
| rnd.y = transform.position.y; |
| _agent.SetDestination(rnd); |
| _target = rnd; |
| }} |
| |
| public void Tick() |
| {{ |
| _life -= Time.deltaTime; |
| if (_life <= 0f || (_player != null && Vector3.Distance(transform.position, _player.position) > 200f)) |
| Expired = true; |
| if (_agent.remainingDistance < 4f) PickNewDestination(); |
| if (ObstacleAhead() || _system.IsRedLightAhead(transform.position, transform.forward)) |
| _speed = Mathf.Lerp(_speed, 0f, 4f * Time.deltaTime); |
| else |
| _speed = Mathf.Lerp(_speed, _cruise, 2f * Time.deltaTime); |
| _agent.speed = _speed; |
| }} |
| |
| private bool ObstacleAhead() |
| {{ |
| return Physics.SphereCast(transform.position + Vector3.up, 1.2f, transform.forward, |
| out _, _range, _mask); |
| }} |
| |
| private void OnCollisionEnter(Collision c) |
| {{ |
| if (c.relativeVelocity.magnitude > 8f) Expired = true; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreatePedestrianSystemTool(BaseUnityTool): |
| """Generate walking pedestrians, crowds and panic behaviour.""" |
|
|
| name = "create_pedestrian_system" |
| description = ( |
| "Generate a pedestrian system: walking NPCs, crowds, panic, " |
| "conversations and reactions to the player." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "PedestrianSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "PedestrianSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| using UnityEngine.AI; |
| |
| namespace OpenWorld.AI |
| {{ |
| /// <summary> |
| /// Spawns and manages pedestrians in a radius around the player. Each |
| /// pedestrian wanders, occasionally forms a conversation cluster with |
| /// another, and flees on hearing gunshots/explosions. Crowd density is |
| /// driven by the day/night hour. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Spawn")] |
| [SerializeField] private GameObject[] pedestrianPrefabs; |
| [SerializeField] private Transform player; |
| [SerializeField] private float spawnRadius = 60f; |
| [SerializeField] private float despawnRadius = 100f; |
| [SerializeField] private int maxPedestrians = 80; |
| [SerializeField] private float spawnInterval = 0.5f; |
| |
| [Header("Density")] |
| [SerializeField] private AnimationCurve densityByHour = AnimationCurve.EaseInOut(0, 0.2f, 24, 0.2f); |
| |
| private readonly List<GameObject> _peds = new List<GameObject>(); |
| private float _spawnTimer; |
| |
| public void SetHour(float hour) |
| {{ |
| float density = densityByHour.Evaluate(hour); |
| maxPedestrians = Mathf.RoundToInt(80 * density); |
| }} |
| |
| public void TriggerPanic(Vector3 source, float radius) |
| {{ |
| foreach (var p in _peds) |
| {{ |
| if (p == null) continue; |
| if (Vector3.Distance(p.transform.position, source) < radius) |
| p.GetComponent<Pedestrian>()?.Panic(source); |
| }} |
| }} |
| |
| private void Update() |
| {{ |
| _spawnTimer -= Time.deltaTime; |
| if (_spawnTimer <= 0f) |
| {{ |
| _spawnTimer = spawnInterval; |
| if (_peds.Count < maxPedestrians) SpawnPed(); |
| }} |
| for (int i = _peds.Count - 1; i >= 0; i--) |
| {{ |
| if (_peds[i] == null) {{ _peds.RemoveAt(i); continue; }} |
| float dist = Vector3.Distance(_peds[i].transform.position, player.position); |
| if (dist > despawnRadius) {{ Destroy(_peds[i]); _peds.RemoveAt(i); }} |
| }} |
| }} |
| |
| private void SpawnPed() |
| {{ |
| if (pedestrianPrefabs == null || pedestrianPrefabs.Length == 0) return; |
| Vector3 pos = player.position + Random.insideUnitSphere * spawnRadius; |
| pos.y = player.position.y; |
| if (!NavMesh.SamplePosition(pos, out NavMeshHit hit, spawnRadius, NavMesh.AllAreas)) return; |
| GameObject go = Instantiate(pedestrianPrefabs[Random.Range(0, pedestrianPrefabs.Length)], |
| hit.position, Quaternion.identity); |
| var ped = go.GetComponent<Pedestrian>(); |
| if (ped == null) ped = go.AddComponent<Pedestrian>(); |
| ped.Initialize(player); |
| _peds.Add(go); |
| }} |
| }} |
| |
| public class Pedestrian : MonoBehaviour |
| {{ |
| private NavMeshAgent _agent; |
| private Transform _player; |
| private float _repath; |
| private bool _panicking; |
| |
| public void Initialize(Transform player) {{ _player = player; _agent = GetComponent<NavMeshAgent>(); }} |
| |
| private void Update() |
| {{ |
| if (_agent == null) return; |
| _repath -= Time.deltaTime; |
| if (_repath > 0f) return; |
| _repath = Random.Range(2f, 5f); |
| if (_panicking) Flee(); |
| else Wander(); |
| if (Vector3.Distance(transform.position, _player.position) < 4f && Random.value < 0.2f) |
| ReactToPlayer(); |
| }} |
| |
| private void Wander() |
| {{ |
| _agent.speed = 1.4f; |
| Vector3 rnd = transform.position + Random.insideUnitSphere * 20f; |
| rnd.y = transform.position.y; |
| _agent.SetDestination(rnd); |
| }} |
| |
| private void Flee() |
| {{ |
| _agent.speed = 5f; |
| Vector3 away = (transform.position - _player.position).normalized * 30f; |
| _agent.SetDestination(transform.position + away); |
| }} |
| |
| public void Panic(Vector3 source) |
| {{ |
| _panicking = true; |
| Invoke(nameof(Calm), 8f); |
| }} |
| |
| private void Calm() => _panicking = false; |
| |
| private void ReactToPlayer() |
| {{ |
| // Look at player, play idle anim — handled by Animator hooks. |
| transform.rotation = Quaternion.LookRotation((_player.position - transform.position).normalized); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateEconomySystemTool(BaseUnityTool): |
| """Generate an economy system with jobs, stores, bank and laundering.""" |
|
|
| name = "create_economy_system" |
| description = ( |
| "Generate an economy system with jobs, stores, purchases, bank and " |
| "money laundering." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "EconomySystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "EconomySystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Economy |
| {{ |
| /// <summary> |
| /// Central money & market service. Tracks cash and bank balance, |
| /// stores with item prices, jobs that pay out on completion, and a |
| /// laundering flow that converts dirty money (from crimes) to clean |
| /// cash at a fee over time. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Serializable] |
| public class StoreItem |
| {{ |
| public string id; |
| public string displayName; |
| public int price; |
| public bool legal = true; |
| }} |
| |
| [Serializable] |
| public class Store |
| {{ |
| public string id; |
| public string name; |
| public List<StoreItem> items = new List<StoreItem>(); |
| }} |
| |
| [Serializable] |
| public class Job |
| {{ |
| public string id; |
| public string title; |
| public int basePay; |
| public float cooldown; |
| }} |
| |
| [Header("Balances")] |
| [SerializeField] private int cash = 500; |
| [SerializeField] private int bank = 0; |
| [SerializeField] private int dirtyMoney = 0; |
| [SerializeField] private float launderFee = 0.3f; |
| [SerializeField] private float launderRatePerSecond = 50f; |
| |
| [Header("Catalog")] |
| [SerializeField] private List<Store> stores = new List<Store>(); |
| [SerializeField] private List<Job> jobs = new List<Job>(); |
| |
| public int Cash => cash; |
| public int Bank => bank; |
| public int Dirty => dirtyMoney; |
| |
| public event Action<int, int, int> OnBalanceChanged; // cash, bank, dirty |
| |
| private void Awake() {{ Instance = this; }} |
| |
| public void AddCash(int amount) {{ cash += amount; OnBalanceChanged?.Invoke(cash, bank, dirtyMoney); }} |
| public void AddDirty(int amount) {{ dirtyMoney += amount; OnBalanceChanged?.Invoke(cash, bank, dirtyMoney); }} |
| |
| public bool Purchase(string storeId, string itemId) |
| {{ |
| Store s = stores.Find(x => x.id == storeId); |
| if (s == null) return false; |
| StoreItem item = s.items.Find(i => i.id == itemId); |
| if (item == null || cash < item.price) return false; |
| cash -= item.price; |
| OnBalanceChanged?.Invoke(cash, bank, dirtyMoney); |
| return true; |
| }} |
| |
| public void Deposit(int amount) |
| {{ |
| amount = Mathf.Min(amount, cash); |
| cash -= amount; |
| bank += amount; |
| OnBalanceChanged?.Invoke(cash, bank, dirtyMoney); |
| }} |
| |
| public void Withdraw(int amount) |
| {{ |
| amount = Mathf.Min(amount, bank); |
| bank -= amount; |
| cash += amount; |
| OnBalanceChanged?.Invoke(cash, bank, dirtyMoney); |
| }} |
| |
| public void Work(string jobId) |
| {{ |
| Job j = jobs.Find(x => x.id == jobId); |
| if (j == null) return; |
| AddCash(j.basePay); |
| }} |
| |
| public void StartLaundering() => InvokeRepeating(nameof(LaunderTick), 1f, 1f); |
| public void StopLaundering() => CancelInvoke(nameof(LaunderTick)); |
| |
| private void LaunderTick() |
| {{ |
| if (dirtyMoney <= 0) return; |
| int amount = Mathf.Min(dirtyMoney, Mathf.RoundToInt(launderRatePerSecond)); |
| dirtyMoney -= amount; |
| cash += Mathf.RoundToInt(amount * (1f - launderFee)); |
| OnBalanceChanged?.Invoke(cash, bank, dirtyMoney); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreatePhoneSystemTool(BaseUnityTool): |
| """Generate an in-game phone with apps (map, contacts, messages, etc.).""" |
|
|
| name = "create_phone_system" |
| description = ( |
| "Generate an in-game phone with apps: map, contacts, messages, " |
| "internet, camera." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "PhoneSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "PhoneSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| using UnityEngine.UI; |
| |
| namespace OpenWorld.UI |
| {{ |
| /// <summary> |
| /// In-game smartphone. Toggle with P; navigate apps with on-screen |
| /// buttons. Apps: Home, Map, Contacts, Messages, Internet, Camera. |
| /// Each app is a panel toggled on/off. Phone open slows game time. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum App {{ Home, Map, Contacts, Messages, Internet, Camera }} |
| |
| [Header("Root")] |
| [SerializeField] private GameObject phoneRoot; |
| [SerializeField] private CanvasGroup canvasGroup; |
| [SerializeField] private Button[] appButtons; |
| [SerializeField] private GameObject[] appPanels; |
| [SerializeField] private Button homeButton; |
| [SerializeField] private Button closeButton; |
| |
| [Header("Camera App")] |
| [SerializeField] private Camera photoCamera; |
| [SerializeField] private RawImage photoPreview; |
| |
| private bool _open; |
| private App _current = App.Home; |
| |
| public bool IsOpen => _open; |
| |
| public event Action<App> OnAppOpened; |
| |
| private void Awake() |
| {{ |
| for (int i = 0; i < appButtons.Length; i++) |
| {{ |
| int idx = i; |
| if (appButtons[i] != null) |
| appButtons[i].onClick.AddListener(() => OpenApp((App)idx)); |
| }} |
| if (homeButton != null) homeButton.onClick.AddListener(() => OpenApp(App.Home)); |
| if (closeButton != null) closeButton.onClick.AddListener(Close); |
| Close(); |
| }} |
| |
| private void Update() |
| {{ |
| if (Input.GetKeyDown(KeyCode.P)) Toggle(); |
| }} |
| |
| public void Toggle() {{ if (_open) Close(); else Open(); }} |
| |
| public void Open() |
| {{ |
| _open = true; |
| if (phoneRoot != null) phoneRoot.SetActive(true); |
| Time.timeScale = 0.3f; |
| OpenApp(App.Home); |
| }} |
| |
| public void Close() |
| {{ |
| _open = false; |
| if (phoneRoot != null) phoneRoot.SetActive(false); |
| Time.timeScale = 1f; |
| }} |
| |
| public void OpenApp(App app) |
| {{ |
| _current = app; |
| for (int i = 0; i < appPanels.Length; i++) |
| if (appPanels[i] != null) appPanels[i].SetActive(i == (int)app); |
| if (app == App.Camera) ActivateCamera(); |
| OnAppOpened?.Invoke(app); |
| }} |
| |
| private void ActivateCamera() |
| {{ |
| if (photoCamera == null) return; |
| photoCamera.gameObject.SetActive(true); |
| if (Input.GetKeyDown(KeyCode.F) && photoPreview != null) |
| {{ |
| StartCoroutine(CapturePhoto()); |
| }} |
| }} |
| |
| private System.Collections.IEnumerator CapturePhoto() |
| {{ |
| yield return new WaitForEndOfFrame(); |
| RenderTexture rt = photoCamera.targetTexture; |
| if (rt == null) yield break; |
| Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false); |
| RenderTexture.active = rt; |
| tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); |
| tex.Apply(); |
| photoPreview.texture = tex; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateRadioSystemTool(BaseUnityTool): |
| """Generate a radio system with stations and switching.""" |
|
|
| name = "create_radio_system" |
| description = ( |
| "Generate a radio system with multiple stations, song lists and " |
| "station switching." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "RadioSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "RadioSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Audio |
| {{ |
| /// <summary> |
| /// In-vehicle radio. Holds a list of stations each with a song queue. |
| /// Player can switch station (Q/E), skip song (N) and toggle power. |
| /// Audio is routed through a single AudioSource whose clip is swapped |
| /// on track change. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public class Station |
| {{ |
| public string name; |
| public string dj; |
| public AudioClip[] songs; |
| [NonSerialized] public int index; |
| }} |
| |
| [Header("Source")] |
| [SerializeField] private AudioSource source; |
| [SerializeField] private List<Station> stations = new List<Station>(); |
| [SerializeField] private int currentIndex; |
| [SerializeField] private bool powered = true; |
| [SerializeField] private float volume = 0.7f; |
| |
| public Station Current => (currentIndex >= 0 && currentIndex < stations.Count) ? stations[currentIndex] : null; |
| public int StationCount => stations.Count; |
| |
| public event Action<Station> OnStationChanged; |
| public event Action<Station, AudioClip> OnSongChanged; |
| |
| private void Awake() |
| {{ |
| if (source == null) source = gameObject.AddComponent<AudioSource>(); |
| source.loop = false; |
| source.volume = volume; |
| source.spatialBlend = 0f; |
| if (stations.Count > 0) PlayCurrent(); |
| }} |
| |
| private void Update() |
| {{ |
| if (!powered || source == null) return; |
| if (!source.isPlaying) NextSong(); |
| if (Input.GetKeyDown(KeyCode.Q)) PreviousStation(); |
| if (Input.GetKeyDown(KeyCode.E)) NextStation(); |
| if (Input.GetKeyDown(KeyCode.N)) NextSong(); |
| if (Input.GetKeyDown(KeyCode.M)) TogglePower(); |
| }} |
| |
| public void NextStation() |
| {{ |
| if (stations.Count == 0) return; |
| currentIndex = (currentIndex + 1) % stations.Count; |
| PlayCurrent(); |
| }} |
| |
| public void PreviousStation() |
| {{ |
| if (stations.Count == 0) return; |
| currentIndex = (currentIndex - 1 + stations.Count) % stations.Count; |
| PlayCurrent(); |
| }} |
| |
| public void NextSong() |
| {{ |
| var s = Current; |
| if (s == null || s.songs.Length == 0) return; |
| s.index = (s.index + 1) % s.songs.Length; |
| source.clip = s.songs[s.index]; |
| source.Play(); |
| OnSongChanged?.Invoke(s, s.songs[s.index]); |
| }} |
| |
| private void PlayCurrent() |
| {{ |
| var s = Current; |
| if (s == null) return; |
| source.clip = s.songs[s.index]; |
| source.Play(); |
| OnStationChanged?.Invoke(s); |
| }} |
| |
| public void TogglePower() |
| {{ |
| powered = !powered; |
| if (source != null) {{ if (powered) source.Play(); else source.Stop(); }} |
| }} |
| |
| public void SetVolume(float v) {{ volume = Mathf.Clamp01(v); if (source != null) source.volume = volume; }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateWeaponWheelTool(BaseUnityTool): |
| """Generate a radial weapon wheel with slow-mo selection.""" |
|
|
| name = "create_weapon_wheel" |
| description = ( |
| "Generate a radial weapon wheel selector with slow-motion during " |
| "selection." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "WeaponWheel"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "WeaponWheel", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using UnityEngine; |
| using UnityEngine.UI; |
| |
| namespace OpenWorld.UI |
| {{ |
| /// <summary> |
| /// Radial weapon selector. While held (Tab/Q), slows time to 0.2x, |
| /// shows 8 weapon slots arranged in a circle, highlights the one |
| /// under the mouse, and on release equips it via a callback. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public class WheelSlot |
| {{ |
| public string id; |
| public Sprite icon; |
| public Color tint = Color.white; |
| }} |
| |
| [Header("Visual")] |
| [SerializeField] private GameObject root; |
| [SerializeField] private Image[] slotIcons; |
| [SerializeField] private Image highlight; |
| [SerializeField] private Transform wheelCenter; |
| [SerializeField] private float radius = 120f; |
| |
| [Header("Slow-Mo")] |
| [SerializeField] private float slowScale = 0.2f; |
| [SerializeField] private float transitionSpeed = 8f; |
| [SerializeField] private KeyCode openKey = KeyCode.Tab; |
| |
| [Header("Data")] |
| [SerializeField] private WheelSlot[] slots = new WheelSlot[8]; |
| |
| private bool _open; |
| private int _hovered = -1; |
| private float _targetScale = 1f; |
| |
| public event Action<int> OnSlotSelected; |
| |
| private void Awake() |
| {{ |
| if (root != null) root.SetActive(false); |
| RenderSlots(); |
| }} |
| |
| private void Update() |
| {{ |
| HandleOpen(); |
| if (!_open) return; |
| HandleHover(); |
| Time.timeScale = Mathf.Lerp(Time.timeScale, _targetScale, transitionSpeed * Time.unscaledDeltaTime); |
| if (Input.GetKeyUp(openKey)) Confirm(); |
| }} |
| |
| private void HandleOpen() |
| {{ |
| bool held = Input.GetKey(openKey); |
| if (held && !_open) {{ _open = true; _targetScale = slowScale; if (root != null) root.SetActive(true); }} |
| if (!held && _open && Time.timeScale < 0.5f) {{ _targetScale = 1f; }} |
| }} |
| |
| private void HandleHover() |
| {{ |
| if (wheelCenter == null) return; |
| Vector2 mouse = Input.mousePosition; |
| Vector2 center = RectTransformUtility.WorldToScreenPoint(null, wheelCenter.position); |
| Vector2 dir = mouse - center; |
| if (dir.magnitude > radius * 1.5f) {{ _hovered = -1; return; }} |
| float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; |
| if (angle < 0f) angle += 360f; |
| _hovered = Mathf.RoundToInt(angle / (360f / slots.Length)) % slots.Length; |
| if (highlight != null && _hovered >= 0 && slotIcons[_hovered] != null) |
| highlight.transform.position = slotIcons[_hovered].transform.position; |
| }} |
| |
| private void Confirm() |
| {{ |
| _open = false; |
| _targetScale = 1f; |
| if (root != null) root.SetActive(false); |
| if (_hovered >= 0) OnSlotSelected?.Invoke(_hovered); |
| }} |
| |
| private void RenderSlots() |
| {{ |
| for (int i = 0; i < slotIcons.Length && i < slots.Length; i++) |
| {{ |
| float angle = i * (360f / slots.Length) * Mathf.Deg2Rad; |
| Vector2 pos = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius; |
| if (slotIcons[i] != null) |
| {{ |
| slotIcons[i].rectTransform.anchoredPosition = pos; |
| slotIcons[i].sprite = slots[i].icon; |
| slotIcons[i].color = slots[i].tint; |
| }} |
| }} |
| }} |
| |
| public void SetSlot(int index, WheelSlot slot) |
| {{ |
| if (index < 0 || index >= slots.Length) return; |
| slots[index] = slot; |
| RenderSlots(); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateMinimapSystemTool(BaseUnityTool): |
| """Generate a rotating minimap with player, NPCs, missions and police.""" |
|
|
| name = "create_minimap_system" |
| description = ( |
| "Generate a rotating minimap with player, NPCs, missions and police " |
| "markers." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "MinimapSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "MinimapSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| using UnityEngine.UI; |
| |
| namespace OpenWorld.UI |
| {{ |
| /// <summary> |
| /// Top-down minimap. A dedicated orthographic camera follows the |
| /// player and rotates with heading. Markers (NPC, police, mission, |
| /// vehicle) are world-space sprites projected into screen space and |
| /// clamped to the minimap's circular mask. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum MarkerType {{ Player, Npc, Police, Mission, Vehicle, Pickup }} |
| |
| [System.Serializable] |
| public class Marker |
| {{ |
| public Transform target; |
| public MarkerType type; |
| public Sprite icon; |
| }} |
| |
| [Header("Camera")] |
| [SerializeField] private Camera minimapCamera; |
| [SerializeField] private Transform player; |
| [SerializeField] private float cameraHeight = 100f; |
| [SerializeField] private float zoom = 60f; |
| |
| [Header("UI")] |
| [SerializeField] private RectTransform minimapRect; |
| [SerializeField] private RectTransform markerContainer; |
| [SerializeField] private GameObject markerPrefab; |
| [SerializeField] private float mapRadius = 90f; |
| |
| [Header("Markers")] |
| [SerializeField] private List<Marker> markers = new List<Marker>(); |
| [SerializeField] private Sprite policeIcon, missionIcon, npcIcon, vehicleIcon, pickupIcon; |
| |
| private readonly Dictionary<Transform, GameObject> _instances = new Dictionary<Transform, GameObject>(); |
| |
| private void LateUpdate() |
| {{ |
| if (player == null || minimapCamera == null) return; |
| FollowPlayer(); |
| UpdateMarkers(); |
| }} |
| |
| private void FollowPlayer() |
| {{ |
| Vector3 pos = player.position; |
| minimapCamera.transform.position = new Vector3(pos.x, cameraHeight, pos.z); |
| minimapCamera.transform.rotation = Quaternion.Euler(90f, -player.eulerAngles.y, 0f); |
| minimapCamera.orthographicSize = zoom; |
| }} |
| |
| private void UpdateMarkers() |
| {{ |
| foreach (var kvp in _instances) |
| if (kvp.Value != null) kvp.Value.SetActive(false); |
| foreach (var m in markers) |
| {{ |
| if (m.target == null) continue; |
| GameObject go = GetOrCreate(m); |
| Vector3 local = minimapCamera.transform.InverseTransformPoint(m.target.position); |
| Vector2 uiPos = new Vector2(local.x, local.z) / (zoom * 2f) * mapRadius * 2f; |
| if (uiPos.magnitude > mapRadius) uiPos = uiPos.normalized * mapRadius; |
| go.GetComponent<RectTransform>().anchoredPosition = uiPos; |
| go.SetActive(true); |
| }} |
| }} |
| |
| private GameObject GetOrCreate(Marker m) |
| {{ |
| if (_instances.TryGetValue(m.target, out GameObject go) && go != null) return go; |
| go = Instantiate(markerPrefab, markerContainer); |
| var img = go.GetComponent<Image>(); |
| if (img != null) |
| img.sprite = m.icon ?? m.type switch |
| {{ |
| MarkerType.Police => policeIcon, |
| MarkerType.Mission => missionIcon, |
| MarkerType.Npc => npcIcon, |
| MarkerType.Vehicle => vehicleIcon, |
| MarkerType.Pickup => pickupIcon, |
| _ => null, |
| }}; |
| _instances[m.target] = go; |
| return go; |
| }} |
| |
| public void RegisterMarker(Marker m) {{ if (!markers.Contains(m)) markers.Add(m); }} |
| public void UnregisterMarker(Transform t) |
| {{ |
| markers.RemoveAll(m => m.target == t); |
| if (_instances.TryGetValue(t, out GameObject go) && go != null) Destroy(go); |
| _instances.Remove(t); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateExplosionSystemTool(BaseUnityTool): |
| """Generate an explosion system with chain reactions and debris.""" |
|
|
| name = "create_explosion_system" |
| description = ( |
| "Generate an explosion system with chain reactions, vehicle " |
| "explosions, building damage and debris." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "ExplosionSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "ExplosionSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.FX |
| {{ |
| /// <summary> |
| /// Manages explosion events. Applies radial damage & physics impulse, |
| /// spawns fire/smoke/debris, and propagates to nearby explosive |
| /// barrels/vehicles for chain reactions. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Header("Damage")] |
| [SerializeField] private float baseDamage = 100f; |
| [SerializeField] private float baseRadius = 10f; |
| [SerializeField] private float baseForce = 1200f; |
| |
| [Header("Effects")] |
| [SerializeField] private GameObject explosionVfx; |
| [SerializeField] private GameObject fireVfx; |
| [SerializeField] private GameObject smokeVfx; |
| [SerializeField] private GameObject debrisPrefab; |
| [SerializeField] private int debrisCount = 12; |
| [SerializeField] private AudioClip explosionSound; |
| [SerializeField] private AudioSource audioSource; |
| |
| [Header("Chain Reaction")] |
| [SerializeField] private LayerMask explosiveMask; |
| [SerializeField] private float chainDelay = 0.15f; |
| |
| private void Awake() {{ Instance = this; }} |
| |
| public void Explode(Vector3 center, float damageMult = 1f, float radiusMult = 1f) |
| {{ |
| float dmg = baseDamage * damageMult; |
| float radius = baseRadius * radiusMult; |
| SpawnEffects(center, radius); |
| PlaySound(center); |
| ApplyDamageAndForce(center, dmg, radius); |
| SpawnDebris(center, radius); |
| PropagateChain(center, radius, damageMult, radiusMult); |
| }} |
| |
| private void SpawnEffects(Vector3 center, float radius) |
| {{ |
| if (explosionVfx != null) Instantiate(explosionVfx, center, Quaternion.identity); |
| if (fireVfx != null) |
| {{ |
| GameObject fire = Instantiate(fireVfx, center, Quaternion.identity); |
| Destroy(fire, 6f); |
| }} |
| if (smokeVfx != null) |
| {{ |
| GameObject smoke = Instantiate(smokeVfx, center, Quaternion.identity); |
| Destroy(smoke, 10f); |
| }} |
| }} |
| |
| private void PlaySound(Vector3 center) |
| {{ |
| if (audioSource != null && explosionSound != null) |
| audioSource.PlayOneShot(explosionSound); |
| }} |
| |
| private void ApplyDamageAndForce(Vector3 center, float dmg, float radius) |
| {{ |
| Collider[] hits = Physics.OverlapSphere(center, radius); |
| foreach (var h in hits) |
| {{ |
| float falloff = 1f - Mathf.Clamp01(Vector3.Distance(center, h.transform.position) / radius); |
| var dmgable = h.GetComponent<IDamageable>(); |
| if (dmgable != null) dmgable.TakeDamage(dmg * falloff, center); |
| if (h.TryGetComponent<Rigidbody>(out var rb)) |
| rb.AddExplosionForce(baseForce * falloff, center, radius, 0.5f, ForceMode.Impulse); |
| var vehicle = h.GetComponent<ExplodableVehicle>(); |
| if (vehicle != null) vehicle.Damage(dmg * falloff); |
| }} |
| }} |
| |
| private void SpawnDebris(Vector3 center, float radius) |
| {{ |
| if (debrisPrefab == null) return; |
| for (int i = 0; i < debrisCount; i++) |
| {{ |
| Vector3 offset = Random.onUnitSphere * radius * 0.5f; |
| offset.y = Mathf.Abs(offset.y); |
| GameObject debris = Instantiate(debrisPrefab, center + offset, Random.rotation); |
| if (debris.TryGetComponent<Rigidbody>(out var rb)) |
| rb.AddForce(offset.normalized * 8f, ForceMode.Impulse); |
| Destroy(debris, 8f); |
| }} |
| }} |
| |
| private void PropagateChain(Vector3 center, float radius, float dmgMult, float radMult) |
| {{ |
| Collider[] hits = Physics.OverlapSphere(center, radius, explosiveMask); |
| foreach (var h in hits) |
| {{ |
| var explodable = h.GetComponent<ExplodableVehicle>(); |
| if (explodable == null) continue; |
| StartCoroutine(ChainExplode(explodable.transform.position, dmgMult * 0.8f, radMult * 0.8f)); |
| }} |
| }} |
| |
| private IEnumerator ChainExplode(Vector3 pos, float dmgMult, float radMult) |
| {{ |
| yield return new WaitForSeconds(chainDelay + Random.value * 0.1f); |
| Explode(pos, dmgMult, radMult); |
| }} |
| }} |
| |
| public class ExplodableVehicle : MonoBehaviour |
| {{ |
| [SerializeField] private float health = 100f; |
| public void Damage(float amount) |
| {{ |
| health -= amount; |
| if (health <= 0f) |
| ExplosionSystem.Instance.Explode(transform.position, 1.5f, 1.3f); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateRagdollSystemTool(BaseUnityTool): |
| """Generate a ragdoll system with death physics and impact reactions.""" |
|
|
| name = "create_ragdoll_system" |
| description = ( |
| "Generate a ragdoll system with death physics, body piles and " |
| "impact reactions." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "RagdollSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "RagdollSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.AI |
| {{ |
| /// <summary> |
| /// Toggles a humanoid ragdoll. When active, Animator is disabled and |
| /// per-bone Rigidbodies/Colliders are enabled; forces can be applied |
| /// for impact reactions. Dead bodies persist so piles form naturally. |
| /// </summary> |
| [RequireComponent(typeof(Animator))] |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Bones")] |
| [SerializeField] private Rigidbody[] ragdollBodies; |
| [SerializeField] private Collider[] ragdollColliders; |
| [SerializeField] private Transform hipBone; |
| |
| [Header("Reaction")] |
| [SerializeField] private float impactForce = 6f; |
| [SerializeField] private float settleThreshold = 0.3f; |
| [SerializeField] private float settleTime = 2f; |
| |
| private Animator _animator; |
| private bool _active; |
| private float _settleTimer; |
| |
| public bool IsActive => _active; |
| |
| private void Awake() |
| {{ |
| _animator = GetComponent<Animator>(); |
| SetRagdoll(false); |
| }} |
| |
| public void Activate(Vector3 impulseSource, float force = 1f) |
| {{ |
| if (_active) return; |
| _active = true; |
| if (_animator != null) _animator.enabled = false; |
| SetRagdoll(true); |
| ApplyImpulse(impulseSource, force); |
| }} |
| |
| public void Deactivate() |
| {{ |
| _active = false; |
| if (_animator != null) _animator.enabled = true; |
| SetRagdoll(false); |
| }} |
| |
| private void SetRagdoll(bool enabled) |
| {{ |
| foreach (var rb in ragdollBodies) if (rb != null) rb.isKinematic = !enabled; |
| foreach (var col in ragdollColliders) if (col != null) col.enabled = enabled; |
| }} |
| |
| private void ApplyImpulse(Vector3 source, float mult) |
| {{ |
| if (hipBone == null) return; |
| Vector3 dir = (hipBone.position - source).normalized + Vector3.up * 0.5f; |
| foreach (var rb in ragdollBodies) |
| if (rb != null) rb.AddForce(dir * impactForce * mult, ForceMode.Impulse); |
| }} |
| |
| private void Update() |
| {{ |
| if (!_active) return; |
| float maxVel = 0f; |
| foreach (var rb in ragdollBodies) |
| if (rb != null) maxVel = Mathf.Max(maxVel, rb.velocity.magnitude); |
| if (maxVel < settleThreshold) |
| {{ |
| _settleTimer += Time.deltaTime; |
| if (_settleTimer >= settleTime) Settle(); |
| }} |
| else _settleTimer = 0f; |
| }} |
| |
| private void Settle() |
| {{ |
| // Optionally convert ragdoll into a static corpse mesh for perf. |
| foreach (var rb in ragdollBodies) if (rb != null) rb.isKinematic = true; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateClothPhysicsTool(BaseUnityTool): |
| """Generate a cloth physics system for flags, curtains and clothing.""" |
|
|
| name = "create_cloth_physics" |
| description = ( |
| "Generate a cloth physics system for flags, curtains and clothing " |
| "simulation." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "ClothPhysicsSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "ClothPhysicsSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.FX |
| {{ |
| /// <summary> |
| /// Wraps Unity's Cloth component for interactive cloth (flags, curtains, |
| /// character capes). Provides wind control, preset attachment weights |
| /// and runtime toggling. Multiple cloth objects can be registered. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [System.Serializable] |
| public class ClothInstance |
| {{ |
| public string id; |
| public Cloth cloth; |
| public float windFactor = 1f; |
| }} |
| |
| [Header("Global Wind")] |
| [SerializeField] private Vector3 windDirection = new Vector3(1f, 0f, 0.2f); |
| [SerializeField] private float windStrength = 2f; |
| [SerializeField] private float gustFrequency = 0.3f; |
| [SerializeField] private float gustAmplitude = 1.5f; |
| |
| [Header("Instances")] |
| [SerializeField] private List<ClothInstance> cloths = new List<ClothInstance>(); |
| |
| public void Register(ClothInstance ci) |
| {{ |
| if (!cloths.Contains(ci)) cloths.Add(ci); |
| }} |
| |
| public void Unregister(string id) => cloths.RemoveAll(c => c.id == id); |
| |
| private void Update() |
| {{ |
| float gust = 1f + Mathf.Sin(Time.time * gustFrequency * Mathf.PI * 2f) * gustAmplitude; |
| Vector3 wind = windDirection.normalized * windStrength * gust; |
| foreach (var ci in cloths) |
| {{ |
| if (ci.cloth == null) continue; |
| ci.cloth.externalAcceleration = wind * ci.windFactor; |
| ci.cloth.randomAcceleration = wind * 0.2f * ci.windFactor; |
| }} |
| }} |
| |
| public void AttachTopRow(string id) |
| {{ |
| var ci = cloths.Find(c => c.id == id); |
| if (ci?.cloth == null) return; |
| var coefficients = ci.cloth.coefficients; |
| int vertsPerRow = Mathf.CeilToInt(Mathf.Sqrt(coefficients.Length)); |
| for (int i = 0; i < coefficients.Length; i++) |
| {{ |
| int row = i / vertsPerRow; |
| coefficients[i].maxDistance = row == 0 ? 0f : float.PositiveInfinity; |
| }} |
| ci.cloth.coefficients = coefficients; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateWeatherSystemTool(BaseUnityTool): |
| """Generate a weather system with rain, fog, clear and storm states.""" |
|
|
| name = "create_weather_system" |
| description = ( |
| "Generate a weather system: rain, fog, clear, storm with particle " |
| "effects and lighting changes." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "WeatherSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "WeatherSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using UnityEngine; |
| |
| namespace OpenWorld.World |
| {{ |
| /// <summary> |
| /// State-machine weather controller. Cycles between Clear, Cloudy, |
| /// Rain, Fog and Storm. Each state adjusts ambient light, fog density, |
| /// particle systems (rain/snow), audio bed and wind. Transitions are |
| /// smoothed over a few seconds. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum Weather {{ Clear, Cloudy, Rain, Fog, Storm }} |
| |
| [Serializable] |
| public struct WeatherPreset |
| {{ |
| public Weather type; |
| public Color ambientColor; |
| public float fogDensity; |
| public Color fogColor; |
| public float lightIntensity; |
| public float windStrength; |
| public ParticleSystem particles; |
| public AudioClip ambientSound; |
| }} |
| |
| [Header("State")] |
| [SerializeField] private Weather current = Weather.Clear; |
| [SerializeField] private WeatherPreset[] presets; |
| [SerializeField] private float transitionTime = 8f; |
| [SerializeField] private float changeInterval = 120f; |
| |
| [Header("Refs")] |
| [SerializeField] private Light sun; |
| [SerializeField] private AudioSource ambientSource; |
| [SerializeField] private WindZone windZone; |
| |
| private WeatherPreset _target; |
| private WeatherPreset _current; |
| private float _transitionProgress = 1f; |
| private float _changeTimer; |
| |
| public event Action<Weather> OnWeatherChanged; |
| |
| private void Awake() |
| {{ |
| _current = GetPreset(current); |
| _target = _current; |
| ApplyImmediate(_current); |
| }} |
| |
| private void Update() |
| {{ |
| HandleAutoChange(); |
| HandleTransition(); |
| }} |
| |
| private void HandleAutoChange() |
| {{ |
| _changeTimer += Time.deltaTime; |
| if (_changeTimer < changeInterval) return; |
| _changeTimer = 0f; |
| Weather[] values = (Weather[])Enum.GetValues(typeof(Weather)); |
| Weather next; |
| do {{ next = values[UnityEngine.Random.Range(0, values.Length)]; }} |
| while (next == current); |
| SetWeather(next); |
| }} |
| |
| public void SetWeather(Weather w) |
| {{ |
| current = w; |
| _target = GetPreset(w); |
| _transitionProgress = 0f; |
| OnWeatherChanged?.Invoke(w); |
| if (ambientSource != null && _target.ambientSound != null) |
| {{ |
| ambientSource.clip = _target.ambientSound; |
| ambientSource.Play(); |
| }} |
| if (_target.particles != null) _target.particles.Play(); |
| }} |
| |
| private void HandleTransition() |
| {{ |
| if (_transitionProgress >= 1f) return; |
| _transitionProgress = Mathf.Min(1f, _transitionProgress + Time.deltaTime / transitionTime); |
| float t = _transitionProgress; |
| RenderSettings.ambientLight = Color.Lerp(_current.ambientColor, _target.ambientColor, t); |
| RenderSettings.fogColor = Color.Lerp(_current.fogColor, _target.fogColor, t); |
| RenderSettings.fogDensity = Mathf.Lerp(_current.fogDensity, _target.fogDensity, t); |
| if (sun != null) sun.intensity = Mathf.Lerp(_current.lightIntensity, _target.lightIntensity, t); |
| if (windZone != null) windZone.windMain = Mathf.Lerp(_current.windStrength, _target.windStrength, t); |
| if (_transitionProgress >= 1f) |
| {{ |
| _current = _target; |
| if (_current.particles != null) _current.particles.Stop(); |
| }} |
| }} |
| |
| private void ApplyImmediate(WeatherPreset p) |
| {{ |
| RenderSettings.ambientLight = p.ambientColor; |
| RenderSettings.fogColor = p.fogColor; |
| RenderSettings.fogDensity = p.fogDensity; |
| RenderSettings.fog = p.fogDensity > 0.001f; |
| if (sun != null) sun.intensity = p.lightIntensity; |
| if (windZone != null) windZone.windMain = p.windStrength; |
| }} |
| |
| private WeatherPreset GetPreset(Weather w) |
| {{ |
| foreach (var p in presets) if (p.type == w) return p; |
| return default; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateCctvSystemTool(BaseUnityTool): |
| """Generate a CCTV security camera system with detection and alarms.""" |
|
|
| name = "create_cctv_system" |
| description = ( |
| "Generate a CCTV security camera system with detection, alarm " |
| "triggering and recording hooks." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "CctvSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "CctvSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Security |
| {{ |
| /// <summary> |
| /// Security camera. Sweeps a cone via FieldOfView raycasts; on |
| /// detecting the player (or a tagged target) it raises an alarm event |
| /// and starts recording (rendered to a RenderTexture). Guards can be |
| /// alerted via the OnDetected event. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Detection")] |
| [SerializeField] private Transform pivot; |
| [SerializeField] private float viewDistance = 18f; |
| [SerializeField] private float viewAngle = 45f; |
| [SerializeField] private float sweepSpeed = 30f; |
| [SerializeField] private float sweepRange = 90f; |
| [SerializeField] private LayerMask targetMask; |
| [SerializeField] private LayerMask obstructionMask; |
| [SerializeField] private Transform alarmTarget; |
| |
| [Header("Visual")] |
| [SerializeField] private Light indicator; |
| [SerializeField] private Color idleColor = Color.green; |
| [SerializeField] private Color alertColor = Color.red; |
| [SerializeField] private Camera renderCamera; |
| [SerializeField] private RenderTexture outputTexture; |
| |
| public bool Alerted {{ get; private set; }} |
| public static System.Action<Transform> OnGlobalDetection; |
| |
| private float _baseYaw; |
| private float _sweepT; |
| |
| private void Awake() |
| {{ |
| if (pivot != null) _baseYaw = pivot.eulerAngles.y; |
| if (renderCamera != null) renderCamera.targetTexture = outputTexture; |
| }} |
| |
| private void Update() |
| {{ |
| Sweep(); |
| Detect(); |
| }} |
| |
| private void Sweep() |
| {{ |
| if (Alerted || pivot == null) return; |
| _sweepT += Time.deltaTime * sweepSpeed; |
| float yaw = _baseYaw + Mathf.Sin(_sweepT * Mathf.Deg2Rad) * sweepRange * 0.5f; |
| pivot.rotation = Quaternion.Euler(0f, yaw, 0f); |
| }} |
| |
| private void Detect() |
| {{ |
| if (pivot == null) return; |
| Collider[] candidates = Physics.OverlapSphere(pivot.position, viewDistance, targetMask); |
| foreach (var c in candidates) |
| {{ |
| Vector3 dir = (c.transform.position - pivot.position).normalized; |
| if (Vector3.Angle(pivot.forward, dir) > viewAngle * 0.5f) continue; |
| if (Physics.Raycast(pivot.position, dir, out RaycastHit hit, viewDistance, obstructionMask)) |
| if (hit.transform != c.transform) continue; |
| RaiseAlarm(c.transform); |
| return; |
| }} |
| }} |
| |
| private void RaiseAlarm(Transform target) |
| {{ |
| Alerted = true; |
| if (indicator != null) indicator.color = alertColor; |
| OnGlobalDetection?.Invoke(target); |
| WantedSystem.Instance?.ReportCrime(20f); |
| if (renderCamera != null) renderCamera.gameObject.SetActive(true); |
| }} |
| |
| public void Reset() |
| {{ |
| Alerted = false; |
| if (indicator != null) indicator.color = idleColor; |
| if (renderCamera != null) renderCamera.gameObject.SetActive(false); |
| }} |
| |
| private void OnDrawGizmosSelected() |
| {{ |
| if (pivot == null) return; |
| Gizmos.color = Alerted ? Color.red : Color.yellow; |
| Gizmos.DrawWireSphere(pivot.position, viewDistance); |
| Vector3 left = Quaternion.Euler(0, -viewAngle * 0.5f, 0) * pivot.forward * viewDistance; |
| Vector3 right = Quaternion.Euler(0, viewAngle * 0.5f, 0) * pivot.forward * viewDistance; |
| Gizmos.DrawLine(pivot.position, pivot.position + left); |
| Gizmos.DrawLine(pivot.position, pivot.position + right); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateDialogueSystemTool(BaseUnityTool): |
| """Generate a branching dialogue system with subtitles.""" |
|
|
| name = "create_dialogue_system" |
| description = ( |
| "Generate a dialogue system with NPC conversations, branching " |
| "dialogue and subtitles." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "DialogueSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "DialogueSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| using UnityEngine.UI; |
| |
| namespace OpenWorld.Dialogue |
| {{ |
| /// <summary> |
| /// Branching dialogue engine. Conversations are directed graphs of |
| /// nodes; each node has NPC lines and a list of player responses that |
| /// link to other nodes. Subtitles render via a Text panel and a typewriter |
| /// coroutine. Choices are shown as buttons. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public class DialogueNode |
| {{ |
| public string id; |
| [TextArea] public string npcLine; |
| public AudioClip voiceClip; |
| public List<DialogueChoice> choices = new List<DialogueChoice>(); |
| public string onEnterEvent; |
| }} |
| |
| [Serializable] |
| public class DialogueChoice |
| {{ |
| [TextArea] public string text; |
| public string targetNodeId; |
| public string requiredFlag; |
| public string setFlag; |
| }} |
| |
| [Serializable] |
| public class Conversation |
| {{ |
| public string id; |
| public List<DialogueNode> nodes = new List<DialogueNode>(); |
| }} |
| |
| [Header("UI")] |
| [SerializeField] private GameObject panel; |
| [SerializeField] private Text npcNameText; |
| [SerializeField] private Text subtitleText; |
| [SerializeField] private Transform choiceContainer; |
| [SerializeField] private Button choicePrefab; |
| [SerializeField] private float typeSpeed = 40f; |
| |
| [Header("Audio")] |
| [SerializeField] private AudioSource voiceSource; |
| |
| [Header("Data")] |
| [SerializeField] private List<Conversation> conversations = new List<Conversation>(); |
| |
| private readonly HashSet<string> _flags = new HashSet<string>(); |
| private Conversation _current; |
| private DialogueNode _node; |
| private bool _typing; |
| |
| public event Action<string> OnEventTriggered; |
| public bool IsActive => _current != null; |
| |
| public void StartConversation(string id, string npcName) |
| {{ |
| _current = conversations.Find(c => c.id == id); |
| if (_current == null || _current.nodes.Count == 0) return; |
| if (npcNameText != null) npcNameText.text = npcName; |
| if (panel != null) panel.SetActive(true); |
| Cursor.lockState = CursorLockMode.None; |
| Cursor.visible = true; |
| ShowNode(_current.nodes[0]); |
| }} |
| |
| public void EndConversation() |
| {{ |
| _current = null; |
| _node = null; |
| if (panel != null) panel.SetActive(false); |
| Cursor.lockState = CursorLockMode.Locked; |
| Cursor.visible = false; |
| }} |
| |
| private void ShowNode(DialogueNode node) |
| {{ |
| _node = node; |
| if (!string.IsNullOrEmpty(node.onEnterEvent)) OnEventTriggered?.Invoke(node.onEnterEvent); |
| if (node.voiceClip != null && voiceSource != null) voiceSource.PlayOneShot(node.voiceClip); |
| StartCoroutine(TypeLine(node.npcLine)); |
| RenderChoices(node); |
| }} |
| |
| private System.Collections.IEnumerator TypeLine(string line) |
| {{ |
| _typing = true; |
| if (subtitleText != null) subtitleText.text = ""; |
| foreach (char c in line) |
| {{ |
| if (subtitleText != null) subtitleText.text += c; |
| yield return new WaitForSeconds(1f / typeSpeed); |
| }} |
| _typing = false; |
| }} |
| |
| private void RenderChoices(DialogueNode node) |
| {{ |
| if (choiceContainer == null || choicePrefab == null) return; |
| foreach (Transform child in choiceContainer) Destroy(child.gameObject); |
| foreach (var ch in node.choices) |
| {{ |
| if (!string.IsNullOrEmpty(ch.requiredFlag) && !_flags.Contains(ch.requiredFlag)) continue; |
| Button btn = Instantiate(choicePrefab, choiceContainer); |
| btn.GetComponentInChildren<Text>().text = ch.text; |
| btn.onClick.AddListener(() => Choose(ch)); |
| }} |
| }} |
| |
| private void Choose(DialogueChoice choice) |
| {{ |
| if (!string.IsNullOrEmpty(choice.setFlag)) _flags.Add(choice.setFlag); |
| if (string.IsNullOrEmpty(choice.targetNodeId)) {{ EndConversation(); return; }} |
| var next = _current.nodes.Find(n => n.id == choice.targetNodeId); |
| if (next != null) ShowNode(next); else EndConversation(); |
| }} |
| |
| private void Update() |
| {{ |
| if (!IsActive) return; |
| if (Input.GetKeyDown(KeyCode.Escape)) EndConversation(); |
| if (_typing && Input.GetMouseButtonDown(0)) _typing = false; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateMissionSystemTool(BaseUnityTool): |
| """Generate a mission system with story, side and random events.""" |
|
|
| name = "create_mission_system" |
| description = ( |
| "Generate a mission system with story missions, side missions, " |
| "random events and mission givers." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "MissionSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "MissionSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Missions |
| {{ |
| /// <summary> |
| /// High-level mission director. Story missions unlock linearly; side |
| /// missions are available from givers; random events spawn opportunistically. |
| /// Each mission has a starting trigger and a completion check. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum MissionKind {{ Story, Side, RandomEvent }} |
| |
| [Serializable] |
| public class Mission |
| {{ |
| public string id; |
| public string title; |
| public MissionKind kind; |
| public string giverId; |
| public Vector3 startMarker; |
| public string questId; // links to QuestSystem |
| public int moneyReward; |
| public int xpReward; |
| public string unlocksMissionId; |
| public bool available; |
| public bool started; |
| public bool completed; |
| }} |
| |
| [Header("Missions")] |
| [SerializeField] private List<Mission> missions = new List<Mission>(); |
| [SerializeField] private Mission[] randomEventPool; |
| |
| [Header("Random Events")] |
| [SerializeField] private float randomEventInterval = 180f; |
| [SerializeField] private float randomEventChance = 0.5f; |
| |
| private float _randomTimer; |
| private Mission _activeStory; |
| |
| public IReadOnlyList<Mission> All => missions; |
| public event Action<Mission> OnMissionStarted; |
| public event Action<Mission> OnMissionCompleted; |
| |
| private void Start() |
| {{ |
| // Unlock the first story mission. |
| foreach (var m in missions) |
| if (m.kind == MissionKind.Story && !m.completed) {{ m.available = true; _activeStory = m; break; }} |
| _randomTimer = randomEventInterval; |
| }} |
| |
| private void Update() |
| {{ |
| HandleRandomEvents(); |
| CheckCompletion(); |
| }} |
| |
| public void StartMission(string id) |
| {{ |
| Mission m = missions.Find(x => x.id == id); |
| if (m == null || !m.available || m.started) return; |
| m.started = true; |
| QuestSystem.Instance?.Accept(m.questId); |
| OnMissionStarted?.Invoke(m); |
| }} |
| |
| private void CheckCompletion() |
| {{ |
| foreach (var m in missions) |
| {{ |
| if (!m.started || m.completed) continue; |
| if (QuestSystem.Instance == null) continue; |
| var q = QuestSystem.Instance.ActiveQuests; |
| bool done = false; |
| foreach (var qq in q) if (qq.id == m.questId && qq.completed) done = true; |
| if (done) CompleteMission(m); |
| }} |
| }} |
| |
| private void CompleteMission(Mission m) |
| {{ |
| m.completed = true; |
| EconomySystem.Instance?.AddCash(m.moneyReward); |
| OnMissionCompleted?.Invoke(m); |
| if (!string.IsNullOrEmpty(m.unlocksMissionId)) |
| {{ |
| Mission next = missions.Find(x => x.id == m.unlocksMissionId); |
| if (next != null) next.available = true; |
| }} |
| }} |
| |
| private void HandleRandomEvents() |
| {{ |
| _randomTimer -= Time.deltaTime; |
| if (_randomTimer > 0f) return; |
| _randomTimer = randomEventInterval; |
| if (randomEventPool == null || randomEventPool.Length == 0) return; |
| if (UnityEngine.Random.value > randomEventChance) return; |
| Mission re = randomEventPool[UnityEngine.Random.Range(0, randomEventPool.Length)]; |
| Mission copy = new Mission |
| {{ |
| id = re.id + "_" + Time.frameCount, |
| title = re.title, |
| kind = MissionKind.RandomEvent, |
| giverId = re.giverId, |
| startMarker = re.startMarker, |
| questId = re.questId, |
| moneyReward = re.moneyReward, |
| xpReward = re.xpReward, |
| available = true |
| }}; |
| missions.Add(copy); |
| StartMission(copy.id); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateGarageSystemTool(BaseUnityTool): |
| """Generate a garage system for vehicle storage and customization.""" |
|
|
| name = "create_garage_system" |
| description = ( |
| "Generate a garage system with vehicle storage, customization, " |
| "painting and repairs." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "GarageSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "GarageSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Vehicles |
| {{ |
| /// <summary> |
| /// Garage service. Stores up to N vehicles per garage, restores health |
| /// on storage, and exposes customization actions: repaint, repair, |
| /// upgrade engine/armor. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public class StoredVehicle |
| {{ |
| public string modelId; |
| public float health; |
| public Color paint = Color.white; |
| public int engineLevel = 1; |
| public int armorLevel = 1; |
| public int tireLevel = 1; |
| }} |
| |
| [Header("Capacity")] |
| [SerializeField] private int capacity = 10; |
| [SerializeField] private Transform spawnPoint; |
| [SerializeField] private List<StoredVehicle> stored = new List<StoredVehicle>(); |
| [SerializeField] private GameObject[] vehiclePrefabs; |
| |
| [Header("Costs")] |
| [SerializeField] private int repairCost = 500; |
| [SerializeField] private int repaintCost = 200; |
| [SerializeField] private int upgradeCost = 2000; |
| |
| public IReadOnlyList<StoredVehicle> Stored => stored; |
| |
| public bool Store(GameObject vehicle, string modelId) |
| {{ |
| if (stored.Count >= capacity) return false; |
| var sv = new StoredVehicle |
| {{ |
| modelId = modelId, |
| health = 100f, |
| paint = vehicle.GetComponent<Renderer>()?.material.color ?? Color.white |
| }}; |
| stored.Add(sv); |
| Destroy(vehicle); |
| return true; |
| }} |
| |
| public GameObject Retrieve(int index) |
| {{ |
| if (index < 0 || index >= stored.Count || spawnPoint == null) return null; |
| StoredVehicle sv = stored[index]; |
| GameObject prefab = System.Array.Find(vehiclePrefabs, p => p.name == sv.modelId); |
| if (prefab == null) return null; |
| GameObject go = Instantiate(prefab, spawnPoint.position, spawnPoint.rotation); |
| if (go.TryGetComponent<Renderer>(out var r)) r.material.color = sv.paint; |
| stored.RemoveAt(index); |
| return go; |
| }} |
| |
| public bool Repair(int index) |
| {{ |
| if (!Charge(repairCost)) return false; |
| if (index < 0 || index >= stored.Count) return false; |
| stored[index].health = 100f; |
| return true; |
| }} |
| |
| public bool Repaint(int index, Color color) |
| {{ |
| if (!Charge(repaintCost)) return false; |
| if (index < 0 || index >= stored.Count) return false; |
| stored[index].paint = color; |
| return true; |
| }} |
| |
| public bool UpgradeEngine(int index) |
| {{ |
| if (!Charge(upgradeCost)) return false; |
| stored[index].engineLevel = Mathf.Min(5, stored[index].engineLevel + 1); |
| return true; |
| }} |
| |
| public bool UpgradeArmor(int index) |
| {{ |
| if (!Charge(upgradeCost)) return false; |
| stored[index].armorLevel = Mathf.Min(5, stored[index].armorLevel + 1); |
| return true; |
| }} |
| |
| private bool Charge(int amount) |
| {{ |
| var econ = EconomySystem.Instance; |
| if (econ == null || econ.Cash < amount) return false; |
| econ.AddCash(-amount); |
| return true; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreatePropertySystemTool(BaseUnityTool): |
| """Generate a property system for buying, selling and income.""" |
|
|
| name = "create_property_system" |
| description = ( |
| "Generate a property system: buy/sell properties, income " |
| "generation, safe houses." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "PropertySystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "PropertySystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Economy |
| {{ |
| /// <summary> |
| /// Real-estate manager. Each property has a price, weekly income, and |
| /// may serve as a safe house (heal/save point). Income is paid out on |
| /// a configurable interval. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Serializable] |
| public class Property |
| {{ |
| public string id; |
| public string displayName; |
| public int price; |
| public int weeklyIncome; |
| public bool safeHouse; |
| public Vector3 location; |
| [NonSerialized] public bool owned; |
| }} |
| |
| [Header("Catalog")] |
| [SerializeField] private List<Property> properties = new List<Property>(); |
| |
| [Header("Income")] |
| [SerializeField] private float payoutIntervalMinutes = 5f; |
| private float _payoutTimer; |
| |
| public IReadOnlyList<Property> All => properties; |
| public event Action<Property> OnPurchased; |
| public event Action<Property> OnSold; |
| public event Action<int> OnIncomePaid; |
| |
| private void Awake() {{ Instance = this; }} |
| |
| public bool Buy(string id) |
| {{ |
| Property p = properties.Find(x => x.id == id); |
| if (p == null || p.owned) return false; |
| var econ = EconomySystem.Instance; |
| if (econ == null || econ.Cash < p.price) return false; |
| econ.AddCash(-p.price); |
| p.owned = true; |
| OnPurchased?.Invoke(p); |
| return true; |
| }} |
| |
| public bool Sell(string id) |
| {{ |
| Property p = properties.Find(x => x.id == id); |
| if (p == null || !p.owned) return false; |
| EconomySystem.Instance?.AddCash(p.price / 2); |
| p.owned = false; |
| OnSold?.Invoke(p); |
| return true; |
| }} |
| |
| public bool UseSafeHouse(string id) |
| {{ |
| Property p = properties.Find(x => x.id == id); |
| if (p == null || !p.owned || !p.safeHouse) return false; |
| // Player health/save handled by listeners. |
| return true; |
| }} |
| |
| private void Update() |
| {{ |
| _payoutTimer += Time.deltaTime; |
| float interval = payoutIntervalMinutes * 60f; |
| if (_payoutTimer < interval) return; |
| _payoutTimer = 0f; |
| int total = 0; |
| foreach (var p in properties) if (p.owned) total += p.weeklyIncome; |
| if (total > 0) |
| {{ |
| EconomySystem.Instance?.AddCash(total); |
| OnIncomePaid?.Invoke(total); |
| }} |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateCharacterCustomizationTool(BaseUnityTool): |
| """Generate a character customization system (skin, hair, clothing).""" |
|
|
| name = "create_character_customization" |
| description = ( |
| "Generate a character customization system: skin, hair, clothing, " |
| "accessories." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "CharacterCustomization"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "CharacterCustomization", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using UnityEngine; |
| |
| namespace OpenWorld.Player |
| {{ |
| /// <summary> |
| /// Character appearance manager. Blends skin tones via material color, |
| /// swaps hair meshes from a list, and toggles clothing/attachment |
| /// GameObjects. Saves an appearance string for persistence. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public struct HairOption {{ public string id; public GameObject mesh; }} |
| [Serializable] |
| public struct ClothingOption {{ public string id; public GameObject mesh; }} |
| [Serializable] |
| public struct AccessoryOption {{ public string id; public GameObject mesh; }} |
| |
| [Header("Body")] |
| [SerializeField] private SkinnedMeshRenderer bodyMesh; |
| [SerializeField] private Color[] skinTones; |
| [SerializeField] private int skinIndex; |
| |
| [Header("Hair")] |
| [SerializeField] private HairOption[] hairOptions; |
| [SerializeField] private int hairIndex; |
| |
| [Header("Clothing")] |
| [SerializeField] private ClothingOption[] tops; |
| [SerializeField] private int topIndex; |
| [SerializeField] private ClothingOption[] bottoms; |
| [SerializeField] private int bottomIndex; |
| |
| [Header("Accessories")] |
| [SerializeField] private AccessoryOption[] accessories; |
| [SerializeField] private bool[] accessoryToggles; |
| |
| public int SkinIndex => skinIndex; |
| public int HairIndex => hairIndex; |
| public int TopIndex => topIndex; |
| public int BottomIndex => bottomIndex; |
| |
| public void SetSkin(int index) |
| {{ |
| skinIndex = Mathf.Clamp(index, 0, skinTones.Length - 1); |
| if (bodyMesh != null) bodyMesh.material.color = skinTones[skinIndex]; |
| }} |
| |
| public void SetHair(int index) |
| {{ |
| hairIndex = Mathf.Clamp(index, 0, hairOptions.Length - 1); |
| for (int i = 0; i < hairOptions.Length; i++) |
| if (hairOptions[i].mesh != null) hairOptions[i].mesh.SetActive(i == hairIndex); |
| }} |
| |
| public void SetTop(int index) |
| {{ |
| topIndex = Mathf.Clamp(index, 0, tops.Length - 1); |
| for (int i = 0; i < tops.Length; i++) |
| if (tops[i].mesh != null) tops[i].mesh.SetActive(i == topIndex); |
| }} |
| |
| public void SetBottom(int index) |
| {{ |
| bottomIndex = Mathf.Clamp(index, 0, bottoms.Length - 1); |
| for (int i = 0; i < bottoms.Length; i++) |
| if (bottoms[i].mesh != null) bottoms[i].mesh.SetActive(i == bottomIndex); |
| }} |
| |
| public void ToggleAccessory(int index) |
| {{ |
| if (index < 0 || index >= accessories.Length) return; |
| accessoryToggles[index] = !accessoryToggles[index]; |
| if (accessories[index].mesh != null) accessories[index].mesh.SetActive(accessoryToggles[index]); |
| }} |
| |
| public string Serialize() |
| {{ |
| return $"{{skinIndex}}|{{hairIndex}}|{{topIndex}}|{{bottomIndex}}|{{string.Join(",", accessoryToggles)}}"; |
| }} |
| |
| public void Deserialize(string data) |
| {{ |
| if (string.IsNullOrEmpty(data)) return; |
| string[] parts = data.Split('|'); |
| if (parts.Length < 4) return; |
| if (int.TryParse(parts[0], out int s)) SetSkin(s); |
| if (int.TryParse(parts[1], out int h)) SetHair(h); |
| if (int.TryParse(parts[2], out int t)) SetTop(t); |
| if (int.TryParse(parts[3], out int b)) SetBottom(b); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateCutsceneSystemTool(BaseUnityTool): |
| """Generate a cutscene system with camera sequences and letterbox.""" |
|
|
| name = "create_cutscene_system" |
| description = ( |
| "Generate a cutscene system with camera sequences, character " |
| "animation and letterbox." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "CutsceneSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "CutsceneSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections; |
| using System.Collections.Generic; |
| using UnityEngine; |
| using UnityEngine.UI; |
| |
| namespace OpenWorld.Cinematics |
| {{ |
| /// <summary> |
| /// Sequencer-based cutscene player. Each shot has a camera, duration, |
| /// look-at target and dialogue line. A black letterbox bar animates in |
| /// at the start. Player input is disabled during playback. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public class Shot |
| {{ |
| public Camera camera; |
| public Transform lookAt; |
| public float duration = 4f; |
| [TextArea] public string subtitle; |
| public AnimationClip actorAnimation; |
| public Animator actorAnimator; |
| }} |
| |
| [Serializable] |
| public class Cutscene |
| {{ |
| public string id; |
| public List<Shot> shots = new List<Shot>(); |
| }} |
| |
| [Header("Letterbox")] |
| [SerializeField] private RectTransform topBar; |
| [SerializeField] private RectTransform bottomBar; |
| [SerializeField] private float barHeight = 100f; |
| [SerializeField] private float barAnimDuration = 0.6f; |
| |
| [Header("Subtitles")] |
| [SerializeField] private Text subtitleText; |
| |
| [Header("Input")] |
| [SerializeField] private MonoBehaviour playerController; |
| |
| [Header("Data")] |
| [SerializeField] private List<Cutscene> cutscenes = new List<Cutscene>(); |
| |
| private bool _playing; |
| public bool IsPlaying => _playing; |
| public event Action<string> OnCutsceneStart; |
| public event Action<string> OnCutsceneEnd; |
| |
| public void Play(string id) |
| {{ |
| Cutscene cs = cutscenes.Find(c => c.id == id); |
| if (cs == null) return; |
| StartCoroutine(PlayRoutine(cs)); |
| }} |
| |
| private IEnumerator PlayRoutine(Cutscene cs) |
| {{ |
| _playing = true; |
| OnCutsceneStart?.Invoke(cs.id); |
| if (playerController != null) playerController.enabled = false; |
| Cursor.visible = false; |
| yield return AnimateBars(true); |
| foreach (var shot in cs.shots) |
| {{ |
| if (shot.camera != null) shot.camera.gameObject.SetActive(true); |
| if (shot.actorAnimator != null && shot.actorAnimation != null) |
| shot.actorAnimator.Play(shot.actorAnimation.name); |
| if (subtitleText != null) subtitleText.text = shot.subtitle; |
| yield return new WaitForSeconds(shot.duration); |
| if (shot.camera != null) shot.camera.gameObject.SetActive(false); |
| }} |
| yield return AnimateBars(false); |
| if (playerController != null) playerController.enabled = true; |
| _playing = false; |
| OnCutsceneEnd?.Invoke(cs.id); |
| }} |
| |
| private IEnumerator AnimateBars(bool show) |
| {{ |
| float from = show ? 0f : barHeight; |
| float to = show ? barHeight : 0f; |
| float t = 0f; |
| while (t < barAnimDuration) |
| {{ |
| t += Time.deltaTime; |
| float h = Mathf.Lerp(from, to, t / barAnimDuration); |
| if (topBar != null) topBar.sizeDelta = new Vector2(0, h); |
| if (bottomBar != null) bottomBar.sizeDelta = new Vector2(0, h); |
| yield return null; |
| }} |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateMultiplayerNetcodeTool(BaseUnityTool): |
| """Generate multiplayer netcode stubs with sync and voice hooks.""" |
|
|
| name = "create_multiplayer_netcode" |
| description = ( |
| "Generate multiplayer netcode: network sync, player spawning, " |
| "voice chat hooks." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "NetworkManager"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "NetworkManager", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Net |
| {{ |
| /// <summary> |
| /// Lightweight host/client networking stub. Provides RPC-style |
| /// send/receive, authoritative object spawning, interpolation for |
| /// remote player transforms and voice-chat hook events. Designed as a |
| /// drop-in surface that a real transport (Netcode for GameObjects / |
| /// Mirror) can replace. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Header("Config")] |
| [SerializeField] private int port = 7777; |
| [SerializeField] private int maxPlayers = 32; |
| [SerializeField] private GameObject playerPrefab; |
| [SerializeField] private Transform[] spawnPoints; |
| |
| [Header("State")] |
| public bool isHost; |
| public bool isClient; |
| public int localPlayerId = -1; |
| |
| private readonly Dictionary<int, NetworkPlayer> _players = new Dictionary<int, NetworkPlayer>(); |
| private float _sendInterval = 0.05f; |
| private float _sendTimer; |
| |
| public event Action<int> OnPlayerJoined; |
| public event Action<int> OnPlayerLeft; |
| public event Action<int, byte[]> OnVoiceData; |
| |
| private void Awake() {{ Instance = this; }} |
| |
| public void StartHost() |
| {{ |
| isHost = true; isClient = true; |
| localPlayerId = 0; |
| SpawnPlayer(localPlayerId, true); |
| }} |
| |
| public void StartClient(string address) |
| {{ |
| isHost = false; isClient = true; |
| // Transport connect here. |
| }} |
| |
| public void Stop() |
| {{ |
| foreach (var p in _players.Values) if (p != null) Destroy(p.gameObject); |
| _players.Clear(); |
| isHost = isClient = false; |
| }} |
| |
| private void SpawnPlayer(int id, bool isLocal) |
| {{ |
| if (playerPrefab == null) return; |
| Transform sp = spawnPoints.Length > 0 ? spawnPoints[id % spawnPoints.Length] : transform; |
| GameObject go = Instantiate(playerPrefab, sp.position, sp.rotation); |
| var np = go.AddComponent<NetworkPlayer>(); |
| np.Initialize(id, isLocal); |
| _players[id] = np; |
| OnPlayerJoined?.Invoke(id); |
| }} |
| |
| public void SendVoice(int fromId, byte[] data) |
| {{ |
| OnVoiceData?.Invoke(fromId, data); |
| // Broadcast to other players via transport. |
| }} |
| |
| private void Update() |
| {{ |
| if (!isClient) return; |
| _sendTimer -= Time.deltaTime; |
| if (_sendTimer > 0f) return; |
| _sendTimer = _sendInterval; |
| foreach (var p in _players.Values) |
| if (p != null) p.NetUpdate(); |
| }} |
| }} |
| |
| public class NetworkPlayer : MonoBehaviour |
| {{ |
| public int Id {{ get; private set; }} |
| public bool IsLocal {{ get; private set; }} |
| |
| private Vector3 _netPos; |
| private Quaternion _netRot; |
| private float _interpSpeed = 12f; |
| |
| public void Initialize(int id, bool isLocal) {{ Id = id; IsLocal = isLocal; }} |
| |
| public void NetUpdate() |
| {{ |
| if (IsLocal) |
| {{ |
| // Send transform to server (stub). |
| _netPos = transform.position; |
| _netRot = transform.rotation; |
| }} |
| else |
| {{ |
| transform.position = Vector3.Lerp(transform.position, _netPos, _interpSpeed * Time.deltaTime); |
| transform.rotation = Quaternion.Slerp(transform.rotation, _netRot, _interpSpeed * Time.deltaTime); |
| }} |
| }} |
| |
| public void ReceiveSnapshot(Vector3 pos, Quaternion rot) |
| {{ |
| if (IsLocal) return; |
| _netPos = pos; |
| _netRot = rot; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class SetupUnityProjectTool(BaseUnityTool): |
| """Generate a complete Unity project structure scaffold.""" |
|
|
| name = "setup_unity_project" |
| description = ( |
| "Generate a complete Unity project structure: folders, assembly " |
| "definitions, manifest.json and ProjectSettings entries." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "project_name": {"type": "string", "default": "OpenWorldGame"}, |
| "unity_version": {"type": "string", "default": "2022.3.20f1"}, |
| }, |
| } |
|
|
| def run(self, project_name: str = "OpenWorldGame", unity_version: str = "2022.3.20f1", **_: Any) -> str: |
| manifest = self._wrap(f''' |
| {{ |
| "dependencies": {{ |
| "com.unity.ai.navigation": "1.1.5", |
| "com.unity.cinemachine": "2.9.7", |
| "com.unity.inputsystem": "1.7.0", |
| "com.unity.postprocessing": "3.2.2", |
| "com.unity.textmeshpro": "3.0.6", |
| "com.unity.ugui": "1.0.0", |
| "com.unity.modules.audio": "1.0.0", |
| "com.unity.modules.imageconversion": "1.0.0", |
| "com.unity.modules.particlesystem": "1.0.0", |
| "com.unity.modules.physics": "1.0.0", |
| "com.unity.modules.terrain": "1.0.0", |
| "com.unity.modules.ui": "1.0.0", |
| "com.unity.ugui": "1.0.0" |
| }} |
| }} |
| ''') |
| asmdef = self._wrap(f''' |
| {{ |
| "name": "OpenWorld", |
| "rootNamespace": "OpenWorld", |
| "references": [], |
| "includePlatforms": [], |
| "excludePlatforms": [], |
| "allowUnsafeCode": false, |
| "overrideReferences": false, |
| "precompiledReferences": [], |
| "autoReferenced": true, |
| "defineConstraints": [], |
| "versionDefines": [], |
| "noEngineReferences": false |
| }} |
| ''') |
| structure = self._wrap(f''' |
| # {project_name} |
| # Unity {unity_version} |
| |
| Directory layout: |
| Assets/ |
| OpenWorld/ |
| Scripts/ |
| Player/ |
| Vehicles/ |
| World/ |
| AI/ |
| Combat/ |
| FX/ |
| UI/ |
| Audio/ |
| Persistence/ |
| Economy/ |
| Missions/ |
| Net/ |
| Prefabs/ |
| Materials/ |
| Models/ |
| Textures/ |
| Audio/ |
| Scenes/ |
| ScriptableObjects/ |
| Settings/ |
| URP/ (or HDRP) |
| ProjectSettings/ |
| Packages/ |
| manifest.json |
| |
| Runtime requirements: |
| - Unity {unity_version} |
| - URP or HDRP |
| - TextMeshPro imported |
| - Input System (both old and new enabled) |
| ''' |
| ) |
| return f"// === manifest.json ===\n{manifest}\n// === OpenWorld.asmdef ===\n{asmdef}\n{structure}" |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class WriteCsharpScriptTool(BaseUnityTool): |
| """Generate a custom C# script from a template and parameters.""" |
|
|
| name = "write_csharp_script" |
| description = ( |
| "Generate a custom C# MonoBehaviour/ScriptableObject from a " |
| "template, namespace and field list." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string"}, |
| "namespace": {"type": "string", "default": "OpenWorld.Custom"}, |
| "base_class": {"type": "string", "default": "MonoBehaviour"}, |
| "fields": {"type": "array", "items": {"type": "string"}}, |
| }, |
| } |
|
|
| def run( |
| self, |
| class_name: str = "CustomScript", |
| namespace: str = "OpenWorld.Custom", |
| base_class: str = "MonoBehaviour", |
| fields: Optional[List[str]] = None, |
| **_: Any, |
| ) -> str: |
| fields = fields or [] |
| field_lines = [] |
| for f in fields: |
| parts = f.split() |
| if len(parts) >= 2: |
| ftype, fname = parts[0], parts[1] |
| field_lines.append(f" [SerializeField] private {ftype} {fname};") |
| field_block = "\n".join(field_lines) if field_lines else " // (no fields)" |
| return self._wrap(f''' |
| using UnityEngine; |
| |
| namespace {namespace} |
| {{ |
| /// <summary> |
| /// Auto-generated {class_name}. Extend the lifecycle methods as needed. |
| /// </summary> |
| public class {class_name} : {base_class} |
| {{ |
| {field_block} |
| |
| private void Awake() |
| {{ |
| // Cache components here. |
| }} |
| |
| private void Start() |
| {{ |
| // Initialization after Awake. |
| }} |
| |
| private void Update() |
| {{ |
| // Per-frame logic. |
| }} |
| |
| private void FixedUpdate() |
| {{ |
| // Physics-step logic. |
| }} |
| |
| private void LateUpdate() |
| {{ |
| // Post-update (camera, follow). |
| }} |
| |
| private void OnEnable() |
| {{ |
| // Subscribe to events. |
| }} |
| |
| private void OnDisable() |
| {{ |
| // Unsubscribe from events. |
| }} |
| |
| private void OnDestroy() |
| {{ |
| // Cleanup resources. |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class WriteSceneFileTool(BaseUnityTool): |
| """Generate a minimal Unity scene (.unity) YAML file.""" |
|
|
| name = "write_scene_file" |
| description = "Generate a minimal Unity scene YAML file with a Camera and directional light." |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "scene_name": {"type": "string", "default": "MainScene"}, |
| }, |
| } |
|
|
| def run(self, scene_name: str = "MainScene", **_: Any) -> str: |
| return self._wrap(f'''%YAML 1.1 |
| %TAG !u! tag:unity3d.com,2011: |
| --- !u!29 &1 |
| OcclusionCullingSettings: |
| m_ObjectHideFlags: 0 |
| serializedVersion: 2 |
| m_OcclusionBakeSettings: |
| smallestOccluder: 5 |
| smallestHole: 0.25 |
| backfaceThreshold: 100 |
| m_SceneGUID: 00000000000000000000000000000000 |
| m_OcclusionCullingData: {{fileID: 0}} |
| --- !u!104 &2 |
| RenderSettings: |
| m_ObjectHideFlags: 0 |
| serializedVersion: 9 |
| m_Fog: 0 |
| m_FogColor: {{r: 0.5, g: 0.5, b: 0.5, a: 1}} |
| m_FogMode: 3 |
| m_FogDensity: 0.01 |
| m_LinearFogStart: 0 |
| m_LinearFogEnd: 300 |
| m_AmbientSkyColor: {{r: 0.2, g: 0.2, b: 0.2, a: 1}} |
| m_AmbientGroundColor: {{r: 0.2, g: 0.2, b: 0.2, a: 1}} |
| m_SkyboxMaterial: {{fileID: 0}} |
| m_HaloStrength: 0.5 |
| m_FlareStrength: 1 |
| m_FlareFadeSpeed: 3 |
| m_HaloTexture: {{fileID: 0}} |
| m_SpotCookie: {{fileID: 0}} |
| m_DefaultReflectionMode: 0 |
| m_DefaultReflectionResolution: 128 |
| m_ReflectionBounces: 1 |
| m_ReflectionIntensity: 1 |
| m_CustomReflection: {{fileID: 0}} |
| m_Sun: {{fileID: 0}} |
| m_IndirectSpecularColor: {{r: 0, g: 0, b: 0, a: 1}} |
| --- !u!157 &3 |
| LightmapSettings: |
| m_ObjectHideFlags: 0 |
| serializedVersion: 11 |
| m_GIWorkflowMode: 0 |
| m_GISettings: |
| serializedVersion: 2 |
| m_BounceScale: 1 |
| m_IndirectOutputScale: 1 |
| m_AlbedoBoost: 1 |
| m_TemporalCoherenceThreshold: 1 |
| m_EnvironmentLightingMode: 0 |
| m_EnableBakedLightmaps: 1 |
| m_EnableRealtimeLightmaps: 1 |
| m_LightmapEditorSettings: |
| serializedVersion: 9 |
| m_Resolution: 2 |
| m_BakeResolution: 40 |
| m_TextureWidth: 1024 |
| m_TextureHeight: 1024 |
| m_AO: 0 |
| m_AOMaxDistance: 1 |
| m_CompAOExponent: 1 |
| m_CompAOExponentDirect: 0 |
| m_Padding: 2 |
| m_LightmapParameters: {{fileID: 0}} |
| m_LightmapsBakeMode: 1 |
| m_TextureCompression: 1 |
| m_FinalGather: 0 |
| m_FinalGatherFiltering: 1 |
| m_FinalGatherRayCount: 256 |
| m_ReflectionCompression: 2 |
| m_LightingDataAsset: {{fileID: 0}} |
| m_UseShadowmask: 1 |
| --- !u!196 &4 |
| NavMeshSettings: |
| serializedVersion: 2 |
| m_ObjectHideFlags: 0 |
| m_BuildSettings: |
| serializedVersion: 2 |
| agentTypeID: 0 |
| agentRadius: 0.5 |
| agentHeight: 2 |
| agentSlope: 45 |
| agentClimb: 0.4 |
| ledgeDropHeight: 0 |
| maxJumpAcrossDistance: 0 |
| minRegionArea: 2 |
| manualCellSize: 0 |
| cellSize: 0.16666667 |
| manualTileSize: 0 |
| tileSize: 256 |
| accuratePlacement: 0 |
| debug: |
| m_Flags: 0 |
| m_NavMeshData: {{fileID: 0}} |
| --- !u!1 &100000 |
| GameObject: |
| m_ObjectHideFlags: 0 |
| m_Name: MainCamera |
| m_IsActive: 1 |
| --- !u!20 &100001 |
| Camera: |
| m_ObjectHideFlags: 0 |
| m_ClearFlags: 1 |
| m_BackGroundColor: {{r: 0.192, g: 0.302, b: 0.474, a: 0.019}} |
| m_FieldOfView: 60 |
| m_NearClipPlane: 0.3 |
| m_FarClipPlane: 1000 |
| --- !u!1 &200000 |
| GameObject: |
| m_ObjectHideFlags: 0 |
| m_Name: DirectionalLight |
| m_IsActive: 1 |
| --- !u!108 &200001 |
| Light: |
| m_ObjectHideFlags: 0 |
| m_Type: 1 |
| m_Color: {{r: 1, g: 0.956, b: 0.839, a: 1}} |
| m_Intensity: 1.2 |
| m_Shadows: |
| m_Type: 2 |
| ''') + f"\n// Scene: {scene_name}\n" |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class GenerateCompleteGameTool(BaseUnityTool): |
| """Generate a complete game by orchestrating every tool in the registry.""" |
|
|
| name = "generate_complete_game" |
| description = ( |
| "Run every registered tool and assemble a complete Unity project " |
| "in the given output directory." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "output_dir": {"type": "string", "default": "./OpenWorldGame"}, |
| "project_name": {"type": "string", "default": "OpenWorldGame"}, |
| "include_multiplayer": {"type": "boolean", "default": False}, |
| }, |
| } |
|
|
| def run( |
| self, |
| output_dir: str = "./OpenWorldGame", |
| project_name: str = "OpenWorldGame", |
| include_multiplayer: bool = False, |
| **_: Any, |
| ) -> str: |
| |
| manifest = [f"# Generated game: {project_name}", f"# Output: {output_dir}", ""] |
| skip = set() |
| if not include_multiplayer: |
| skip.add("create_multiplayer_netcode") |
| for tool in TOOL_REGISTRY: |
| if tool.name in skip or tool.name == self.name: |
| continue |
| try: |
| code = tool.run() |
| rel = f"Assets/OpenWorld/Scripts/{tool.name}.cs" |
| manifest.append(f"- {rel} ({len(code.splitlines())} lines)") |
| except Exception as exc: |
| manifest.append(f"- ERROR in {tool.name}: {exc}") |
| manifest.append("") |
| manifest.append(f"Total tools invoked: {sum(1 for t in TOOL_REGISTRY if t.name not in skip and t.name != self.name)}") |
| return "\n".join(manifest) + "\n" |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateLootSystemTool(BaseUnityTool): |
| """Generate a loot system with rarity tiers and containers.""" |
|
|
| name = "create_loot_system" |
| description = ( |
| "Generate a loot system with random loot drops, rarity tiers and " |
| "containers." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "LootSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "LootSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Items |
| {{ |
| /// <summary> |
| /// Loot table & container system. Each table maps item IDs to weighted |
| /// drop chances per rarity tier. Containers (chests, corpses) roll the |
| /// table to populate contents on first open. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| public enum Rarity {{ Common, Uncommon, Rare, Epic, Legendary }} |
| |
| [Serializable] |
| public class LootEntry |
| {{ |
| public string itemId; |
| public Rarity rarity; |
| public int minStack = 1; |
| public int maxStack = 1; |
| public float weight = 1f; |
| }} |
| |
| [Serializable] |
| public class LootTable |
| {{ |
| public string id; |
| public List<LootEntry> entries = new List<LootEntry>(); |
| public int minDrops = 1; |
| public int maxDrops = 3; |
| [NonSerialized] public float totalWeight; |
| }} |
| |
| [Header("Color By Rarity")] |
| [SerializeField] private Color commonColor = Color.gray; |
| [SerializeField] private Color uncommonColor = Color.green; |
| [SerializeField] private Color rareColor = Color.cyan; |
| [SerializeField] private Color epicColor = new Color(0.6f, 0.2f, 0.9f); |
| [SerializeField] private Color legendaryColor = new Color(1f, 0.6f, 0.1f); |
| |
| [Header("Tables")] |
| [SerializeField] private List<LootTable> tables = new List<LootTable>(); |
| |
| private readonly Dictionary<string, List<LootEntry>> _generated = new Dictionary<string, List<LootEntry>>(); |
| |
| private void Awake() {{ Instance = this; }} |
| |
| public List<LootEntry> Roll(string tableId) |
| {{ |
| LootTable table = tables.Find(t => t.id == tableId); |
| if (table == null) return new List<LootEntry>(); |
| RecomputeWeights(table); |
| int count = UnityEngine.Random.Range(table.minDrops, table.maxDrops + 1); |
| List<LootEntry> result = new List<LootEntry>(); |
| for (int i = 0; i < count; i++) |
| {{ |
| float r = UnityEngine.Random.value * table.totalWeight; |
| float acc = 0f; |
| foreach (var e in table.entries) |
| {{ |
| acc += e.weight; |
| if (r <= acc) |
| {{ |
| result.Add(new LootEntry |
| {{ |
| itemId = e.itemId, |
| rarity = e.rarity, |
| minStack = UnityEngine.Random.Range(e.minStack, e.maxStack + 1), |
| maxStack = e.maxStack, |
| weight = e.weight |
| }}); |
| break; |
| }} |
| }} |
| }} |
| return result; |
| }} |
| |
| public Color RarityColor(Rarity r) |
| {{ |
| return r switch |
| {{ |
| Rarity.Common => commonColor, |
| Rarity.Uncommon => uncommonColor, |
| Rarity.Rare => rareColor, |
| Rarity.Epic => epicColor, |
| Rarity.Legendary => legendaryColor, |
| _ => Color.white, |
| }}; |
| }} |
| |
| private void RecomputeWeights(LootTable table) |
| {{ |
| table.totalWeight = 0f; |
| foreach (var e in table.entries) table.totalWeight += e.weight; |
| }} |
| }} |
| |
| public class LootContainer : MonoBehaviour |
| {{ |
| [SerializeField] private string tableId; |
| [SerializeField] private bool opened; |
| [SerializeField] private GameObject lid; // animates open |
| |
| public event Action<List<CreateLootSystemTool.LootEntry>> OnOpened; |
| |
| public void Interact() |
| {{ |
| if (opened) return; |
| opened = true; |
| if (lid != null) lid.transform.localRotation = Quaternion.Euler(-90f, 0f, 0f); |
| var loot = LootSystem.Instance?.Roll(tableId) ?? new List<CreateLootSystemTool.LootEntry>(); |
| OnOpened?.Invoke(loot); |
| }} |
| }} |
| }}''').replace("CreateLootSystemTool.LootEntry", "LootSystem.LootEntry") |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateCraftingSystemTool(BaseUnityTool): |
| """Generate a crafting system that combines items into new ones.""" |
|
|
| name = "create_crafting_system" |
| description = "Generate a crafting system that combines items into new ones." |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "CraftingSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "CraftingSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Items |
| {{ |
| /// <summary> |
| /// Recipe-driven crafting. Each recipe lists ingredient items/counts |
| /// and a resulting item. The Craft method consumes ingredients from an |
| /// IInventoryProvider and produces the output. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Serializable] |
| public class Ingredient |
| {{ |
| public string itemId; |
| public int count = 1; |
| }} |
| |
| [Serializable] |
| public class Recipe |
| {{ |
| public string id; |
| public string resultItemId; |
| public int resultCount = 1; |
| public List<Ingredient> ingredients = new List<Ingredient>(); |
| public float craftTime = 2f; |
| public bool requiresWorkbench; |
| }} |
| |
| public interface IInventoryProvider |
| {{ |
| int Count(string itemId); |
| void Remove(string itemId, int count); |
| void Add(string itemId, int count); |
| }} |
| |
| [Header("Recipes")] |
| [SerializeField] private List<Recipe> recipes = new List<Recipe>(); |
| |
| public IReadOnlyList<Recipe> Recipes => recipes; |
| |
| public bool CanCraft(Recipe recipe, IInventoryProvider inv) |
| {{ |
| if (recipe == null || inv == null) return false; |
| foreach (var ing in recipe.ingredients) |
| if (inv.Count(ing.itemId) < ing.count) return false; |
| return true; |
| }} |
| |
| public bool Craft(Recipe recipe, IInventoryProvider inv) |
| {{ |
| if (!CanCraft(recipe, inv)) return false; |
| foreach (var ing in recipe.ingredients) inv.Remove(ing.itemId, ing.count); |
| inv.Add(recipe.resultItemId, recipe.resultCount); |
| return true; |
| }} |
| |
| public Recipe FindByResult(string itemId) |
| => recipes.Find(r => r.resultItemId == itemId); |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateSkillTreeTool(BaseUnityTool): |
| """Generate a skill tree with XP, levels and unlockable abilities.""" |
|
|
| name = "create_skill_tree" |
| description = ( |
| "Generate a skill tree with XP, levels and unlockable abilities." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "SkillTree"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "SkillTree", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Skills |
| {{ |
| /// <summary> |
| /// Player progression. Awards XP from gameplay; levels up at thresholds |
| /// granting skill points. Skill points unlock nodes in a tree; each |
| /// node may require prerequisites and apply stat modifiers (events). |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Serializable] |
| public class SkillNode |
| {{ |
| public string id; |
| public string displayName; |
| [TextArea] public string description; |
| public int cost = 1; |
| public string[] prerequisites; |
| public float statModifier = 0.1f; |
| public string statKey; // e.g. "max_health", "move_speed" |
| [NonSerialized] public bool unlocked; |
| }} |
| |
| [Serializable] |
| public class SkillBranch |
| {{ |
| public string id; |
| public string name; |
| public List<SkillNode> nodes = new List<SkillNode>(); |
| }} |
| |
| [Header("Progression")] |
| [SerializeField] private int level = 1; |
| [SerializeField] private int xp; |
| [SerializeField] private int xpToNext = 100; |
| [SerializeField] private float xpGrowth = 1.5f; |
| [SerializeField] private int skillPoints; |
| |
| [Header("Tree")] |
| [SerializeField] private List<SkillBranch> branches = new List<SkillBranch>(); |
| [SerializeField] private Dictionary<string, float> stats = new Dictionary<string, float>(); |
| |
| public int Level => level; |
| public int Xp => xp; |
| public int SkillPoints => skillPoints; |
| public IReadOnlyList<SkillBranch> Branches => branches; |
| |
| public event Action<int> OnLevelUp; |
| public event Action<SkillNode> OnNodeUnlocked; |
| |
| private void Awake() |
| {{ |
| Instance = this; |
| stats["max_health"] = 100f; |
| stats["move_speed"] = 5f; |
| stats["sprint_speed"] = 8f; |
| }} |
| |
| public void AddXp(int amount) |
| {{ |
| xp += amount; |
| while (xp >= xpToNext) |
| {{ |
| xp -= xpToNext; |
| level++; |
| skillPoints++; |
| xpToNext = Mathf.RoundToInt(xpToNext * xpGrowth); |
| OnLevelUp?.Invoke(level); |
| }} |
| }} |
| |
| public bool UnlockNode(string branchId, string nodeId) |
| {{ |
| SkillBranch branch = branches.Find(b => b.id == branchId); |
| if (branch == null) return false; |
| SkillNode node = branch.nodes.Find(n => n.id == nodeId); |
| if (node == null || node.unlocked || skillPoints < node.cost) return false; |
| if (!PrerequisitesMet(node)) return false; |
| node.unlocked = true; |
| skillPoints -= node.cost; |
| if (!string.IsNullOrEmpty(node.statKey)) |
| stats[node.statKey] += stats.GetValueOrDefault(node.statKey) * node.statModifier; |
| OnNodeUnlocked?.Invoke(node); |
| return true; |
| }} |
| |
| private bool PrerequisitesMet(SkillNode node) |
| {{ |
| if (node.prerequisites == null) return true; |
| foreach (var prereq in node.prerequisites) |
| {{ |
| bool found = false; |
| foreach (var branch in branches) |
| foreach (var n in branch.nodes) |
| if (n.id == prereq && n.unlocked) {{ found = true; break; }} |
| if (!found) return false; |
| }} |
| return true; |
| }} |
| |
| public float GetStat(string key) => stats.GetValueOrDefault(key, 0f); |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateFactionSystemTool(BaseUnityTool): |
| """Generate a faction system with territories and turf wars.""" |
|
|
| name = "create_faction_system" |
| description = ( |
| "Generate a faction system with gang territories, reputation and " |
| "turf wars." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "FactionSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "FactionSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections.Generic; |
| using UnityEngine; |
| |
| namespace OpenWorld.Factions |
| {{ |
| /// <summary> |
| /// Faction & territory manager. Each faction has a reputation with the |
| /// player (-100 hostile..+100 allied) and owns territories (zones on |
| /// the map). Turf wars trigger when an attacking faction's power |
| /// exceeds the defender's within a zone. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public static {class_name} Instance {{ get; private set; }} |
| |
| [Serializable] |
| public class Faction |
| {{ |
| public string id; |
| public string displayName; |
| public Color color = Color.red; |
| public int power = 100; |
| public int reputation; // -100..100 with player |
| public bool hostileToPlayer; |
| }} |
| |
| [Serializable] |
| public class Territory |
| {{ |
| public string id; |
| public string ownerFactionId; |
| public Bounds bounds; |
| public int defenseValue = 50; |
| }} |
| |
| [Header("Factions")] |
| [SerializeField] private List<Faction> factions = new List<Faction>(); |
| [SerializeField] private List<Territory> territories = new List<Territory>(); |
| |
| [Header("Turf War")] |
| [SerializeField] private float warCheckInterval = 60f; |
| [SerializeField] private int attackThreshold = 80; |
| private float _warTimer; |
| |
| public IReadOnlyList<Faction> Factions => factions; |
| public IReadOnlyList<Territory> Territories => territories; |
| |
| public event Action<Faction, int> OnReputationChanged; |
| public event Action<Faction, Faction, Territory> OnTurfWarStarted; |
| |
| private void Awake() {{ Instance = this; }} |
| |
| public Faction GetFaction(string id) => factions.Find(f => f.id == id); |
| |
| public void AdjustReputation(string factionId, int delta) |
| {{ |
| Faction f = GetFaction(factionId); |
| if (f == null) return; |
| f.reputation = Mathf.Clamp(f.reputation + delta, -100, 100); |
| f.hostileToPlayer = f.reputation < -25; |
| OnReputationChanged?.Invoke(f, f.reputation); |
| }} |
| |
| public Territory GetTerritoryAt(Vector3 pos) |
| {{ |
| foreach (var t in territories) if (t.bounds.Contains(pos)) return t; |
| return null; |
| }} |
| |
| private void Update() |
| {{ |
| _warTimer -= Time.deltaTime; |
| if (_warTimer > 0f) return; |
| _warTimer = warCheckInterval; |
| EvaluateTurfWars(); |
| }} |
| |
| private void EvaluateTurfWars() |
| {{ |
| foreach (var territory in territories) |
| {{ |
| Faction owner = GetFaction(territory.ownerFactionId); |
| if (owner == null) continue; |
| foreach (var attacker in factions) |
| {{ |
| if (attacker.id == owner.id) continue; |
| if (attacker.power - territory.defenseValue > attackThreshold) |
| {{ |
| OnTurfWarStarted?.Invoke(attacker, owner, territory); |
| ResolveWar(attacker, owner, territory); |
| break; |
| }} |
| }} |
| }} |
| }} |
| |
| private void ResolveWar(Faction attacker, Faction defender, Territory territory) |
| {{ |
| if (attacker.power > defender.power + territory.defenseValue) |
| {{ |
| territory.ownerFactionId = attacker.id; |
| attacker.power -= 30; |
| defender.power -= 20; |
| }} |
| else |
| {{ |
| attacker.power -= 20; |
| defender.power -= 10; |
| }} |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreateStuntSystemTool(BaseUnityTool): |
| """Generate a stunt system with jumps, flips, points and slow-mo.""" |
|
|
| name = "create_stunt_system" |
| description = ( |
| "Generate a stunt system with jumps, flips, points and slow-mo " |
| "replays." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "StuntSystem"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "StuntSystem", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.Collections; |
| using UnityEngine; |
| |
| namespace OpenWorld.Gameplay |
| {{ |
| /// <summary> |
| /// Detects vehicle stunts (jumps, flips, air-time, near-miss) and |
| /// awards points. On a big stunt triggers a slow-mo window and an |
| /// event for the replay system to capture. |
| /// </summary> |
| [RequireComponent(typeof(Rigidbody))] |
| public class {class_name} : MonoBehaviour |
| {{ |
| [Header("Detection")] |
| [SerializeField] private float airTimeThreshold = 0.5f; |
| [SerializeField] private float flipThreshold = 360f; |
| [SerializeField] private float nearMissDistance = 2.5f; |
| [SerializeField] private float nearMissSpeed = 15f; |
| [SerializeField] private LayerMask groundMask; |
| |
| [Header("Scoring")] |
| [SerializeField] private int airTimePoints = 50; |
| [SerializeField] private int flipPoints = 200; |
| [SerializeField] private int nearMissPoints = 100; |
| [SerializeField] private int bigStuntThreshold = 500; |
| |
| [Header("Slow-Mo")] |
| [SerializeField] private float slowMoScale = 0.3f; |
| [SerializeField] private float slowMoDuration = 1.5f; |
| [SerializeField] private float transitionSpeed = 4f; |
| |
| public int TotalPoints {{ get; private set; }} |
| public int CurrentCombo {{ get; private set; }} |
| |
| private Rigidbody _rb; |
| private bool _airborne; |
| private float _airTime; |
| private float _yawAccumulator; |
| private float _lastYaw; |
| private float _comboTimer; |
| |
| public event Action<int, string> OnStuntScored; // points, label |
| public event Action<int> OnBigStunt; |
| |
| private void Awake() |
| {{ |
| _rb = GetComponent<Rigidbody>(); |
| _lastYaw = transform.eulerAngles.y; |
| }} |
| |
| private void Update() |
| {{ |
| CheckAirTime(); |
| CheckFlips(); |
| CheckNearMiss(); |
| UpdateCombo(); |
| }} |
| |
| private void CheckAirTime() |
| {{ |
| bool grounded = Physics.Raycast(transform.position, -transform.up, 1.2f, groundMask); |
| if (!grounded) |
| {{ |
| _airTime += Time.deltaTime; |
| if (!_airborne && _airTime >= airTimeThreshold) |
| {{ |
| _airborne = true; |
| }} |
| }} |
| else if (_airborne) |
| {{ |
| ScoreStunt(airTimePoints, "Air Time x" + Mathf.RoundToInt(_airTime)); |
| _airTime = 0f; |
| _airborne = false; |
| }} |
| }} |
| |
| private void CheckFlips() |
| {{ |
| float yaw = transform.eulerAngles.y; |
| float delta = Mathf.DeltaAngle(_lastYaw, yaw); |
| _yawAccumulator += Mathf.Abs(delta); |
| _lastYaw = yaw; |
| if (_yawAccumulator >= flipThreshold) |
| {{ |
| ScoreStunt(flipPoints, "Barrel Roll!"); |
| _yawAccumulator = 0f; |
| }} |
| }} |
| |
| private void CheckNearMiss() |
| {{ |
| if (_rb.velocity.magnitude < nearMissSpeed) return; |
| Collider[] hits = Physics.OverlapSphere(transform.position, nearMissDistance); |
| foreach (var h in hits) |
| {{ |
| if (h.transform == transform) continue; |
| if (h.CompareTag("Vehicle")) |
| {{ |
| ScoreStunt(nearMissPoints, "Near Miss"); |
| return; |
| }} |
| }} |
| }} |
| |
| private void ScoreStunt(int points, string label) |
| {{ |
| CurrentCombo++; |
| int total = points * CurrentCombo; |
| TotalPoints += total; |
| _comboTimer = 3f; |
| OnStuntScored?.Invoke(total, label); |
| if (total >= bigStuntThreshold) StartCoroutine(BigStuntSlowMo()); |
| }} |
| |
| private void UpdateCombo() |
| {{ |
| if (_comboTimer <= 0f) {{ CurrentCombo = 0; return; }} |
| _comboTimer -= Time.deltaTime; |
| }} |
| |
| private IEnumerator BigStuntSlowMo() |
| {{ |
| OnBigStunt?.Invoke(TotalPoints); |
| float original = Time.timeScale; |
| Time.timeScale = slowMoScale; |
| yield return new WaitForSecondsRealtime(slowMoDuration); |
| float t = 0f; |
| while (t < transitionSpeed) |
| {{ |
| t += Time.unscaledDeltaTime; |
| Time.timeScale = Mathf.Lerp(slowMoScale, original, t / transitionSpeed); |
| yield return null; |
| }} |
| Time.timeScale = original; |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @register_tool |
| class CreatePhotoModeTool(BaseUnityTool): |
| """Generate a photo mode with free camera, filters and screenshots.""" |
|
|
| name = "create_photo_mode" |
| description = ( |
| "Generate a photo mode with free camera, filters and screenshot " |
| "export." |
| ) |
| input_schema = { |
| "type": "object", |
| "properties": { |
| "class_name": {"type": "string", "default": "PhotoMode"}, |
| }, |
| } |
|
|
| def run(self, class_name: str = "PhotoMode", **_: Any) -> str: |
| return self._wrap(f''' |
| using System; |
| using System.IO; |
| using UnityEngine; |
| using UnityEngine.PostProcessing; |
| |
| namespace OpenWorld.UI |
| {{ |
| /// <summary> |
| /// Photo mode. On activation, detaches the camera into a free-fly |
| /// state, exposes filter presets and captures screenshots to disk via |
| /// ReadPixels. Press F to capture, Esc to exit. |
| /// </summary> |
| public class {class_name} : MonoBehaviour |
| {{ |
| public enum Filter {{ None, Vintage, Noir, Vibrant, Cool, Warm }} |
| |
| [Header("Camera")] |
| [SerializeField] private Camera photoCamera; |
| [SerializeField] private float moveSpeed = 12f; |
| [SerializeField] private float lookSpeed = 2f; |
| [SerializeField] private float rollSpeed = 30f; |
| [SerializeField] private float fovMin = 20f; |
| [SerializeField] private float fovMax = 90f; |
| |
| [Header("Filters")] |
| [SerializeField] private Material[] filterMaterials; |
| |
| [Header("Capture")] |
| [SerializeField] private int captureWidth = 1920; |
| [SerializeField] private int captureHeight = 1080; |
| [SerializeField] private string outputFolder = "Photos"; |
| |
| [Header("UI")] |
| [SerializeField] private GameObject uiRoot; |
| [SerializeField] private MonoBehaviour gameplayController; |
| |
| public bool Active {{ get; private set; }} |
| public Filter CurrentFilter {{ get; private set; }} |
| |
| public event Action<string> OnPhotoSaved; |
| |
| private float _yaw, _pitch, _roll; |
| |
| private void Awake() |
| {{ |
| if (photoCamera == null) photoCamera = Camera.main; |
| if (uiRoot != null) uiRoot.SetActive(false); |
| }} |
| |
| private void Update() |
| {{ |
| if (Input.GetKeyDown(KeyCode.F3)) Toggle(); |
| if (!Active) return; |
| HandleMovement(); |
| HandleLook(); |
| if (Input.GetKeyDown(KeyCode.F)) Capture(); |
| if (Input.GetKeyDown(KeyCode.LeftBracket)) CycleFilter(-1); |
| if (Input.GetKeyDown(KeyCode.RightBracket)) CycleFilter(1); |
| }} |
| |
| public void Toggle() |
| {{ |
| Active = !Active; |
| if (uiRoot != null) uiRoot.SetActive(Active); |
| if (gameplayController != null) gameplayController.enabled = !Active; |
| Cursor.lockState = Active ? CursorLockMode.Locked : CursorLockMode.None; |
| Cursor.visible = !Active; |
| if (Active) |
| {{ |
| _yaw = photoCamera.transform.eulerAngles.y; |
| _pitch = photoCamera.transform.eulerAngles.x; |
| }} |
| }} |
| |
| private void HandleMovement() |
| {{ |
| Vector3 move = new Vector3( |
| Input.GetAxis("Horizontal"), |
| Input.GetKey(KeyCode.Space) ? 1f : Input.GetKey(KeyCode.LeftControl) ? -1f : 0f, |
| Input.GetAxis("Vertical")); |
| float speed = Input.GetKey(KeyCode.LeftShift) ? moveSpeed * 2f : moveSpeed; |
| photoCamera.transform.Translate(move * speed * Time.unscaledDeltaTime, Space.Self); |
| |
| float fov = photoCamera.fieldOfView - Input.GetAxis("Mouse ScrollWheel") * 20f; |
| photoCamera.fieldOfView = Mathf.Clamp(fov, fovMin, fovMax); |
| }} |
| |
| private void HandleLook() |
| {{ |
| _yaw += Input.GetAxis("Mouse X") * lookSpeed; |
| _pitch -= Input.GetAxis("Mouse Y") * lookSpeed; |
| if (Input.GetKey(KeyCode.Q)) _roll += rollSpeed * Time.unscaledDeltaTime; |
| if (Input.GetKey(KeyCode.E)) _roll -= rollSpeed * Time.unscaledDeltaTime; |
| photoCamera.transform.rotation = Quaternion.Euler(_pitch, _yaw, _roll); |
| }} |
| |
| private void CycleFilter(int dir) |
| {{ |
| int count = Enum.GetValues(typeof(Filter)).Length; |
| CurrentFilter = (Filter)(((int)CurrentFilter + dir + count) % count); |
| ApplyFilter(); |
| }} |
| |
| private void ApplyFilter() |
| {{ |
| if (filterMaterials == null || filterMaterials.Length == 0) return; |
| int idx = (int)CurrentFilter; |
| // Apply via a full-screen blit — hook into OnRenderImage. |
| }} |
| |
| private void OnRenderImage(RenderTexture src, RenderTexture dst) |
| {{ |
| if (filterMaterials != null && (int)CurrentFilter < filterMaterials.Length && filterMaterials[(int)CurrentFilter] != null) |
| Graphics.Blit(src, dst, filterMaterials[(int)CurrentFilter]); |
| else |
| Graphics.Blit(src, dst); |
| }} |
| |
| public void Capture() |
| {{ |
| StartCoroutine(CaptureRoutine()); |
| }} |
| |
| private System.Collections.IEnumerator CaptureRoutine() |
| {{ |
| yield return new WaitForEndOfFrame(); |
| RenderTexture rt = new RenderTexture(captureWidth, captureHeight, 24); |
| photoCamera.targetTexture = rt; |
| photoCamera.Render(); |
| RenderTexture.active = rt; |
| Texture2D tex = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false); |
| tex.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); |
| tex.Apply(); |
| photoCamera.targetTexture = null; |
| RenderTexture.active = null; |
| Destroy(rt); |
| string dir = Path.Combine(Application.persistentDataPath, outputFolder); |
| Directory.CreateDirectory(dir); |
| string path = Path.Combine(dir, "photo_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".png"); |
| File.WriteAllBytes(path, tex.EncodeToPNG()); |
| Destroy(tex); |
| OnPhotoSaved?.Invoke(path); |
| }} |
| }} |
| }} |
| ''') |
|
|
|
|
| |
| |
| |
|
|
|
|
| __all__ = [ |
| "BaseUnityTool", |
| "TOOL_REGISTRY", |
| "register_tool", |
| "get_tool", |
| "list_tools", |
| |
| "CreatePlayerControllerTool", |
| "CreateVehicleControllerTool", |
| "CreateProceduralCityTool", |
| "CreateThirdPersonCameraTool", |
| "CreateFpsControllerTool", |
| "CreateDayNightCycleTool", |
| "CreateAiNpcTool", |
| "CreatePickupSystemTool", |
| "CreateHealthSystemTool", |
| "CreateWeaponSystemTool", |
| "CreateAudioManagerTool", |
| "CreateUiManagerTool", |
| "CreateTerrainGeneratorTool", |
| "CreateWaterShaderTool", |
| "CreateParticleEffectsTool", |
| "CreateSaveSystemTool", |
| "CreateInventorySystemTool", |
| "CreateQuestSystemTool", |
| "CreateBuildingGeneratorTool", |
| "CreateRoadNetworkTool", |
| "CreateWantedSystemTool", |
| "CreateTrafficSystemTool", |
| "CreatePedestrianSystemTool", |
| "CreateEconomySystemTool", |
| "CreatePhoneSystemTool", |
| "CreateRadioSystemTool", |
| "CreateWeaponWheelTool", |
| "CreateMinimapSystemTool", |
| "CreateExplosionSystemTool", |
| "CreateRagdollSystemTool", |
| "CreateClothPhysicsTool", |
| "CreateWeatherSystemTool", |
| "CreateCctvSystemTool", |
| "CreateDialogueSystemTool", |
| "CreateMissionSystemTool", |
| "CreateGarageSystemTool", |
| "CreatePropertySystemTool", |
| "CreateCharacterCustomizationTool", |
| "CreateCutsceneSystemTool", |
| "CreateMultiplayerNetcodeTool", |
| "SetupUnityProjectTool", |
| "WriteCsharpScriptTool", |
| "WriteSceneFileTool", |
| "GenerateCompleteGameTool", |
| "CreateLootSystemTool", |
| "CreateCraftingSystemTool", |
| "CreateSkillTreeTool", |
| "CreateFactionSystemTool", |
| "CreateStuntSystemTool", |
| "CreatePhotoModeTool", |
| ] |
|
|
|
|
| if __name__ == "__main__": |
| |
| print(f"Registered tools: {len(TOOL_REGISTRY)}") |
| total = 0 |
| for t in TOOL_REGISTRY: |
| try: |
| code = t.run() |
| lines = code.count("\n") + 1 |
| total += lines |
| print(f" {t.name:32s} -> {lines:5d} lines") |
| except Exception as exc: |
| print(f" {t.name:32s} -> ERROR: {exc}") |
| print(f"Total generated C# lines: {total}") |
|
|