Skip to content

Commit 3c00cb2

Browse files
committed
Multi-Room Strawberry Seeds
1 parent 26ceb46 commit 3c00cb2

File tree

11 files changed

+365
-1
lines changed

11 files changed

+365
-1
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module SpringCollab2020MultiRoomStrawberry
2+
3+
using ..Ahorn, Maple
4+
5+
@mapdef Entity "SpringCollab2020/MultiRoomStrawberry" MultiRoomStrawberry(x::Integer, y::Integer,
6+
name::String="multi_room_strawberry",winged::Bool=false, moon::Bool=false, checkpointID::Integer=-1, order::Integer=-1)
7+
8+
const placements = Ahorn.PlacementDict(
9+
"Multi-Room Strawberry (Spring Collab 2020)" => Ahorn.EntityPlacement(
10+
MultiRoomStrawberry
11+
)
12+
)
13+
14+
# winged, moon
15+
sprites = Dict{Tuple{Bool, Bool}, String}(
16+
(false, false) => "collectables/strawberry/normal00",
17+
(true, false) => "collectables/strawberry/wings01",
18+
(false, true) => "collectables/moonBerry/normal00",
19+
(true, true) => "collectables/moonBerry/normal00"
20+
)
21+
22+
function Ahorn.selection(entity::MultiRoomStrawberry)
23+
x, y = Ahorn.position(entity)
24+
25+
moon = get(entity.data, "moon", false)
26+
winged = get(entity.data, "winged", false)
27+
sprite = sprites[(winged, moon)]
28+
29+
return Ahorn.getSpriteRectangle(sprite, x, y)
30+
end
31+
32+
function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::MultiRoomStrawberry, room::Maple.Room)
33+
x, y = Ahorn.position(entity)
34+
35+
moon = get(entity.data, "moon", false)
36+
winged = get(entity.data, "winged", false)
37+
sprite = sprites[(winged, moon)]
38+
39+
Ahorn.drawSprite(ctx, sprite, x, y)
40+
end
41+
42+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module SpringCollab2020MultiRoomStrawberrySeed
2+
3+
using ..Ahorn, Maple
4+
5+
@mapdef Entity "SpringCollab2020/MultiRoomStrawberrySeed" MultiRoomStrawberrySeed(x::Integer, y::Integer,
6+
strawberryName::String="multi_room_strawberry", sprite::String="strawberry/seed", ghostSprite::String="ghostberry/seed", index::Int=-1)
7+
8+
const placements = Ahorn.PlacementDict(
9+
"Multi-Room Strawberry Seed (Spring Collab 2020)" => Ahorn.EntityPlacement(
10+
MultiRoomStrawberrySeed
11+
)
12+
)
13+
14+
15+
function Ahorn.selection(entity::MultiRoomStrawberrySeed)
16+
x, y = Ahorn.position(entity)
17+
sprite = "collectables/" * get(entity.data, "sprite", "strawberry/seed") * "00"
18+
19+
return Ahorn.getSpriteRectangle(sprite, x, y)
20+
end
21+
22+
function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::MultiRoomStrawberrySeed, room::Maple.Room)
23+
sprite = "collectables/" * get(entity.data, "sprite", "strawberry/seed") * "00"
24+
25+
Ahorn.drawSprite(ctx, sprite, 0, 0)
26+
end
27+
28+
end

Entities/MultiRoomStrawberry.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using Celeste.Mod.Entities;
2+
using Microsoft.Xna.Framework;
3+
using Monocle;
4+
using System.Collections.Generic;
5+
6+
namespace Celeste.Mod.SpringCollab2020.Entities {
7+
[CustomEntity("SpringCollab2020/MultiRoomStrawberry")]
8+
[RegisterStrawberry(true, false)]
9+
class MultiRoomStrawberry : Strawberry {
10+
private int seedCount;
11+
private EntityID id;
12+
13+
public MultiRoomStrawberry(EntityData data, Vector2 offset, EntityID gid) : base(data, offset, gid) {
14+
seedCount = data.Int("seedCount");
15+
id = gid;
16+
}
17+
18+
public override void Added(Scene scene) {
19+
// trick the berry into thinking it has vanilla seeds, so that it doesn't appear right away
20+
StrawberrySeed dummySeed = new StrawberrySeed(this, Vector2.Zero, 0, false);
21+
Seeds = new List<StrawberrySeed> { dummySeed };
22+
23+
base.Added(scene);
24+
25+
scene.Remove(dummySeed);
26+
Seeds = null;
27+
}
28+
29+
public override void Update() {
30+
base.Update();
31+
32+
if (WaitingOnSeeds) {
33+
Player player = Scene.Tracker.GetEntity<Player>();
34+
if (player != null) {
35+
// look at all the player followers, and filter all the seeds that match our berry.
36+
List<StrawberrySeed> matchingSeeds = new List<StrawberrySeed>();
37+
foreach (Follower follower in player.Leader.Followers) {
38+
if (follower.Entity is MultiRoomStrawberrySeed seed) {
39+
if (seed.BerryID.Level == id.Level && seed.BerryID.ID == id.ID) {
40+
matchingSeeds.Add(seed);
41+
}
42+
}
43+
}
44+
45+
if (matchingSeeds.Count >= seedCount) {
46+
// all seeds have been gathered! associate the berry and the seeds, then trigger the cutscene.
47+
Seeds = matchingSeeds;
48+
foreach (StrawberrySeed seed in matchingSeeds) {
49+
seed.Strawberry = this;
50+
}
51+
52+
Scene.Add(new CSGEN_StrawberrySeeds(this));
53+
54+
// also clean up the session, since the seeds are now gone.
55+
List<SpringCollab2020Session.MultiRoomStrawberrySeedInfo> seedList = SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds;
56+
for (int i = 0; i < seedList.Count; i++) {
57+
if (seedList[i].BerryID.Level == id.Level && seedList[i].BerryID.ID == id.ID) {
58+
seedList.RemoveAt(i);
59+
i--;
60+
}
61+
}
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
using Celeste.Mod.Entities;
2+
using Microsoft.Xna.Framework;
3+
using Monocle;
4+
using System;
5+
using System.Reflection;
6+
7+
namespace Celeste.Mod.SpringCollab2020.Entities {
8+
[CustomEntity("SpringCollab2020/MultiRoomStrawberrySeed")]
9+
class MultiRoomStrawberrySeed : StrawberrySeed {
10+
public static void Load() {
11+
On.Celeste.Level.LoadLevel += onLoadLevel;
12+
}
13+
14+
public static void Unload() {
15+
On.Celeste.Level.LoadLevel -= onLoadLevel;
16+
}
17+
18+
private static void onLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level self, Player.IntroTypes playerIntro, bool isFromLoader) {
19+
orig(self, playerIntro, isFromLoader);
20+
21+
if (playerIntro != Player.IntroTypes.Transition) {
22+
Player player = self.Tracker.GetEntity<Player>();
23+
24+
if (player != null) {
25+
Vector2 seedPosition = player.Position;
26+
27+
// we have to restore collected strawberry seeds.
28+
foreach (SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo in SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds) {
29+
seedPosition += new Vector2(-12 * (int) player.Facing, -8f);
30+
31+
self.Add(new MultiRoomStrawberrySeed(player, seedPosition, sessionSeedInfo));
32+
}
33+
}
34+
}
35+
}
36+
37+
private static FieldInfo seedFollower = typeof(StrawberrySeed).GetField("follower", BindingFlags.NonPublic | BindingFlags.Instance);
38+
private static FieldInfo seedCanLoseTimer = typeof(StrawberrySeed).GetField("canLoseTimer", BindingFlags.NonPublic | BindingFlags.Instance);
39+
private static FieldInfo seedSprite = typeof(StrawberrySeed).GetField("sprite", BindingFlags.NonPublic | BindingFlags.Instance);
40+
41+
private int index;
42+
public EntityID BerryID;
43+
44+
private float canLoseTimerMirror;
45+
private Player player;
46+
private bool spawnedAsFollower = false;
47+
48+
private string sprite;
49+
private bool ghost;
50+
51+
public MultiRoomStrawberrySeed(Vector2 position, int index, bool ghost, string sprite, string ghostSprite) : base(null, position, index, ghost) {
52+
this.index = index;
53+
this.ghost = ghost;
54+
this.sprite = ghost ? ghostSprite : sprite;
55+
56+
foreach (Component component in this) {
57+
if (component is PlayerCollider playerCollider) {
58+
playerCollider.OnCollide = OnPlayer;
59+
}
60+
}
61+
}
62+
63+
public MultiRoomStrawberrySeed(EntityData data, Vector2 offset) : this(data.Position + offset, data.Int("index"),
64+
SaveData.Instance.CheckStrawberry(new EntityID(data.Attr("berryLevel"), data.Int("berryID"))),
65+
data.Attr("sprite", "strawberry/seed"), data.Attr("ghostSprite", "ghostberry/seed")) {
66+
67+
BerryID = new EntityID(data.Attr("berryLevel"), data.Int("berryID"));
68+
}
69+
70+
private MultiRoomStrawberrySeed(Player player, Vector2 position, SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo)
71+
: this(position, sessionSeedInfo.Index, SaveData.Instance.CheckStrawberry(sessionSeedInfo.BerryID), sessionSeedInfo.Sprite, sessionSeedInfo.Sprite) {
72+
73+
BerryID = sessionSeedInfo.BerryID;
74+
75+
// the seed is collected right away.
76+
this.player = player;
77+
spawnedAsFollower = true;
78+
}
79+
80+
public override void Added(Scene scene) {
81+
base.Added(scene);
82+
83+
if (!spawnedAsFollower) {
84+
if (SceneAs<Level>().Session.GetFlag("collected_seeds_of_" + BerryID.ToString())) {
85+
// if all seeds for this berry were already collected (the berry was already formed), commit remove self.
86+
RemoveSelf();
87+
} else {
88+
// if the seed already follows the player, commit remove self.
89+
foreach (SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo in SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds) {
90+
if (sessionSeedInfo.Index == index && sessionSeedInfo.BerryID.ID == BerryID.ID && sessionSeedInfo.BerryID.Level == BerryID.Level) {
91+
RemoveSelf();
92+
break;
93+
}
94+
}
95+
}
96+
}
97+
}
98+
99+
public override void Awake(Scene scene) {
100+
base.Awake(scene);
101+
102+
if ((ghost && sprite != "ghostberry/seed") || (!ghost && sprite != "strawberry/seed")) {
103+
// the sprite is non-default. replace it.
104+
Sprite vanillaSprite = (Sprite) seedSprite.GetValue(this);
105+
106+
// build the new sprite.
107+
MTexture frame0 = GFX.Game["collectables/" + sprite + "00"];
108+
MTexture frame1 = GFX.Game["collectables/" + sprite + "01"];
109+
110+
Sprite modSprite = new Sprite(GFX.Game, sprite);
111+
modSprite.CenterOrigin();
112+
modSprite.Justify = new Vector2(0.5f, 0.5f);
113+
modSprite.AddLoop("idle", 0.1f, new MTexture[] {
114+
frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0,
115+
frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame1
116+
});
117+
modSprite.AddLoop("noFlash", 0.1f, new MTexture[] { frame0 });
118+
119+
// copy over the values from the vanilla sprite
120+
modSprite.Position = vanillaSprite.Position;
121+
modSprite.Color = vanillaSprite.Color;
122+
modSprite.OnFrameChange = vanillaSprite.OnFrameChange;
123+
modSprite.Play("idle");
124+
modSprite.SetAnimationFrame(vanillaSprite.CurrentAnimationFrame);
125+
126+
// and replace it for good
127+
Remove(vanillaSprite);
128+
Add(modSprite);
129+
seedSprite.SetValue(this, modSprite);
130+
}
131+
132+
if (spawnedAsFollower) {
133+
player.Leader.GainFollower((Follower) seedFollower.GetValue(this));
134+
canLoseTimerMirror = 0.25f;
135+
Collidable = false;
136+
Depth = -1000000;
137+
AddTag(Tags.Persistent);
138+
}
139+
}
140+
141+
private void OnPlayer(Player player) {
142+
Audio.Play("event:/game/general/seed_touch", Position, "count", index);
143+
player.Leader.GainFollower((Follower) seedFollower.GetValue(this));
144+
canLoseTimerMirror = 0.25f;
145+
Collidable = false;
146+
Depth = -1000000;
147+
AddTag(Tags.Persistent);
148+
149+
// Add the info for this berry seed to the session.
150+
SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo = new SpringCollab2020Session.MultiRoomStrawberrySeedInfo();
151+
sessionSeedInfo.Index = index;
152+
sessionSeedInfo.BerryID = BerryID;
153+
sessionSeedInfo.Sprite = sprite;
154+
SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds.Add(sessionSeedInfo);
155+
}
156+
157+
public override void Update() {
158+
base.Update();
159+
160+
// be sure the canLoseTimer always has a positive value. we don't want the player to lose this berry seed.
161+
canLoseTimerMirror -= Engine.DeltaTime;
162+
if (canLoseTimerMirror < 1f) {
163+
canLoseTimerMirror = 1000f;
164+
seedCanLoseTimer.SetValue(this, 1000f);
165+
}
166+
}
167+
}
168+
}
221 Bytes
Loading
191 Bytes
Loading
184 Bytes
Loading
191 Bytes
Loading

SpringCollab2020MapDataProcessor.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ class SpringCollab2020MapDataProcessor : EverestMapDataProcessor {
88
public static List<List<Dictionary<string, List<EntityID>>>> FlagTouchSwitches = new List<List<Dictionary<string, List<EntityID>>>>();
99
private string levelName;
1010

11+
// we want to match multi-room strawberry seeds with the strawberry that has the same name.
12+
private Dictionary<string, List<BinaryPacker.Element>> multiRoomStrawberrySeedsByName = new Dictionary<string, List<BinaryPacker.Element>>();
13+
private Dictionary<string, EntityID> multiRoomStrawberryIDsByName = new Dictionary<string, EntityID>();
14+
private Dictionary<string, BinaryPacker.Element> multiRoomStrawberriesByName = new Dictionary<string, BinaryPacker.Element>();
15+
1116
public override Dictionary<string, Action<BinaryPacker.Element>> Init() {
1217
return new Dictionary<string, Action<BinaryPacker.Element>> {
1318
{
@@ -36,6 +41,31 @@ class SpringCollab2020MapDataProcessor : EverestMapDataProcessor {
3641
// add this flag touch switch to the dictionary.
3742
entityIDs.Add(new EntityID(levelName, flagTouchSwitch.AttrInt("id")));
3843
}
44+
},
45+
{
46+
"entity:SpringCollab2020/MultiRoomStrawberrySeed", strawberrySeed => {
47+
// auto-attribute indices for seeds, and save them.
48+
string berryName = strawberrySeed.Attr("strawberryName");
49+
if (multiRoomStrawberrySeedsByName.ContainsKey(berryName)) {
50+
if (strawberrySeed.AttrInt("index") < 0) {
51+
strawberrySeed.SetAttr("index", multiRoomStrawberrySeedsByName[berryName].Count);
52+
}
53+
multiRoomStrawberrySeedsByName[berryName].Add(strawberrySeed);
54+
} else {
55+
if (strawberrySeed.AttrInt("index") < 0) {
56+
strawberrySeed.SetAttr("index", 0);
57+
}
58+
multiRoomStrawberrySeedsByName[berryName] = new List<BinaryPacker.Element>() { strawberrySeed };
59+
}
60+
}
61+
},
62+
{
63+
"entity:SpringCollab2020/MultiRoomStrawberry", strawberry => {
64+
// save the strawberry IDs.
65+
string berryName = strawberry.Attr("name");
66+
multiRoomStrawberryIDsByName[berryName] = new EntityID(levelName, strawberry.AttrInt("id"));
67+
multiRoomStrawberriesByName[berryName] = strawberry;
68+
}
3969
}
4070
};
4171
}
@@ -55,7 +85,24 @@ public override void Reset() {
5585
}
5686

5787
public override void End() {
58-
// nothing required.
88+
foreach (string strawberryName in multiRoomStrawberrySeedsByName.Keys) {
89+
if (!multiRoomStrawberryIDsByName.ContainsKey(strawberryName)) {
90+
Logger.Log(LogLevel.Warn, "SpringCollab2020MapDataProcessor", $"Multi-room strawberry seeds with name {strawberryName} didn't match any multi-room strawberry");
91+
} else {
92+
// give the strawberry ID to the seeds.
93+
EntityID strawberryID = multiRoomStrawberryIDsByName[strawberryName];
94+
foreach (BinaryPacker.Element strawberrySeed in multiRoomStrawberrySeedsByName[strawberryName]) {
95+
strawberrySeed.SetAttr("berryLevel", strawberryID.Level);
96+
strawberrySeed.SetAttr("berryID", strawberryID.ID);
97+
}
98+
99+
// and give the expected seed count to the strawberry.
100+
multiRoomStrawberriesByName[strawberryName].SetAttr("seedCount", multiRoomStrawberrySeedsByName[strawberryName].Count);
101+
}
102+
}
103+
104+
multiRoomStrawberrySeedsByName.Clear();
105+
multiRoomStrawberryIDsByName.Clear();
59106
}
60107
}
61108
}

SpringCollab2020Module.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public override void Load() {
2727
CrystalBombDetonatorRenderer.Load();
2828
FlagTouchSwitch.Load();
2929
DisableIcePhysicsTrigger.Load();
30+
MultiRoomStrawberrySeed.Load();
3031
}
3132

3233
public override void LoadContent(bool firstLoad) {
@@ -48,6 +49,7 @@ public override void Unload() {
4849
CrystalBombDetonatorRenderer.Unload();
4950
FlagTouchSwitch.Unload();
5051
DisableIcePhysicsTrigger.Unload();
52+
MultiRoomStrawberrySeed.Unload();
5153
}
5254

5355
public override void PrepareMapDataProcessors(MapDataFixup context) {

0 commit comments

Comments
 (0)