Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interactive "Door" objects. #12

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

helioscope
Copy link
Contributor

@helioscope helioscope commented Oct 23, 2024

Adds "Door" object, extending FinishFlag as another way to transition between levels (or between different places in the same level). Requires a button-press to activate. There's quite a bit of refactoring in here to avoid redundant code (with NPC interactivity, with FinishFlag level-exit behavior, with the FinishFlag tooltip, etc). Currently things are still configured so that doors and flags can both target each other. (This configuration is just-barely leading in the poll right now, and where I'm leaning too if we revise the level-entrance picker to first choose the destination level, then pick the destination object.)

Some things to take a look at:

  • PlayerInteractionHandler.js -- handles drawing of arrows, targeting one interaction at a time, whether or not a player can and will interact, whether or not a player is allowed to jump (when there’s an interaction)
  • PlayMode.playerExitLevel(customExit) — a static method to trigger exiting a level. In the future, maybe this could be used for dialog and events. (I would also like to specify the transition here, so that doors can have a different transition than finish flags, and so specific transitions could be used in dialogs and events. I haven't gotten around to this yet.)
  • Hidden option for changeableAttributes that are listed in SpritePixelArray.js -- set this to true to skip over them in the ObjectToolTip (instead of type-checking for FinishFlag and Door). (We could instead create a new formElement type (e.g. “custom”) to achieve the same effect and be clearer about what’s happening, but I’m not sure how much it matters — being to hide attributes in general this way also seems potentially handy)
  • htmlRenderers/LevelEntrancePicker.js -- the level-entrance picker used by the ObjectsTooltipElementsRenderer for both FinishFlags and Doors
  • Door class
  • FinishFlag revisions
  • NPC revisions (blinking arrow indicator and activation moved to PlayerInteractionHandler)

@helioscope helioscope marked this pull request as ready for review October 24, 2024 05:13
@helioscope helioscope marked this pull request as draft October 24, 2024 05:14
@l0bster2
Copy link
Owner

I understand it's a WIP, but i just write some things down i realized while testing, in case you didn't know them:

  • the default NPC indicator became arrow down, it should stay arrow up
  • exported game is not working (DOOR_TRIGGERS is not defined)
  • Would be nice if the text was Activation-trigger, not in camel case. Also i would add the class "subSection" to this div, to have a margin and a line separator
    image

Comment on lines 6 to 10
if (Controller.jump) {
if (!PlayerInteractionHandler.willAllowJump() || PauseHandler.justClosedPauseScreen) {
// no jumping, sorry
}
else if (player.swimming) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just keep the condition as it was?

Suggested change
if (Controller.jump) {
if (!PlayerInteractionHandler.willAllowJump() || PauseHandler.justClosedPauseScreen) {
// no jumping, sorry
}
else if (player.swimming) {
if (Controller.jump && !PlayerInteractionHandler.willAllowJump() || PauseHandler.justClosedPauseScreen) {
if (player.swimming) {

If that's not possible, i would rethink how to make this condition. not a fan of empty IFs :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 99% just personal style choice for readability. Shorter conditional lines and nesting less deeply tends to make things more readable, in my experience. There are other approaches than a comment-only if, though.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, but in that case it actually also changes the logic which condition fires. with my condition, the code wouldn't go inside the outer if. with your condition it would, as there is no return or something. and then it would trigger the "else" inside.
i don't know the consequences right away, but i would keep my condition tbh.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right! Actually one of the consequences of this (making the outer condition just be Controller.jump by itself) is that it prevents a bug where you can accidentally jump after passing interactable objects with the jump button already held down. It happens in the main branch right now and isn't too much of an issue there, but it can get weird in some edge cases with doors.

return;
}

// frame-based blink timing copied from orignal NPC code. might want timestamps instead?
Copy link
Owner

@l0bster2 l0bster2 Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No timestamps pls, because fps takes lag into consideration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting! Thanks! The comment there was mostly a note-to-self about timing readability — maybe using a constant or two there could be helpful instead.

const startFlagsInLevel = levelObjects.filter(levelObject => levelObject.type === ObjectTypes.START_FLAG);
const doorsInLevel = levelObjects.filter(levelObject => levelObject.type === ObjectTypes.DOOR);
const defaultStartFlag = startFlagsInLevel.find(startFlag => startFlag.extraAttributes?.defaultStartFlag);
const fallbackStartFlag = startFlagsInLevel[startFlagsInLevel.length - 1];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startFlagsInLevel length could be 0, and the index could be -1 then, or am i wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re correct, yeah—in that case fallbackStartFlag will end up being undefined because there aren’t any start flags in the level. Currently this is handled further down below by printing a warning to the console when no suitable start location is found. In practice, this occurs when you have a level that is only accessible via door, and will trigger every time you access such a level in build mode.
This can actually be pretty common/easy to encounter, so maybe this behavior should be revised a bit to accept Doors as a fallback too. (Or to give Doors the same defaultStartFlag attribute as StartFlags, but that seems a bit overkill?)

Comment on lines 251 to 252
const startFlagsInLevel = levelObjects.filter(levelObject => levelObject.type === ObjectTypes.START_FLAG);
const doorsInLevel = levelObjects.filter(levelObject => levelObject.type === ObjectTypes.DOOR);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could combine those filters into 1 array. if type === ObjectTyptes.START_FLAG || type === ObjectTypes.DOOR
as far as i've seen you don't need 2 separate arrays

Copy link
Contributor Author

@helioscope helioscope Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, at this stage I think they can probably just be handled identically. They will most likely be merged together in the final PR.

Comment on lines 259 to 267
if (destinationData.type === ObjectTypes.DOOR) {
destinationObject = doorsInLevel.find((door) => {
return door.extraAttributes?.flagIndex === PlayMode.customExit.flagIndex;
});
} else if (!destinationData.type || destinationData.type === ObjectTypes.START_FLAG) {
destinationObject = startFlagsInLevel.find((startFlag) => {
return startFlag.extraAttributes?.flagIndex === PlayMode.customExit.flagIndex
});
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here you could have a longer if condition, and if flags and doors are in 1 array, you could iterate over this array and find flagIndex

@@ -244,6 +246,44 @@ class TileMapHandler {
}
}

resetPlayerStartPoint() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it's WIP, but in general, there already is a function resetPosition in Player.js. not sure what your plan is with it. expand/delete it?

Copy link
Contributor Author

@helioscope helioscope Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember right, this is based on code in StartFlag (which is probably still in the main branch now) that seemed to be running every time a StartFlag is instantiated. The plan for that code seemed to be to find and adopt the start point for the level, so the appropriate position values are passed to the player. I refactored that so that it should only happen once when loading the level, but if that's redundant now, this is definitely worth trimming or deleting.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. you can also do it later on. if it works as it is now, it's also fine

Comment on lines 129 to 130
let playerX = player.x;
let playerY = player.y;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wouldn't create a variable for them, it just uses more memory, just use player.x and y down below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that’s fine. I was taught to cache one-time work before loops to optimize for speed, but honestly A) it’s just lookups here so any gain would be tiny and B) optimizing either way here seems unlikely to matter given such a small list. Deleting those lines will make the code a bit more concise without losing clarity though, so that’s a win.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i think cache makes sense, if you do some operation inside the for-loop each time. but here you are just creating a variable. so in this case at least, i would delete it

Comment on lines 77 to 81
// should probably merge updateControl stuff into Controller and read from there instead?
// controls need to be updated every frame
this.updateControl('jump');
this.updateControl('up');
this.updateControl('down');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if would be awesome if we would have a "just pressed" variable in controller :)
but it's ok if not

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, I agree, but maybe we can add that in a future update. I think the unmerged ladder PR touched on some issues that would require a Controller update -- maybe it could be bundled into that?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah, pls don't touch the ladder PR :D it's not a lot of code and there were lots of problems with it
if you want, you can create a separate PR in the future

@@ -0,0 +1,201 @@
class PlayerInteractionHandler {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after a second read-through i have to say, i really like how you implemented this class 👏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@@ -0,0 +1,174 @@
class LevelEntrancePicker {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for creating a separate file for it!
That makes it more readable

@@ -387,7 +404,7 @@ class BuildMode {

const xPos = canvasOffsetLeft + (tilePosX * this.tileMapHandler.tileSize) - 120 - Camera.viewport.left;
const yPos = canvasOffsetTop + (tilePosY * this.tileMapHandler.tileSize) + this.tileMapHandler.tileSize + 6 - Camera.viewport.top;
TooltipHandler.repositionAndShowTooltip("canvasObjectToolTip", yPos, xPos, heading, content)
TooltipHandler.repositionAndShowTooltip("canvasObjectToolTip", yPos, xPos, heading, content);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for all the small clean ups as this :)

Comment on lines 135 to 137
let xDist = target.x - playerX;
let yDist = target.y - playerY;
let dist = Math.sqrt((xDist * xDist) + (yDist * yDist));
Copy link
Owner

@l0bster2 l0bster2 Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for pretty-code reasons: all those can be "const" as they don't change later on. It is also true for a lot of other usages of "let" that can be changed to "const" in your PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something you'd like to introduce as part of a "style guide" for the repo? I didn't notice much of a pattern in the codebase so far, but could adopt it going forward.
From a personal standpoint, I feel like I've annoyed myself just frequently enough with presumptuous const-ing (where later things change and it turns out I need a let instead) that I tend to use it pretty sparingly. I've also seen suggestions to avoid const strictly out of people being confused about the particulars of it (allowing mutability of a referenced object), but haven't been bit by that so far.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honestly i don't care that much about let/const^^ i would say if you are 100% sure that something is a const, try using it, but i won't block any changes of yours because of it

Comment on lines 49 to 57
let levelIndex = parseInt(valueArray[0]);
let destType = valueArray[1];
let id = valueArray[2];

return {
levelIndex: levelIndex,
type:destType,
flagIndex: id
};
Copy link
Owner

@l0bster2 l0bster2 Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let levelIndex = parseInt(valueArray[0]);
let destType = valueArray[1];
let id = valueArray[2];
return {
levelIndex: levelIndex,
type:destType,
flagIndex: id
};
return {
levelIndex: parseInt(valueArray[0]),
type: valueArray[1],
flagIndex: valueArray[2]
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, this is just a style/readability choice. It seems like style guide entry number 2 for this repo might be something like "Don't declare local variables for available lookups. (Even if you personally feel like it's more readable with those declarations, [insert reasoning to not do so here].)"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel in this case lookups are really not necessary. you have an object with keys and values, and the keys already describe what the values do. imo there is no reason to also have variables before that. my reasoning is mostly keeping the code short if possible :)

objects/Door.js Outdated
Comment on lines 41 to 47
if (targetObjectType !== ObjectTypes.DOOR) {
return;
}
if (!reciprocatingLevelIndex) {
// (in this case, this door is probably the one who is reciprocating)
return;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (targetObjectType !== ObjectTypes.DOOR) {
return;
}
if (!reciprocatingLevelIndex) {
// (in this case, this door is probably the one who is reciprocating)
return;
}
if (targetObjectType !== ObjectTypes.DOOR || !reciprocatingLevelIndex) {
return;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. This is another style/readability choice. Maybe style guide entry number 3 would be something like "Consolidate conditionals." I feel like there should be a length limit before finding a good place to line-break or something though. Long, line-wrapping conditionals are like run-on sentences and can take a bit of dedication to correctly untangle.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i agree! with a certain amount of conditions, we should think about readability. but with 2 conditions, i would prefer keeping the code shorter

Comment on lines +37 to +39
let targetLevelIndex = customExit.levelIndex;
let targetObjectType = customExit.type;
let targetFlagIndex = customExit.flagIndex;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try not to reassign variables where there is no need for please. just use customExit.type where you need it down below f.e.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, if you prefer. These local variables are just for readability. I feel like the naming here otherwise requires a broader awareness/understanding of other code and would be a bit harder to jump into.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, in that case the naming helps with readability, i agree 👍 let's keep it

// overrides
this.lockedSpriteIndex = SpritePixelArrays.getIndexOfSprite(ObjectTypes.LOCKED_DOOR);
this.lockedSpriteYPos = this.lockedSpriteIndex * this.tileSize;
this.sound = SoundHandler.door;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wouldn't save the sound in a variable. it's like 100kb, and a bunch of doors means, every door instance needs to hold that in it's memory. i would save SoundsHandler.door.key f.e. in the variable and when you need to play it, you can use
SoundHandler[instance.sound].stopAndPlay();
hope i made that clear
and same goes for FinishFlag.js

Copy link
Contributor Author

@helioscope helioscope Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn’t this just store a reference to the same Sound object that SoundHandler.door references? I don't think it would make a new copy each time.

You can test this out in the console:

const w1 = SoundHandler.win;
const w2 = SoundHandler.win;
console.log(w1.loaded); // prints: true
console.log(w2.loaded); // prints: true
w1.loaded = ":)"; // should mutate the object that both w1 and w2 refer to
console.log(w1.loaded); // prints: ":)"
console.log(w2.loaded); // prints: ":)"

Of course, if you make something assign SoundHandler.winDefault as the value, then THAT will create a new unique copy of the massive base64-encoded string, because string is a “primitive” type and not an object.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, yes. you are right :) let's keep it then

…ess (they're also generic enough to apply to other interactions)
…dd more robust method for creating select elements therein
…tom' changeable attribute form elements where we're generating bespoke form elements
…hey now keep doing so, instead of pointing back to the original level they were copied from)
…ag; fix default new-level data to have the StartFlag always marked as the startingflag
…d tile in the default black background color
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants