Now we're going to start putting together Sakuya's @MainLoop, which will handle the bulk of her shot function.
We'll do it the same way we do it for enemies, but on a much tighter scale; we'll create a variable called "count" that will go up by one in each iteration of @MainLoop, which tracks the number of frames past. Then we can make it fire bullets on certain frame numbers. The difference here is that the player won't always be shooting bullets; therefore, bullets will only be shot if count is 0 or more, and we'll have it sit at -1 if the button isn't being pressed.
let count=-1;
let i=0;
@MainLoop{
if((GetKeyState(VK_SHOT)==KEY_PUSH || GetKeyState(VK_SHOT)==KEY_HOLD) && count==-1){
count = 0;
}
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 0){
}
}else{
if(count%8 == 0){
}
}
if(count >= 0){
count++;
}
if(count >= 6 && (GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD)){
count=-1;
}
if(count >= 8){
count=-1;
}
yield;
}
See how this will pan out? First it checks if you're pressing the button when it wasn't pressed before, setting count to 0. Count can then start counting up each frame. When it runs up to 6 or 8 (depending on if you're focusing or not) the count will reset to -1, requiring the button to be checked again. Why did I make the cycle different for focus and unfocused? Because I want her focused shot to have a higher fire rate. Now let's add the unfocused shot first, since it's simpler. It's a fan of knives that flies straight.
@MainLoop
~~~
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 0){
}
}else{
if(count%8 == 0){
i=-3;
while(i<=3){
CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
i++;
}
}
}
~~~
CreatePlayerShot01 is the only bullet function for players; anything else will need to be an object shot. Anyway, it's pretty similar to the enemy version, CreateShot01. The difference is the addition of two arguments, which is the two next-to-last. The first (2.5) is the damage the bullet will do. The second (1) is the penetration, or the number of hits the bullet can score, once per frame. If you set the penetration to some huge number the bullet will be able to shoot through every enemy it hits, but be sure to set the damage low if you do something like that! The final argument is the bullet graphic, in this case the blue knives we defined earlier. Player shots cannot have delay.
So anyway yeah, that's our unfocused shot. It's a fan of 7 knives that fly in 5 degree increments in front of you. But wait! That's only half the attack! We still have the options' shots. Let's add a similar function to our options.
task Option(position){
let objoption=Obj_Create(OBJ_EFFECT);
Obj_SetAlpha(objoption,200);
ObjEffect_SetTexture(objoption,img_sakuya); //uses star orb from spritesheet
ObjEffect_SetRenderState(objoption,ALPHA);
ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
ObjEffect_CreateVertex(objoption,4); // square object with 4 vertexes
ObjEffect_SetVertexUV(objoption,0,145,1); // four coordinates of orb on spritesheet
ObjEffect_SetVertexUV(objoption,1,159,1); // object is 15x15
ObjEffect_SetVertexUV(objoption,2,159,15);
ObjEffect_SetVertexUV(objoption,3,145,15);
if(position=="LEFT"){
while(!Obj_BeDeleted(objoption)){
ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
i++;
}
}
}
yield;
}
}else{
while(!Obj_BeDeleted(objoption)){
ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
i++;
}
}
}
yield;
}
}
}
There, pretty simple, even if it did make Option() a lot bigger. Each option will now fire a fan of five purple knives on an alternating count with the blue ones. The purple ones are angled to side a bit to increase coverage, but do less damage. Note that "count" is a global variable, so all tasks in script_player_main can use it, so we don't need to do with count what we did in @MainLoop.
Now, let's compile all that and see if it works.
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Sakuya A]
#Text[Sakuya Izayoi - Illusion Sign
Shot:
Unfocused: Jack the Ludo Bile (spread)
Focused: Jack the Ripper (homing)
Spell Card:
Illusion Sign �uIndiscriminate�v
Illusion Sign �uKiller Doll�v]
#Image[.\sakuya_select.png]
#ReplayName[SakuyaA]
script_player_main{
let img_sakuya = GetCurrentScriptDirectory()~"sakuya.png";
let img_cutin = GetCurrentScriptDirectory()~"sakuya_select.png";
let optionxpos=16;
let optionypos=0;
let count=-1;
let i=0;
task Option(position){
let objoption=Obj_Create(OBJ_EFFECT);
Obj_SetAlpha(objoption,200);
ObjEffect_SetTexture(objoption,img_sakuya); //uses star orb from spritesheet
ObjEffect_SetRenderState(objoption,ALPHA);
ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
ObjEffect_CreateVertex(objoption,4); // square object with 4 vertexes
ObjEffect_SetVertexUV(objoption,0,145,1); // four coordinates of orb on spritesheet
ObjEffect_SetVertexUV(objoption,1,159,1); // object is 15x15
ObjEffect_SetVertexUV(objoption,2,159,15);
ObjEffect_SetVertexUV(objoption,3,145,15);
if(position=="LEFT"){
while(!Obj_BeDeleted(objoption)){
ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
i++;
}
}
}
yield;
}
}else{
while(!Obj_BeDeleted(objoption)){
ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
i++;
}
}
}
yield;
}
}
}
task JackTheRipper(x,y,graphic){
}
@Initialize{
LoadGraphic(img_sakuya);
LoadGraphic(img_cutin);
LoadPlayerShotData(GetCurrentScriptDirectory()~"sakuya_shotdata.txt");
SetPlayerLifeImage(img_sakuya, 96, 48, 143, 95);
SetSpeed(3.5, 2.5);
Option("LEFT");
Option("RIGHT");
}
@MainLoop{
if((GetKeyState(VK_SHOT)==KEY_PUSH || GetKeyState(VK_SHOT)==KEY_HOLD) && count==-1){
count = 0;
}
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 0){
}
}else{
if(count%8 == 0){
i=-3;
while(i<=3){
CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
i++;
}
}
}
if(count >= 0){
count++;
}
if(count >= 6 && (GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD)){
count=-1;
}
if(count >= 8){
count=-1;
}
yield;
}
@Missed{
}
@SpellCard{
}
@DrawLoop{
SetTexture(img_sakuya);
if(GetKeyState(VK_LEFT)==KEY_PUSH || GetKeyState(VK_LEFT)==KEY_HOLD){
SetGraphicRect(49.5, 1.5, 95.5, 47.5); // left movement frame
}else if(GetKeyState(VK_RIGHT)==KEY_PUSH || GetKeyState(VK_RIGHT)==KEY_HOLD){
SetGraphicRect(97.5, 1.5, 143.5, 47.5); // right movement frame
}else{
SetGraphicRect(1.5, 1.5, 47.5, 47.5); // neutral frame
}
DrawGraphic(GetPlayerX(), GetPlayerY());
}
@Finalize{
DeleteGraphic(img_sakuya);
DeleteGraphic(img_cutin);
}
}
script_spell Indiscriminate{
task Spell_Knife(x,y,angle,type){
}
@Initialize{
}
@MainLoop{
}
@Finalize{
}
}
script_spell KillerDoll{
task Spell_Knife(x,y,angle,type){
}
@Initialize{
}
@MainLoop{
}
@Finalize{
}
}

Yep. So far so good.
Now we need to make the focused shot, which will have homing capabilities. This is a bit trickier, and we're going to use a task to make this shot. Let's start by looking at the skeleton we placed earlier.
task JackTheRipper(x,y,graphic){
}
Each instance of JackTheRipper will be used to generate one homing knife. x and y will be the coordinates at which it is spawned, and graphic will be the shot ID it uses. Now, this task is going to need to do two things: first it will need to pick a target and find the angle to it, and then throw a knife at it. Thankfully, because the knife itself doesn't need to do anything special once its been launched, it doesn't need to be an object bullet. We just need to find the angle to throw it at, then we can make it with CreatePlayerShot01 as normal. First, I'll make the code that finds an enemy target.
task JackTheRipper(x,y,graphic){
let enemy_target=-1;
ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
enemy_target=EnumEnemyGetID(i);
}
}
Now, we COULD just use GetEnemyX and GetEnemyY to calculate where we're throwing this, but the problem with it is that it doesn't have error checking and can cause slowdown if there are no enemies to target. So we're going to do it in a way that's a bit more classy. First we've made the variable "enemy_target" which will keep track of what enemy we're targetting. Now we have two functions you may not be familiar with, EnumEnemyBegin and EnumEnemyEnd. Now, I am not entirely familiar with how these work, but I do know that each enemy onscreen is stored in an index, and EnumEnemyBegin has the first number of that index and EnumEnemyEnd has the last. We want to sort through this index and find a suitable target. The enemy index is saved to "i" each loop, and we use that index with EnumEnemyGetID to return its ID to enemy_target. We can now use the value in enemy_target for the GetEnemyInfo functions we're about to add. By the way, if no enemies are onscreen, the loop will fail, and enemy_target will remain at -1. Now let's add some lines to check the angle of the target we've obtained.
task JackTheRipper(x,y,graphic){
let enemy_target=-1;
let test_angle=0;
ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
enemy_target=EnumEnemyGetID(i);
test_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-GetPlayerY, GetEnemyInfo(enemy_target,ENEMY_X)-GetPlayerX);
if(test_angle<=-60 && test_angle>=-120){
i=EnumEnemyEnd;
}else{
enemy_target=-1;
}
}
}
New variable: test_angle. We'll use this to check the angle the enemy is at compared to Sakuya. Now, the formula we're returning to test_angle may look really complicated, so let's simplify it in plain english:
test_angle = atan2(enemy y - player y, enemy x - player x);- atan2 returns the arc tangent (angle) to a given point from 0,0, so we give it the enemy y and x values, adjusted for the position of Sakuya by subtracting her y and x from each.
- GetEnemyInfo returns the info you ask for about the enemy with the given ID. You can put in ENEMY_X, ENEMY_Y, or ENEMY_LIFE to get any of that data. In this case we want the x and y positions of the enemy, so we use this function for enemy y and enemy x.
- GetPlayerY and GetPlayerX do exactly what they say, so we use these functions for player y and player x.
Make sense? So now test_angle has the angle from Sakuya to the enemy in enemy_target. Now we want to see if that angle is within Sakuya's homing arc. Now, while we would refer to straight up as 270 degrees in Danmakufu's system, Danmakufu tracks them a bit differently internally. It uses negative values counting backwards from 0 until it gets to 180, so Danmakufu's degree range is -180 to 180 rather than 0 to 360. (This gave me a lot of trouble until I made a script to test it.) That means straight up is -90 degrees. I want Sakuya's homing arc to be 60 degrees, so I check if test_angle is between -60 and -120, 30 degrees offset from -90 in each direction. If it is, we set i to EnumEnemyEnd to end the loop, because that means we've got a suitable target. If it's not, we set enemy_target back to -1 to indicate we don't have a target to home in on yet, and this will continue the loop.
When all this is said and done, enemy_target will either have the ID of the enemy to fire the knife at, or it will be at -1, meaning there is no target. So let's go ahead and add the code for shooting the knife.
task JackTheRipper(x,y,graphic){
let enemy_target=-1;
let target_angle=270;
let test_angle=0;
ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
enemy_target=EnumEnemyGetID(i);
test_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-GetPlayerY, GetEnemyInfo(enemy_target,ENEMY_X)-GetPlayerX);
if(test_angle<=-60 && test_angle>=-120){
i=EnumEnemyEnd;
}else{
enemy_target=-1;
}
}
if(enemy_target!=-1){
target_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-y, GetEnemyInfo(enemy_target,ENEMY_X)-x);
CreatePlayerShot01(x,y,15,target_angle,6,1,graphic);
}else{
CreatePlayerShot01(x,y,15,270,6,1,graphic);
}
}
If enemy_target isn't -1 then we have a target, so we calculate the angle again, but this time it's from the knife to the enemy, so we use x and y instead of GetPlayerX and GetPlayerY. (x and y were the values we passed to JackTheRipper to indicate the knife's starting point, in case you forgot). Then it shoots a knife at that angle. Simple! Sort of. If enemy_target is -1 then we didn't find an enemy in our homing arc, so Sakuya just fires it straight ahead at 270 degrees.
With this task built, we can call it anywhere we want to make a homing knife. So we go back to our @MainLoop and Option() to add this new task.
In @MainLoop:
~~~
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 0){
JackTheRipper(GetPlayerX-8,GetPlayerY-8,11);
JackTheRipper(GetPlayerX+8,GetPlayerY-8,11);
}
}else{
if(count%8 == 0){
i=-3;
while(i<=3){
CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
i++;
}
}
}
~~~
While focused, Sakuya will shoot two JackTheRipper knives, each originating 8 pixels up and out diagonally from her center.
In Option():
~~~
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
JackTheRipper(GetPlayerX-optionxpos-1,GetPlayerY+optionypos-8,12);
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
i++;
}
}
}
~~~
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
JackTheRipper(GetPlayerX+optionxpos,GetPlayerY+optionypos-8,12);
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
i++;
}
}
}
~~~
Each option will fire one JackTheRipper 8 pixels up from its center point, on alternating counts with the ones in MainLoop.
And that should get our homing knives up and running! There's just a few more things to do before we move onto spellcards.
First of all, Sakuya needs a hitbox. I'm sure it'd be nice to play without one, but unfortunately it is required. For this we just add one line to @MainLoop.
SetIntersectionCircle(GetPlayerX, GetPlayerY, 2);
Should be obvious right? This puts Sakuya's hitbox on her center point, with a 2 pixel radius. I'm not sure exactly how big Reimu and Marisa's hitboxes are in Danmakufu (tests were inconclusive) but in the Windows games Reimu's hitbox is 3 pixels across, and everyone else's is 5. Therefore a radius of 2 should be about normal (it's either 3 or 5 diameter, not sure if it counts its center pixel). You could make it 1 if you wanted a character with a small hitbox, but Sakuya ain't got one.
The next bit is just for effect's sake; we'll have Sakuya's options reposition when she focuses, coming up front. If you'll recall, we have two variables that track the options' position: optionxpos and optionypos. I'll just have them adjust like so:
In @MainLoop:
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 0){
JackTheRipper(GetPlayerX-8,GetPlayerY-8,11);
JackTheRipper(GetPlayerX+8,GetPlayerY-8,11);
}
if(optionxpos>12){optionxpos--;}
if(optionypos>-12){optionypos--;}
}else{
if(count%8 == 0){
i=-3;
while(i<=3){
CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
i++;
}
}
if(optionxpos<20){optionxpos++;}
if(optionypos<0){optionypos++;}
}
See the new pair of if statements involving the variables in question? The way this will work is simple: while focused, the option will move towards 12,-12, and while unfocused it will move towards 20,0.
That should be it! Let's assemble it into the final product. This has everything working except spellcards.
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Sakuya A]
#Text[Sakuya Izayoi - Illusion Sign
Shot:
Unfocused: Jack the Ludo Bile (spread)
Focused: Jack the Ripper (homing)
Spell Card:
Illusion Sign �uIndiscriminate�v
Illusion Sign �uKiller Doll�v]
#Image[.\sakuya_select.png]
#ReplayName[SakuyaA]
script_player_main{
let img_sakuya = GetCurrentScriptDirectory()~"sakuya.png";
let img_cutin = GetCurrentScriptDirectory()~"sakuya_select.png";
let optionxpos=16;
let optionypos=0;
let count=-1;
let i=0;
task Option(position){
let objoption=Obj_Create(OBJ_EFFECT);
Obj_SetAlpha(objoption,200);
ObjEffect_SetTexture(objoption,img_sakuya); //uses star orb from spritesheet
ObjEffect_SetRenderState(objoption,ALPHA);
ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
ObjEffect_CreateVertex(objoption,4); // square object with 4 vertexes
ObjEffect_SetVertexUV(objoption,0,145,1); // four coordinates of orb on spritesheet
ObjEffect_SetVertexUV(objoption,1,159,1); // object is 15x15
ObjEffect_SetVertexUV(objoption,2,159,15);
ObjEffect_SetVertexUV(objoption,3,145,15);
if(position=="LEFT"){
while(!Obj_BeDeleted(objoption)){
ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
JackTheRipper(GetPlayerX-optionxpos-1,GetPlayerY+optionypos-8,12);
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
i++;
}
}
}
yield;
}
}else{
while(!Obj_BeDeleted(objoption)){
ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 3){
JackTheRipper(GetPlayerX+optionxpos,GetPlayerY+optionypos-8,12);
}
}else{
if(count%8 == 4){
i=-2;
while(i<=2){
CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
i++;
}
}
}
yield;
}
}
}
task JackTheRipper(x,y,graphic){
let enemy_target=-1;
let target_angle=270;
let test_angle=0;
ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
enemy_target=EnumEnemyGetID(i);
test_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-GetPlayerY, GetEnemyInfo(enemy_target,ENEMY_X)-GetPlayerX);
if(test_angle<=-60 && test_angle>=-120){
i=EnumEnemyEnd;
}else{
enemy_target=-1;
}
}
if(enemy_target!=-1){
target_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-y, GetEnemyInfo(enemy_target,ENEMY_X)-x);
CreatePlayerShot01(x,y,15,target_angle,6,1,graphic);
}else{
CreatePlayerShot01(x,y,15,270,6,1,graphic);
}
}
@Initialize{
LoadGraphic(img_sakuya);
LoadGraphic(img_cutin);
LoadPlayerShotData(GetCurrentScriptDirectory()~"sakuya_shotdata.txt");
SetPlayerLifeImage(img_sakuya, 96, 48, 143, 95);
SetSpeed(3.5, 2.5);
Option("LEFT");
Option("RIGHT");
}
@MainLoop{
if((GetKeyState(VK_SHOT)==KEY_PUSH || GetKeyState(VK_SHOT)==KEY_HOLD) && count==-1){
count = 0;
}
if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
if(count%6 == 0){
JackTheRipper(GetPlayerX-8,GetPlayerY-8,11);
JackTheRipper(GetPlayerX+8,GetPlayerY-8,11);
}
if(optionxpos>12){optionxpos--;}
if(optionypos>-12){optionypos--;}
}else{
if(count%8 == 0){
i=-3;
while(i<=3){
CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
i++;
}
}
if(optionxpos<20){optionxpos++;}
if(optionypos<0){optionypos++;}
}
if(count >= 0){
count++;
}
if(count >= 6 && (GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD)){
count=-1;
}
if(count >= 8){
count=-1;
}
SetIntersectionCircle(GetPlayerX, GetPlayerY, 2);
yield;
}
@Missed{
}
@SpellCard{
}
@DrawLoop{
SetTexture(img_sakuya);
if(GetKeyState(VK_LEFT)==KEY_PUSH || GetKeyState(VK_LEFT)==KEY_HOLD){
SetGraphicRect(49.5, 1.5, 95.5, 47.5); // left movement frame
}else if(GetKeyState(VK_RIGHT)==KEY_PUSH || GetKeyState(VK_RIGHT)==KEY_HOLD){
SetGraphicRect(97.5, 1.5, 143.5, 47.5); // right movement frame
}else{
SetGraphicRect(1.5, 1.5, 47.5, 47.5); // neutral frame
}
DrawGraphic(GetPlayerX(), GetPlayerY());
}
@Finalize{
DeleteGraphic(img_sakuya);
DeleteGraphic(img_cutin);
}
}
script_spell Indiscriminate{
task Spell_Knife(x,y,angle,type){
}
@Initialize{
}
@MainLoop{
}
@Finalize{
}
}
script_spell KillerDoll{
task Spell_Knife(x,y,angle,type){
}
@Initialize{
}
@MainLoop{
}
@Finalize{
}
}
Let's see.

Knives home in on enemy while focused? Check.
Options move while focused? Check.
Sakuya dies when she is killed? Check.
So far so good! Next, Sakuya A's first spellcard, Indiscriminate.