Duplication issue? How to cancel/stop a future-yielding script correctly?

Let’s say I have a role-play job system that consist of these steps:

  1. Grab item in the floor
  2. Show an arrow indicating the player where to go
  3. Deliver it to a door with a proximity prompt (wait for player to trigger the prompt, then continue to next step)
  4. Repeat until player clicks end job shift

Everything is fine, however, ending the job is where the issue is. When the job is ended, all of the proximity prompts gets their Enabled property set to false, and the arrow gets deleted. However, the proximity prompt would be still waiting for the player to trigger the prompt, and when it triggers it again in the future, the steps will be duplicated and in this case create a second arrow (and also continue the other steps, which would be waiting again for the proximity prompt to be triggered and repeat the steps) because in one job shift the proximity prompt was waiting for it to be triggered, and in this current shift it got triggered so both of the shifts were executed. How can this be fixed?

I know I could use a flag for this (a variable indicating, in this case, if the job has ended then doing checks), but if I do so, in my real code there will be a lot of if conditions checking for that flag, because I have extra steps and some of them are also proximity prompts waiting to be triggered, plus, it can also be considered code smell.

It’s pretty complicated to explain, tried my best.

Hello, could you provide us with scripts please?
(Not the actual job part where player does stuff, just starting and ending. Basically main aspects that cause the problem)

1 Like

This is done in roblox-ts, it’s almost the same as Luau, if you have any questions to know what does what, tell me.

import TargetArrow from "@Client/Classes/TargetArrow";
import _JobsController, { JobModule } from "@Client/Controllers/Jobs";
import { Proton } from "@rbxts/proton";
import { Workspace } from "@rbxts/services";

const JobsController = Proton.get(_JobsController);

const _S = {
	Rounds: 4,
};

const Instances = Workspace.Jobs.Mailman;

let JobInterface: ReplicatedStorage["Interfaces"]["JobInterface"];
let CurrentTargetArrow: TargetArrow | undefined;

function WaitForPromptTrigger(prompt: ProximityPrompt): void {
	prompt.Enabled = true;
	prompt.Triggered.Wait();
	prompt.Enabled = false;
}

function HandleNextRound(): void {
	const InsightsStatsFrame = JobInterface.Content.Insights.Stats;
	InsightsStatsFrame.Rounds.Amount.Value += 1;

	if (InsightsStatsFrame.Rounds.Amount.Value >= _S.Rounds) return JobsController.End();
	StartJob();
}

function StartJob(): void {
	const Postboxes = Instances.Postboxes.GetChildren();
	const SelectedPostbox = Postboxes[new Random().NextInteger(1, Postboxes.size()) - 1];
	const SelectedPostboxPackage = SelectedPostbox.WaitForChild("Package") as MeshPart;
	const SelectedPostboxPrompt = SelectedPostboxPackage.WaitForChild("GrabPackagePrompt") as ProximityPrompt;

	SelectedPostboxPackage.Transparency = 0;
	WaitForPromptTrigger(SelectedPostboxPrompt);
	CurrentTargetArrow = new TargetArrow(Instances.PostOffice);
	SelectedPostboxPackage.Transparency = 1;

	WaitForPromptTrigger(Instances.PostOffice.DeliverPackagePrompt);
	CurrentTargetArrow.Destroy();
	CurrentTargetArrow = undefined;

	HandleNextRound();
}

// ─── Job Events ──────────────────────────────────────────────────────────────

function OnStart(jobInterface: ReplicatedStorage["Interfaces"]["JobInterface"]): void {
	JobInterface = jobInterface;
	StartJob();
}

function OnEnd(): void {
	if (CurrentTargetArrow) CurrentTargetArrow.Destroy();

	Instances.PostOffice.GrabPackagePrompt.Enabled = false;
	Instances.PostOffice.DeliverPackagePrompt.Enabled = false;

	Instances.Houses.GetChildren().forEach((children) => {
		if (!children.IsA("Part")) return;
		(children.WaitForChild("DeliverPackagePrompt") as ProximityPrompt).Enabled = false;
	});

	Instances.Postboxes.GetChildren().forEach((children) => {
		if (!children.IsA("Model")) return;
		const PackageModel = children.WaitForChild("Package") as MeshPart;
		PackageModel.Transparency = 1;
		(PackageModel.WaitForChild("GrabPackagePrompt") as ProximityPrompt).Enabled = false;
	});
}

export default {
	OnStart,
	OnEnd,
} as JobModule;

1 Like

I’m pretty sure this is exactly what the promise pattern solves

1 Like

How could promises be used here in this case?

1 Like

I never got fully in to it but here’s a guide Promises and Why You Should Use Them Check out the tweening example, specifically the onCancel stuff.

1 Like

I don’t think promises might be useful in this case… Considering promises are to perform asynchronous actions and then do something with the result of that action, I don’t think they will be helpful because the steps of the job are happening in a sequential order, and do not involve any asynchronous operations… Thank you so much your help and your time though! :heart: