I am of the belief that, overall, you can make whatever 2d game you need with whatever system the game needs, whether stacking buffs, equips, anything. Maybe not one-click out of the box, but, idunno, I don't touch JS and yet to hit any dead ends with some wacky ambitious ideas. I'd be curious to know what isn't possible in events or sdk-safe JS that can't be done for making any 2D system. Or is it to eek out more performance by using these undocumented js functions?
Jase00 - ugh, I wrote a novel. TLDR at the bottom, but I'm not sure it makes sense with the context. Nobody should feel the need to read this, but I'll post it anyway. The reason I tended towards using internal api is to increase performance where I could, or avoid repetition. More on that later
I think you are already familiar with some of my complaints, and I'll clarify as best I can. If you have a game with a fairly rigid organization, known ahead of runtime and before devtime, then producing it in c3 is fairly straight forward. Producing a game like Mario is a breeze. Making a game like pokemon, only slightly more difficult.
The stacking buffs example is actually a good one. If you know in advance the types of buffs that can exist, and the number of stats they affect, then making a system for that is easy. When things become difficult is when you don't know how many types of effects will exist before hand(or the number is high), nor the objects that can be affected. Some a problem is best solved by a completely abstract system to accommodate designers future ambitions.
Take mario as an example: We have a character that can walk/run, duck slide, wall jump, and a few other things. Now lets give mario an inventory for items that can alter his stats, and even add completely new abilities. Without deep oop, or ecs architecture, the easiest way to solve this problem is to embed conditional branches in mario's behavior logic that checks for items containing abilities. Not too bad... so far... especially if the number is low.
But now extend that to all characters and allow any character to have any ability. Now every character needs conditional branches. Now keep adding abilities into the game. With new abilities might come the need for new stats, etc... and the base class for characters is getting hefty.
The issue at this point isn't that you can't make the game, Its that you've created a situation where adding a new behavior to a single character requires a deep understanding of all characters and all abilities in the game so far, typically requires modyifying the code base in multiple places, and if you want to remove an ability, you have to remember all the places it has code for it. This becomes unmaintainable. A single developer likely can do this if well organized- but only to a point; and as part of a team - this is nearly impossible if you have two people working on the same system.
The second issue is that every character, from a performance standpoint, no matter how simple, is as complicated as the most complicated character, as every character needs to have a conditional tree for every ability it could posses and the stats to go with it. Every object is bloated with data it doesn't need and this can hurt performance. We haven't even gotten to input support, ai support, or flexible and dynamic stats, etc... which also has to balloon to fit the conditionals. If you take a game like vampire survivors, and require bullets to also have abilities or dynamic effectors, you now have a code structure that kills performance so completely you have to do something about it.
So the need to change can be 1 or both. Poor performance, or difficultly adding, maintaining, or removing code.
Enter ECS or OOP. The problem outlined above is a fairly classic problem and there are multiple ways to deal with. My favorite is ECS, but i'll cover both and why they aren't great in c3.
In either system, the goal is to create a framework that efficiently facilitates adding functionality to objects arbitrarily at runtime, and reduces the complexity of maintaining and creating new functionality in this environment. Good for the game, good for the developer.
But construct makes this incredibly difficult, as it can't natively do OOP or ECS.
As will be pointed out, you can code javascript. Construct has a great event editor and that is a major selling point. It is fast to iterate with and I like it. That is why I use it. But coding in c3 is more verbose and difficult to learn than pretty much any other engine I have used. I started in c2, so I should be biased to it, but I am not. Creating addons is painfully slow compared to creating editor integrated scripts in unity, and simply coding bare functionality in c3 takes more writing to achieve the same results in unity. That isn't to say some people may still prefer c3 in this regard, but it is a factual claim to say it takes more typing to get the same thing done in c3 than in a program like unity. Other game engines are much better organized at a architectural api level, so the power and flexibility is also typically higher than in construct. Compare the unity collision api to constructs and you'll get a simple overview of just how tiny construct is in comparison.
OOP in c3:
Families currently allow only one layer of abstraction in construct, (since you can't nest families in families), so to create a better OOP system in events, you have to get creative. You can simulate deeper structures by adding objects into multiple families, and then use a uid picking framework to select multiple families from a single object. The organization of these families and which objects go in them has to be documented and remembered by the dev. You then can assume (as the dev) that any objectA in some family will also be in another family that it "inherits from". If you want that base functionality, you then pick that family by the object uid you currently have picked. But this forces every object to now require complex picking boiler plate and for each loops (which reduces performance). Worse, you also have to repeat the boiler code events for every level of depth you wish to add for an object, and for every structure you create, meaning the exercise of abstraction isn't itself abstracted - this also creates significant event sheet performance overhead. If you also want inherited custom actions, you now have to repeat the creation of those actions for each family in a tree, and link them together. Every object added will require this boiler plate. The deeper the structure being simulated, the more events per object that have to be run and the more boiler plate you need to set it up, increasing dev time per object added. So even if things were running okay, once you add this system to ease development, your performance may likely go down. It will depend on how many abilities you have in game, vs the average number per object, contrasted to the depth required to simulate OOP. The processing power per empty event is not trivial when duplicating structures like this with nested loops. I have found this method to be cumbersome to set up and don't recommend it. It creates less complexity to add and modify abilities or effects, but is itself difficult to maintain at any scale and tedious to expand.
If construct allowed families to inherit from families, this would be a non-issue. Another solution would be containers, but containers don't work with families. So you have a weird dynamic where you have to pick your poison.
ECS
:
Setting up ECS is easier imo, but similar to simulated OOP, it requires repeating boiler plate for every system you need to handle ECS. In a nutshell, you have to use some OOP style systems like described above, but you use it only for attaching the ecs behavior to objects. The depth of the oop is shallow, but it still requires boiler plate per added object type. Basically you create a family for "systems", and family for "components". The raw requirements for these families is that the system handles component addition and removal, and routes functionality to the components. You can connect this via UIDs or create a parent/child graph and use that. In a platformer example, a system could be a character handler, and a component would be the ability or behavior. The handler performs the abstract task of routing input and state to the active behavior. Some people use state machine type logic where the components define transitions to other behaviors, but I prefer the transitions to be handled without a component specifying the components it transitions to.
At the end of the day, for ECS, you will have to use a Foreach loop for every system, and then loop through the components, running various custom actions like "Check" "Enter" "exit" "update" etc... The overhead of the small scale OOP to pick related families, dictionary lists of components, etc... and then dynamic routing of state/input through components, means that the performance overhead of each system scales with its complexity and its possible to achieve better performance than the simple/basic branching conditionals if the project is complicated. Also, adding functionality is as easy as the system is designed for, and allows the functionality to be self contained. apart from the fact that you still have to boiler plate every object.
In other words, if I want mario to be able to do a ground pound one day, I clone the object type "BehaviorComponentsTemplate", name it "Behavior_groundPound", copy the template eventsheet for the boiler plate (replacing appropriate code), and then manually add every action needing overridden, and then add in specific functionality. The actual functionality is faster to write than setting up the boiler plate. Then add an include in the correct event sheet and go.
The payoff? All the functionality for ground pound is now a component I could add to any character, without having to add conditionals, and all contained in one place instead of being in multiple locations.
The downside: This system requires a lot of nested loops and picking so you do take a performance hit over a manually crafted rigid system. Any character that has rigid abilities should still be manually coded, meaning you now potentially have duplicated code all over or need a simple light weight router system. Again, this is alot of work to set up, and requires ongoing boiler plate management; Meaning if you change the system, you may have to change every single bit of boiler plate through the entire project.
BOOM. That is BAD, BAD, BAD.
TLDR
So basically, c3 forces bad types of programing in order to avoid bad types of programing while solving complex/advanced problems, and the solutions themselves are difficult to maintain or scale. You avoid one issue, but create a different issue.
The takeaway? Construct isn't the right solution for a sufficiently complex project with high dynamic object counts. It literally becomes a case of, this isn't possible, and better off in a different engine.
For my own project, I really wanted to finish a game with construct, so I went from a bullet hell style game with high numbers to a game with fewer but much more impactful threats. It actually turned out to be good for game feel, as it reduces noise and makes choices more meaningful. Each enemy is an intimate threat, instead of one of a 1000.