~Hakurei Shrine~ > Rika and Nitori's Garage Experiments
Danmakufu Intermediate Tutorial
Naut:
Danmakufu Intermediate Tutorial!
This tutorial assumes you've read and understand Blargel's Basic Tutorial, and have attempted to make patterns with it. If you haven't: what are you waiting for? Get to it!
In this tutorial, the following, among other things, will be explained:
* How to create and spawn bullets in and around a uniform circle.
* How to move the boss around the field.
* How to play sound effects and music files.
* How to use create and use tasks.
* How to create indestructable bullets that react to their environment.
* How to make familiars.
* How to string together scripts to make a mini-boss.
* How to make a stage script.
* How to make an event script (when two characters talk to eachother).This tutorial is mainly an index of various helpful posts around this forum, but all of this information is very useful for the intermediate Danmakufu coder.
Consider this a group effort between Iryan, Naut and Stuffman, even if the latter doesn't realize it.
Naut:
Intermediate Bullet Patterns!
Since you've read Blargel's Basic tutorial, you should be familiar with all the CreateShot functions, as well as loop(){}. The latter you will likely be using often, as you can spawn a myriad of patterns by repeating a function while augmenting a value. Don't worry -- I'll show you what I mean. Note that anything after and on the same line as "//" in Danmakufu scripts is a comment, and will not be read by Danmakufu. Be sure to comment your work often so you don't forget what you're doing.
--- Code: ---#TouhouDanmakufu
#Title[Spawning Bullets in a Circle]
#Text[Using loop to spawn bullets in a circular pattern.]
#Player[FREE]
#ScriptVersion[2]
script_enemy_main{
let imgExRumia="script\ExRumia\img\ExRumia.png";
let frame = 0;
let angle = 0;
@Initialize{
SetLife(1500);
SetTimer(60);
SetInvincibility(30);
LoadGraphic(imgExRumia); //Load the boss graphic.
SetMovePosition02(GetCenterX, GetCenterY - 100, 120); //Move the boss to the top center of the screen.
}
@MainLoop{
SetCollisionA(GetX, GetY, 32);
SetCollisionB(GetX, GetY, 16);
frame++;
if(frame==120){
loop(36){
CreateShot01(GetX, GetY, 3, angle, BLUE12, 0);
angle += 360/36;
}
angle = 0;
frame = 60;
}
}
@DrawLoop{
//All this foolishness pertains to drawing the boss. Ignore everything in @Drawloop unless you have read and understand Nuclear Cheese's Drawing Tutorial.
SetColor(255,255,255);
SetRenderState(ALPHA);
SetTexture(imgExRumia);
SetGraphicRect(64,1,127,64);
DrawGraphic(GetX,GetY);
}
@Finalize
{
DeleteGraphic(imgExRumia);
}
}
--- End code ---
Begin by creating a text file with the former code in it, and run it. The main thing you should be looking at is in @Mainloop: the if(){} statement. You'll notice we loop CreateShot01 and angle. What loop does is it repeats everything inside it's braces however many times you've defined it to, in the same frame. In this case, we told Danmakufu to repeat everything inside loop 36 times, which means it will spawn 36 bullets on the boss' position, at an angle of angle, in the same frame. At the start of the script, we defined angle as 0, so all the bullets should fire at 0 degrees, or to the right. But if you run this script, you'll see that they form a perfect circle. Why? Well, that's because we set angle to be angle += 360/36 everytime the loop is run, so angle will hold a different value (in this case, 10 more than it used to) everytime we create a bullet. What this does is creates a perfect circle, because every bullet created will fire at ten degrees more than the last one, which will total to be 360 degrees because the loop is run 36 times (36x10=360).
The reason I put angle += 360/36; instead of angle += 10; was to show you that for any number that you loop(num), the angle you'll want to increment by in your loop will be angle += 360/num;. So if you want 24 bullets in your circle, the code would be:
--- Code: ---if(frame==120){
loop(24){
CreateShot01(GetX, GetY, 3, angle, BLUE12, 0);
angle+=360/24;
}
angle = 0;
frame = 60;
}
--- End code ---
We reset the value of angle after the loop to make sure it still holds a value that we can work with later in the script, but we don't really need to do this. Sometimes purposefully dividing by a different value than what was looped can create an interesting pattern, and you might not want angle to be reset afterwards.
A simple way to change this code would be to have the angular (4th) parameter of CreateShot01 be angle + GetAngleToPlayer, which would force one particle to always be shot towards the player.
Alright, you now know how to spawn bullets in a circle, and how to increment values in loop structures to create circular patterns. But what if you want the bullets to be spawned some distance away from the boss, but still in a circle? For that, we'll use the trigonometric functions sin and cos.
The sin of an angle is the y-coordinate of a point along a circle with a radius of one.
The cos of an angle is the x-coordinate of a point along a circle with a radius of one.
Where L = the radius of the circle, A = the angle from the origin, X = cos(A) and Y = sin(A), which form the point (cos(A), sin(A)).
Note the reversed y-coordinates, remember that Danmakufu has the positive y direction pointing downward.
So, if we want something to spawn at the point (cos60, sin60), where the boss is the origin of the circle, we say:
--- Code: ---CreateShot01(GetX + cos(60), GetY + sin(60), 3, 60, BLUE12, 10);
--- End code ---
But this code won't really do anything, because we're only spawning it a distance (radius) of 1 pixel away from the boss, which we won't see. So to spawn it with a distance of say, 100 pixels away from the boss, the code would be:
--- Code: ---CreateShot01(GetX + 100*cos(60), GetY + 100*sin(60), 60, BLUE12, 10);
--- End code ---
Now we'll see some effects. A distance of 100 pixels away from the boss is pretty noticable. Just multiply sin and cos by a value, which will be your radius, to spawn particles that distance away from your origin, which in this case is the boss. So, to combine and summarize everything so far, I give you the following to place in the beginning script:
--- Code: ---if(frame==120){
loop(36){
CreateShot01(GetX + 60*cos(angle), GetY + 60*sin(angle), 3, angle, BLUE12, 12);
angle += 360/36;
}
angle += 4;
frame = 112;
}
--- End code ---
This code will continuously spawn bullets in a circle around the boss with a radius of 60 pixels. I set angle to be angle += 4; outside the loop to make the circle shift it's position 4 degrees everytime we spawn it, so it looks like the circle is spinning.
The format for spawning bullets in a uniform circle is:
[x, y] -> [originX + radius*cos(angle), originY + radius*sin(angle)]
Keep in mind that you don't just have to increment angle in a loop structure. You can increment any other variable, like radius, as well. Incrementing radius will make the circle look like it is expanding or contracting, or have other interesting effects if included inside the actual loop{}.
Using only the trigonometric functions, as well as our knowledge of loop structures and incrementing values, we can make some pretty nice patterns. Here's an example script using everything we've learned so far. Can you tell what's happening?
--- Code: ---#TouhouDanmakufu
#Title[Border of Wave and Tutorial]
#Text[How to use incrementing values and loop structures to create complex danmaku.]
#Player[FREE]
#ScriptVersion[2]
script_enemy_main{
let imgExRumia="script\ExRumia\img\ExRumia.png";
let frame = 0;
let frame2 = 0;
let angle = 0;
let angleAcc = 0;
let radius = 0;
@Initialize{
SetLife(4000);
SetTimer(60);
SetInvincibility(30);
LoadGraphic(imgExRumia);
SetMovePosition02(GetCenterX, GetCenterY - 120, 120);
}
@MainLoop{
SetCollisionA(GetX, GetY, 32);
SetCollisionB(GetX, GetY, 16);
frame++;
frame2++;
if(frame==120){
loop(6){
CreateShot01(GetX + radius*cos(angle), GetY + radius*sin(angle), 3, angle, BLUE12, 12);
angle += 360/6;
}
angle += angleAcc;
angleAcc += 0.1;
frame = 119;
}
if(frame2>=-140 && frame2 <=110){
radius++;
}
if(frame2>=111 && frame2 <= 360){
radius--;
}
if(frame2==360){
frame2=-141;
}
}
@DrawLoop{
SetColor(255,255,255);
SetRenderState(ALPHA);
SetTexture(imgExRumia);
SetGraphicRect(64,1,127,64);
DrawGraphic(GetX,GetY);
}
@Finalize
{
DeleteGraphic(imgExRumia);
}
}
--- End code ---
In this code, we have bullets spawn at six points around a uniform circle with a radius radius, which is being controlled by a second frame counter frame2. If frame2 is between -140 and 110, then the radius will increase, if it is between 111 and 360, the radius will decrease. The angle these bullets is being fired off at is always being increased by angleAcc, which is also increasing, so the circle will spin at an increasingly faster rate. Combining the oscillation of radius and the ever increasing rate of angle creates a pretty complex pattern, don'tcha think? Feel free to experiment by changing values, like changing angleAcc+=0.2 or radius+=0.5.
Naut:
How to make the boss move!
I'll bet many of you have been waiting for this for quite a while... Well, it's incredibly easy and requires little explanation, as it's just like firing a bullet. Here are some boss movement functions for you to play around with:
SetMovePosition01(X-coordinate, Y-coordinate, velocity);
Tells Danmakufu to make the boss start moving towards the X and Y coordinates specified, with the velocity you declare. Typically we use SetMovePosition03 instead of this one, because this one isn't very fluid.
SetMovePosition02(X-coordinate, Y-coordinate, frames);
Tells Danmakufu to make the boss start moving towards the X and Y coordinates specified, taking "frames" long to get there. 60 frames is one second, 120 frames is two seconds, etc.
SetMovePosition03(X-coordinate, Y-coordinate, weight, velocity);
Tells Danmakufu to make the boss start moving towards the X and Y coordinates specified, with the velocity you declared. This is a special movement function which includes "weight", which tells Danmakufu to make the boss decelerate as it's reaching it's goal, making a more fluid motion. A higher weight value will make the deceleration more sudden.
SetMovePositionRandom01(X-distance, Y-distance, velocity, left-boundry, top-boundry, right-boundry, lower-boundry);
Tells Danmakufu to make the boss move around randomly inside a rectangle that you declare. The boss will move the X and Y distance you tell it to, with the velocity you declare, within the rectangle you designate by declaring it's left-most coordinate, upper-most coordinate, right-most coordinate and lower-most coordinate.
SetMovePositionHermite is complex as all blazes, so I'mma let Stuffman explain this:
--- Quote from: Stuffman on May 13, 2009, 11:05:47 PM ---
--- Code: ---SetMovePositionHermite(x,y,distance1,angle1,distance2,angle2,frames);
--- End code ---
* x/y: This is the destination point of the boss, obviously.
* distance1: If you were to draw a line between the starting point and destination point, this value is how far the boss moves from that line. I'm not quite sure how this is calculated but as a rule of thumb, three times the length of the line gives you a circular movement arc.
* angle1: This is the angle you are moving at when you begin the movement.
* distance2: Same as above but for the second half of the movement.
* angle2: This is the angle you are moving at when you finish the movement.
* frames: This is how long it takes to complete the movement. In other words it sets the speed, just like SetMovePosition02.
As a sample from one of my spinner enemies in PoSR:
--- Code: ---SetMovePositionHermite(GetX()+100,GetY(),300,90,300,270,40);
--- End code ---
The enemy comes onto the screen straight down and does a 180 to its right. The helmite part of its movement involves moving 100 pixels to the right, starting at 90 degrees (down) and ending at 270 degrees (up). The distance in both cases is 300 (3*100 pixel distance) for a round movement arc. It takes 40 frames to complete this movement.
That should be enough for practical use, but you can get some really wacky movement if you play around with it. It's actually rather easy to use once you understand the arguments. The distance arguments are the only ones I don't fully understand.
--- End quote ---
You would use these functions just like calling a bullet in @MainLoop:
--- Code: ---frame++;
if(frame==120){
SetMovePosition03(GetCenterX, GetCenterY, 10, 3);
}
--- End code ---
Tells the boss, at frame 120, to move towards the center of the playing field with a moderate velocity and decelerate as it's reaching its goal.
How to add sound effects and music!
For now, your scripts may be fun to play and/or look pretty. However, an easy way to enrich your scripts is to have shots, movements, pattern changes and other happenings be accompanied by sound effects and even controllable music files. Music and sound effects are fairly simple, just play them the same frame you want the sound effect to be heard.
However, you'll need to load it first, preferably in @Initialize, but you can load it anywhere before it's played. To load a sound effect, you simply type:
LoadSE(sound file);
And play it using the function:
PlaySE(sound file);
So in the middle of a script, it could look something like this:
--- Code: ---script_enemy_main{
let shot = GetCurrentScriptDirectory~"shot.wav";
let frame = 0;
@Initialize{
LoadSE(shot);
}
@MainLoop{
frame++;
if(frame==60){
CreateShot01(GetX, GetY, 3, GetAngleToPlayer, RED01, 0);
PlaySE(shot);
frame = 0;
}
}
@Finalize{
DeleteSE(shot);
}
}
--- End code ---
Basically, this will play the shot sound the same frame that the RED01 and thus make it sound as if the shot actually has a sound effect. Something notable is that if you shoot more than one shot a frame, you need only play the shot sound once, as otherwise it will waste your computer's resources and possibly cause people to FPS spike. However, if you fire two shots on different frames, you'll want to play the shot sound on each frame. There is no reason to play the same shot sound more than once per frame. Don't do it.
You may have noticed the function DeleteSE in @Finalize inside the previous script. This is another sound function, this unloads the sound effect from memory to free up space for other loadable things for the next spellcard or script. Basically, if you loaded something in that script, you'll want to unload it in @Finalize. There is an exception to this, in stage scripts particularly, but I'll go over that in a bit.
A few other notable sound functions, this time for music files. You may be wondering, "I already know how to play music using #BGM, why do these function exist?" Well, what if you wanted to play certian music at a certain time, instead of autoplaying at the beginning of your script and automatically looping? Well, we've got functions for you:
PlayMusic(music file);
LoadMusic(music file);
DeleteMusic(music file);
These all do the exact same thing as their SE counterparts, except for larger files. This way, you can control when the music starts and stops playing in any script. Particularly useful for stage scripts.
Alright, as I mentioned before, the is an exception to the "anything loaded you want to unload in the same script" rule. Well, sort of. For spawning enemies or familiars in stage scripts or spell cards, sometimes you'll want the sound effect to play when they shoot bullets. For these scripts, it is not necessary to load or delete the sound effect before and after you use it, since you've likely loaded it in the other script (it will just find the memory slot and pull it from there). As a matter of fact, if you delete a sound effect in an enemy script (when the enemy dies), you'll no longer be able to use that sound until you load it again, which means the sound effect will just not play whenever you call it -- until LoadSE appears again. Not related to sound effects, but it should be noted that this same thing occurs for graphics too. If you load a graphic and unload it in an enemy script, then the graphic will stop being displayed for all enemies until you load it again. Just something to be aware of.
Naut:
How to use Tasks!
With the CreateShotA command, you can already program rather complex bullet movements. However, what if you want to create indestructable bullets? What if you want your bullet to react to stuff that happens after you fired it, for example, to bounce off of the border of the playing field? That is when you will need object bullets.
To program an object bullet, you have to create a task.
Tasks are basically smaller scripts that run alongside the regular MainLoop. The task script is usually inserted after the @Finalize loop before the closing bracket of script_enemy_main and can later be called like a regular function. To script a task, the following code is used:
--- Code: ---task NameOfTheTask(put, variables, here) {
stuff happens;
}
--- End code ---
So far, it looks much like a script for a regular function, and it pretty much is. To really make it run besides your main loop, you have to include a while statement. That way, the task works as long as this condition is met. Example:
--- Code: ---task Testtask(a, b, c) {
let timer=60;
while(timer>0) {
stuff happens;
timer--;
}
}
--- End code ---
For a duration of 60 frames, every frame stuff will happen. After that, the "while" condition is no longer met, and the task expires.
But, because tasks run separate from the MainLoop, there is a very important command:
--- Code: ---yield;
--- End code ---
The yield command says the program to stop the task it is working on and to look if there is other code to perform. This includes the MainLoop. That means you have to insert the yield; command at the end of the MainLoop and inside the recurring part of your task. Otherwise, danmakufu will proceed to ignore everything else and keep focused on the MainLoop (or the task).
That means the above code is actually wrong as it will do the "stuff" 60 times, but all during the same frame. The corrected version of that code would be:
--- Code: ---task Testtask(a, b, c) {
let timer=60;
while(timer>0) {
stuff happens;
timer--;
yield;
}
}
--- End code ---
The yield command can also be called repeatedly to suspend the task. For each yield inserted, danmakufu will run the MainLoop once before the stuff after the yields proceeds. This means that by inserting the code:
--- Code: --- loop(n){ yield; }
--- End code ---
into the task, the following code will suspend for n frames.
Tasks performed by danmakufu are independant from each other.
This means that you can call the task multiple times while it is still running. These don't affect each other and are performed side by side. It also means that, like with regular loops, variables and arrays created in a task don't exist outside the task.
So, now that you know something about tasks, how does one make object bullets?
First, you have to assign an ID for your bullet. As the ID is assigned inside the task and thus doesn't exist outside of it, you can practically use anything. The most common way is to call it "obj". You assign the ID like this:
--- Code: --- let obj=Obj_Create(OBJ_SHOT);
--- End code ---
This creates an object that is classified as an object bullet and can be referred to by the name "obj". It has, however, no properties as of now - they all have to be assigned with a separate command. The most important are:
--- Code: --- Obj_SetPosition(obj, x coordinate, y coordinate);
Obj_SetAngle(obj, angle);
Obj_SetSpeed(obj, speed);
ObjShot_SetGraphic(obj, graphic);
ObjShot_SetDelay (obj, delay);
ObjShot_SetBombResist (obj, true or false);
--- End code ---
In order, they set the position, angle, speed, bullet graphic, delay before changing from vapor to damaging bullets, and whether the bullet is immune to bullets and death explosions (true) or can be bombed like a regular bullet (false).
These commands should be called right after the declaration of your object bullet. The can, however, come in handy to regularly change the property of your bullet. For example, there is no simple command to give the bullet a specified angular velocity. To give it one, you have to call Obj_SetAngle every loop.
Speaking of which, how do you define your looping action for a bullet?
Aside from some advanced shenanigans, the looping part should be called every frame, as long as the bullet still exists, right? In danmakufu terms, this is expressed as follows:
--- Code: --- while(Obj_BeDeleted(obj)==false) {
stuff happens;
yield;
}
--- End code ---
The stuff happens once every frame, as long as the statement "the object has been deleted" is a false statement. Or, to put it easier, it happens once every frame as long as the bullet still exists.
So now we have the basic structure to create an object bullet:
--- Code: ---task Bullet(x, y, v, angle) {
let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y);
Obj_SetAngle(obj, angle);
Obj_SetSpeed(obj, v);
ObjShot_SetGraphic(obj, RED01);
ObjShot_SetDelay (obj, 0);
ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) {
yield;
}
}
--- End code ---
Put this after (not inside) "@Finalize{ }", before ending script_enemy_main{}, and you can use "Bullet(x, y, v, angle);" just like a regular function to create a normal indestructable bullet.
Now, let's say we want the bullet to react to something. For the bullt to react to something, we must be able to check whether or not the conditions of reaction are given. For this, we have the information gathering commands, such as:
--- Code: ---Obj_GetX(obj);
Obj_GetY(obj);
Obj_GetAngle(obj);
Obj_GetSpeed(obj);
--- End code ---
I'm pretty sure you can guess what these do. :P
Now, let's say that we want a bullet that reflects from the left and right border of the playing field. To do this, we specify in the "while" part that, if the bullet is too far on the left or right, it's angle shall be adjusted like with a real light reflection - incoming angle equals leaving angle. Because of how angles are coded, this requires some trickery:
--- Code: ---task Bullet(x, y, v, angle) {
let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y);
Obj_SetAngle(obj, angle);
Obj_SetSpeed(obj, v);
ObjShot_SetGraphic(obj, RED01);
ObjShot_SetDelay (obj, 0);
ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) {
if(Obj_GetX(obj)<GetClipMinX) {
Obj_SetAngle(obj, 180 - Obj_GetAngle(obj) );
Obj_SetX(obj, Obj_GetX(obj) + 0.1);
}
if(Obj_GetX(obj)>GetClipMaxX) {
Obj_SetAngle(obj, 180 - Obj_GetAngle(obj) );
Obj_SetX(obj, Obj_GetX(obj) - 0.1);
}
yield;
}
}
--- End code ---
This code creates an indestructable bullet that checks each frame wheter it is right or left from the actual playing field. If that is the case, it will adjust the angle to mimic a real reflection, then move the bullet a little towards the center of the field to prevent it from rapid bouncing along the border.
/\ 270°
<---- 180° 0° --->
\/ 90°
I just noticed that I lack the proper english vocabulary to describe why the reflected angle is calculated as it is. :-\
It would be nice if someone else could elaborate. You know, someone who has a great deal of knowledge about angles and danmakufu and who also likes to explain stuff to others.
Nevertheless, now you can fire your very own indestructable-bouncing-of-the-sides-bullets, just with a little "Bullet(x, y, v, angle);"!
Just don't forget to insert a yield command in your MainLoop, or the bullet won't bounce!
Naut:
How to use Tasks part 2: "Great Whirlwind"
So, you are familiar with this spellcard and maybe even captured it. But how does it work?
Well, each seperate cyclone is a cluster of several bullets with the same starting point and angular velocity, but with different starting angles and speeds. The same angular velocity ensures that every bullet of the cluster will return to the center at the same time, then the cluster will unfold again, dependent on the different angles and speeds of the individual bullets.
So far, this would be easy to replicate with a CreateShotA, right? There is a little problem, however: the cyclones move downward, and still, they manage to stick to their circling motion. That is quite tougher to program.
There are actually two ways to do this; one involves CreateShotA_XY and trigonometric functions, the other involves object bullets, but since I'm here to showcase how to have fun with object bullets, we'll use these.
Let's start out with the basic framework from the previous post, with a different bullet sprite:
--- Code: ---task Bullet(x, y, v, angle) {
let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y);
Obj_SetAngle(obj, angle);
Obj_SetSpeed(obj, v);
ObjShot_SetGraphic(obj, BLUE21);
ObjShot_SetDelay (obj, 0);
ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) {
yield;
}
}
--- End code ---
We said that the bullets have an angular velocity, so we'll work in a command that adjusts the angle of the bullet by a fixed amount every frame:
--- Code: --- Obj_SetAngle(obj, Obj_GetAngle(obj) + 2);
--- End code ---
Now we can create an object bullet that will move in a circle. Firing a ring of these bullets will get you a pulsating circle with a radius of
360*(velocity of the bullets) / (2*Pi).
Using an angular veloctiy of 2 means that the bullet will have moved a full circle after 360/2 frames. That is exactly three seconds after being fired.
The bullets still won't move downwards, though. To accomplish that, we have to add a command that moves the bullet downward every frame, independently from the regular movement direction and speed. To simulate this movement, we can simply set the position of the bullet to a place the has a distance of, say, one pixel, every frame. A command that does this looks like this:
--- Code: --- Obj_SetPosition(Obj_GetX(obj), Obj_GetY(obj) + 1);
--- End code ---
The bullet is moved straight down by one pixel every frame. Of course, if we wanted, we could make the whole cyclone move in a specific pattern itself, like waving to left and right while moving to the bottom of the screen ~~~~ , adjusting the x coordinate every second dependent on the value of a trigonometric function. You can do that as practice, if you like. ;D
So, inserting both of these commands into the bullet task, we get:
--- Code: ---task Bullet(x, y, v, angle) {
let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y);
Obj_SetAngle(obj, angle);
Obj_SetSpeed(obj, v);
ObjShot_SetGraphic(obj, BLUE21);
ObjShot_SetDelay (obj, 0);
ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) {
Obj_SetAngle(obj, Obj_GetAngle(obj) + 2);
Obj_SetPosition(obj, Obj_GetX(obj), Obj_GetY(obj) + 1);
yield;
}
}
--- End code ---
This task creates a single bullet that spirals down the screen. To craft a cyclone, you have to fire a ring of these bullets. Several rings with different bullet speeds, actually. For this, we can define a function:
--- Code: ---function cyclone(x, y){
ascent(i in 0..10){
ascent(k in 0..6){
Bullet(x, y, 0.5*(1+k), i*40);
}
}
}
--- End code ---
This will fire 6 rings of 9 stormbullets each. The rings have an individual bullet speed of 0.5, 1, 1.5, 2, 2.5 and 3, and thus different radii. Calling This function for a specified place will call down a whirlwind that folds and unfolds indefinitely as it carves it's way to the bottom of the screen.
Now, to emulate the spellcard, define the task after @finalize, define the function in the mainscript and then call the function with your main loop in regular intervals.
Again, do not forget the yield; in the MainLoop!