Platformer Example

import { wait } from "util/time.js";
import { BlockPhysics } from "planet/chunk/physics.js";
import { BLOCK_AIR } from "planet/chunk/builder.js";
import { Vector3, Box2, Vector2 } from "three";
import { NET_SERVER } from "net/ibs/share/const.js";

export const BlocksPlatformer =
{
	name: "BlocksPlatformer",
	assets: ["assets/engine/mods/BlocksPlatformer/tex-blocks-platformer.png"],
	
	blocks:
	[
		{
			name: "bb.block.platformer.checkpoint",
			display: "Checkpoint",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-checkpoint.jpg" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(0, 0), new Vector2(256, 256)) } }]
		},
		{
			name: "bb.block.platformer.death",
			display: "Block of Death",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-death.png" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(256, 0), new Vector2(512, 256)) } }]
		},
		{
			name: "bb.block.platformer.superjump",
			display: "Super Jump",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-superjump.jpg" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(0, 256), new Vector2(256, 512)) } }]
		},
		{
			name: "bb.block.platformer.superspeed",
			display: "Super Speed",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-superspeed.png" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(0, 640), new Vector2(64, 704)) } }]
		},
		{
			name: "bb.block.platformer.trapdoor",
			display: "Trapdoor",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-trapdoor.png" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(256, 256), new Vector2(512, 512)) } }]
		},
		{
			name: "bb.block.platformer.springjumps",
			display: "Spring Jumps",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-spring.png" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(0, 512), new Vector2(128, 640)) } }]
		},
		{
			name: "bb.block.platformer.nodamage",
			display: "No Damage",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-nodamage.png" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(128, 512), new Vector2(256, 640)) } }]
		},
		{
			name: "bb.block.platformer.hoverboots",
			display: "Hover Boots",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-hoverboots.png" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(256, 512), new Vector2(384, 640)) } }]
		},
		{
			name: "bb.block.platformer.tempflying",
			display: "Flying Enabled",
			meta: { material: { default: "assets/engine/mods/BlocksPlatformer/icons/tex-blocks-platformer-canfly.png" } },
			faces: [{ texture: { asset: "tex-blocks-platformer", pos: new Box2(new Vector2(384, 512), new Vector2(512, 640)) } }]
		}
	],
	
	server()
	{
		var getBlock = (name) => DEBUG.BB.planet.scene.blockName2Type.get(name);
		
		getBlock("bb.block.platformer.checkpoint").addEventListener(NET_SERVER, "playerfootenter", function({ player, x, y, z })
		{
			var newSpawn = new Vector3(x + 0.5, y + 1, z + 0.5);
			if(player.spawnPos.equals(newSpawn)) return;
			
			var planet = player.planet;
			player.serverSetSpawn(newSpawn);
			planet.serverSetMessage(planet.net?.peers.get(player.owner), "Checkpoint reached!", 2);
		});
		
		//must be server sided, at least for now until we can create an RBChunk on the client side
		getBlock("bb.block.platformer.trapdoor").addEventListener(NET_SERVER, "playerfootenter", async function({ player, x, y, z })
		{
			var scene = player.planet.scene;
			var prvShape = scene.getShape(x, y, z);
			
			//"this" refers to the engine's BlockType class instance for trapdoors
			//block's unsnapAnimation must be set to ragdoll in order to get RBChunk
			var rbchunk = BlockPhysics.unsnapSingle(scene, x, y, z, false, this);
			
			//make block spin randomly. clients will not see this!
			rbchunk.phys.setAngularVelocity((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10);
			
			await wait(3);
			
			//only build a new trapdoor if the spot is still empty
			if(scene.getShape(x, y, z) === BLOCK_AIR)
			{
				if(!rbchunk.disposed)
					rbchunk.dispose();
				
				scene.setBlock(x, y, z, prvShape, this.name, true);
			}
		});
	},
	
	/* javascript-obfuscator:disable */
	//obfuscator enable+disable comments required when client script is inside engine source code
	//colon is required here. no shorthand
	client: function()
	{
		var getBlock = (name) => DEBUG.BB.planet.scene.blockName2Type.get(name);
		
		getBlock("bb.block.platformer.death").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player })
		{
			player.addHealth(-player.maxHealth);
		});
		
		getBlock("bb.block.platformer.superjump").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player })
		{
			powerUpBlock(player, "Super Jump!", 23, 10, player.jumpVelocity, (x) => player.jumpVelocity = x);
		});
		
		getBlock("bb.block.platformer.superspeed").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player })
		{
			powerUpBlock(player, "Super Speed!", 15, 10, player.speedWalk, (x) => player.speedWalk = x);
		});
		
		getBlock("bb.block.platformer.springjumps").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player })
		{
			player.planet.serverSetMessage(null, "Spring Coil!", 4);
			player.jump(300, true);
		});
		
		getBlock("bb.block.platformer.nodamage").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player })
		{
			powerUpBlock(player, "Navi - No Damage!", false, 10, player.fallDamage, (x) => player.fallDamage = x);
		});
		
		getBlock("bb.block.platformer.hoverboots").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player })
		{
			powerUpBlock(player, "Hover Boots!", 10, 10, player.coyoteTime, (x) => player.coyoteTime = x);
		});
		
		getBlock("bb.block.platformer.tempflying").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player })
		{
			powerUpBlock(player, "Flying Enabled - 10 Seconds", true, 10, player.canFly, (x) => player.setCanFly(x));
		});
		
		//make trapdoor block disappear instantly on client side to prevent host advantage
		getBlock("bb.block.platformer.trapdoor").addEventListener(DEBUG.NET_CLIENT, "playerfootenter", function({ player, x, y, z })
		{
			var planet = player.planet;
			if(!planet.isHosting())
				planet.scene.setBlock(x, y, z, 0, "bb.block.platformer.trapdoor", false);
		});
		
		//changes a player property to [newVal] for [time] seconds
		//still need to add a listener for player death that removes powerup
		async function powerUpBlock(player, name, newVal, time, prvVal, cb)
		{
			var planet = player.planet;
			
			var metadata;
			if(player.metadata.has("powerUpData"))
			{
				metadata = player.metadata.get("powerUpData");
			}
			else
			{
				metadata = {};
				player.metadata.set("powerUpData", metadata);
			}
			
			var powerUpData = metadata[name] ??= {};
			
			var prv;
			if(powerUpData.clearTimeout)
			{
				//boost the powerup the player already has
				prv = powerUpData.prv;
				powerUpData.clearTimeout();
			}
			else
			{
				//create new powerup
				prv = prvVal;
				powerUpData.prv = prv;
			}
			
			cb(newVal);
			planet.serverSetMessage(null, name, time); //null for client sided message
			
			var promise = DEBUG.wait(time);
			powerUpData.clearTimeout = promise.clearTimeout; //save the timeout in case it needs to be cancelled by a newer powerup
			await promise;
			
			delete metadata[name];
			cb(prv);
		}
	}
	/* javascript-obfuscator:enable */
};

Last updated