HeavenStudio/Assets/Scripts/Games/NailCarpenter/NailCarpenter.cs
minenice55 45a4b6d6b4
Nail Carpenter Swing Support (#829)
* BurstLinq

make BGM resync when changing pitch (to test)

* autoswing

some game implementations, most games already work fine

* more game tweaks

* 16th note swing

more game fixes
make pitch change resync optional in the API

* suppress some common warnings

* Update Credits.txt

* nail carpenter swing support
2024-04-07 04:56:37 +00:00

520 lines
21 KiB
C#

using HeavenStudio.Util;
using HeavenStudio.InputSystem;
using Jukebox;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace HeavenStudio.Games.Loaders
{
using static Minigames;
public static class PcoNailLoader
{
public static Minigame AddGame(EventCaller eventCaller)
{
return new Minigame("nailCarpenter", "Nail Carpenter", "fab96e", false, false, new List<GameAction>()
{
new GameAction("puddingNail", "Pudding Nail")
{
defaultLength = 8f,
resizable = true
},
new GameAction("cherryNail", "Cherry Nail")
{
defaultLength = 4f,
resizable = true
},
new GameAction("cakeNail", "Cake Nail")
{
defaultLength = 4f,
resizable = true
},
new GameAction("cakeLongNail", "Cake Long Nail")
{
defaultLength = 4f,
resizable = true
},
new GameAction("slideFusuma", "Slide Shoji")
{
function = delegate {
var e = eventCaller.currentEntity;
NailCarpenter.instance.SlideShoji(e.beat, e.length, e["fillRatio"], e["ease"], e["mute"]);
},
defaultLength = 1f,
resizable = true,
parameters = new List<Param>()
{
new Param("fillRatio", new EntityTypes.Float(0f, 1f, 0.3f), "Ratio", "Set how much of the screen the shoji covers."),
new Param("ease", Util.EasingFunction.Ease.Linear, "Ease", "Set the easing of the action."),
new Param("mute", false, "Mute", "Toggle if the cue should be muted.")
}
},
},
new List<string>() { "pco", "normal" },
"pconail", "en",
new List<string>() { },
chronologicalSortKey: 20121009
);
}
}
}
namespace HeavenStudio.Games
{
using Scripts_NailCarpenter;
public class NailCarpenter : Minigame
{
const double PATTERN_SEEK_TIME = 8.0;
[Serializable]
public struct ObjectPatternItem
{
public double beat;
public ObjectType type;
}
public enum ObjectType
{
Nail,
LongNail,
Sweet,
ForceCherry,
ForcePudding,
ForceCherryPudding,
ForceShortCake,
ForceLayerCake,
None,
LongCharge
}
struct ScheduledPattern
{
public double beat;
public double length;
public PatternType type;
}
enum PatternType
{
Pudding,
Cherry,
Cake,
CakeLong,
None
}
[SerializeField] ObjectPatternItem[] puddingPattern;
[SerializeField] ObjectPatternItem[] cherryPattern;
[SerializeField] ObjectPatternItem[] cakePattern;
[SerializeField] ObjectPatternItem[] cakeLongPattern;
[SerializeField] float scrollMetresPerBeat = 4f;
[SerializeField] float boardWidth = 19.2f;
public GameObject baseNail;
public GameObject baseLongNail;
public GameObject baseSweet;
public Animator Carpenter;
public Animator EffectExclamRed;
public Animator EffectExclamBlue;
public Transform scrollingHolder;
public Transform nailHolder;
public Transform boardTrans;
public Transform shojiTrans;
private bool missed;
private bool hasSlurped;
const int IAAltDownCat = IAMAXCAT;
const int IASweetsCat = IAMAXCAT + 1;
protected static bool IA_PadAltPress(out double dt)
{
return PlayerInput.GetPadDown(InputController.ActionsPad.South, out dt);
}
protected static bool IA_BatonAltPress(out double dt)
{
return PlayerInput.GetSqueezeDown(out dt);
}
protected static bool IA_TouchRegularPress(out double dt)
{
return PlayerInput.GetTouchDown(InputController.ActionsTouch.Tap, out dt)
&& !instance.IsExpectingInputNow(InputAction_AltPress);
}
protected static bool IA_TouchAltPress(out double dt)
{
return PlayerInput.GetTouchDown(InputController.ActionsTouch.Tap, out dt)
&& instance.IsExpectingInputNow(InputAction_AltPress)
&& !instance.IsExpectingInputNow(InputAction_RegPress);
}
protected static bool IA_PadSweetsCheck(out double dt)
{
return (PlayerInput.GetPadDown(InputController.ActionsPad.South, out dt)
|| PlayerInput.GetPadDown(InputController.ActionsPad.East, out dt))
&& !(instance.IsExpectingInputNow(InputAction_RegPress) || instance.IsExpectingInputNow(InputAction_AltPress));
}
protected static bool IA_BatonSweetsCheck(out double dt)
{
return (PlayerInput.GetBatonDown(InputController.ActionsBaton.Face, out dt)
|| PlayerInput.GetSqueezeDown(out dt))
&& !(instance.IsExpectingInputNow(InputAction_RegPress) || instance.IsExpectingInputNow(InputAction_AltPress));
}
protected static bool IA_TouchSweetsCheck(out double dt)
{
return PlayerInput.GetTouchDown(InputController.ActionsTouch.Tap, out dt)
&& !(instance.IsExpectingInputNow(InputAction_RegPress) || instance.IsExpectingInputNow(InputAction_AltPress));
}
public static PlayerInput.InputAction InputAction_RegPress =
new("PcoNailRegStart", new int[] { IAPressCat, IAPressCat, IAPressCat },
IA_PadBasicPress, IA_TouchRegularPress, IA_BatonBasicPress);
public static PlayerInput.InputAction InputAction_AltPress =
new("PcoNailAltStart", new int[] { IAAltDownCat, IAAltDownCat, IAAltDownCat },
IA_PadAltPress, IA_TouchAltPress, IA_BatonAltPress);
public static PlayerInput.InputAction InputAction_SweetsHit =
new("PcoNailSweetsHit", new int[] { IASweetsCat, IASweetsCat, IASweetsCat },
IA_PadSweetsCheck, IA_TouchSweetsCheck, IA_BatonSweetsCheck);
public static NailCarpenter instance;
// Start is called before the first frame update
void Awake()
{
instance = this;
}
List<ScheduledPattern> scheduledPatterns = new List<ScheduledPattern>();
double patternStartBeat, gameStartBeat;
PatternType patternType, lastPatternType;
int patternIndex, lastPatternIndex = -1;
double slideBeat = double.MaxValue;
double slideLength;
double cachedPatternLengthPudding, cachedPatternLengthCherry, cachedPatternLengthCake, cachedPatternLengthCakeLong;
Util.EasingFunction.Ease slideEase;
float slideRatioLast = 0, slideRatioNext = 0;
void Start()
{
if (!conductor.isPlaying) return;
UpdatePatterns();
}
public override void OnBeatPulse(double beat)
{
if (!IsExpectingInputNow(InputAction_AltPress) && UnityEngine.Random.value < 0.1f)
{
Carpenter.Play("eyeBlinkFast", 1, 0);
}
}
void Update()
{
double currentBeat = conductor.unswungSongPositionInBeatsAsDouble;
if (!conductor.isPlaying) return;
if (PlayerInput.GetIsAction(InputAction_RegPress) && !IsExpectingInputNow(InputAction_RegPress))
{
ScoreMiss();
SoundByte.PlayOneShot("miss");
Carpenter.DoScaledAnimationAsync("carpenterHit", 0.25f);
hasSlurped = false;
}
if (PlayerInput.GetIsAction(InputAction_AltPress) && !IsExpectingInputNow(InputAction_AltPress))
{
ScoreMiss();
SoundByte.PlayOneShot("miss");
Carpenter.DoScaledAnimationAsync("carpenterHit", 0.25f);
hasSlurped = false;
}
// Board scroll.
Vector3 boardPos = boardTrans.localPosition;
double newBoardX = currentBeat * scrollMetresPerBeat;
newBoardX %= boardWidth;
boardTrans.localPosition = new Vector3((float)newBoardX, boardPos.y, boardPos.z);
UpdatePatterns();
UpdateShoji(currentBeat);
}
public override void OnGameSwitch(double beat)
{
cachedPatternLengthPudding = puddingPattern[^1].beat;
cachedPatternLengthCherry = cherryPattern[^1].beat;
cachedPatternLengthCake = cakePattern[^1].beat;
cachedPatternLengthCakeLong = cakeLongPattern[^1].beat;
double endBeat = double.MaxValue;
var entities = gameManager.Beatmap.Entities;
gameStartBeat = beat;
patternStartBeat = gameStartBeat;
// find out when the next game switch (or remix end) happens
RiqEntity firstEnd = entities.Find(c => (c.datamodel.StartsWith("gameManager/switchGame") || c.datamodel.Equals("gameManager/end")) && c.beat > gameStartBeat);
endBeat = firstEnd?.beat ?? endBeat;
List<RiqEntity> events = entities.FindAll(v => (v.datamodel is "nailCarpenter/puddingNail" or "nailCarpenter/cherryNail" or "nailCarpenter/cakeNail" or "nailCarpenter/cakeLongNail") && v.beat >= gameStartBeat && v.beat < endBeat);
scheduledPatterns.Clear();
patternIndex = 0;
foreach (var evt in events)
{
if (evt.length == 0) continue;
int patternDivisions = (int)Math.Ceiling(evt.length / PATTERN_SEEK_TIME);
PatternType patternType = evt.datamodel switch
{
"nailCarpenter/puddingNail" => PatternType.Pudding,
"nailCarpenter/cherryNail" => PatternType.Cherry,
"nailCarpenter/cakeNail" => PatternType.Cake,
"nailCarpenter/cakeLongNail" => PatternType.CakeLong,
_ => throw new NotImplementedException()
};
for (int i = 0; i < patternDivisions; i++)
{
var pattern = new ScheduledPattern
{
beat = evt.beat + (PATTERN_SEEK_TIME * i),
length = Math.Min(evt.length - (PATTERN_SEEK_TIME * i), PATTERN_SEEK_TIME),
type = patternType
};
scheduledPatterns.Add(pattern);
}
}
}
public override void OnPlay(double beat)
{
OnGameSwitch(beat);
}
void UpdatePatterns()
{
double beat = conductor.songPositionInBeatsAsDouble;
while (patternStartBeat < beat + PATTERN_SEEK_TIME)
{
if (patternIndex < scheduledPatterns.Count)
{
var pattern = scheduledPatterns[patternIndex];
if (pattern.type == PatternType.None)
{
patternIndex++;
continue;
}
if (pattern.beat + pattern.length < patternStartBeat)
{
patternIndex++;
continue;
}
SpawnPattern(pattern.beat, pattern.length, pattern.type);
patternStartBeat = pattern.beat + pattern.length;
lastPatternIndex = patternIndex;
patternIndex++;
}
else
{
break;
}
}
}
public void SlideShoji(double beat, double length, float fillRatio, int ease, bool mute)
{
if (!mute) SoundByte.PlayOneShotGame("nailCarpenter/open", beat, forcePlay: true);
slideBeat = beat;
slideLength = length;
slideEase = (Util.EasingFunction.Ease)ease;
slideRatioLast = slideRatioNext;
slideRatioNext = fillRatio;
}
void UpdateShoji(double beat)
{
if (beat >= slideBeat)
{
float slideLast = 17.8f * (1 - slideRatioLast);
float slideNext = 17.8f * (1 - slideRatioNext);
Util.EasingFunction.Function func = Util.EasingFunction.GetEasingFunction(slideEase);
float slideProg = Conductor.instance.GetPositionFromBeat(slideBeat, slideLength, true);
slideProg = Mathf.Clamp01(slideProg);
float slide = func(slideLast, slideNext, slideProg);
shojiTrans.localPosition = new Vector3(slide, 0, 0);
}
}
private void SpawnPattern(double beat, double length, PatternType pattern)
{
if (pattern == PatternType.None) return;
double patternLength = pattern switch
{
PatternType.Pudding => cachedPatternLengthPudding,
PatternType.Cherry => cachedPatternLengthCherry,
PatternType.Cake => cachedPatternLengthCake,
PatternType.CakeLong => cachedPatternLengthCakeLong,
_ => throw new NotImplementedException()
};
patternType = pattern;
int patternIterations = (int)Math.Ceiling(length / patternLength);
for (int i = 0; i < patternIterations; i++)
{
SpawnPatternSegment(beat + (patternLength * i), gameStartBeat, pattern switch
{
PatternType.Pudding => puddingPattern,
PatternType.Cherry => cherryPattern,
PatternType.Cake => cakePattern,
PatternType.CakeLong => cakeLongPattern,
_ => throw new NotImplementedException()
});
lastPatternType = patternType;
}
}
private void SpawnPatternSegment(double beat, double startbeat, ObjectPatternItem[] pattern)
{
foreach (var item in pattern)
{
double itemBeat = beat + item.beat;
switch (item.type)
{
case ObjectType.LongCharge:
SoundByte.PlayOneShotGame("nailCarpenter/signal2", itemBeat, forcePlay: true);
BeatAction.New(instance, new List<BeatAction.Action>()
{
new BeatAction.Action(itemBeat, delegate
{
Carpenter.DoScaledAnimationAsync("carpenterArmUp", 0.25f);
}),
});
break;
case ObjectType.Nail:
SpawnNail(itemBeat, startbeat);
break;
case ObjectType.LongNail:
SpawnLongNail(itemBeat, startbeat);
break;
case ObjectType.Sweet:
// dynamically determine sweet based on pattern and last pattern
Sweet.sweetsType sweetType = Sweet.sweetsType.None;
switch (patternType)
{
case PatternType.Pudding:
SoundByte.PlayOneShotGame("nailCarpenter/one", itemBeat, forcePlay: true);
sweetType = Sweet.sweetsType.Pudding;
break;
case PatternType.Cherry:
SoundByte.PlayOneShotGame("nailCarpenter/three", itemBeat, forcePlay: true);
sweetType = Sweet.sweetsType.CherryPudding;
break;
case PatternType.Cake:
SoundByte.PlayOneShotGame("nailCarpenter/alarm", itemBeat, forcePlay: true);
sweetType = Sweet.sweetsType.ShortCake;
BeatAction.New(instance, new List<BeatAction.Action>()
{
new BeatAction.Action(itemBeat, delegate
{
EffectExclamRed.DoScaledAnimationAsync("exclamAppear", 0.25f);
})
});
break;
case PatternType.CakeLong:
SoundByte.PlayOneShotGame("nailCarpenter/signal1", itemBeat, forcePlay: true);
sweetType = Sweet.sweetsType.LayerCake;
BeatAction.New(instance, new List<BeatAction.Action>()
{
new BeatAction.Action(itemBeat, delegate
{
EffectExclamBlue.DoScaledAnimationAsync("exclamAppear", 0.25f);
}),
});
break;
default:
break;
}
if (lastPatternType == PatternType.Cake)
{
SpawnSweet(itemBeat, startbeat, Sweet.sweetsType.Cherry);
}
else if (sweetType != Sweet.sweetsType.None)
{
SpawnSweet(itemBeat, startbeat, sweetType);
}
break;
case ObjectType.ForceCherry:
SpawnSweet(itemBeat, startbeat, Sweet.sweetsType.Cherry);
break;
case ObjectType.ForcePudding:
SoundByte.PlayOneShotGame("nailCarpenter/one", itemBeat);
SpawnSweet(itemBeat, startbeat, Sweet.sweetsType.Pudding);
break;
case ObjectType.ForceCherryPudding:
SoundByte.PlayOneShotGame("nailCarpenter/three", itemBeat, forcePlay: true);
SpawnSweet(itemBeat, startbeat, Sweet.sweetsType.CherryPudding);
break;
case ObjectType.ForceShortCake:
SoundByte.PlayOneShotGame("nailCarpenter/alarm", itemBeat, forcePlay: true);
BeatAction.New(instance, new List<BeatAction.Action>()
{
new BeatAction.Action(itemBeat, delegate
{
EffectExclamRed.DoScaledAnimationAsync("exclamAppear", 0.25f);
})
});
SpawnSweet(itemBeat, startbeat, Sweet.sweetsType.ShortCake);
break;
case ObjectType.ForceLayerCake:
SoundByte.PlayOneShotGame("nailCarpenter/signal1", itemBeat, forcePlay: true);
BeatAction.New(instance, new List<BeatAction.Action>()
{
new BeatAction.Action(itemBeat, delegate
{
EffectExclamBlue.DoScaledAnimationAsync("exclamAppear", 0.25f);
}),
});
SpawnSweet(itemBeat, startbeat, Sweet.sweetsType.LayerCake);
break;
default:
break;
}
}
}
private void SpawnNail(double beat, double startBeat)
{
var newNail = Instantiate(baseNail, nailHolder).GetComponent<Nail>();
newNail.targetBeat = beat;
newNail.targetX = nailHolder.position.x;
newNail.metresPerSecond = scrollMetresPerBeat;
newNail.Init();
newNail.gameObject.SetActive(true);
}
private void SpawnLongNail(double beat, double startBeat)
{
var newNail = Instantiate(baseLongNail, nailHolder).GetComponent<LongNail>();
newNail.targetBeat = beat;
newNail.targetX = nailHolder.position.x;
newNail.metresPerSecond = scrollMetresPerBeat;
newNail.Init();
newNail.gameObject.SetActive(true);
}
private void SpawnSweet(double beat, double startBeat, Sweet.sweetsType sweetType)
{
var newSweet = Instantiate(baseSweet, nailHolder).GetComponent<Sweet>();
newSweet.targetBeat = beat;
newSweet.sweetType = sweetType;
newSweet.targetX = nailHolder.position.x;
newSweet.metresPerSecond = scrollMetresPerBeat;
newSweet.gameObject.SetActive(true);
newSweet.Init();
}
}
}