Game flow and coloring done
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Core.Contracts.Features.Coloring;
|
||||
|
||||
public interface IColoringController
|
||||
{
|
||||
UniTask InitializeRegionsAsync(IDrawingTemplate template, IReadOnlyDictionary<string, Color> savedColors,
|
||||
CancellationToken ct);
|
||||
|
||||
void PaintRegion(string regionId, Color color);
|
||||
IReadOnlyDictionary<string, Color> GetCurrentColors();
|
||||
void Clear();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5ba5a7108234751ac286a57795fac06
|
||||
timeCreated: 1779971202
|
||||
@@ -10,8 +10,10 @@ namespace Darkmatter.Core.Contracts.Features.DrawingCatalog
|
||||
string Id { get; }
|
||||
string DisplayName { get; }
|
||||
Sprite DefaultThumbnail { get; }
|
||||
GameObject Prefab { get; }
|
||||
GameObject DrawingPrefab { get; }
|
||||
GameObject ColoringPrefab { get; }
|
||||
IReadOnlyList<ShapeSO> Pieces { get; }
|
||||
IReadOnlyList<ColorRegionDTO> Regions { get; }
|
||||
string ColorPaletteId { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace Darkmatter.Core.Contracts.Features.GameplayFlow
|
||||
{
|
||||
public interface IGameplayFlowController
|
||||
{
|
||||
UniTask BackAsync();
|
||||
UniTask SaveAsync();
|
||||
UniTask NextAsync();
|
||||
void OnApplicationPaused();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fee5907f5643d492ab8ba177f84c30f6
|
||||
@@ -7,9 +7,11 @@ namespace Darkmatter.Core.Contracts.Features.History
|
||||
event Action OnStackChanged;
|
||||
bool CanUndo { get; }
|
||||
bool CanRedo { get; }
|
||||
void Push(ICommand cmd); // executes + appends
|
||||
void Push(ICommand cmd);
|
||||
void Append(ICommand cmd);
|
||||
void Undo();
|
||||
void Redo();
|
||||
void Clear();
|
||||
void Clear(); // reverts every command via Undo, then drops (user-facing "wipe" semantics)
|
||||
void Drop(); // drops the list without reverting (controller cleanup — avoids touching destroyed views)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ namespace Darkmatter.Core.Contracts.Features.Progression
|
||||
IReadOnlyCollection<string> CompletedTemplateIds { get; }
|
||||
void MarkCompleted(string templateId);
|
||||
|
||||
string LastOpenedTemplateId { get; }
|
||||
UniTask SetLastOpenedAsync(string templateId);
|
||||
|
||||
DrawingProgress? GetProgress(string templateId);
|
||||
UniTask SaveProgressAsync(DrawingProgress progress);
|
||||
UniTask SaveProgressAsync(DrawingProgress progress, byte[] thumbnailPng);
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Core.Data.Dynamic.Features.Coloring
|
||||
{
|
||||
public readonly struct ColorRegionDTO
|
||||
{
|
||||
public string RegionId { get; }
|
||||
public Sprite Sprite { get; }
|
||||
public string Id { get; }
|
||||
public Image Image { get; }
|
||||
public Vector2 AnchoredPosition { get; }
|
||||
public Color InitialColor { get; }
|
||||
|
||||
public ColorRegionDTO(string regionId, Sprite sprite, Vector2 anchoredPosition, Color initialColor)
|
||||
public ColorRegionDTO(string id, Image image, Vector2 anchoredPosition, Color initialColor)
|
||||
{
|
||||
RegionId = regionId;
|
||||
Sprite = sprite;
|
||||
Id = id;
|
||||
Image = image;
|
||||
AnchoredPosition = anchoredPosition;
|
||||
InitialColor = initialColor;
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ namespace Darkmatter.Core.Data.Dynamic.Features.Progression
|
||||
public struct ProgressionRootDto
|
||||
{
|
||||
public List<DrawingProgress> records;
|
||||
public string lastOpenedTemplateId;
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,10 @@ namespace Darkmatter.Core.Data.Static.Features.DrawingTemplate
|
||||
[field: SerializeField] public string Id { get; private set; }
|
||||
[field: SerializeField] public string DisplayName { get; private set; }
|
||||
[field: SerializeField] public Sprite DefaultThumbnail { get; private set; }
|
||||
[field: SerializeField] public GameObject Prefab { get; private set; }
|
||||
[field: SerializeField] public GameObject DrawingPrefab { get; private set; }
|
||||
[field: SerializeField] public GameObject ColoringPrefab { get; private set; }
|
||||
[field: SerializeField] public IReadOnlyList<ShapeSO> Pieces { get; private set; }
|
||||
[field: SerializeField] public IReadOnlyList<ColorRegionDTO> Regions { get; private set; }
|
||||
[field: SerializeField] public string ColorPaletteId { get; private set; } = "defaultPalette";
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ namespace Darkmatter.Core.Data.Static.Features.ShapeBuilder
|
||||
{
|
||||
[Header("Radii (canvas units; reference resolution 2048x2048)")]
|
||||
[SerializeField] private float snapRadius = 100f;
|
||||
[SerializeField] private float snapGraceMultiplier = 1.5f;
|
||||
[SerializeField] private float previewRadius = 200f;
|
||||
|
||||
[Header("Tween durations (seconds)")]
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace Darkmatter.Core.Enums.Services.Audio
|
||||
ShapeHover = 100,
|
||||
ShapeSnap = 101,
|
||||
ShapeReturn = 102,
|
||||
UiTap = 200,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
"references": [
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:c176ee863a5e74e88a6517f9f102cf92",
|
||||
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23"
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:80ecb87cae9c44d19824e70ea7229748"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -1,26 +1,62 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.Loading;
|
||||
using Darkmatter.Core.Contracts.Features.Progression;
|
||||
using Darkmatter.Core.Contracts.Services.Scenes;
|
||||
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
||||
using Darkmatter.Core.Enums.Services.Scenes;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.Colorbook.System;
|
||||
|
||||
public class ColorbookFlowController : IAsyncStartable
|
||||
public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
{
|
||||
private readonly IDrawingCatalogController _drawingCatalog;
|
||||
private readonly ILoadingScreen _loadingScreen;
|
||||
private readonly IProgressionSystem _progression;
|
||||
private readonly ISceneService _scenes;
|
||||
private readonly IEventBus _bus;
|
||||
|
||||
public ColorbookFlowController(IDrawingCatalogController drawingCatalog, ILoadingScreen loadingScreen)
|
||||
private IDisposable _selectedSub;
|
||||
|
||||
public ColorbookFlowController(
|
||||
IDrawingCatalogController drawingCatalog,
|
||||
ILoadingScreen loadingScreen,
|
||||
IProgressionSystem progression,
|
||||
ISceneService scenes,
|
||||
IEventBus bus)
|
||||
{
|
||||
_drawingCatalog = drawingCatalog;
|
||||
_loadingScreen = loadingScreen;
|
||||
_progression = progression;
|
||||
_scenes = scenes;
|
||||
_bus = bus;
|
||||
}
|
||||
|
||||
public async UniTask StartAsync(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
_loadingScreen.SetProgress(1f);
|
||||
await _drawingCatalog.InitializeAsync(cancellation);
|
||||
_selectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
|
||||
_loadingScreen.Hide();
|
||||
}
|
||||
|
||||
private void OnDrawingSelected(DrawingSelectedSignal signal)
|
||||
{
|
||||
HandleSelectionAsync(signal.TemplateId).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid HandleSelectionAsync(string templateId)
|
||||
{
|
||||
await _progression.SetLastOpenedAsync(templateId);
|
||||
await _scenes.LoadSceneAsync(GameScene.Gameplay, progress: null, cancellationToken: default);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_selectedSub?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Darkmatter.Core.Contracts.Features.History;
|
||||
using Darkmatter.Features.Coloring.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.Commands;
|
||||
|
||||
public class ColorRegionCommand : ICommand
|
||||
{
|
||||
private readonly ColorRegionView _view;
|
||||
private readonly Color _from;
|
||||
private readonly Color _to;
|
||||
|
||||
public ColorRegionCommand(ColorRegionView view, Color from, Color to)
|
||||
{
|
||||
_view = view;
|
||||
_from = from;
|
||||
_to = to;
|
||||
}
|
||||
|
||||
public void Execute() => _view.SetColor(_to);
|
||||
public void Undo() => _view.SetColor(_from);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c212dc9aaa7e4445968fee229a715b3b
|
||||
timeCreated: 1779972906
|
||||
@@ -1,3 +1,5 @@
|
||||
using Darkmatter.Core.Contracts.Features.Coloring;
|
||||
using Darkmatter.Features.Coloring.Systems;
|
||||
using Darkmatter.Features.Coloring.UI;
|
||||
using Darkmatter.Libs.Installers;
|
||||
using UnityEngine;
|
||||
@@ -17,6 +19,10 @@ namespace Darkmatter.Features.Coloring
|
||||
builder.RegisterComponent(paletteHolderView);
|
||||
builder.RegisterEntryPoint<ColorPaletteHolderPresenter>().WithParameter(paletteHolderView);
|
||||
}
|
||||
|
||||
builder.Register<IColorButtonFactory, ColorButtonFactory>(Lifetime.Singleton);
|
||||
builder.Register<ColoringStateRepository>(Lifetime.Singleton);
|
||||
builder.Register<IColoringController, ColoringController>(Lifetime.Singleton);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Darkmatter.Core.Contracts.Services.Audio;
|
||||
using Darkmatter.Features.Coloring.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.Systems;
|
||||
|
||||
public class ColorButtonFactory : IColorButtonFactory
|
||||
{
|
||||
private readonly ColorPaletteHolderView _holder;
|
||||
private readonly ColoringStateRepository _repository;
|
||||
private readonly ISfxPlayer _sfx;
|
||||
|
||||
public ColorButtonFactory(
|
||||
ColorPaletteHolderView holder,
|
||||
ColoringStateRepository repository,
|
||||
ISfxPlayer sfx)
|
||||
{
|
||||
_holder = holder;
|
||||
_repository = repository;
|
||||
_sfx = sfx;
|
||||
}
|
||||
|
||||
public ColorButton Create(GameObject prefab, Color color)
|
||||
{
|
||||
var go = Object.Instantiate(prefab, _holder.SpawnRoot);
|
||||
var btn = go.GetComponent<ColorButton>();
|
||||
btn.Setup(color, _repository, _sfx);
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bba9a928a76471391068acd46c4f684
|
||||
timeCreated: 1779970637
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.Coloring;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Contracts.Features.History;
|
||||
using Darkmatter.Core.Contracts.Services.Assets;
|
||||
using Darkmatter.Core.Data.Signals.Features.Coloring;
|
||||
using Darkmatter.Core.Data.Static.Features.Coloring;
|
||||
using Darkmatter.Features.Coloring.Commands;
|
||||
using Darkmatter.Features.Coloring.UI;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
using ZLinq;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.Systems;
|
||||
|
||||
public class ColoringController : IColoringController, IDisposable
|
||||
{
|
||||
private const string ColorButtonPrefabKey = "colorButton";
|
||||
|
||||
private readonly ColoringStateRepository _repository;
|
||||
private readonly IColorButtonFactory _buttonFactory;
|
||||
private readonly IEventBus _bus;
|
||||
private readonly IAssetProviderService _assetProviderService;
|
||||
private readonly IUndoStack _history;
|
||||
private readonly IGameplaySceneRefs _refs;
|
||||
|
||||
private GameObject _colorInstance;
|
||||
private GameObject _colorButtonPrefab;
|
||||
private readonly List<ColorRegionView> _regions = new();
|
||||
private readonly List<ColorButton> _buttons = new();
|
||||
|
||||
public ColoringController(
|
||||
ColoringStateRepository repository,
|
||||
IColorButtonFactory buttonFactory,
|
||||
IEventBus bus,
|
||||
IAssetProviderService assetProviderService,
|
||||
IUndoStack history,
|
||||
IGameplaySceneRefs refs)
|
||||
{
|
||||
_repository = repository;
|
||||
_buttonFactory = buttonFactory;
|
||||
_bus = bus;
|
||||
_assetProviderService = assetProviderService;
|
||||
_history = history;
|
||||
_refs = refs;
|
||||
}
|
||||
|
||||
public async UniTask InitializeRegionsAsync(IDrawingTemplate template,
|
||||
IReadOnlyDictionary<string, Color> savedColors, CancellationToken ct)
|
||||
{
|
||||
Clear();
|
||||
await TryLoadColorButtonPrefabAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
await TryLoadColorPaletteAsync(template, ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
CreateColorButtonInstances();
|
||||
InitializeColorRegions(template, savedColors);
|
||||
}
|
||||
|
||||
public void PaintRegion(string regionId, Color color)
|
||||
{
|
||||
var region = _regions.AsValueEnumerable().FirstOrDefault(r => r.RegionId == regionId);
|
||||
if (region == null) return;
|
||||
|
||||
var from = region.Color;
|
||||
_history.Push(new ColorRegionCommand(region, from, color));
|
||||
_bus.Publish(new ColorAppliedSignal(regionId, color));
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Color> GetCurrentColors()
|
||||
{
|
||||
var snapshot = new Dictionary<string, Color>(_regions.Count);
|
||||
foreach (var region in _regions)
|
||||
snapshot[region.RegionId] = region.Color;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_history.Drop();
|
||||
|
||||
foreach (var button in _buttons)
|
||||
if (button != null) UnityEngine.Object.Destroy(button.gameObject);
|
||||
_buttons.Clear();
|
||||
|
||||
_regions.Clear();
|
||||
|
||||
if (_colorInstance != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(_colorInstance);
|
||||
_colorInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Clear();
|
||||
|
||||
private void InitializeColorRegions(IDrawingTemplate template, IReadOnlyDictionary<string, Color> savedColors)
|
||||
{
|
||||
_colorInstance = UnityEngine.Object.Instantiate(template.ColoringPrefab, _refs.PaperRoot);
|
||||
var views = _colorInstance.GetComponentsInChildren<ColorRegionView>(includeInactive: true);
|
||||
|
||||
foreach (var region in views)
|
||||
{
|
||||
var id = region.RegionId;
|
||||
var authoredColor = region.Color;
|
||||
var resumeColor = savedColors != null && savedColors.TryGetValue(id, out var saved)
|
||||
? saved
|
||||
: authoredColor;
|
||||
|
||||
if (resumeColor != authoredColor)
|
||||
_history.Append(new ColorRegionCommand(region, authoredColor, resumeColor));
|
||||
|
||||
_regions.Add(region);
|
||||
region.Initialize(resumeColor, () => PaintRegion(id, _repository.SelectedColor));
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask TryLoadColorButtonPrefabAsync(CancellationToken ct)
|
||||
{
|
||||
if (_colorButtonPrefab != null) return;
|
||||
|
||||
_colorButtonPrefab = await _assetProviderService.LoadAssetAsync<GameObject>(
|
||||
ColorButtonPrefabKey, progress: null, cancellationToken: ct);
|
||||
if (_colorButtonPrefab == null)
|
||||
throw new Exception($"No color button prefab at '{ColorButtonPrefabKey}'");
|
||||
}
|
||||
|
||||
private async UniTask TryLoadColorPaletteAsync(IDrawingTemplate template, CancellationToken ct)
|
||||
{
|
||||
var palette = await _assetProviderService.LoadAssetAsync<ColorPaletteSO>(
|
||||
template.ColorPaletteId, progress: null, cancellationToken: ct);
|
||||
if (palette == null)
|
||||
throw new Exception($"No color palette at '{template.ColorPaletteId}'");
|
||||
_repository.SetPalette(palette);
|
||||
}
|
||||
|
||||
private void CreateColorButtonInstances()
|
||||
{
|
||||
foreach (var color in _repository.SelectedPalette.Colors)
|
||||
_buttons.Add(_buttonFactory.Create(_colorButtonPrefab, color));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0849f8cc757842278c38ee6f4d61b985
|
||||
timeCreated: 1779971175
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Darkmatter.Core.Contracts.Features.Coloring;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.Systems;
|
||||
|
||||
public class ColoringStateRepository
|
||||
{
|
||||
public IColorPalette SelectedPalette { get; private set; }
|
||||
public int SelectedIndex { get; private set; }
|
||||
public Color SelectedColor =>
|
||||
SelectedPalette != null && SelectedIndex >= 0 && SelectedIndex < SelectedPalette.Colors.Count
|
||||
? SelectedPalette.Colors[SelectedIndex]
|
||||
: Color.white;
|
||||
|
||||
public event Action SelectedIndexChanged;
|
||||
|
||||
public void SetPalette(IColorPalette palette)
|
||||
{
|
||||
SelectedPalette = palette;
|
||||
SelectedIndex = 0;
|
||||
SelectedIndexChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SelectColor(Color color)
|
||||
{
|
||||
if (SelectedPalette == null) return;
|
||||
var idx = SelectedPalette.Colors.IndexOf(color);
|
||||
if (idx < 0) return;
|
||||
SelectedIndex = idx;
|
||||
SelectedIndexChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a288824e4f554000994bec451c09957c
|
||||
timeCreated: 1779970861
|
||||
@@ -0,0 +1,9 @@
|
||||
using Darkmatter.Features.Coloring.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.Systems;
|
||||
|
||||
public interface IColorButtonFactory
|
||||
{
|
||||
ColorButton Create(GameObject prefab,Color color);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91b71be7f45a41cbb17492f82a3f0967
|
||||
timeCreated: 1779970564
|
||||
54
Assets/Darkmatter/Code/Features/Coloring/UI/ColorButton.cs
Normal file
54
Assets/Darkmatter/Code/Features/Coloring/UI/ColorButton.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Darkmatter.Core.Contracts.Services.Audio;
|
||||
using Darkmatter.Core.Enums.Services.Audio;
|
||||
using Darkmatter.Features.Coloring.Systems;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.UI
|
||||
{
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class ColorButton : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Image swatch;
|
||||
[SerializeField] private GameObject selectedUI;
|
||||
private Button _button;
|
||||
private Color _color;
|
||||
private ColoringStateRepository _repository;
|
||||
private ISfxPlayer _sfx;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_button = GetComponent<Button>();
|
||||
}
|
||||
|
||||
public void Setup(Color color, ColoringStateRepository repository, ISfxPlayer sfx)
|
||||
{
|
||||
_color = color;
|
||||
_repository = repository;
|
||||
_sfx = sfx;
|
||||
swatch.color = color;
|
||||
|
||||
_button.onClick.AddListener(OnClick);
|
||||
_repository.SelectedIndexChanged += UpdateSelectedUI;
|
||||
UpdateSelectedUI();
|
||||
}
|
||||
|
||||
private void OnClick()
|
||||
{
|
||||
_sfx.Play(SfxId.UiTap);
|
||||
_repository.SelectColor(_color);
|
||||
}
|
||||
|
||||
private void UpdateSelectedUI()
|
||||
{
|
||||
if (selectedUI != null)
|
||||
selectedUI.SetActive(_repository.SelectedColor == _color);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_button != null) _button.onClick.RemoveListener(OnClick);
|
||||
if (_repository != null) _repository.SelectedIndexChanged -= UpdateSelectedUI;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e64b829b90144faa3bd2c7f5c0ad291
|
||||
timeCreated: 1779970210
|
||||
@@ -1,19 +0,0 @@
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.UI
|
||||
{
|
||||
public class ColorPalettePresenter : IStartable
|
||||
{
|
||||
private readonly ColorPaletteView _view;
|
||||
|
||||
public ColorPalettePresenter(ColorPaletteView view)
|
||||
{
|
||||
_view = view;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0146ec43f1205479194de6448b58407e
|
||||
@@ -1,8 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.UI
|
||||
{
|
||||
public class ColorPaletteView : MonoBehaviour
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0db39fd358084d4caa1fb8f030523c4c
|
||||
timeCreated: 1779969088
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
@@ -12,17 +13,17 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
public Color Color => _image.color;
|
||||
|
||||
private Image _image;
|
||||
|
||||
private event Action OnRegionClicked;
|
||||
private void Awake()
|
||||
{
|
||||
_image = GetComponent<Image>();
|
||||
_image.alphaHitTestMinimumThreshold = alphaHitThreshold;
|
||||
}
|
||||
|
||||
public void Initialize(string id, Color color)
|
||||
public void Initialize(Color color, Action onRegionClicked)
|
||||
{
|
||||
RegionId = id;
|
||||
_image.color = color;
|
||||
OnRegionClicked = onRegionClicked;
|
||||
}
|
||||
|
||||
public void SetColor(Color color)
|
||||
@@ -32,6 +33,12 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
OnRegionClicked?.Invoke();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OnRegionClicked = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,12 @@
|
||||
"rootNamespace": "Darkmatter.Features.GameplayFlow",
|
||||
"references": [
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:c176ee863a5e74e88a6517f9f102cf92",
|
||||
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1"
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:80ecb87cae9c44d19824e70ea7229748"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Features.GameplayFlow.SceneRefs;
|
||||
using Darkmatter.Features.GameplayFlow.Systems;
|
||||
using Darkmatter.Libs.Installers;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
@@ -15,6 +16,10 @@ namespace Darkmatter.Features.GameplayFlow
|
||||
{
|
||||
if (sceneRefs != null)
|
||||
builder.RegisterComponent<IGameplaySceneRefs>(sceneRefs);
|
||||
|
||||
builder.Register<GameplayFlowController>(Lifetime.Singleton)
|
||||
.As<IGameplayFlowController>()
|
||||
.As<IAsyncStartable>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef06e7bcb7ee346d894886c4a4848bcd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.Coloring;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Contracts.Features.Progression;
|
||||
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Contracts.Services.Scenes;
|
||||
using Darkmatter.Core.Data.Dynamic.Features.Progression;
|
||||
using Darkmatter.Core.Data.Signals.Features.Coloring;
|
||||
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Enums.Features.Progression;
|
||||
using Darkmatter.Core.Enums.Services.Scenes;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
{
|
||||
public sealed class GameplayFlowController : IAsyncStartable, IGameplayFlowController, IDisposable
|
||||
{
|
||||
private const int AutosaveDebounceMs = 500;
|
||||
|
||||
private readonly IProgressionSystem _progression;
|
||||
private readonly IDrawingTemplateCatalog _catalog;
|
||||
private readonly IShapeBuilderController _shapeBuilder;
|
||||
private readonly IColoringController _coloring;
|
||||
private readonly ISceneService _scenes;
|
||||
private readonly IEventBus _bus;
|
||||
|
||||
private IDrawingTemplate _template;
|
||||
private string _templateId;
|
||||
private DrawingPhase _phase = DrawingPhase.ShapeBuilding;
|
||||
|
||||
private IDisposable _assembledSub;
|
||||
private IDisposable _colorAppliedSub;
|
||||
private CancellationTokenSource _autosaveCts;
|
||||
private CancellationTokenSource _scopeCts;
|
||||
|
||||
public GameplayFlowController(
|
||||
IProgressionSystem progression,
|
||||
IDrawingTemplateCatalog catalog,
|
||||
IShapeBuilderController shapeBuilder,
|
||||
IColoringController coloring,
|
||||
ISceneService scenes,
|
||||
IEventBus bus)
|
||||
{
|
||||
_progression = progression;
|
||||
_catalog = catalog;
|
||||
_shapeBuilder = shapeBuilder;
|
||||
_coloring = coloring;
|
||||
_scenes = scenes;
|
||||
_bus = bus;
|
||||
}
|
||||
|
||||
public async UniTask StartAsync(CancellationToken cancellation)
|
||||
{
|
||||
_scopeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
var ct = _scopeCts.Token;
|
||||
|
||||
_templateId = _progression.LastOpenedTemplateId;
|
||||
if (string.IsNullOrEmpty(_templateId))
|
||||
throw new Exception("[GameplayFlow] No LastOpenedTemplateId set. ColorbookFlow must call _progression.SetLastOpenedAsync(id) before loading Gameplay.");
|
||||
|
||||
_template = await _catalog.LoadAsync(_templateId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var progress = _progression.GetProgress(_templateId);
|
||||
_phase = progress?.phase ?? DrawingPhase.ShapeBuilding;
|
||||
|
||||
await _shapeBuilder.InitializeAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
|
||||
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
|
||||
|
||||
if (_phase == DrawingPhase.Coloring)
|
||||
{
|
||||
// Resume direct into coloring: pre-snap every piece. ShapeBuilder
|
||||
// emits ShapeAssembledSignal once counter hits expected; that path
|
||||
// also triggers InitializeColoring with savedColors.
|
||||
var allIds = CollectAllPieceIds(_template);
|
||||
await _shapeBuilder.BuildAsync(_template, allIds, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _shapeBuilder.BuildAsync(_template, progress?.snappedPieces, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask BackAsync()
|
||||
{
|
||||
await SaveCurrentAsync(captureThumbnail: _phase == DrawingPhase.Coloring);
|
||||
_shapeBuilder.Clear();
|
||||
_coloring.Clear();
|
||||
await _scenes.LoadSceneAsync(GameScene.Colorbook, progress: null, cancellationToken: default);
|
||||
}
|
||||
|
||||
public async UniTask SaveAsync()
|
||||
{
|
||||
// Explicit user-pressed save — capture thumbnail + (planned) native gallery export.
|
||||
await SaveCurrentAsync(captureThumbnail: true);
|
||||
// TODO: route through ICaptureService + IGalleryService once those exist.
|
||||
}
|
||||
|
||||
public async UniTask NextAsync()
|
||||
{
|
||||
await SaveCurrentAsync(captureThumbnail: true);
|
||||
_progression.MarkCompleted(_templateId);
|
||||
|
||||
var nextId = _catalog.GetNextTemplate(_templateId);
|
||||
if (string.IsNullOrEmpty(nextId))
|
||||
{
|
||||
await _scenes.LoadSceneAsync(GameScene.Colorbook, progress: null, cancellationToken: default);
|
||||
return;
|
||||
}
|
||||
|
||||
await _progression.SetLastOpenedAsync(nextId);
|
||||
_shapeBuilder.Clear();
|
||||
_coloring.Clear();
|
||||
await _scenes.LoadSceneAsync(GameScene.Gameplay, progress: null, cancellationToken: default);
|
||||
}
|
||||
|
||||
public void OnApplicationPaused()
|
||||
{
|
||||
// Fire-and-forget — pause window is small; PlayerPrefs write is sync under the hood.
|
||||
SaveCurrentAsync(captureThumbnail: false).Forget();
|
||||
}
|
||||
|
||||
private void OnShapeAssembled(ShapeAssembledSignal signal)
|
||||
{
|
||||
if (signal.TemplateId != _templateId) return;
|
||||
EnterColoringAsync().Forget();
|
||||
}
|
||||
|
||||
private async UniTask EnterColoringAsync()
|
||||
{
|
||||
_phase = DrawingPhase.Coloring;
|
||||
var progress = _progression.GetProgress(_templateId);
|
||||
var savedColors = ToColorDict(progress?.regionColors);
|
||||
|
||||
await _coloring.InitializeRegionsAsync(_template, savedColors, _scopeCts.Token);
|
||||
|
||||
// Bare-assembled snapshot — catalog should show the puzzle outline even if
|
||||
// the child never paints (per readme §12b save matrix).
|
||||
await SaveCurrentAsync(captureThumbnail: true);
|
||||
}
|
||||
|
||||
private void OnColorApplied(ColorAppliedSignal _)
|
||||
{
|
||||
if (_phase != DrawingPhase.Coloring) return;
|
||||
_autosaveCts?.Cancel();
|
||||
_autosaveCts?.Dispose();
|
||||
_autosaveCts = CancellationTokenSource.CreateLinkedTokenSource(_scopeCts.Token);
|
||||
DebouncedAutosaveAsync(_autosaveCts.Token).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid DebouncedAutosaveAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(AutosaveDebounceMs, cancellationToken: ct);
|
||||
await SaveCurrentAsync(captureThumbnail: false);
|
||||
}
|
||||
catch (OperationCanceledException) { /* superseded by next paint or scope end */ }
|
||||
}
|
||||
|
||||
private async UniTask SaveCurrentAsync(bool captureThumbnail)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_templateId)) return;
|
||||
|
||||
var snappedIds = new List<string>(_shapeBuilder.GetSnappedPieceIds());
|
||||
var regionEntries = new List<RegionColorEntry>();
|
||||
foreach (var kv in _coloring.GetCurrentColors())
|
||||
regionEntries.Add(new RegionColorEntry { regionId = kv.Key, color = kv.Value });
|
||||
|
||||
var existing = _progression.GetProgress(_templateId);
|
||||
var progress = new DrawingProgress
|
||||
{
|
||||
templateId = _templateId,
|
||||
phase = _phase,
|
||||
snappedPieces = snappedIds,
|
||||
regionColors = regionEntries,
|
||||
hasThumbnail = captureThumbnail || (existing?.hasThumbnail ?? false),
|
||||
hasBeenCompleted = existing?.hasBeenCompleted ?? false,
|
||||
completionCount = existing?.completionCount ?? 0,
|
||||
UpdatedUtc = DateTime.UtcNow,
|
||||
FirstCompletedUtc = existing?.FirstCompletedUtc,
|
||||
};
|
||||
|
||||
byte[] thumbnailPng = null;
|
||||
if (captureThumbnail)
|
||||
{
|
||||
// TODO: replace with ICaptureService.CaptureAsync() once available.
|
||||
// For now we record hasThumbnail=true but write no PNG.
|
||||
}
|
||||
|
||||
await _progression.SaveProgressAsync(progress, thumbnailPng);
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<string> CollectAllPieceIds(IDrawingTemplate template)
|
||||
{
|
||||
var ids = new List<string>(template.Pieces.Count);
|
||||
foreach (var p in template.Pieces) ids.Add(p.Id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, Color> ToColorDict(List<RegionColorEntry> entries)
|
||||
{
|
||||
var dict = new Dictionary<string, Color>();
|
||||
if (entries == null) return dict;
|
||||
foreach (var e in entries)
|
||||
if (!string.IsNullOrEmpty(e.regionId))
|
||||
dict[e.regionId] = e.color;
|
||||
return dict;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_assembledSub?.Dispose();
|
||||
_colorAppliedSub?.Dispose();
|
||||
_autosaveCts?.Cancel();
|
||||
_autosaveCts?.Dispose();
|
||||
_scopeCts?.Cancel();
|
||||
_scopeCts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54c118b4507054746befcaa85a1e9578
|
||||
@@ -24,6 +24,14 @@ namespace Darkmatter.Features.History
|
||||
OnStackChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Append(ICommand cmd)
|
||||
{
|
||||
_undo.AddLast(cmd);
|
||||
if (_undo.Count > Capacity) _undo.RemoveFirst();
|
||||
_redo.Clear();
|
||||
OnStackChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
if (!CanUndo) return;
|
||||
@@ -50,5 +58,12 @@ namespace Darkmatter.Features.History
|
||||
_redo.Clear();
|
||||
OnStackChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Drop()
|
||||
{
|
||||
_undo.Clear();
|
||||
_redo.Clear();
|
||||
OnStackChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,10 @@ public sealed class ProgressionRepository
|
||||
private const string ThumbnailsFolder = "thumbnails";
|
||||
|
||||
private readonly Dictionary<string, DrawingProgress> _records = new();
|
||||
private string _lastOpenedTemplateId;
|
||||
|
||||
public IEnumerable<DrawingProgress> AllProgress() => _records.Values;
|
||||
public string LastOpenedTemplateId => _lastOpenedTemplateId;
|
||||
|
||||
public DrawingProgress? TryGet(string templateId) =>
|
||||
_records.TryGetValue(templateId, out var p) ? p : (DrawingProgress?)null;
|
||||
@@ -45,6 +47,14 @@ public sealed class ProgressionRepository
|
||||
_records[rec.templateId] = rec;
|
||||
}
|
||||
|
||||
_lastOpenedTemplateId = root.lastOpenedTemplateId;
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public UniTask SetLastOpenedAsync(string templateId)
|
||||
{
|
||||
_lastOpenedTemplateId = templateId;
|
||||
FlushRoot();
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -121,6 +131,7 @@ public sealed class ProgressionRepository
|
||||
var root = new ProgressionRootDto
|
||||
{
|
||||
records = _records.Values.ToList(),
|
||||
lastOpenedTemplateId = _lastOpenedTemplateId,
|
||||
};
|
||||
var json = JsonUtility.ToJson(root);
|
||||
ProtectedPlayerPrefs.SetString(PlayerPrefsKeys.Progression, json);
|
||||
|
||||
@@ -15,6 +15,10 @@ public class ProgressionSystem : IProgressionSystem
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
|
||||
public IReadOnlyCollection<string> CompletedTemplateIds => _completed;
|
||||
public string LastOpenedTemplateId => _repository.LastOpenedTemplateId;
|
||||
|
||||
public UniTask SetLastOpenedAsync(string templateId) =>
|
||||
_repository.SetLastOpenedAsync(templateId);
|
||||
|
||||
public ProgressionSystem(ProgressionRepository repository)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.ShapeBuilder.Commands
|
||||
{
|
||||
internal sealed class SnapPieceCommand : ICommand
|
||||
public sealed class SnapPieceCommand : ICommand
|
||||
{
|
||||
private readonly ShapePiece _piece;
|
||||
private readonly Vector2 _prevPos;
|
||||
|
||||
@@ -6,6 +6,6 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
{
|
||||
public interface IShapePieceFactory
|
||||
{
|
||||
ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos, bool preSnapped);
|
||||
ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Contracts.Features.History;
|
||||
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Contracts.Services.Assets;
|
||||
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
|
||||
using Darkmatter.Features.ShapeBuilder.Commands;
|
||||
using Darkmatter.Features.ShapeBuilder.UI;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
@@ -26,6 +28,7 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
private GameObject _drawingInstance;
|
||||
private GameObject _piecePrefab;
|
||||
private IDisposable _snappedSub;
|
||||
private IDisposable _unsnappedSub;
|
||||
|
||||
private readonly List<string> _snappedPieceIds = new();
|
||||
private readonly List<ShapePiece> _pieces = new();
|
||||
@@ -34,6 +37,7 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
private readonly IShapePieceFactory _factory;
|
||||
private readonly ShapeHolderView _holder;
|
||||
private readonly IGameplaySceneRefs _refs;
|
||||
private readonly IUndoStack _undo;
|
||||
|
||||
|
||||
public ShapeBuilderController(
|
||||
@@ -41,27 +45,41 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
IAssetProviderService assetProviderService,
|
||||
IShapePieceFactory factory,
|
||||
ShapeHolderView holder,
|
||||
IGameplaySceneRefs refs)
|
||||
IGameplaySceneRefs refs,
|
||||
IUndoStack undo)
|
||||
{
|
||||
_bus = bus;
|
||||
_assetProviderService = assetProviderService;
|
||||
_factory = factory;
|
||||
_holder = holder;
|
||||
_refs = refs;
|
||||
_undo = undo;
|
||||
}
|
||||
|
||||
public async UniTask InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await TryLoadPiecePrefabAsync(cancellationToken);
|
||||
_snappedSub = _bus.Subscribe<PieceSnappedSignal>(OnPieceSnapped);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_snappedSub?.Dispose();
|
||||
_unsnappedSub?.Dispose();
|
||||
_snappedSub = _bus.Subscribe<PieceSnappedSignal>(OnPieceSnapped);
|
||||
_unsnappedSub = _bus.Subscribe<PieceUnsnappedSignal>(OnPieceUnsnapped);
|
||||
}
|
||||
|
||||
private void OnPieceSnapped(PieceSnappedSignal obj)
|
||||
{
|
||||
_snapped++;
|
||||
_snappedPieceIds.Add(obj.PieceId);
|
||||
CheckIfShapeAssembled();
|
||||
}
|
||||
|
||||
private void OnPieceUnsnapped(PieceUnsnappedSignal obj)
|
||||
{
|
||||
_snapped = Mathf.Max(0, _snapped - 1);
|
||||
_snappedPieceIds.Remove(obj.PieceId);
|
||||
}
|
||||
|
||||
public async UniTask BuildAsync(
|
||||
IDrawingTemplate template,
|
||||
IReadOnlyCollection<string> preSnappedIds,
|
||||
@@ -69,15 +87,16 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
{
|
||||
Clear();
|
||||
|
||||
if (template.Prefab == null)
|
||||
if (template.DrawingPrefab == null)
|
||||
throw new System.Exception($"No drawing layout prefab for '{template.Id}'");
|
||||
|
||||
_drawingInstance = Object.Instantiate(template.Prefab, _refs.PaperRoot);
|
||||
_drawingInstance = Object.Instantiate(template.DrawingPrefab, _refs.PaperRoot);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var slots = _drawingInstance.GetComponentsInChildren<SlotMarker>(includeInactive: true);
|
||||
|
||||
await TryLoadPiecePrefabAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
int count = template.Pieces.Count;
|
||||
float trayW = _holder.SpawnWidth;
|
||||
@@ -111,10 +130,16 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
var trayPos = new Vector2(pitch * (i + 1) - trayW * 0.5f, 0f);
|
||||
bool preSnapped = preSnappedIds != null && preSnappedIds.Contains(shape.Id);
|
||||
|
||||
var piece = _factory.Create(_piecePrefab, shape, slot, trayPos, preSnapped);
|
||||
var piece = _factory.Create(_piecePrefab, shape, slot, trayPos);
|
||||
_pieces.Add(piece);
|
||||
|
||||
if (preSnapped) _snapped++;
|
||||
if (preSnapped)
|
||||
{
|
||||
_undo.Append(new SnapPieceCommand(piece));
|
||||
piece.SnapInstantly();
|
||||
_snapped++;
|
||||
_snappedPieceIds.Add(shape.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +175,8 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_undo.Drop();
|
||||
|
||||
foreach (var piece in _pieces)
|
||||
if (piece != null) Object.Destroy(piece.gameObject);
|
||||
_pieces.Clear();
|
||||
@@ -166,6 +193,7 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
public void Dispose()
|
||||
{
|
||||
_snappedSub?.Dispose();
|
||||
_unsnappedSub?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
_undo = undo;
|
||||
}
|
||||
|
||||
public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos, bool preSnapped)
|
||||
public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos)
|
||||
{
|
||||
var go = Object.Instantiate(prefab, _holder.SpawnRoot);
|
||||
go.name = $"Piece_{shape.Id}";
|
||||
|
||||
var piece = go.GetComponent<ShapePiece>();
|
||||
piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos, preSnapped);
|
||||
piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos);
|
||||
return piece;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
ISfxPlayer sfx,
|
||||
IEventBus bus,
|
||||
IUndoStack undo,
|
||||
Vector2 trayPos,
|
||||
bool preSnapped)
|
||||
Vector2 trayPos)
|
||||
{
|
||||
_shape = shape;
|
||||
_slot = slot;
|
||||
@@ -62,8 +61,6 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
|
||||
image.sprite = shape.Sprite;
|
||||
ApplyTrayPose();
|
||||
|
||||
if (preSnapped) SnapInstantly();
|
||||
}
|
||||
|
||||
public void OnBeginDrag(PointerEventData e)
|
||||
@@ -154,7 +151,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
_bus.Publish(new PieceUnsnappedSignal(_shape.Id));
|
||||
}
|
||||
|
||||
private void SnapInstantly()
|
||||
public void SnapInstantly()
|
||||
{
|
||||
Lock();
|
||||
var slot = _slot.RectTransform;
|
||||
@@ -192,5 +189,12 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
_parentRect, screenPos, _eventCam, out var local);
|
||||
return local;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Kill any tweens that were targeting this piece's RectTransform so
|
||||
// PrimeTween doesn't keep writing into a destroyed transform.
|
||||
Tween.StopAll(onTarget: RectTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,11 +164,6 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Features.GameplayFlow::Darkmatter.Features.GameplayFlow.SceneRefs.GameplaySceneRefs
|
||||
paperRoot: {fileID: 0}
|
||||
slotsParent: {fileID: 0}
|
||||
piecesParent: {fileID: 0}
|
||||
regionsParent: {fileID: 0}
|
||||
hudRoot: {fileID: 0}
|
||||
trayPanel: {fileID: 0}
|
||||
--- !u!1 &590523272
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -436,6 +431,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: d0dc926588daf4a139e807f48f664baf, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Features.Coloring::Darkmatter.Features.Coloring.ColoringFeatureModule
|
||||
paletteHolderView: {fileID: 0}
|
||||
--- !u!1 &1965442262
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -516,6 +512,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Features.ShapeBuilder::Darkmatter.Features.ShapeBuilder.Installers.ShapeBuilderFeatureModule
|
||||
config: {fileID: 0}
|
||||
holderView: {fileID: 0}
|
||||
--- !u!1 &2069155637
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
BIN
Readme.docx
BIN
Readme.docx
Binary file not shown.
151
Readme.md
151
Readme.md
@@ -630,7 +630,7 @@ public readonly struct PaperSavedSignal {
|
||||
- Regions are pre-authored as children of the drawing prefab (under `PaperRoot`). Each is a `ColorRegionView` (UI `Image` + `IPointerClickHandler`). On `Awake`, the view sets `Image.alphaHitTestMinimumThreshold` from a serialized field (default `0.01f`; tune up to `0.5f` for tighter hits) so taps on transparent pixels pass through to the next region below.
|
||||
- **`ColorPaletteHolderView`** owns the palette container `RectTransform` + slide-in/out animation (same pattern as `ShapeHolderView` — `Tween.UIAnchoredPosition` + `Tween.Alpha` on `CanvasGroup`, hides off-screen right).
|
||||
- **`ColorPaletteHolderPresenter`** subscribes to `ShapeAssembledSignal` → `view.Show()`. Hide is invoked from scene tear-down or future phase-exit hook.
|
||||
- **`ColorPaletteView` + `ColorPalettePresenter`** (planned): view spawns one color button per `IColorPalette.Colors` entry under the holder's `SpawnRoot`; presenter wires button click → `ColoringStateRepository.CurrentColor`.
|
||||
- **`ColorButton` + `IColorButtonFactory`** (planned): each color is its own self-contained MB (swatch `Image` + `IPointerClickHandler`). `ColorButtonFactory` instantiates the button prefab under `ColorPaletteHolderView.SpawnRoot` and calls `button.Setup(color, state, sfx)`. On click, the button writes directly to `ColoringStateRepository.SelectColor(myColor)` — no view/presenter intermediary. `ColoringController` iterates `IColorPalette.Colors` once on init and calls `factory.Create(color)` per entry. Mirrors `ShapePieceFactory` for symmetry.
|
||||
- Each region's `OnPointerClick` → `ColoringController.PaintRegion(view)`.
|
||||
- Controller builds `PaintRegionCommand(regionId, oldColor, newColor)` and pushes to `IUndoStack`. Command sets `Image.color` on Execute/Undo.
|
||||
- Publishes `ColorAppliedSignal` for SFX / sparkle effects.
|
||||
@@ -1107,7 +1107,7 @@ public sealed class ColoringModule : MonoBehaviour, IServiceModule {
|
||||
builder.Register<ColoringController>(Lifetime.Scoped)
|
||||
.As<IColoringController>()
|
||||
.AsSelf();
|
||||
builder.Register<ColorPalettePresenter>(Lifetime.Scoped).AsSelf();
|
||||
builder.Register<IColorButtonFactory, ColorButtonFactory>(Lifetime.Scoped);
|
||||
builder.RegisterEntryPoint<ColoringInputBinder>();
|
||||
}
|
||||
}
|
||||
@@ -1215,75 +1215,98 @@ public sealed class BoundedUndoStack : IUndoStack {
|
||||
|
||||
---
|
||||
|
||||
## 25. View / Presenter Pair — Color Palette
|
||||
## 25. Self-contained Button + Factory — Color Palette
|
||||
|
||||
### View (MonoBehaviour, setters only)
|
||||
The palette deliberately **skips the View/Presenter pair**. Each color is its own self-contained `ColorButton` MB, spawned by a factory that mirrors `ShapePieceFactory`. The button writes directly to `ColoringStateRepository` on click — no intermediary indexed event, no presenter wiring `_state.SelectedIndexChanged → _view.SetSelected`.
|
||||
|
||||
Why: palette behavior is per-button (each one is a discrete tap target with its own selected/hover anim), and adding per-button variants later (locked colors, magic "rainbow" button, premium colors) is just a new MB type that the factory can branch to. A View+Presenter pair would force every variant through a single `SetColors(IReadOnlyList<Color>)` setter.
|
||||
|
||||
### Button (self-contained MB)
|
||||
|
||||
```csharp
|
||||
namespace Darkmatter.Features.Coloring.UI;
|
||||
|
||||
public sealed class ColorPaletteView : MonoBehaviour, IColorPaletteView {
|
||||
[SerializeField, RequireInterface(typeof(IColorButtonView))]
|
||||
private MonoBehaviour[] _buttonsRaw;
|
||||
public sealed class ColorButton : MonoBehaviour, IPointerClickHandler {
|
||||
[SerializeField] private Image swatch;
|
||||
[SerializeField] private GameObject selectedRing; // optional highlight
|
||||
|
||||
private IColorButtonView[] _buttons;
|
||||
private Color _color;
|
||||
private ColoringStateRepository _state;
|
||||
private ISfxPlayer _sfx;
|
||||
|
||||
public event Action<int> OnColorButtonClicked;
|
||||
public Color Color => _color;
|
||||
|
||||
private void Awake() {
|
||||
_buttons = _buttonsRaw.Cast<IColorButtonView>().ToArray();
|
||||
for (var i = 0; i < _buttons.Length; i++) {
|
||||
var idx = i;
|
||||
_buttons[i].OnClicked += () => OnColorButtonClicked?.Invoke(idx);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetColors(IReadOnlyList<Color> colors) {
|
||||
for (var i = 0; i < _buttons.Length; i++)
|
||||
_buttons[i].SetVisible(i < colors.Count);
|
||||
for (var i = 0; i < colors.Count; i++)
|
||||
_buttons[i].SetColor(colors[i]);
|
||||
}
|
||||
|
||||
public void SetSelected(int index) {
|
||||
for (var i = 0; i < _buttons.Length; i++)
|
||||
_buttons[i].SetSelected(i == index);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Presenter (pure C#)
|
||||
|
||||
```csharp
|
||||
namespace Darkmatter.Features.Coloring.UI;
|
||||
|
||||
public sealed class ColorPalettePresenter : IStartable, IDisposable {
|
||||
private readonly IColorPaletteView _view;
|
||||
private readonly ColoringStateRepository _state;
|
||||
|
||||
public ColorPalettePresenter(IColorPaletteView view, ColoringStateRepository state) {
|
||||
_view = view;
|
||||
public void Setup(Color color, ColoringStateRepository state, ISfxPlayer sfx) {
|
||||
_color = color;
|
||||
_state = state;
|
||||
_sfx = sfx;
|
||||
swatch.color = color;
|
||||
_state.SelectedColorChanged += OnSelectedChanged;
|
||||
OnSelectedChanged(_state.CurrentColor);
|
||||
}
|
||||
|
||||
public void Start() {
|
||||
_view.SetColors(_state.Palette.Colors);
|
||||
_view.SetSelected(_state.SelectedIndex);
|
||||
_view.OnColorButtonClicked += OnClicked;
|
||||
_state.SelectedIndexChanged += OnIndexChanged;
|
||||
public void OnPointerClick(PointerEventData _) {
|
||||
_sfx.Play(SfxId.UiTap);
|
||||
_state.SelectColor(_color);
|
||||
}
|
||||
|
||||
private void OnClicked(int index) => _state.SelectColor(index);
|
||||
private void OnIndexChanged(int index) => _view.SetSelected(index);
|
||||
private void OnSelectedChanged(Color current) {
|
||||
if (selectedRing != null) selectedRing.SetActive(current == _color);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_view.OnColorButtonClicked -= OnClicked;
|
||||
_state.SelectedIndexChanged -= OnIndexChanged;
|
||||
private void OnDestroy() {
|
||||
if (_state != null) _state.SelectedColorChanged -= OnSelectedChanged;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Same shape repeats for every feature's UI.
|
||||
### Factory (mirrors ShapePieceFactory)
|
||||
|
||||
```csharp
|
||||
namespace Darkmatter.Features.Coloring.Systems;
|
||||
|
||||
public interface IColorButtonFactory {
|
||||
ColorButton Create(Color color);
|
||||
}
|
||||
|
||||
public sealed class ColorButtonFactory : IColorButtonFactory {
|
||||
private readonly ColorPaletteHolderView _holder;
|
||||
private readonly GameObject _buttonPrefab;
|
||||
private readonly ColoringStateRepository _state;
|
||||
private readonly ISfxPlayer _sfx;
|
||||
|
||||
public ColorButtonFactory(
|
||||
ColorPaletteHolderView holder,
|
||||
GameObject buttonPrefab,
|
||||
ColoringStateRepository state,
|
||||
ISfxPlayer sfx) {
|
||||
_holder = holder;
|
||||
_buttonPrefab = buttonPrefab;
|
||||
_state = state;
|
||||
_sfx = sfx;
|
||||
}
|
||||
|
||||
public ColorButton Create(Color color) {
|
||||
var go = Object.Instantiate(_buttonPrefab, _holder.SpawnRoot);
|
||||
var btn = go.GetComponent<ColorButton>();
|
||||
btn.Setup(color, _state, _sfx);
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Spawn loop (inside `ColoringController.InitializeAsync`)
|
||||
|
||||
```csharp
|
||||
foreach (var color in _palette.Colors)
|
||||
_buttonFactory.Create(color);
|
||||
```
|
||||
|
||||
That's the whole feature wiring. No `IColorPaletteView`, no `OnColorButtonClicked` indexed event, no `SelectedIndex ↔ SetSelected` round-trip.
|
||||
|
||||
### When to use View/Presenter instead
|
||||
|
||||
Other features (history buttons, drawing catalog) still use View/Presenter — that pair fits when a single canvas of fixed sub-widgets needs to react to one model. Use button+factory when each item is **spawned dynamically from data** and has independent click behavior.
|
||||
|
||||
---
|
||||
|
||||
@@ -1592,7 +1615,8 @@ Comprehensive index — every script (existing or planned) grouped by its module
|
||||
| `Features/ShapeBuilder/Systems/` | `ShapeBuilderController`, `IShapePieceFactory`, `ShapePieceFactory` | ✅ |
|
||||
| `Features/ShapeBuilder/Installers/` | `ShapeBuilderFeatureModule` | ✅ |
|
||||
| `Features/Coloring/Systems/` | `ColoringController`, `ColoringStateRepository` | ⚠️ planned |
|
||||
| `Features/Coloring/UI/` | `ColorRegionView` ✅, `ColorPaletteHolderView` ✅, `ColorPaletteHolderPresenter` ✅, `ColorPaletteView` ⚠️, `ColorPalettePresenter` ⚠️ | |
|
||||
| `Features/Coloring/UI/` | `ColorRegionView` ✅, `ColorPaletteHolderView` ✅, `ColorPaletteHolderPresenter` ✅, `ColorButton` ⚠️ planned | |
|
||||
| `Features/Coloring/Systems/` | `IColorButtonFactory`, `ColorButtonFactory` | ⚠️ planned |
|
||||
| `Features/Coloring/Commands/` | `PaintRegionCommand` | ⚠️ |
|
||||
| `Features/Coloring/Installers/` | `ColoringFeatureModule` | ⚠️ |
|
||||
| `Features/Capture/Systems/` | `CaptureController` (light wrapper around `ICaptureService`) | ⚠️ |
|
||||
@@ -2093,14 +2117,14 @@ In-memory model. Owns "currently selected color" and the palette in use.
|
||||
```csharp
|
||||
public sealed class ColoringStateRepository {
|
||||
public IColorPalette Palette { get; private set; }
|
||||
public int SelectedIndex { get; private set; }
|
||||
public Color CurrentColor => Palette.Colors[SelectedIndex];
|
||||
public event Action<int> SelectedIndexChanged;
|
||||
public void SetPalette(IColorPalette palette); // resets SelectedIndex to 0
|
||||
public void SelectColor(int index);
|
||||
public Color CurrentColor { get; private set; }
|
||||
public event Action<Color> SelectedColorChanged;
|
||||
public void SetPalette(IColorPalette palette); // resets CurrentColor to Colors[0]
|
||||
public void SelectColor(Color color); // called directly by ColorButton on click
|
||||
}
|
||||
```
|
||||
- **Why a repository:** presenter and controller both need to read/write current color; an event-emitting POCO is simpler than wiring two signals.
|
||||
- **Why a repository:** `ColorButton` (palette taps) and `ColoringController` (paint command builder) both need to read/write the current color; an event-emitting POCO is simpler than wiring two signals.
|
||||
- **Color-keyed, not index-keyed:** with `ColorButton + Factory` (§25), each button owns its `Color` directly. Index-based addressing is unnecessary — the button passes its color straight to `SelectColor(color)`.
|
||||
|
||||
#### `ColoringController` *(Systems — planned)* — implements `IColoringController`
|
||||
Wires pre-authored `ColorRegionView` children, applies saved colors on resume, builds and pushes `PaintRegionCommand` instances on click.
|
||||
@@ -2156,8 +2180,8 @@ No `ColoringInputBinder` class needed. Unity's EventSystem fires `OnPointerClick
|
||||
#### `PaintRegionCommand` *(Commands)*
|
||||
Source in section 23. Holds `view`, `fromColor`, `toColor`, `bus`. Symmetrical execute/undo.
|
||||
|
||||
#### `ColorPaletteView`, `ColorPalettePresenter` *(UI — stubs exist)*
|
||||
Files exist but bodies are empty. Target shape: view spawns one color button per `IColorPalette.Colors` entry under `ColorPaletteHolderView.SpawnRoot`; presenter binds `ColoringStateRepository.SelectedIndexChanged` ↔ view highlight, and view click events → repository.
|
||||
#### `ColorButton` + `IColorButtonFactory` / `ColorButtonFactory` *(UI + Systems — planned)*
|
||||
Replaces the View/Presenter pair for the palette. Each color is its own self-contained MB with click handler; factory instantiates the prefab under `ColorPaletteHolderView.SpawnRoot` and runs `Setup(color, state, sfx)`. Button writes `_state.SelectColor(_color)` on click. See §25 for the full pattern and rationale. The stale `ColorPaletteView.cs` / `ColorPalettePresenter.cs` stubs should be deleted when this lands.
|
||||
|
||||
#### `ColorPaletteHolderView` / `ColorPaletteHolderPresenter` *(UI — ✅ exists)*
|
||||
View owns the palette container `RectTransform` (`SpawnRoot`) + slide/fade show-hide animation (same `Tween.UIAnchoredPosition` + `Tween.Alpha` pattern as `ShapeHolderView`, default hide off-screen right). Presenter subscribes `ShapeAssembledSignal` → `view.Show()`. Hide is invoked externally (scope tear-down or future phase-exit signal).
|
||||
@@ -2165,6 +2189,7 @@ View owns the palette container `RectTransform` (`SpawnRoot`) + slide/fade show-
|
||||
#### Removed / not needed
|
||||
|
||||
- **`ColorRegionFactory`** — regions are pre-authored as children of the drawing prefab. The controller wires existing views; it doesn't spawn anything.
|
||||
- **`ColorPaletteView` + `ColorPalettePresenter`** — replaced by self-contained `ColorButton` + `ColorButtonFactory`. The stub files at `Features/Coloring/UI/ColorPaletteView.cs` and `ColorPalettePresenter.cs` should be deleted; see §25 for rationale.
|
||||
|
||||
---
|
||||
|
||||
@@ -2356,6 +2381,8 @@ Implemented as `MonoBehaviour` per feature/service so scopes can drag them in th
|
||||
| `ColoringStateRepository` | Feature.Coloring | Current color model | — |
|
||||
| `ColoringController` | Feature.Coloring | Wires pre-authored regions + paint cmd + autosave hook | undo, state, refs, flow, bus |
|
||||
| `ColorPaletteHolderView` / `ColorPaletteHolderPresenter` | Feature.Coloring | Palette container + slide/fade show/hide; presenter reacts to `ShapeAssembledSignal` | bus |
|
||||
| `ColorButton` | Feature.Coloring | Self-contained palette swatch MB; click writes to repository (no view/presenter pair) | state, sfx |
|
||||
| `IColorButtonFactory` / `ColorButtonFactory` | Feature.Coloring | Instantiates a `ColorButton` under `ColorPaletteHolderView.SpawnRoot` + runs `Setup` | holder, prefab, state, sfx |
|
||||
| `ColorRegionView` | Feature.Coloring | Region UI Image + `IPointerClickHandler` | controller |
|
||||
| `PaintRegionCommand` | Feature.Coloring | Undoable paint (sets `Image.color`) | view, bus |
|
||||
| `HistoryController` | Feature.History | Undo/redo facade | undo stack, bus |
|
||||
|
||||
Reference in New Issue
Block a user