Skip to content

Commit 4eb2186

Browse files
committed
Add AmbientLight control commands. Clean up Viewport a little.
1 parent eda6eca commit 4eb2186

File tree

5 files changed

+213
-25
lines changed

5 files changed

+213
-25
lines changed

EventCommandCodex.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<Version>0.6.0-beta</Version>
3+
<Version>0.7.0-beta</Version>
44
<TargetFramework>net6.0</TargetFramework>
55

66
<ModFolderName>$(MOD_NAME)</ModFolderName>

docs/author-guide.md

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ This document explains how to use the event commands added by this mod.
2222
* [Viewport Control](#viewport-control)
2323
* [ViewportMove](#viewportmove)
2424
* [ViewportAwait](#viewportawait)
25-
* [ViewportStop](#viewportstop)
25+
* [ViewportHalt](#viewporthalt)
26+
* [Ambient Light Control](#ambient-light-control)
27+
* [AmbientLightShift](#ambientlightshift)
28+
* [AmbientLightAwait](#ambientlightawait)
29+
* [AmbientLightHalt](#ambientlighthalt)
2630
* [World Control](#world-control)
2731
* [WorldAdvanceTime](#worldadvancetime)
2832
* [TemporaryMapTiles](#temporarymaptiles)
@@ -277,7 +281,7 @@ well.
277281

278282
### `ViewportMove`
279283

280-
`ichortower.ECC_ViewportMove <x> <y> <time> [override] [wait]`
284+
`ichortower.ECC_ViewportMove <x> <y> <duration> [override] [wait]`
281285

282286
This command sets up a viewport movement.
283287

@@ -289,7 +293,7 @@ case-insensitive), so e.g. `a14 a20` would mean to move the viewport to (14,20).
289293
You may mix and match these, so `a22 3` is valid and means to move to x 22 and
290294
y 3 tiles down from the current position.
291295

292-
`time` is in milliseconds and determines how long the move will take to
296+
`duration` is in milliseconds and determines how long the move will take to
293297
complete.
294298

295299
By default, a viewport move will be queued behind any ongoing moves, and the
@@ -309,11 +313,61 @@ vanilla's `viewport move` moves the viewport. Do not mix and match them.
309313
This command blocks until all queued viewport moves have completed.
310314

311315

312-
### `ViewportStop`
316+
### `ViewportHalt`
313317

314-
`ichortower.ECC_ViewportStop`
318+
`ichortower.ECC_ViewportHalt`
315319

316-
This command immediately halts and empties the viewport move queue.
320+
This command immediately halts any ongoing viewport moves and empties the
321+
viewport move queue.
322+
323+
324+
## Ambient Light Control
325+
326+
Although vanilla has the `ambientLight` command which lets you set the ambient
327+
light color and intensity at any time, it is merely immediate and there is no
328+
way to transition smoothly. These commands give you the ability to fade it
329+
gradually and queue the operations, just like with the viewport control
330+
commands; you can use this to help simulate things like sunsets.
331+
332+
333+
### `AmbientLightShift`
334+
335+
`ichortower.ECC_AmbientLightShift <red> <green> <blue> <duration> [override] [wait]`
336+
337+
This command sets up a gradual ambient light shift.
338+
339+
`red`, `green`, and `blue` should be integers from 0 to 255, representing the
340+
RGB values of the color that will be **subtracted** from white to generate the
341+
game tint, just as it is with vanilla's `ambientLight` command.
342+
343+
**Note:** The game's draw code has a special case for when all three values of
344+
the ambient light color are 255 (which would normally mean full darkness): this
345+
is treated as full brightness instead, the same as `0 0 0`. I recommend
346+
avoiding this value, and using `254 254 254` instead if you need pitch black;
347+
it's close enough and won't cause flashing in and out of darkness.
348+
349+
`duration` is in milliseconds and determines how long the shift will take to
350+
complete.
351+
352+
Like with `ViewportMove`, by default, the shift will be queued behind any
353+
ongoing shifts, and the command will not block. Just like that command, you can
354+
give the optional argument `override` to first empty the queue before starting,
355+
and you can give the optional argument `wait` to block until the queue empties.
356+
357+
358+
### `AmbientLightAwait`
359+
360+
`ichortower.ECC_AmbientLightAwait`
361+
362+
This command blocks until all queued ambient light shifts have completed.
363+
364+
365+
### `AmbientLightHalt`
366+
367+
`ichortower.ECC_AmbientLightHalt`
368+
369+
This command immediately halts all ongoing ambient light shifts and empties
370+
the light shift queue.
317371

318372

319373
## World Control

src/AmbientLight.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using Microsoft.Xna.Framework;
2+
using StardewValley;
3+
using StardewValley.Extensions;
4+
using StardewModdingAPI.Events;
5+
using System.Collections.Generic;
6+
7+
using Log = ichortower.TowerCore.Log;
8+
using Main = ichortower.TowerCore.Main;
9+
using SEvent = StardewValley.Event;
10+
11+
namespace ichortower.ECC;
12+
13+
/*
14+
*
15+
* Event commands for adjusting the ambient light of the event map over time.
16+
* Vanilla has `ambientLight` but it's merely instant, so this lets you do
17+
* gradual transitions (e.g. for a sunset effect).
18+
*
19+
*/
20+
21+
internal class AmbientLight
22+
{
23+
24+
public static void command_AmbientLightShift(Event evt, string[] args, EventContext context)
25+
{
26+
bool queueMode = true;
27+
string error;
28+
if (!ArgUtility.TryGetInt(args, 1, out int red, out error, "int red") ||
29+
!ArgUtility.TryGetInt(args, 2, out int green, out error, "int green") ||
30+
!ArgUtility.TryGetInt(args, 3, out int blue, out error, "int blue") ||
31+
!ArgUtility.TryGetInt(args, 4, out int duration, out error, "int duration")) {
32+
context.LogErrorAndSkip(error);
33+
return;
34+
}
35+
for (int i = 5; i < args.Length; ++i) {
36+
if (args[i].EqualsIgnoreCase("override")) {
37+
queueMode = false;
38+
}
39+
else if (args[i].EqualsIgnoreCase("wait")) {
40+
evt.InsertNextCommand($"{Main.ModId}_AmbientLightAwait");
41+
}
42+
else {
43+
context.LogError($"unknown argument '{args[i]}'",
44+
willSkip: false);
45+
}
46+
}
47+
if (!queueMode) {
48+
// this also clears the queue
49+
StopAmbientLightWatcher();
50+
}
51+
ambientLightQueue.Add(new AmbientLightShift(new Color(red, green, blue), duration));
52+
StartAmbientLightWatcher();
53+
++evt.CurrentCommand;
54+
}
55+
56+
public static void command_AmbientLightAwait(Event evt, string[] args, EventContext context)
57+
{
58+
if (ambientLightQueue.Count == 0) {
59+
++evt.CurrentCommand;
60+
}
61+
}
62+
63+
public static void command_AmbientLightHalt(Event evt, string[] args, EventContext context)
64+
{
65+
StopAmbientLightWatcher();
66+
++evt.CurrentCommand;
67+
}
68+
69+
70+
/*
71+
* Implementation details
72+
*/
73+
74+
private static List<AmbientLightShift> ambientLightQueue = new();
75+
76+
private static System.EventHandler<UpdateTickedEventArgs> ambientLightWatcher = null;
77+
78+
private static void StartAmbientLightWatcher()
79+
{
80+
if (ambientLightWatcher != null) {
81+
return;
82+
}
83+
Log.Debug("Starting ambientLight watcher");
84+
ambientLightWatcher = AmbientLightFunction;
85+
ichortower.TowerCore.Main.Helper.Events.GameLoop.UpdateTicked += ambientLightWatcher;
86+
}
87+
88+
private static void AmbientLightFunction(object sender, UpdateTickedEventArgs e)
89+
{
90+
if (!ichortower.TowerCore.Game.IsActive()) {
91+
return;
92+
}
93+
if (Game1.eventOver || !Game1.eventUp || ambientLightQueue.Count == 0) {
94+
StopAmbientLightWatcher();
95+
return;
96+
}
97+
int now = (int)Game1.currentGameTime.TotalGameTime.TotalMilliseconds;
98+
AmbientLightShift head = ambientLightQueue[0];
99+
if (head.StartMs == 0) {
100+
head.StartMs = now;
101+
head.Start = Game1.ambientLight;
102+
Log.Debug($"starting shift {head.Start} -> {head.End}");
103+
return;
104+
}
105+
if (now >= head.StartMs + head.Duration) {
106+
Log.Debug("ambientLight shift complete");
107+
Game1.ambientLight = head.End;
108+
ambientLightQueue.RemoveAt(0);
109+
return;
110+
}
111+
float t = (float)(now - head.StartMs) / (float)head.Duration;
112+
int r = (int)Utility.Lerp((float)head.Start.R, (float)head.End.R, t);
113+
int g = (int)Utility.Lerp((float)head.Start.G, (float)head.End.G, t);
114+
int b = (int)Utility.Lerp((float)head.Start.B, (float)head.End.B, t);
115+
Game1.ambientLight = new Color(r, g, b);
116+
}
117+
118+
private static void StopAmbientLightWatcher()
119+
{
120+
ambientLightQueue.Clear();
121+
if (ambientLightWatcher is null) {
122+
return;
123+
}
124+
Log.Debug("Stopping ambientLight watcher");
125+
ichortower.TowerCore.Main.Helper.Events.GameLoop.UpdateTicked -= ambientLightWatcher;
126+
ambientLightWatcher = null;
127+
}
128+
129+
}
130+
131+
internal class AmbientLightShift
132+
{
133+
public Color Start = Color.White;
134+
public Color End = Color.White;
135+
public int Duration = 0;
136+
public int StartMs = 0;
137+
138+
// doesn't set Start and StartMs because those are read when the queue starts the shift
139+
public AmbientLightShift(Color target, int duration)
140+
{
141+
End = target;
142+
Duration = duration;
143+
}
144+
}

src/Main.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public override void Entry(IModHelper helper)
2020

2121
List<Type> types = new() {
2222
typeof(ichortower.ECC.Actor),
23+
typeof(ichortower.ECC.AmbientLight),
2324
typeof(ichortower.ECC.Stream),
2425
typeof(ichortower.ECC.Viewport),
2526
typeof(ichortower.ECC.World),

src/Viewport.cs

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,12 @@ internal class Viewport
4141
*/
4242
public static void command_ViewportMove(Event evt, string[] args, EventContext context)
4343
{
44-
if (args.Length < 4) {
45-
context.LogErrorAndSkip("requires at least three arguments (x y time)");
46-
return;
47-
}
4844
bool queueMode = true;
4945
string err = "";
50-
if (!TryGetTarget(args[1], out int xDest, out ViewportMoveType xType, out err)) {
51-
context.LogErrorAndSkip($"failed to parse x coordinate: {err}");
52-
return;
53-
}
54-
if (!TryGetTarget(args[2], out int yDest, out ViewportMoveType yType, out err)) {
55-
context.LogErrorAndSkip($"failed to parse y coordinate: {err}");
56-
return;
57-
}
58-
if (!int.TryParse(args[3], out int duration)) {
59-
context.LogErrorAndSkip($"failed to parse duration from '{args[3]}'");
46+
if (!TryGetTarget(args[1], out int xDest, out ViewportMoveType xType, out err) ||
47+
!TryGetTarget(args[2], out int yDest, out ViewportMoveType yType, out err) ||
48+
!ArgUtility.TryGetInt(args, 3, out int duration, out err, "int duration")) {
49+
context.LogErrorAndSkip(err);
6050
return;
6151
}
6252
for (int i = 4; i < args.Length; ++i) {
@@ -187,10 +177,9 @@ private static void ViewportFunction(object sender, UpdateTickedEventArgs e) {
187177
return;
188178
}
189179
// FIXME also do the raindrop position adjustment
190-
Game1.viewport.X = (int)Utility.Lerp((float)head.StartX, (float)head.EndX,
191-
(float)(now - head.StartMs) / (float)head.Duration);
192-
Game1.viewport.Y = (int)Utility.Lerp((float)head.StartY, (float)head.EndY,
193-
(float)(now - head.StartMs) / (float)head.Duration);
180+
float t = (float)(now - head.StartMs) / (float)head.Duration;
181+
Game1.viewport.X = (int)Utility.Lerp((float)head.StartX, (float)head.EndX, t);
182+
Game1.viewport.Y = (int)Utility.Lerp((float)head.StartY, (float)head.EndY, t);
194183
}
195184

196185
private static void StopViewportWatcher()

0 commit comments

Comments
 (0)