Imported from AlexChichibabin/Gnomopocalypse (
AGENTS.md). Install upstream withnpx skills add AlexChichibabin/Gnomopocalypse. Copyright stays with the author.
Project Agent Notes
Project Shape
- This is a Unity project.
- Main game code lives under
Assets/_Project/_Scripts. - Main scenes live under
Assets/_Project/_Scenes. - Project-specific prefabs, resources, and art live under
Assets/_Project/Prefabs,Assets/_Project/Resources, andAssets/_Project/Art. - Zenject is used for dependency injection. Prefer existing installers and project architecture when adding production code.
Prototyping Area
Assets/_Project/Prototypingis an isolated prototyping sandbox.Assets/_Project/ShootPrototypingis also an isolated prototyping sandbox for shooting/slingshot experiments.- Code, prefabs, scenes, and configs in
Prototypingare not yet fully part of the main project architecture. - The same is true for
ShootPrototyping: do not treat it as production architecture unless the user explicitly says so. - Treat prototyping code as experimental unless the user explicitly asks to integrate it.
- Do not move prototyping code into the main architecture without discussing the target shape first.
- When editing prototyping files, keep changes local to
Assets/_Project/Prototyping,Assets/_Project/ShootPrototyping, and any explicitly requested prototype installer/config files. - When a prototype feature becomes production-ready, it should be migrated deliberately and aligned with existing architecture, naming, config flow, and Zenject bindings.
Current Prototype Patterns
PrototypeInstalleris used for prototype-only Zenject bindings.- Prototype pooled objects use nested
MonoMemoryPool<T>classes, for exampleUnit.UnitPool,Tower.TowerPool, andProjectile.ProjectilePool. - Prototype factories currently spawn pooled objects directly and may be intentionally rough or test-driven.
Shootingis a slingshot-style projectile controller. It exposesIsMoving, which becomes true after release.
Current Gameplay Patterns
- Production gameplay services are bound through
GameInstallerandLevelInstallerusing Zenject. GameInstallerbinds global services such asIConfigProvider,IInputService,IPlayerProgress, andIGameStateMachine.LevelInstallerbinds level services such asILevelStateMachine,UnitsFactory,ICoroutineRunner,UnitsSpawnSettings,ShootingAnchor,ProjectileSelection,IPlayerHealth,IGameCondition,IUnitTracker,IPauseState, and memory pools forUnitandProjectile.- The project has a global flow (
GameStateMachine) and a per-level flow (LevelStateMachine).GameBootstrapperappliesGameState.Bootstrap;LevelBootstrapperappliesLevelState.Bootstrap, which then entersLevelState.Gameplay. GameStateMachineloadsMenuScenefromBootstrapSceneand loads the next level throughIPlayerProgress.GetNextLevelConfig().SceneName.- Current main scenes are
MenuScene,GameplayScene, andLevel2Scene; scene names are also stored inConstants. LevelStateMachinerestores player health, initializesUnitTracker, chooses level music by active scene name, unpauses, then enters gameplay. On win/lose it plays SFX, stops music, pauses gameplay, and raisesStateChanged.GameConditiondrives level end conditions: player death appliesLevelState.Lose;UnitTracker.OnAllUnitDeathappliesLevelState.Win.UnitTrackerreadsLevelConfig.UnitCountToWinfor the active scene and counts despawned/dead units throughUnitsFactory.DespawnUnit.IPlayerProgresscurrently stores level progress only in memory.PlayerProgress.AddScoremarks a level complete, andGetNextLevelConfigreturns the first level with score0, falling back to the last level.UnitsFactorystarts spawning only afterILevelStateMachineentersLevelState.Gameplay.UnitsFactoryusesSpawnRateConfigfromResources/Configs/Units/SpawnRateConfigand randomUnitConfigassets fromResources/Configs/Units.ConfigProvider.GetRandomUnitConfigis weighted byUnitConfig.SpawnProbability.UnitConfigalso hasTransformationProbability, butConfigProvider.GetRandomUnitMutationConfigcurrently still usesSpawnProbability.- In
SpawnRateStep,Minutemeans step duration in minutes,UnitsPerMinutemeans spawn frequency during that step, andPauseUntilNextWavemeans the pause in seconds after that step before the next wave. No pause is applied after the last step. UnitsFactorytracks pause with a localisPausedflag fromIPauseState.IsPausedEventand manually waits while paused between spawn waits. It does not currently changeTime.timeScale.UnitsSpawnSettingsprovides the spawn center and radius. If_spawnPointis assigned, spawned units use that transform position; otherwise they use the settings object's own position.UnitsFactoryspawns units inside the configured radius, but Y is quantized to 7 fixed positions. X is still random and clamped to stay inside the circle at the selected Y.- Unit sprite sorting is assigned at spawn from the selected Y lane. Lower Y lanes receive higher
SpriteRenderer.sortingOrder; current orders are13for the highest lane through19for the lowest lane. UnitView.SetSortingOrderapplies the order to all four child unit body sprite renderers (Dirty,Smelly,Sticky,Leaking) regardless of which body GameObject is currently active.- If two units overlap and have the same base sorting order,
Unitresolves the tie inLateUpdatewithPhysics2D.OverlapCollider: one unit temporarily getssortingOrder + 1, then returns to its base order when the conflict ends.Order in Layeris an integer, so do not use fractional values like0.5. Unithas a life phase coroutine that waitsUnitConfig.MinStayTime, plays the drink animation, then mutates to a random mutation config. Mutation does not reset current health; health is reset only when the unit is spawned from the pool.Unitstops movement while drinking, dying, or playing hurt. Drink and death animation completion is reported throughUnitAnimator.AnimationEnded, with a temporary 3 second timeout fallback inUnit.PlayAnimationAndWaitForEnd.- Projectile hits call
Unit.DealMainDamage()orUnit.DealSecondaryDamage(), notUnitHealthdirectly.Unitqueues pending damage, plays thehurtanimation first, waits forEndor a temporary 2 second timeout fallback, then applies the queued health damage. Unitdoes not start drink/mutation if it is already dead (CurrentHealth <= 0or_isDead). If health reaches zero during a pending mutation, the life phase stops and the death routine takes over.Unitlistens toIPauseState.IsPausedand togglesUnitMove.enabledwhile paused.- Be careful with event cleanup around pooled units:
Unitcurrently unsubscribes fromUnitHealth.ZeroHealthon despawn, butIPauseState.IsPausedEventcleanup should be checked when changing unit lifecycle. UnitAnimatormanages four childAnimatorreferences forSmelly,Dirty,Leaking, andSticky, plus a PUF animator. All unit animator controllers are expected to have aWalkstate anddrink/death/hurttrigger parameters.- Unit drink/death/hurt animation clips should call an animation event named
Endat the end of the clip.UnitAnimationEventReceiverreceivesEnd()on the same child GameObject as theAnimatorand forwards it toUnitAnimator. - Drink animation clips can call the animation event function
Puf()to trigger the C#Pufedevent.UnitAnimatorresponds by enabling the PUF object and playing itsPUFstate from the start; the PUF object is reset/hidden only on fresh pool spawn, not on mutation. - Hurt states in unit animator controllers transition back to
WalkwithHas Exit Time = true,Exit Time = 1, and zero transition duration so short hurt clips can finish without being blended away. ProjectileSelectionmanages the current projectile stock as a queue ofProjectileConfigassets fromResources/Configs/Projectiles. It fills the stock at level start,ProjectileFactorytakes the bottom/first config when spawning, andProjectileSelectionimmediately adds a new random config to the top/end.ProjectileSelectionViewdisplays that stock by rotating slot transforms inProjectileSelectionCanvas, not just swapping sprites. The bottom slot is the next projectile; after taking it, that slot moves to the top under the viewport mask and receives the new reserve config.ProjectileFactoryis not click-driven. It spawns the first projectile onStart, initializes itsShooting, subscribes toShooting.Released, and spawns the next projectile after serialized_spawnCooldownseconds.Projectile.Despawn()is guarded against double pool returns with_isDespawned; reset that flag inOnSpawnedwhen changing projectile pooling behavior.Shootingis controlled throughIInputService.MousePosplus Unity mouse callbacks (OnMouseDown,OnMouseDrag,OnMouseUp). It refuses drag/release while the projectile is already moving or whileIPauseState.IsPausedis true.ProjectileTriggerrequires_shooting.IsMovingbefore applying damage. Projectile hits should continue to call methods onUnit, notUnitHealth, so hurt animation timing stays centralized.WinLosePanellistens toLevelStateMachine.StateChanged, shows win/lose sprites, and usesIGameStateMachineto load the next level orSceneManager.LoadSceneto restart. When touching next-level logic, compare scene names usingLevelConfig.SceneName, not the asset name.PauseStatedisables/enables gameplay input throughIInputServiceand raisesIsPausedEvent; pause UI should avoid showing over win/lose states.AudioService.Initcreates a persistent[Audio]GameObject with SFX and music sources, usingAudioConfigfrom resources. Avoid creating extra audio roots unless deliberately changing the audio architecture.
Coding Style
- Keep changes small and focused.
- Prefer existing project patterns over introducing new abstractions.
- Use Zenject injection for dependencies that are already bound through installers.
- For Unity
ScriptableObjectconfigs, useCreateAssetMenuwhen designers need to create assets from the editor. - Use serialized private fields for inspector-configured data and expose read-only properties when external code needs access.
- Avoid unrelated formatting churn in scene, prefab, and meta files.
- Prefer English for new code comments and logs.
- Some files may contain Windows-1251 Cyrillic comments. If text looks corrupted, inspect raw bytes or try Windows-1251 before assuming the content is lost.
Unity Notes
- Be careful editing
.unityand.prefabYAML manually. Prefer script/code edits unless scene or prefab wiring is explicitly requested. - If adding a serialized field to a MonoBehaviour, remember that scene or prefab references may still need to be assigned in the Unity editor.
- For pooled MonoBehaviours, prefer Zenject
MonoMemoryPool<T>and keep spawn/despawn lifecycle methods on the pooled component. - When adding prototype pool bindings, bind them in
PrototypeInstallerunless the user asks for production integration. - For frame-based movement, use
Time.deltaTime; for Rigidbody/Rigidbody2D movement, prefer physics-friendly movement inFixedUpdate. - For
OnPointerClickon world-space 2D objects, make sure the clicked object has aCollider2D, anEventSystemexists, and the camera has aPhysics2DRaycasterwhose event mask includes the object's layer. - For UI clicks, use a
GraphicRaycasteron the canvas instead of a physics raycaster. - For
OnTriggerEnter2D, both objects needCollider2D, at least one collider must be a trigger, and at least one object needsRigidbody2D. - Animation events are resolved on the GameObject that plays the clip. If Unity shows
End (Function Not Supported), make sureUnitAnimationEventReceiveris present on the same child GameObject as that childAnimator. - For 2D Y sorting, do not assume
Transparency Sort Axisalone will solve unit overlap. It only participates afterSorting LayerandOrder in Layer, and equal/tied renderers may still fall back to creation order. Prefer an explicit per-unit sorting strategy based on a stable feet/pivot point when exact front/back ordering matters.
Verification
- When possible, run a focused compile/build check.
- If Unity package or generated-project errors block command-line verification, report that clearly and distinguish them from errors in changed project code.