Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
# Home Protector agent contract
# Home Protector OpenAI Game Builders contract

This repository is a Unity 2022.3.60f1 project completed from the existing `isometric scene` gameplay. Keep changes incremental and preserve working legacy systems while moving ownership to `Assets/_Project`.
This worktree is the WebGL-first OpenAI Game Builders submission lane for the Unity 2022.3.60f1 project. Keep changes incremental and preserve the verified `isometric scene` gameplay while adapting it for browser play.

## Required boundaries

- `HomeProtector.Core.GameSession` is the only source of Preparation, Combat, and Result state.
- Keep `WaveSystem`, `EnemySpawner`, `TowerSpawner`, `TargetManager`, and `ResourceManager` as the combat engine; adapt them through bridges instead of rewriting them.
- Keep CommonSoldier and Monkey. Treat the old PostBox prefab as the refrigerator placeholder and preserve its draggable, target, and resource behavior while migrating it.
- Keep microphone activation and a keyboard fallback on the same activation path.
- Do not add WebGL, GitHub Pages, NAN submission automation, or an in-game AI director.
- Add WebGL and browser compatibility only on `codex/openaigame2026`; keep platform-neutral fixes easy to cherry-pick.
- Do not add NAN submission automation or an in-game AI director.

## Contest delivery

- The required deliverable is a public browser link that starts without installation, approval, or login.
- Browser completion must never depend on microphone permission. Keyboard fallback uses the same player-activation path.
- Request microphone permission only after an explicit user gesture and only on HTTPS. Denial, timeout, or missing devices must fall back cleanly.
- Use this contest worktree as the only Unity writer while WebGL work is active. Treat the original Windows worktree as read-only for contest changes.
- Write local builds only to ignored `Builds/OpenAIGame2026-WebGL`. Never commit generated WebGL output to the source branch.
- If deployment needs a generated-output branch or hosting project, keep it separate from source history.
- Record the pre-challenge baseline and every challenge-period feature in `Docs/Submission/OpenAIGame2026`.
- Internal submission freeze is 2026-08-26 18:00 KST; prioritize a completable hosted build over additional content.

## Unity ownership

Expand All @@ -28,6 +40,7 @@ This repository is a Unity 2022.3.60f1 project completed from the existing `isom

- Let Unity own compile, serialization, reference, EditMode, PlayMode, and build verification.
- Use `Tools/Unity/Invoke-HomeProtectorUnity.ps1` for concise summaries. Read full Unity logs only after a failure.
- WebGL readiness requires a Unity WebGL build plus an HTTPS browser smoke test of Loading to final result, including keyboard fallback.
- Use `Tools/Git/Validate-UnityRepo.ps1` for repository hygiene; do not duplicate Unity semantic checks in shell scripts or skills.
- A missing Unity license is an explicit blocked check, never a passing result.

Expand Down
8 changes: 8 additions & 0 deletions Assets/_Project/Scripts/Editor/WebGLBuildPipeline.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System;
using System.IO;
using System.Linq;
using HomeProtector.Editor.AssetPipeline;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;

namespace HomeProtector.Editor.Build
{
public static class OpenAIGame2026WebGLBuild
{
private const string LoadingScenePath = "Assets/Scenes/StartGame_Loading.unity";
private const string PlayableScenePath = "Assets/Scenes/isometric scene.unity";
private const string OutputFolderName = "OpenAIGame2026-WebGL";
private const string OutputRelativePath = "Builds/" + OutputFolderName;

[MenuItem("Home Protector/OpenAI Game Builders/Build WebGL")]
public static void Build()
{
HomeProtectorAutomation.ValidateProject();

if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.WebGL, BuildTarget.WebGL))
{
throw new BuildFailedException("Unity WebGL build support is not installed.");
}

string[] scenes = EditorBuildSettings.scenes
.Where(scene => scene.enabled)
.Select(scene => scene.path)
.ToArray();
ValidateScenes(scenes);

string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
string outputDirectory = GetSafeOutputDirectory(projectRoot);
ConfigurePlayerSettings();
RecreateOutputDirectory(outputDirectory);

BuildPlayerOptions options = new()
{
scenes = scenes,
locationPathName = outputDirectory,
target = BuildTarget.WebGL,
options = BuildOptions.StrictMode,
};

BuildReport report = BuildPipeline.BuildPlayer(options);
if (report.summary.result != BuildResult.Succeeded)
{
throw new BuildFailedException(
$"WebGL build failed with {report.summary.totalErrors} error(s). " +
$"See the Unity build log for details.");
}

string indexPath = Path.Combine(outputDirectory, "index.html");
if (!File.Exists(indexPath))
{
throw new BuildFailedException($"WebGL build succeeded without '{indexPath}'.");
}

Debug.Log(
$"HOME_PROTECTOR_WEBGL_BUILD_OK output={outputDirectory} " +
$"scenes={scenes.Length} bytes={report.summary.totalSize}");
}

private static void ValidateScenes(string[] scenes)
{
if (scenes.Length < 2 || scenes[0] != LoadingScenePath)
{
throw new BuildFailedException(
$"WebGL build requires '{LoadingScenePath}' as the first enabled scene.");
}

if (!scenes.Contains(PlayableScenePath, StringComparer.Ordinal))
{
throw new BuildFailedException(
$"WebGL build requires enabled playable scene '{PlayableScenePath}'.");
}
}

private static string GetSafeOutputDirectory(string projectRoot)
{
string buildsRoot = Path.GetFullPath(Path.Combine(projectRoot, "Builds"));
string outputDirectory = Path.GetFullPath(Path.Combine(projectRoot, OutputRelativePath));
string requiredPrefix = buildsRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ Path.DirectorySeparatorChar;

if (!outputDirectory.StartsWith(requiredPrefix, StringComparison.OrdinalIgnoreCase) ||
!string.Equals(
Path.GetFileName(outputDirectory),
OutputFolderName,
StringComparison.Ordinal))
{
throw new BuildFailedException($"Unsafe WebGL output path '{outputDirectory}'.");
}

return outputDirectory;
}

private static void ConfigurePlayerSettings()
{
PlayerSettings.defaultScreenWidth = 1280;
PlayerSettings.defaultScreenHeight = 720;
PlayerSettings.defaultWebScreenWidth = 1280;
PlayerSettings.defaultWebScreenHeight = 720;
PlayerSettings.runInBackground = false;
PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Disabled;
PlayerSettings.WebGL.decompressionFallback = false;
PlayerSettings.WebGL.dataCaching = true;
PlayerSettings.WebGL.nameFilesAsHashes = true;
AssetDatabase.SaveAssets();
}

private static void RecreateOutputDirectory(string outputDirectory)
{
string buildsRoot = Directory.GetParent(outputDirectory)?.FullName
?? throw new BuildFailedException(
$"WebGL output has no parent directory: '{outputDirectory}'.");
ThrowIfReparsePoint(buildsRoot);

if (Directory.Exists(outputDirectory))
{
ThrowIfTreeContainsReparsePoint(outputDirectory);
Directory.Delete(outputDirectory, true);
}

Directory.CreateDirectory(outputDirectory);
}

private static void ThrowIfTreeContainsReparsePoint(string directory)
{
ThrowIfReparsePoint(directory);

foreach (string entry in Directory.EnumerateFileSystemEntries(
directory,
"*",
SearchOption.TopDirectoryOnly))
{
ThrowIfReparsePoint(entry);

if (Directory.Exists(entry))
{
ThrowIfTreeContainsReparsePoint(entry);
}
}
}

private static void ThrowIfReparsePoint(string path)
{
if (!File.Exists(path) && !Directory.Exists(path))
{
return;
}

FileAttributes attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.ReparsePoint) != 0)
{
throw new BuildFailedException(
$"Refusing to clean WebGL output through reparse point '{path}'.");
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 52 additions & 4 deletions Assets/_Project/Scripts/Legacy/MicrophoneSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class MicrophoneSystem : MonoBehaviour
[SerializeField] private bool playerActivationEnabled = true; // �÷��̾� Ȱ��ȭ ���� ����
[SerializeField] private bool oneTimeUseOnly = true; // �� ���̺�� �� ���� ��� ���� ����
[SerializeField] private float playerActiveTime = 50f; // �÷��̾� Ȱ��ȭ ���� �ð� (���� ��忡��)
[SerializeField] private bool keyboardFallbackEnabled = true;
[SerializeField] private KeyCode keyboardActivationKey = KeyCode.F;
private bool hasActivatedPlayer = false; // �̹� �÷��̾ Ȱ��ȭ�ߴ��� ����
private Vector3 defaultPlayerPosition = Vector3.zero; // �⺻ �÷��̾� ��ġ (0,0,0)

Expand Down Expand Up @@ -211,9 +213,15 @@ private void Update()
}
else if (timeSystem != null && timeSystem.CurrentTime == TimeOfDay.Evening)
{
if (keyboardFallbackEnabled && Input.GetKeyDown(keyboardActivationKey))
{
RequestPlayerActivation();
}

#if !UNITY_WEBGL || UNITY_EDITOR
// ���� ��忡���� ����ũ ����
// ����ũ ���� Ȯ�� �� ��ġ ��� ��ȯ
if (micClip != null && micName != null && Microphone.IsRecording(micName))
if (!isPlacementMode && micClip != null && micName != null && Microphone.IsRecording(micName))
{
float volume = GetMaxVolume();
scaledVolume = ScaleVolume(volume);
Expand All @@ -231,10 +239,10 @@ private void Update()
if (scaledVolume >= currentActivationThreshold)
{
if (verbose) Debug.Log($"����({scaledVolume})�� �Ӱ谪({currentActivationThreshold})�� �Ѿ� ��ġ ��� ����");
EnterPlacementMode();
RequestPlayerActivation();
}
}
else
else if (!isPlacementMode)
{
// ����ũ�� ���� ���� �ƴ� ���
if (verbose && Time.frameCount % 300 == 0)
Expand All @@ -243,6 +251,7 @@ private void Update()
StartCoroutine(InitializeAndTestMicrophone());
}
}
#endif
}

// ����� ���� ������Ʈ
Expand All @@ -251,12 +260,14 @@ private void Update()

private void OnDisable()
{
#if !UNITY_WEBGL || UNITY_EDITOR
// ����ũ ����
if (micName != null && Microphone.IsRecording(micName))
{
Microphone.End(micName);
if (verbose) Debug.Log("����ũ ���� ������");
}
#endif

// ������ �Ͻ������� ���·� ������� �ʵ��� ��
if (wasGamePaused)
Expand Down Expand Up @@ -285,6 +296,14 @@ private void OnDestroy()
#region Initialization Methods
private IEnumerator InitializeAndTestMicrophone()
{
#if UNITY_WEBGL && !UNITY_EDITOR
if (verbose)
{
Debug.Log("WebGL microphone input is unavailable. Use the keyboard fallback.");
}

yield break;
#else
// �������� �ʱ�ȭ�� ���� ª�� ���
yield return new WaitForSeconds(0.2f);

Expand Down Expand Up @@ -334,6 +353,7 @@ private IEnumerator InitializeAndTestMicrophone()
{
Debug.LogError("��� ������ ����ũ�� �����ϴ�!");
}
#endif
}

private void InitializeFatigueCurve()
Expand Down Expand Up @@ -399,6 +419,9 @@ private Sprite GetCircleSprite()
#region Microphone Volume Processing
private void CheckMicrophoneVolumeForActivation()
{
#if UNITY_WEBGL && !UNITY_EDITOR
return;
#else
if (micName != null && Microphone.IsRecording(micName))
{
float volume = GetMaxVolume();
Expand All @@ -410,18 +433,22 @@ private void CheckMicrophoneVolumeForActivation()
// ������ �Ӱ谪�� ������ ��ġ ��� ��ȯ
if (scaledVolume >= currentActivationThreshold)
{
EnterPlacementMode();
RequestPlayerActivation();
}
}
else if (verbose)
{
Debug.LogWarning("����ũ�� ���� ���� �ƴϾ ���� Ȯ�� �Ұ�");
}
#endif
}

private float GetMaxVolume()
{
if (micClip == null) return 0;
#if UNITY_WEBGL && !UNITY_EDITOR
return 0f;
#else

float[] samples = new float[sampleWindow];
int micPosition = Microphone.GetPosition(micName);
Expand Down Expand Up @@ -452,6 +479,7 @@ private float GetMaxVolume()
}

return maxVolume;
#endif
}

private int ScaleVolume(float volume)
Expand Down Expand Up @@ -481,6 +509,26 @@ private void UpdateActivationThreshold()
#endregion

#region Player Placement Mode
public void RequestPlayerActivation()
{
if (!playerActivationEnabled || isPlacementMode)
{
return;
}

if (timeSystem != null && timeSystem.CurrentTime != TimeOfDay.Evening)
{
return;
}

if (oneTimeUseOnly && hasActivatedPlayer)
{
return;
}

EnterPlacementMode();
}

private void EnterPlacementMode()
{
isPlacementMode = true;
Expand Down
2 changes: 2 additions & 0 deletions Assets/_Project/Scripts/Legacy/unused/MicroPhoneVolumeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public class MicroPhoneVolumeTest : MonoBehaviour
{
#if !UNITY_WEBGL || UNITY_EDITOR
private AudioClip micClip;
private string micName;

Expand Down Expand Up @@ -60,5 +61,6 @@ float GetMaxVolume()
return maxVolume;
}

#endif
}

Loading