HeavenStudioPlus/Assets/Scripts/Games/CropStomp/CropStomp.cs
minenice55 3002e48350
Alternate Control Styles Support (#554)
* add mouse controller

* support different control styles in options

deprecate old input check methods

* fully functional input actions system

* btsds InputAction

* blue bear InputAction

* more games

fix bugs with some input related systems

* coin toss re-toss

* cheer readers touch

* dog ninja touch

* multiple games

* last of the easy games' touch

* more specialized games

* specialized games 2

* finish ktb games

* remove legacy settings disclaimer

* "only" two games left

* karate man touch

* rockers touch

still needs fixes and bad judge strum

* DSGuy flicking animation

* playstyle chart property

* improve performance of minigame preloading

* improve look of cursor

make assetbundles use chunk-based compression
refactor assetbundle loading methods a bit

* prime conductor stream playback to stabilize seeking operations

* fix air rally swing on pad release

* use virtual mouse pointer

* add UniTask

* make BeatAction use UniTask

* implement UniTask to replace some coroutines

* add touch style UI elements and effects

games now support the ability to define two cursor colours if they need split screen touch inputs

* update plugins and buildscript

* implement thresholded pointer position clipping

* fix clamping

* instant show / hide

fix discord game SDK crashes
2023-10-29 19:44:47 +00:00

465 lines
17 KiB
C#

using DG.Tweening;
using NaughtyBezierCurves;
using HeavenStudio.Util;
using System;
using System.Collections.Generic;
using UnityEngine;
using Jukebox;
namespace HeavenStudio.Games.Loaders
{
using static Minigames;
public static class NtrCropLoader
{
public static Minigame AddGame(EventCaller eventCaller) {
return new Minigame("cropStomp", "Crop Stomp", "c0f0b8", false, false, new List<GameAction>()
{
new GameAction("start marching", "Start Marching")
{
function = delegate { CropStomp.instance.StartMarching(eventCaller.currentEntity.beat); },
defaultLength = 2f,
inactiveFunction = delegate { CropStomp.MarchInactive(eventCaller.currentEntity.beat); }
},
new GameAction("veggies", "Veggies")
{
defaultLength = 4f,
resizable = true
},
new GameAction("mole", "Mole")
{
preFunction = delegate
{
if (eventCaller.currentEntity["mute"]) return;
CropStomp.MoleSound(eventCaller.currentEntity.beat);
},
defaultLength = 2f,
parameters = new List<Param>()
{
new Param("mute", false, "Mute", "Should the mole laugh sound be muted?")
},
preFunctionLength = 6
},
new GameAction("end", "End")
{
parameters = new List<Param>()
{
new Param("mute", true, "Mute Humming?")
}
},
new GameAction("plantCollect", "Veggie Collection Values")
{
function = delegate { var e = eventCaller.currentEntity;
CropStomp.instance.SetCollectThresholds(e["threshold"], e["limit"], e["force"], e["forceAmount"]); },
defaultLength = 0.5f,
parameters = new List<Param>()
{
new Param("threshold", new EntityTypes.Integer(1, 80, 8), "Threshold", "For each time the threshold is met a new plant will appear in the veggie bag."),
new Param("limit", new EntityTypes.Integer(1, 1000, 80), "Limit", "What is the limit for plants collected?"),
new Param("force", false, "Force Amount of Collected Plants", "", new List<Param.CollapseParam>()
{
new Param.CollapseParam((x, _) => (bool)x, new string[] { "forceAmount" })
}),
new Param("forceAmount", new EntityTypes.Integer(0, 1000, 0), "Force Amount")
}
}
},
new List<string>() {"ntr", "keep"},
"ntrstomp", "en",
new List<string>() {}
);
}
}
}
namespace HeavenStudio.Games
{
using Scripts_CropStomp;
public class CropStomp : Minigame
{
const float stepDistance = 2.115f;
//public static float[] moleSoundOffsets = new float[]{ 0.134f, 0.05f, 0.061f };
float scrollRate => stepDistance / (Conductor.instance.pitchedSecPerBeat * 2f);
float grassWidth;
float dotsWidth = 19.2f;
private double newBeat = -1f; // So that marching can happen on beat 0.
private double marchStartBeat = -1f;
private double marchEndBeat = double.MaxValue;
private bool willNotHum = true;
private double marchOffset;
private int currentMarchBeat;
private int stepCount;
private bool isStepping;
private static double inactiveStart = -1f;
public bool isMarching => marchStartBeat != -1f;
[NonSerialized] public bool isFlicking;
public GameObject baseVeggie;
public GameObject baseMole;
public Animator legsAnim;
public Animator bodyAnim;
public Transform farmerTrans;
public SpriteRenderer grass;
public Transform grassTrans;
public Transform dotsTrans;
public Transform scrollingHolder;
public Transform veggieHolder;
public Farmer farmer;
public BezierCurve3D pickCurve;
public BezierCurve3D moleCurve;
private Tween shakeTween;
public ParticleSystem hitParticle;
public static CropStomp instance;
public static PlayerInput.InputAction InputAction_Flick =
new("NtrStompFlick", new int[] { IAEmptyCat, IAFlickCat, IAEmptyCat },
IA_Empty, IA_TouchFlick, IA_Empty);
private void Awake()
{
instance = this;// Finding grass sprite width for grass scrolling.
farmer.Init();
var grassSprite = grass.sprite;
var borderLeft = grassSprite.rect.xMin + grassSprite.border.x;
var borderRight = grassSprite.rect.xMax - grassSprite.border.z;
var borderWidthPixels = borderRight - borderLeft;
grassWidth = borderWidthPixels / grassSprite.pixelsPerUnit;
legsAnim.Play("LiftFront", 0, 1); // Start with leg up.
// Initialize vegetables.
var cond = Conductor.instance;
var entities = GameManager.instance.Beatmap.Entities;
double startBeat = cond.songPositionInBeatsAsDouble;
double endBeat = double.MaxValue;
if (inactiveStart == -1f)
{
// Find the beat of the closest "start marching" event.
var marchStarts = entities.FindAll(m => m.datamodel == "cropStomp/start marching");
for (int i = 0; i < marchStarts.Count; i++)
{
var sampleBeat = marchStarts[i].beat;
if (cond.songPositionInBeatsAsDouble <= sampleBeat + 0.25f) // 0.25-beat buffer in case the start marching event is directly next to the game switch event.
{
startBeat = sampleBeat;
break;
}
}
}
else
{
// Find the beat of the next step, assuming marching started at inactiveStart.
int stepsPassed = 0;
while (inactiveStart + (stepsPassed * 2f) < cond.songPositionInBeatsAsDouble)
{
stepsPassed++;
if (stepsPassed > 1000)
{
Debug.Log("Loop broke!");
return;
}
}
startBeat = inactiveStart + (stepsPassed * 2f);
// Cue the marching proper to begin when applicable.
BeatAction.New(this, new List<BeatAction.Action>()
{
new BeatAction.Action(startBeat - 0.25f, delegate { StartMarching(startBeat); })
});
inactiveStart = -1f;
}
// find out when the next game switch (or remix end) happens
var allEnds = EventCaller.GetAllInGameManagerList("gameManager", new string[] { "switchGame", "end" });
if (allEnds.Count == 0)
{
endBeat = double.MaxValue;
}
else
{
allEnds.Sort((x, y) => x.beat.CompareTo(y.beat));
//get the beat of the closest end event
foreach (var end in allEnds)
{
if (end.datamodel != "gameManager/end" && end.datamodel.Split(2) == "cropStomp") continue;
if (end.beat > startBeat)
{
endBeat = end.beat;
break;
}
}
}
// Veggie and mole events.
var vegEvents = entities.FindAll(v => v.datamodel == "cropStomp/veggies");
var moleEvents = entities.FindAll(m => m.datamodel == "cropStomp/mole");
// Spawn veggies.
for (int i = 0; i < vegEvents.Count; i++)
{
var vegBeat = vegEvents[i].beat;
var vegLength = vegEvents[i].length;
// Only consider veggie events that aren't past the start point.
if (startBeat <= vegBeat + vegLength)
{
int veggiesInEvent = Mathf.CeilToInt(vegLength + 1) / 2;
for (int b = 0; b < veggiesInEvent; b++)
{
var targetVeggieBeat = vegBeat + 2f * b;
if (startBeat <= targetVeggieBeat && targetVeggieBeat < endBeat)
{
SpawnVeggie(targetVeggieBeat, startBeat, false);
}
}
}
}
// Spawn moles.
for (int i = 0; i < moleEvents.Count; i++)
{
var moleBeat = moleEvents[i].beat;
if (startBeat <= moleBeat && moleBeat < endBeat)
{
SpawnVeggie(moleBeat, startBeat, true);
}
}
}
List<RiqEntity> cuedMoleSounds = new List<RiqEntity>();
public override void OnGameSwitch(double beat)
{
SetInitTresholds(beat);
SetMarchEndBeat(beat);
}
public override void OnPlay(double beat)
{
SetInitTresholds(beat);
SetMarchEndBeat(beat);
}
private void SetMarchEndBeat(double beat)
{
double nextEndBeat = double.MaxValue;
var nextEnd = EventCaller.GetAllInGameManagerList("gameManager", new string[] { "switchGame", "end" }).Find(e => e.beat > beat);
if (nextEnd != null) nextEndBeat = nextEnd.beat;
var allEnds = EventCaller.GetAllInGameManagerList("cropStomp", new string[] { "end" });
var tempEnds = allEnds.FindAll(x => x.beat >= beat && x.beat < nextEndBeat);
if (tempEnds.Count == 0) return;
marchEndBeat = tempEnds[0].beat;
willNotHum = tempEnds[0]["mute"];
}
public static void MoleSound(double beat)
{
MultiSound.Play(new MultiSound.Sound[]
{
new MultiSound.Sound("cropStomp/moleNyeh", beat - 2, 1, 1, false, 0.134),
new MultiSound.Sound("cropStomp/moleHeh1", beat - 1.5, 1, 1, false, 0.05),
new MultiSound.Sound("cropStomp/moleHeh2", beat - 1, 1, 1, false, 0.061)
}, forcePlay: true);
}
private void Update()
{
var cond = Conductor.instance;
if (!cond.isPlaying)
return;
if (!isMarching)
return;
// Debug.Log(newBeat);
bool cameraLocked = cond.songPositionInBeats >= marchEndBeat;
bool isHumming = !(cameraLocked && willNotHum);
if (cond.ReportBeat(ref newBeat, marchOffset, true))
{
currentMarchBeat += 1;
PlayAnims();
if (currentMarchBeat % 2 != 0 && isHumming) //step sound
{
MultiSound.Play(new MultiSound.Sound[] {new MultiSound.Sound("cropStomp/hmm", newBeat + marchOffset)});
}
}
if (PlayerInput.GetIsAction(InputAction_BasicRelease) && !IsExpectingInputNow(InputAction_BasicRelease))
{
bodyAnim.Play("Raise");
}
if (PlayerInput.GetIsAction(InputAction_Flick) && !IsExpectingInputNow(InputAction_FlickRelease))
{
bodyAnim.Play("Pick");
}
if (cameraLocked) return;
// Object scroll.
var scrollPos = scrollingHolder.localPosition;
var newScrollX = scrollPos.x + (scrollRate * Time.deltaTime);
scrollingHolder.localPosition = new Vector3(newScrollX, scrollPos.y, scrollPos.z);
// Grass scroll.
var grassPos = grassTrans.localPosition;
var newGrassX = grassPos.x + (scrollRate * Time.deltaTime);
newGrassX = (newGrassX % (grassWidth * 4.5f));
grassTrans.localPosition = new Vector3(newGrassX, grassPos.y, grassPos.z);
// Dots scroll
var dotsPos = dotsTrans.localPosition;
var newDotsX = dotsPos.x + (scrollRate * Time.deltaTime);
newDotsX = (newDotsX % dotsWidth);
dotsTrans.localPosition = new Vector3(newDotsX, dotsPos.y, dotsPos.z);
}
private void LateUpdate()
{
if (!isMarching)
return;
isFlicking = false;
}
public void SetCollectThresholds(int thresholdEvolve, int limit, bool force, int forceAmount)
{
farmer.plantThreshold = thresholdEvolve;
farmer.plantLimit = limit;
if (force) Farmer.collectedPlants = forceAmount;
farmer.UpdatePlants();
}
private void SetInitTresholds(double beat)
{
var allCollects = EventCaller.GetAllInGameManagerList("cropStomp", new string[] { "plantCollect" });
if (allCollects.Count == 0) return;
var tempCollect = allCollects.FindLast(x => x.beat < beat);
if (tempCollect == null) return;
SetCollectThresholds(tempCollect["threshold"], tempCollect["limit"], tempCollect["force"], tempCollect["forceAmount"]);
}
public void CollectPlant(int veggieType)
{
farmer.CollectPlant(veggieType);
}
private void PlayAnims()
{
// Step.
if (currentMarchBeat % 2 != 0)
{
// Don't step if already stomped.
if (!isStepping)
{
stepCount += 1;
var stepAnim = (stepCount % 2 != 0 ? "StepFront" : "StepBack");
legsAnim.Play(stepAnim, 0, 0);
isStepping = true;
}
}
// Lift.
else
{
var liftAnim = (stepCount % 2 != 0 ? "LiftBack" : "LiftFront");
legsAnim.Play(liftAnim, 0, 0);
var farmerPos = farmerTrans.localPosition;
farmerTrans.localPosition = new Vector3(farmerPos.x - stepDistance, farmerPos.y, farmerPos.z);
isStepping = false;
}
}
public void StartMarching(double beat)
{
marchStartBeat = beat;
marchOffset = marchStartBeat % 1;
currentMarchBeat = 0;
stepCount = 0;
farmer.nextStompBeat = beat;
}
public void Stomp()
{
// Don't increment step counter if autostep stepped already.
if (!isStepping)
stepCount += 1;
var stompAnim = (stepCount % 2 != 0 ? "StompFront" : "StompBack");
legsAnim.Play(stompAnim, 0, 0);
SoundByte.PlayOneShotGame("cropStomp/stomp");
if (shakeTween != null)
shakeTween.Kill(true);
DOTween.Punch(() => GameCamera.additionalPosition, x => GameCamera.additionalPosition = x, new Vector3(0, 0.75f, 0),
Conductor.instance.pitchedSecPerBeat*0.5f, 18, 1f);
isStepping = true;
}
private void SpawnVeggie(double beat, double startBeat, bool isMole)
{
var newVeggie = GameObject.Instantiate(isMole ? baseMole : baseVeggie, veggieHolder).GetComponent<Veggie>();
newVeggie.targetBeat = beat;
var veggieX = (beat - startBeat) * -stepDistance / 2f;
newVeggie.transform.localPosition = new Vector3((float)veggieX, 0f, 0f);
newVeggie.Init();
newVeggie.gameObject.SetActive(true);
}
public static void MarchInactive(double beat)
{
if (GameManager.instance.currentGame == "cropStomp") //this function is only meant for making march sounds while the game is inactive
{
return;
}
inactiveStart = beat;
RiqEntity gameSwitch = GameManager.instance.Beatmap.Entities.Find(c => c.beat >= beat && c.datamodel == "gameManager/switchGame/cropStomp");
if (gameSwitch == null)
return;
int length = (int)Math.Ceiling((gameSwitch.beat - beat)/2);
MultiSound.Sound[] sounds = new MultiSound.Sound[length];
for(int i = 0; i < length; i++)
{
sounds[i] = new MultiSound.Sound("cropStomp/hmm", beat + i*2);
}
MultiSound.Play(sounds, forcePlay:true);
}
}
}