From 1be4fd9a330ea801ef9f204a2b1e5716c083174e Mon Sep 17 00:00:00 2001 From: thisisuhhplanetring Date: Thu, 10 Sep 2026 21:26:37 +0300 Subject: [PATCH 1/6] Support 2.2 object scaling (128/129) and warp (131/132) Levels built in 2.2 use per-axis scale and warp everywhere, and neither survived the trip into Web Dashers: 128/129 and 131/132 were not parsed at all, so a scaled or warped object loaded at its default size and rotation. Key 32 was parsed but only ever reached the sprite, never the hitbox. Parsing 128/129 are the two scale axes. 2.2 writes them instead of key 32, and writes them independently, so each falls back to 32 and 32 falls back to 1. 131/132 are the object's cocos2d rotationX and rotationY - what the editor calls warp. Equal values are an ordinary rotation, and 2.2 writes the pair INSTEAD of key 6, so the pair wins over key 6 rather than the reverse. They are stored as one rotation plus the spread between the axes, which keeps a warped object's warp when something later rotates it. Rendering Phaser builds a sprite matrix from rotation + scaleX + scaleY, three numbers that can only describe a rectangle. A warped object is a parallelogram, so a warped sprite holds an identity transform of its own and its render call is wrapped to pass the renderer a parent matrix with the real transform folded in. Tint, flip, blend mode, depth and origin are untouched, and the formula reduces to Phaser's own applyITRS when the two axis angles are equal. Hitboxes Solids, hazards, slopes, portals, pads, orbs, coins and speed portals all size from the object's scale now. Warp is deliberately not applied: in GD the hitbox stays a rectangle however far the sprite is sheared. Enter effects updateEnterEffects, updateAudioScale and the orb pulse animate a scale factor and used to write it straight onto the sprite, which assumed every object sits at scale 1 - so a scaled object snapped to full size the moment it entered the screen. This is why key 32 never visibly worked either. They multiply by the object's own scale now. Editor _serializeObject writes 128/129 and 131/132 back, and keeps old levels on key 32 and key 6 so they round-trip unchanged. Verified against level 22, which has 11,897 scaled and 487 warped objects, and against a purpose-built level covering each key alone and combined: a 45 degree turn written as key 6 and as 131/132 renders and collides identically, and every case survives a save/reload. --- assets/scripts/core/level-editor.js | 37 ++++- assets/scripts/core/level.js | 224 ++++++++++++++++++++++------ 2 files changed, 211 insertions(+), 50 deletions(-) diff --git a/assets/scripts/core/level-editor.js b/assets/scripts/core/level-editor.js index b9fb83fe..01336458 100644 --- a/assets/scripts/core/level-editor.js +++ b/assets/scripts/core/level-editor.js @@ -8607,9 +8607,42 @@ _serializeObject(object) { objectData[3] = String(object.y ?? 0); objectData[4] = object.flipX ? "1" : "0"; objectData[5] = object.flipY ? "1" : "0"; - objectData[6] = String(object.rot ?? 0); - objectData[32] = String(object.scale ?? 1); + const dropKey = (key) => { + delete objectData[key]; + delete objectData[String(key)]; + }; + const numberOr = (value, fallback) => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; + }; + + const rotation = numberOr(object.rot, 0); + const warp = numberOr(object.warp, 0); + if (warp) { + dropKey(6); + objectData[131] = String(rotation - warp / 2); + objectData[132] = String(rotation + warp / 2); + } else { + objectData[6] = String(rotation); + dropKey(131); + dropKey(132); + } + + const uniformScale = numberOr(object.scale, 1); + const scaleX = numberOr(object.scaleX, uniformScale); + const scaleY = numberOr(object.scaleY, uniformScale); + const hadAxisScale = object._raw?.[128] !== undefined || object._raw?.["128"] !== undefined + || object._raw?.[129] !== undefined || object._raw?.["129"] !== undefined; + if (scaleX !== scaleY || hadAxisScale) { + dropKey(32); + objectData[128] = String(scaleX); + objectData[129] = String(scaleY); + } else { + objectData[32] = String(scaleX); + dropKey(128); + dropKey(129); + } objectData[20] = String(object.editorLayer ?? object._raw?.[20] ?? object._raw?.["20"] ?? 0); objectData[24] = String(object.zLayer ?? 0); diff --git a/assets/scripts/core/level.js b/assets/scripts/core/level.js index 087b3249..817e1797 100644 --- a/assets/scripts/core/level.js +++ b/assets/scripts/core/level.js @@ -99,6 +99,10 @@ function parseObject(objectString) { } let objectId = parseInt(objectData[1] || "0", 10); const rawGroupValues = []; + const readFloat = (key, fallback) => { + const value = parseFloat(objectData[key]); + return Number.isFinite(value) ? value : fallback; + }; const addRawGroups = (rawValue) => { String(rawValue ?? "") .split(".") @@ -109,6 +113,18 @@ function parseObject(objectString) { addRawGroups(objectData[33]); addRawGroups(objectData[57]); const groupString = [...new Set(rawGroupValues)].join("."); + + const legacyScale = readFloat(32, 1); + const scaleX = readFloat(128, legacyScale); + const scaleY = readFloat(129, legacyScale); + + const baseRotation = readFloat(6, 0); + const rotationX = readFloat(131, baseRotation); + const rotationY = readFloat(132, baseRotation); + + const warp = ((rotationY - rotationX) % 360 + 540) % 360 - 180; + const rotation = rotationX + warp / 2; + if (objectId === 0) { return null; } else { @@ -118,8 +134,11 @@ function parseObject(objectString) { y: parseFloat(objectData[3] || "0"), flipX: objectData[4] === "1", flipY: objectData[5] === "1", - rot: parseFloat(objectData[6] || "0"), - scale: parseFloat(objectData[32] || "1"), + rot: rotation, + scale: legacyScale, + scaleX: scaleX, + scaleY: scaleY, + warp: warp, editorLayer: parseInt(objectData[20] || "0", 10), zLayer: parseInt(objectData[24] || "0", 10), zOrder: parseInt(objectData[25] || "0", 10), @@ -396,6 +415,107 @@ function _isDecorationSlopeId(id) { return false; } +function gdObjectScale(levelObj) { + const uniform = Number(levelObj?.scale); + const fallback = Number.isFinite(uniform) ? uniform : 1; + const scaleX = Number(levelObj?.scaleX); + const scaleY = Number(levelObj?.scaleY); + return { + x: Number.isFinite(scaleX) ? scaleX : fallback, + y: Number.isFinite(scaleY) ? scaleY : fallback + }; +} + +function gdObjectWarp(levelObj) { + const warp = Number(levelObj?.warp); + return Number.isFinite(warp) ? warp : 0; +} + +function gdWarpAxes(rotationDeg, warpDeg) { + const toRad = Math.PI / 180; + return { + rotX: (rotationDeg - warpDeg / 2) * toRad, + rotY: (rotationDeg + warpDeg / 2) * toRad + }; +} + +function gdTransformLocalPoint(x, y, rotationDeg, scaleX, scaleY, warpDeg) { + const { rotX, rotY } = gdWarpAxes(rotationDeg, warpDeg); + return { + x: Math.cos(rotY) * scaleX * x - Math.sin(rotX) * scaleY * y, + y: Math.sin(rotY) * scaleX * x + Math.cos(rotX) * scaleY * y + }; +} + +const _gdWarpLinear = new Phaser.GameObjects.Components.TransformMatrix(); +const _gdWarpParent = new Phaser.GameObjects.Components.TransformMatrix(); + +function _gdWarpParentMatrix(sprite, camera, parentMatrix) { + const warp = sprite._gdWarp; + const rotX = warp.rotationX; + const rotY = warp.rotationY; + + _gdWarpLinear.setTransform( + Math.cos(rotY) * warp.scaleX, Math.sin(rotY) * warp.scaleX, + -Math.sin(rotX) * warp.scaleY, Math.cos(rotX) * warp.scaleY, + 0, 0 + ); + + let originX = sprite.x; + let originY = sprite.y; + if (camera.roundPixels) { + originX = Math.floor(originX); + originY = Math.floor(originY); + } + + if (parentMatrix) _gdWarpParent.copyFrom(parentMatrix); + else _gdWarpParent.loadIdentity(); + + _gdWarpParent.translate(originX, originY); + _gdWarpParent.multiply(_gdWarpLinear, _gdWarpParent); + _gdWarpParent.translate(-originX, -originY); + return _gdWarpParent; +} + +function gdApplyEffectScale(sprite, factor) { + const nextX = factor * (sprite._eeBaseScaleX ?? 1); + const nextY = factor * (sprite._eeBaseScaleY ?? 1); + if (sprite.scaleX !== nextX || sprite.scaleY !== nextY) sprite.setScale(nextX, nextY); +} + +function applyGDObjectTransform(sprite, rotationDeg, scaleX, scaleY, warpDeg) { + if (!sprite) return sprite; + + if (!warpDeg) { + if (rotationDeg) sprite.setAngle(rotationDeg); + if (scaleX !== 1 || scaleY !== 1) sprite.setScale(scaleX, scaleY); + sprite._eeBaseScaleX = scaleX; + sprite._eeBaseScaleY = scaleY; + return sprite; + } + + sprite._eeBaseScaleX = 1; + sprite._eeBaseScaleY = 1; + + const { rotX, rotY } = gdWarpAxes(rotationDeg, warpDeg); + sprite._gdWarp = { rotationX: rotX, rotationY: rotY, scaleX: scaleX, scaleY: scaleY }; + + if (sprite._gdWarpHooked) return sprite; + sprite._gdWarpHooked = true; + + const drawWebGL = sprite.renderWebGL; + const drawCanvas = sprite.renderCanvas; + + sprite.renderWebGL = function (renderer, src, camera, parentMatrix) { + drawWebGL.call(this, renderer, src, camera, _gdWarpParentMatrix(src, camera, parentMatrix)); + }; + sprite.renderCanvas = function (renderer, src, camera, parentMatrix) { + drawCanvas.call(this, renderer, src, camera, _gdWarpParentMatrix(src, camera, parentMatrix)); + }; + + return sprite; +} + function rotateSlopePoint(localX, localY, rotDeg) { const theta = -rotDeg * Math.PI / 180; const cosT = Math.cos(theta), sinT = Math.sin(theta); @@ -407,10 +527,11 @@ function _createSlopeCollider(levelObj, objectDef, worldX, worldY) { if (!slopeData) return null; if (slopeData.sq) return null; if (_isDecorationSlopeId(levelObj.id)) return null; - const _scaleRaw = Number(levelObj.scale); - const _scale = Number.isFinite(_scaleRaw) && _scaleRaw > 0 ? _scaleRaw : 1; - const hw0 = (slopeData.gw * a * _scale) / 2; - const hh0 = (slopeData.gh * a * _scale) / 2; + const _scale = gdObjectScale(levelObj); + const _scaleX = Math.abs(_scale.x) > 0 ? Math.abs(_scale.x) : 1; + const _scaleY = Math.abs(_scale.y) > 0 ? Math.abs(_scale.y) : 1; + const hw0 = (slopeData.gw * a * _scaleX) / 2; + const hh0 = (slopeData.gh * a * _scaleY) / 2; let vRight = { x: hw0, y: -hh0 }; let vLo = { x: -hw0, y: -hh0 }; let vHi = { x: hw0, y: hh0 }; @@ -1608,21 +1729,18 @@ window.LevelObject = class LevelObject { offsetY = -offsetY; } let totalRotation = (sprite.getData("gjBaseRotationDeg") || 0) + objectData.rot; - if (totalRotation !== 0) { - sprite.setAngle(totalRotation); - let rad = totalRotation * Math.PI / 180; - let cosR = Math.cos(rad); - let sinR = Math.sin(rad); - let rx = offsetX * cosR - offsetY * sinR; - let ry = offsetX * sinR + offsetY * cosR; - offsetX = rx; - offsetY = ry; + const objectScale = gdObjectScale(objectData); + const objectWarp = gdObjectWarp(objectData); + if (totalRotation !== 0 || objectScale.x !== 1 || objectScale.y !== 1 || objectWarp !== 0) { + const placed = gdTransformLocalPoint( + offsetX, offsetY, totalRotation, objectScale.x, objectScale.y, objectWarp + ); + offsetX = placed.x; + offsetY = placed.y; } sprite.x += offsetX; sprite.y += offsetY; - if (objectData.scale !== 1) { - sprite.setScale(objectData.scale); - } + applyGDObjectTransform(sprite, totalRotation, objectScale.x, objectScale.y, objectWarp); if (colorData) { const blackDefault = colorData.black === true || colorData.tint === 0; if (colorData.tint !== undefined) { @@ -1952,9 +2070,14 @@ window.LevelObject = class LevelObject { }).setOrigin(0.5); } - const scale = Number.isFinite(Number(levelObj.scale)) ? Number(levelObj.scale) : 1; - textSprite.setScale(scale * (levelObj.flipX ? -1 : 1), scale * (levelObj.flipY ? -1 : 1)); - textSprite.setAngle(levelObj.rot || 0); + const textScale = gdObjectScale(levelObj); + applyGDObjectTransform( + textSprite, + levelObj.rot || 0, + textScale.x * (levelObj.flipX ? -1 : 1), + textScale.y * (levelObj.flipY ? -1 : 1), + gdObjectWarp(levelObj) + ); const depthBase = { "-5": -12, "-3": -9, "-1": -6, 0: 0, 1: 3, 3: 6, 5: 9, 7: 10.5, 9: 12, 11: 13.5 }; const zLayer = parseInt(levelObj.zLayer ?? objectDef?.default_z_layer ?? 3, 10) || 0; @@ -2457,7 +2580,7 @@ window.LevelObject = class LevelObject { } if (objectDef && objectDef.type === ringType) { - sprite.setScale(0.75); + gdApplyEffectScale(sprite, 0.75); sprite._eeAudioScale = true; sprite._orbId = levelObj.id; this._orbSprites.push(sprite); @@ -2468,7 +2591,7 @@ window.LevelObject = class LevelObject { } if (orbGlow) { - orbGlow.setScale(0.75); + gdApplyEffectScale(orbGlow, 0.75); orbGlow._eeAudioScale = true; orbGlow._orbId = levelObj.id; this._orbSprites.push(orbGlow); @@ -2867,6 +2990,11 @@ window.LevelObject = class LevelObject { } }; + const hitScale = gdObjectScale(levelObj); + const hitScaleX = Math.abs(hitScale.x); + const hitScaleY = Math.abs(hitScale.y); + const hitScaleMean = (hitScaleX + hitScaleY) / 2; + const slopeCollider = _createSlopeCollider(levelObj, objectDef, worldX, worldY); if (slopeCollider) { registerCollider(slopeCollider); @@ -2874,8 +3002,8 @@ window.LevelObject = class LevelObject { hasCollisionEntry = true; this._addCollisionToSection(slopeCollider); } else if (objectDef.type === solidType && objectDef.gridW > 0 && objectDef.gridH > 0) { - const w = objectDef.gridW * a; - const h = objectDef.gridH * a; + const w = objectDef.gridW * a * hitScaleX; + const h = objectDef.gridH * a * hitScaleY; const collider = new Collider(solidType, worldX, worldY, w, h, levelObj.rot || 0); collider.objid = levelObj.id; registerCollider(collider); @@ -2892,15 +3020,15 @@ window.LevelObject = class LevelObject { objectDef.hitboxScaleX !== undefined && objectDef.hitboxScaleY !== undefined ) { - hitW = objectDef.spriteW * objectDef.hitboxScaleX * 2; - hitH = objectDef.spriteH * objectDef.hitboxScaleY * 2; + hitW = objectDef.spriteW * objectDef.hitboxScaleX * 2 * hitScaleX; + hitH = objectDef.spriteH * objectDef.hitboxScaleY * 2 * hitScaleY; } else if (objectDef.gridW > 0 && objectDef.gridH > 0) { - hitW = objectDef.gridW * 12; - hitH = objectDef.gridH * 24; + hitW = objectDef.gridW * 12 * hitScaleX; + hitH = objectDef.gridH * 24 * hitScaleY; } const hasHitboxRadius = objectDef.hitbox_radius !== undefined && objectDef.hitbox_radius !== null; - const worldHitboxRadius = hasHitboxRadius ? objectDef.hitbox_radius * 2 : 0; + const worldHitboxRadius = hasHitboxRadius ? objectDef.hitbox_radius * 2 * hitScaleMean : 0; if (hasHitboxRadius && hitW === 0) { hitW = worldHitboxRadius * 2; @@ -2916,8 +3044,8 @@ window.LevelObject = class LevelObject { this._addCollisionToSection(collider); } } else if (objectDef.type === portalType) { - const portalW = objectDef.gridW * a; - const portalH = objectDef.gridH * a; + const portalW = objectDef.gridW * a * hitScaleX; + const portalH = objectDef.gridH * a * hitScaleY; const portalSub = objectDef.sub || { 10: "gravity_flip", 11: "gravity_normal", @@ -2980,8 +3108,8 @@ window.LevelObject = class LevelObject { this._addCollisionToSection(collider); } } else if (objectDef.type === padType) { - const padW = objectDef.gridW * a; - const padH = objectDef.gridH * a; + const padW = objectDef.gridW * a * hitScaleX; + const padH = objectDef.gridH * a * hitScaleY; const padObj = new Collider(jumpPadType, worldX, worldY, padW, padH, levelObj.rot || 0); padObj.padId = levelObj.id; registerCollider(padObj); @@ -3063,8 +3191,8 @@ window.LevelObject = class LevelObject { this.topContainer.add(_padEmitter); } } else if (objectDef.type === ringType) { - const orbW = objectDef.gridW * a; - const orbH = objectDef.gridH * a; + const orbW = objectDef.gridW * a * hitScaleX; + const orbH = objectDef.gridH * a * hitScaleY; const orbObj = new Collider(jumpRingType, worldX, worldY, orbW, orbH, levelObj.rot || 0); orbObj.orbId = levelObj.id; orbObj.orbRotation = levelObj.rot || 0; @@ -3074,8 +3202,8 @@ window.LevelObject = class LevelObject { hasCollisionEntry = true; this._addCollisionToSection(orbObj); } else if (objectDef.type === coinType) { - const coinW = (objectDef.gridW || 1) * a; - const coinH = (objectDef.gridH || 1) * a; + const coinW = (objectDef.gridW || 1) * a * hitScaleX; + const coinH = (objectDef.gridH || 1) * a * hitScaleY; const coinObj = new Collider(coinType, worldX, worldY, coinW, coinH, levelObj.rot || 0); coinObj.coinId = levelObj.id; if (objectId === SECRET_COIN_OBJECT_ID) { @@ -3093,8 +3221,8 @@ window.LevelObject = class LevelObject { hasCollisionEntry = true; this._addCollisionToSection(coinObj); } else if (objectDef.type === speedType) { - const speedW = (objectDef.gridW || 1) * a; - const speedH = (objectDef.gridH || 1) * a; + const speedW = (objectDef.gridW || 1) * a * hitScaleX; + const speedH = (objectDef.gridH || 1) * a * hitScaleY; const speedObj = new Collider(speedType, worldX, worldY, speedW, speedH, levelObj.rot || 0); speedObj.portalY = worldY; @@ -4301,7 +4429,7 @@ window.LevelObject = class LevelObject { visMinSection.x = visMinSection._eeWorldX; visMinSection.y = visMinSection._eeBaseY; if (!visMinSection._eeAudioScale) { - visMinSection.setScale(1); + gdApplyEffectScale(visMinSection, 1); } visMinSection.setAlpha(this._getGroupOpacityForSprite(visMinSection)); } @@ -4347,7 +4475,7 @@ window.LevelObject = class LevelObject { effectSprite.y = effectSprite._eeBaseY; effectSprite.x = effectSprite._eeWorldX; if (!effectSprite._eeAudioScale) { - effectSprite.setScale(1); + gdApplyEffectScale(effectSprite, 1); } effectSprite.setAlpha(this._getGroupOpacityForSprite(effectSprite)); } @@ -4363,7 +4491,7 @@ window.LevelObject = class LevelObject { effectSprite.y = effectSprite._eeBaseY; effectSprite.x = effectSprite._eeWorldX; if (!effectSprite._eeAudioScale) { - effectSprite.setScale(1); + gdApplyEffectScale(effectSprite, 1); } effectSprite.setAlpha(this._getGroupOpacityForSprite(effectSprite)); } @@ -4411,8 +4539,8 @@ window.LevelObject = class LevelObject { if (effectSprite.alpha !== _eeFinalAlpha) { effectSprite.alpha = _eeFinalAlpha; } - if (!effectSprite._eeAudioScale && effectSprite.scaleX !== _0x127ace) { - effectSprite.setScale(_0x127ace); + if (!effectSprite._eeAudioScale) { + gdApplyEffectScale(effectSprite, _0x127ace); } } } @@ -4447,7 +4575,7 @@ window.LevelObject = class LevelObject { const _worldX = _0x24afdb._eeWorldX; if (Number.isFinite(_worldX) && (_worldX < _minVisibleX || _worldX > _maxVisibleX)) continue; if (_0x24afdb._eeLastAudioScale !== _targetAudioScale) { - _0x24afdb.setScale(_targetAudioScale); + gdApplyEffectScale(_0x24afdb, _targetAudioScale); _0x24afdb._eeLastAudioScale = _targetAudioScale; } } @@ -4474,7 +4602,7 @@ window.LevelObject = class LevelObject { } } if (_0xOrbSpr._eeLastOrbScale !== _targetScale) { - _0xOrbSpr.setScale(_targetScale); + gdApplyEffectScale(_0xOrbSpr, _targetScale); _0xOrbSpr._eeLastOrbScale = _targetScale; } } @@ -4534,7 +4662,7 @@ window.LevelObject = class LevelObject { this._secretCoinRunCollected.clear(); this._userCoinRunCollected.clear(); for (let _0x5c5d9a of this._audioScaleSprites) { - _0x5c5d9a.setScale(0.1); + gdApplyEffectScale(_0x5c5d9a, 0.1); } for (const objectSpriteList of this.objectSprites || []) { if (!objectSpriteList) continue; From 7887d92f39723a4418f8beb59266a53cf2b34b82 Mon Sep 17 00:00:00 2001 From: thisisuhhplanetring Date: Thu, 10 Sep 2026 21:37:03 +0300 Subject: [PATCH 2/6] Scale composite objects' child parts with the parent An object built from several sprites positions each part from a localDx/localDy in the object's own space. That offset was rotated with the object but not scaled or warped, so a scaled composite kept its parts at unscaled distances - the pieces sat too close together while each piece drew at the right size. 2576 of the 4082 objects are composites, so this covers most of the object set: portals, orbs, pads and every decorated block. The offset now goes through the same transform as the object. It reduces to the rotation that was there before at scale 1 with no warp, so unscaled objects are untouched. --- assets/scripts/core/level.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/scripts/core/level.js b/assets/scripts/core/level.js index 817e1797..3795bf9f 100644 --- a/assets/scripts/core/level.js +++ b/assets/scripts/core/level.js @@ -2721,9 +2721,13 @@ window.LevelObject = class LevelObject { localDy = -localDy; } - const rot = (levelObj.rot || 0) * Math.PI / 180; - childDx = localDx * Math.cos(rot) - localDy * Math.sin(rot); - childDy = localDx * Math.sin(rot) + localDy * Math.cos(rot); + const childScale = gdObjectScale(levelObj); + const placed = gdTransformLocalPoint( + localDx, localDy, levelObj.rot || 0, + childScale.x, childScale.y, gdObjectWarp(levelObj) + ); + childDx = placed.x; + childDy = placed.y; } const childWorldX = worldX + childDx; From 171c1a004267ed4dc80f07cdbf71a58a015ccc35 Mon Sep 17 00:00:00 2001 From: thisisuhhplanetring Date: Thu, 17 Sep 2026 10:38:30 +0300 Subject: [PATCH 3/6] fixed wave hitbox bug --- assets/scripts/core/player.js | 60 +++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/assets/scripts/core/player.js b/assets/scripts/core/player.js index 0ee23f03..0b0ef86a 100644 --- a/assets/scripts/core/player.js +++ b/assets/scripts/core/player.js @@ -4827,11 +4827,12 @@ if (this.p.isFlying || this.p.isUfo) { } continue; } else if (_colType === solidType) { - let _0x146a97 = playersY - playerSize + gamemodeAddition; - let _0x869e42 = playersLastY - playerSize + gamemodeAddition; - let _0x3e7199 = playersY + playerSize - gamemodeAddition; - let _0x135a9d = playersLastY + playerSize - gamemodeAddition; - const _0x55559d = 9; + const _solidSize = this.p.isWave ? waveHitSize : playerSize; + let _0x146a97 = playersY - _solidSize + gamemodeAddition; + let _0x869e42 = playersLastY - _solidSize + gamemodeAddition; + let _0x3e7199 = playersY + _solidSize - gamemodeAddition; + let _0x135a9d = playersLastY + _solidSize - gamemodeAddition; + const _0x55559d = this.p.isWave ? waveHitSize / 3 : 9; let iscolliding; if (_hasCircleHitbox) { const _sdx = pieceWidth - gameObj.x; @@ -4850,8 +4851,8 @@ if (this.p.isFlying || this.p.isUfo) { const _0xLandTop = (this.p.yVelocity >= 0 || this.p.onGround) && (_0x3e7199 <= top || _0x135a9d <= top); const isstandingOnAPlatform = this.p.gravityFlipped ? _0xLandTop : _0xLandBot; const _slopeLeadIn = this._slopeRiding && (this.p.gravityFlipped - ? _0x3e7199 <= top + playerSize - : _0x146a97 >= bottom - playerSize); + ? _0x3e7199 <= top + _solidSize + : _0x146a97 >= bottom - _solidSize); if (iscolliding && !isstandingOnAPlatform && !_slopeLeadIn) { if (window.noClip) { @@ -4865,16 +4866,16 @@ if (this.p.isFlying || this.p.isUfo) { } const _sDist = Math.abs(playersY - playersLastY); const _normalLandBot = _0x146a97 >= bottom && _0x146a97 - bottom <= _sDist + gamemodeAddition; - const _coyoteBot = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x146a97 >= bottom && _0x146a97 - bottom < playerSize + gamemodeAddition; + const _coyoteBot = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x146a97 >= bottom && _0x146a97 - bottom < _solidSize + gamemodeAddition; const _skipBot = _0x146a97 < bottom && _0x869e42 >= bottom; const _landBot = _normalLandBot || _coyoteBot || _skipBot; const _normalLandTop = _0x3e7199 <= top && top - _0x3e7199 <= _sDist + gamemodeAddition; - const _coyoteTop = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x3e7199 <= top && top - _0x3e7199 < playerSize + gamemodeAddition; + const _coyoteTop = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x3e7199 <= top && top - _0x3e7199 < _solidSize + gamemodeAddition; const _skipTop = _0x3e7199 > top && _0x135a9d <= top; const _landTop = _normalLandTop || _coyoteTop || _skipTop; - if (pieceWidth + playerSize - 5 > left && pieceWidth - playerSize + 5 < right) { + if (pieceWidth + _solidSize - 5 > left && pieceWidth - _solidSize + 5 < right) { if (!this.p.gravityFlipped && _landBot && (this.p.yVelocity <= 0 || this.p.onGround)) { - this.p.y = bottom + playerSize; + this.p.y = bottom + _solidSize; this.hitGround(); _0x30410f = true; this.p.collideBottom = bottom; @@ -4884,7 +4885,7 @@ if (this.p.isFlying || this.p.isUfo) { continue; } if (this.p.gravityFlipped && !this.p.isFlying && _landTop && (this.p.yVelocity >= 0 || this.p.onGround)) { - this.p.y = top - playerSize; + this.p.y = top - _solidSize; this.hitGround(); _0x30410f = true; this.p.onCeiling = true; @@ -4896,14 +4897,14 @@ if (this.p.isFlying || this.p.isUfo) { } if (this.p.isUfo) { if (!this.p.gravityFlipped && _landTop && (this.p.yVelocity >= 0 || this.p.onGround)) { - this.p.y = top - playerSize; + this.p.y = top - _solidSize; this.hitGround(); this.p.onCeiling = true; this.p.collideTop = top; continue; } if (this.p.gravityFlipped && _landBot && (this.p.yVelocity <= 0 || this.p.onGround)) { - this.p.y = bottom + playerSize; + this.p.y = bottom + _solidSize; this.hitGround(); _0x30410f = true; this.p.onCeiling = true; @@ -4912,8 +4913,8 @@ if (this.p.isFlying || this.p.isUfo) { } continue; } - if (_landTop && (this.p.yVelocity >= 0 || this.p.onGround) && this.p.isFlying) { - this.p.y = top - playerSize; + if (_landTop && (this.p.yVelocity >= 0 || this.p.onGround) && (this.p.isFlying || this.p.isWave)) { + this.p.y = top - _solidSize; this.hitGround(); this.p.onCeiling = true; this.p.collideTop = top; @@ -4929,8 +4930,8 @@ if (this.p.isFlying || this.p.isUfo) { } continue; } - if (this.p.gravityFlipped && _landBot && (this.p.yVelocity <= 0 || this.p.onGround) && this.p.isFlying) { - this.p.y = bottom + playerSize; + if (this.p.gravityFlipped && _landBot && (this.p.yVelocity <= 0 || this.p.onGround) && (this.p.isFlying || this.p.isWave)) { + this.p.y = bottom + _solidSize; this.hitGround(); _0x30410f = true; this.p.onCeiling = true; @@ -5233,10 +5234,16 @@ if (this.p.isFlying || this.p.isUfo) { } graphics.lineStyle(1, hexToHexadecimal("0000ff"), 1); + // inner hitbox + graphics.strokeRect(trailX - 9, trailY - 9, 18, 18); + } else { + const waveOuter = entrySize === 18 ? 6 : 9; + const waveInner = waveOuter / 3; + graphics.lineStyle(1, hexToHexadecimal("ff0000"), 0.5); + graphics.strokeRect(trailX - waveOuter, trailY - waveOuter, waveOuter * 2, waveOuter * 2); + graphics.lineStyle(1, hexToHexadecimal("0000ff"), 1); + graphics.strokeRect(trailX - waveInner, trailY - waveInner, waveInner * 2, waveInner * 2); } - - // inner hitbox - graphics.strokeRect(trailX - 9, trailY - 9, 18, 18); }); } @@ -5276,9 +5283,16 @@ if (this.p.isFlying || this.p.isUfo) { } graphics.lineStyle(2, hexToHexadecimal("0000ff"), 1); + // inner hitbox + graphics.strokeRect(_playerDrawX - 9, _0x1e788a - 9, 18, 18); + } else { + const waveOuter = this.p.isMini ? 6 : 9; + const waveInner = waveOuter / 3; + graphics.lineStyle(2, hexToHexadecimal("ff0000"), 0.8); + graphics.strokeRect(_playerDrawX - waveOuter, _0x1e788a - waveOuter, waveOuter * 2, waveOuter * 2); + graphics.lineStyle(2, hexToHexadecimal("0000ff"), 1); + graphics.strokeRect(_playerDrawX - waveInner, _0x1e788a - waveInner, waveInner * 2, waveInner * 2); } - // inner hitbox - graphics.strokeRect(_playerDrawX - 9, _0x1e788a - 9, 18, 18); } playEndAnimation(_0x24408e, _0x281588, _0x54bbf4) { this._endAnimating = true; From 654a498abbc5def41b9086b588c5972220e9de75 Mon Sep 17 00:00:00 2001 From: thisisuhhplanetring Date: Thu, 17 Sep 2026 10:52:15 +0300 Subject: [PATCH 4/6] added d blocks --- assets/scripts/core/level-editor.js | 2 +- assets/scripts/core/level.js | 17 ++++++++++- assets/scripts/core/player.js | 45 +++++++++++++++++++++++------ assets/scripts/game/allObjects.js | 2 +- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/assets/scripts/core/level-editor.js b/assets/scripts/core/level-editor.js index 01336458..eff762db 100644 --- a/assets/scripts/core/level-editor.js +++ b/assets/scripts/core/level-editor.js @@ -1837,7 +1837,7 @@ class LevelEditor { if (this._categoryContainer) this._categoryContainer.destroy(); const OBJECT_CATEGORIES = [ - { id: "blocks", icon: "tab1", types: ["solid", "soliddeco"] }, + { id: "blocks", icon: "tab1", types: ["solid", "soliddeco", "dblock"] }, { id: "slopes", icon: "tab6", types: ["slope"] }, { id: "hazards", icon: "tab2", types: ["hazard", "spike"] }, { id: "orbs", icon: "tab3", types: ["ring", "pad", "portal", "speed", "coin"] }, diff --git a/assets/scripts/core/level.js b/assets/scripts/core/level.js index 3795bf9f..1cf8c168 100644 --- a/assets/scripts/core/level.js +++ b/assets/scripts/core/level.js @@ -225,6 +225,7 @@ const ringType = "ring"; const triggerType = "trigger"; const speedType = "speed"; const slopeType = "slope"; +const dBlockType = "dblock"; const _SLOPE_DATA = { 289:{gw:1,gh:1,angle:45,sq:false,dir:1}, 291:{gw:2,gh:1,angle:22.5,sq:false,dir:-1}, 294:{gw:1,gh:1,angle:45,sq:false},295:{gw:2,gh:1,angle:22.5,sq:false}, @@ -2545,6 +2546,10 @@ window.LevelObject = class LevelObject { sprite._eeBaseY = baseY; sprite._eeZDepth = objZDepth; sprite._eeOrigAlpha = 1; + if (objectDef?.type === dBlockType) { + sprite._eeEditorOnly = true; + sprite.setVisible(!!window.isEditor); + } if (isSawObjectId) { sprite._isSaw = true; const isDecorativeSaw = objectDef?.type === decoType && frameName?.includes("sawblade"); @@ -3014,6 +3019,15 @@ window.LevelObject = class LevelObject { this.objects.push(collider); hasCollisionEntry = true; this._addCollisionToSection(collider); + } else if (objectDef.type === dBlockType && objectDef.gridW > 0 && objectDef.gridH > 0) { + const w = objectDef.gridW * a * hitScaleX; + const h = objectDef.gridH * a * hitScaleY; + const collider = new Collider(dBlockType, worldX, worldY, w, h, levelObj.rot || 0); + collider.objid = levelObj.id; + registerCollider(collider); + this.objects.push(collider); + hasCollisionEntry = true; + this._addCollisionToSection(collider); } else if (objectDef.type === hazardType) { let hitW = 0; let hitH = 0; @@ -4429,7 +4443,8 @@ window.LevelObject = class LevelObject { visMinSection._eeActive = false; const showtheportalthing = !visMinSection._eePortalGuide || (!window.isEditor && window.enablePortalGuide !== false); const showtheorbthing = !visMinSection._eeOrbGuide || (!window.isEditor && window.enableOrbGuide !== false); - visMinSection.visible = showtheportalthing && showtheorbthing; + const showeditoronly = !visMinSection._eeEditorOnly || !!window.isEditor; + visMinSection.visible = showtheportalthing && showtheorbthing && showeditoronly; visMinSection.x = visMinSection._eeWorldX; visMinSection.y = visMinSection._eeBaseY; if (!visMinSection._eeAudioScale) { diff --git a/assets/scripts/core/player.js b/assets/scripts/core/player.js index 0b0ef86a..67579f6f 100644 --- a/assets/scripts/core/player.js +++ b/assets/scripts/core/player.js @@ -3094,7 +3094,7 @@ if (this.p.isFlying || this.p.isUfo) { const wallRight = wallLeft + wallW; const boxBot = gameObj.y - halfH; const boxTop = gameObj.y + halfH; - const inner = 9; + const inner = this.p.isWave ? (this.p.isMini ? 2 : 3) : 9; return (pieceWidth + inner > wallLeft) && (pieceWidth - inner < wallRight) && (playersY + inner > boxBot) && (playersY - inner < boxTop); } @@ -3150,11 +3150,15 @@ if (this.p.isFlying || this.p.isUfo) { if (this.p.isWave) { const waveHalf = this.p.isMini ? 6 : 9; - const insideSolid = gameObj.slopeSolidBelow - ? (playersY - waveHalf < surfaceY) - : (playersY + waveHalf > surfaceY); - if (insideSolid) return { landed: false, died: true, immediate: true }; - return { landed: false, died: false }; + if (!this._waveSlideActive) { + const waveInner = waveHalf / 3; + const insideSolid = gameObj.slopeSolidBelow + ? (playersY - waveInner < surfaceY) + : (playersY + waveInner > surfaceY); + if (insideSolid) return { landed: false, died: true, immediate: true }; + return { landed: false, died: false }; + } + playerSize = waveHalf; } const pLow = playersY - playerSize + gamemodeAddition; @@ -3164,8 +3168,9 @@ if (this.p.isFlying || this.p.isUfo) { const _tanSpeed = playerSpeed * (window.slopeTangentD !== undefined ? window.slopeTangentD : d); const tangent = Math.tan(angle) * _tanSpeed; - if (this.p.isFlying && !this.p.isUfo) { + if ((this.p.isFlying && !this.p.isUfo) || this.p.isWave) { const gFlip = this.p.gravityFlipped; + const slopeTol = this.p.isWave ? 30 : playerSize; const actsAsFloor = (!isCeilSlope && !gFlip) || (isCeilSlope && gFlip); const snapAbove = actsAsFloor !== gFlip; const stickRest = this.p.onGround && !this.p.upKeyDown; @@ -3173,7 +3178,7 @@ if (this.p.isFlying || this.p.isUfo) { if (snapAbove) { const crossedDown = pLastLow >= surfaceY - gamemodeAddition && pLow < surfaceY; if ((this.p.yVelocity <= 0 || (gFlip ? stickPush : stickRest) || crossedDown) && - pLow >= surfaceY - playerSize && pLow <= surfaceY + gamemodeAddition) { + pLow >= surfaceY - slopeTol && pLow <= surfaceY + gamemodeAddition) { if (this._slopeRiding && this._slopeExitVel > 0 && tangent < 0) { return { landed: false, died: false }; } @@ -3186,7 +3191,7 @@ if (this.p.isFlying || this.p.isUfo) { } const crossedUp = pLastHigh <= surfaceY + gamemodeAddition && pHigh > surfaceY; if ((this.p.yVelocity >= 0 || (gFlip ? stickRest : stickPush) || crossedUp) && - pHigh >= surfaceY - playerSize * 1.5 && pHigh <= surfaceY + playerSize) { + pHigh >= surfaceY - slopeTol * 1.5 && pHigh <= surfaceY + slopeTol) { if (this._slopeRiding && this._slopeExitVel < 0 && tangent > 0) { return { landed: false, died: false }; } @@ -3280,6 +3285,7 @@ if (this.p.isFlying || this.p.isUfo) { _applySlopeExitVelocity() { if (this._slopeExitVel === null) return; if (this._skipSlopeExit) return; + if (this.p.isWave) return; if (this.p.isFlying && !this.p.isUfo) { if (!this.p.upKeyDown || this._slopeRidePush) { const shipCap = window.slopeShipCap !== undefined ? window.slopeShipCap : 16; @@ -4210,6 +4216,20 @@ if (this.p.isFlying || this.p.isUfo) { let _slopeTangentThisFrame = 0; let _slopeBlockedThisStep = false; const _0x198534 = this._gameLayer.getNearbySectionObjects(pieceWidth); + this._waveSlideActive = false; + if (this.p.isWave) { + for (let dObj of _0x198534) { + if (dObj.type !== dBlockType) continue; + const dRad = dObj.rotationDegrees * Math.PI / 180; + const dHalfW = Math.abs(dObj.w / 2 * Math.cos(dRad)) + Math.abs(dObj.h / 2 * Math.sin(dRad)); + const dHalfH = Math.abs(dObj.w / 2 * Math.sin(dRad)) + Math.abs(dObj.h / 2 * Math.cos(dRad)); + if (pieceWidth + waveHitSize > dObj.x - dHalfW && pieceWidth - waveHitSize < dObj.x + dHalfW && + playersY + waveHitSize > dObj.y - dHalfH && playersY - waveHitSize < dObj.y + dHalfH) { + this._waveSlideActive = true; + break; + } + } + } for (let gameObj of _0x198534) { let left = gameObj.x - gameObj.w / 2; let right = gameObj.x + gameObj.w / 2; @@ -4847,6 +4867,11 @@ if (this.p.isFlying || this.p.isUfo) { } else { iscolliding = pieceWidth + _0x55559d > left && pieceWidth - _0x55559d < right && playersY + _0x55559d > top && playersY - _0x55559d < bottom; } + if (this.p.isWave && !this._waveSlideActive) { + if (!iscolliding || window.noClip || this.breakabletheblock(gameObj)) continue; + this.killPlayer(); + return; + } const _0xLandBot = (this.p.yVelocity <= 0 || this.p.onGround) && (_0x146a97 >= bottom || _0x869e42 >= bottom); const _0xLandTop = (this.p.yVelocity >= 0 || this.p.onGround) && (_0x3e7199 <= top || _0x135a9d <= top); const isstandingOnAPlatform = this.p.gravityFlipped ? _0xLandTop : _0xLandBot; @@ -5132,6 +5157,8 @@ if (this.p.isFlying || this.p.isUfo) { hitboxColor = 16711935; } else if (nearObject.type === slopeType) { hitboxColor = 65535; + } else if (nearObject.type === dBlockType) { + hitboxColor = 0xaaaaaa; } const xPos = isFlipped ? screenWidth - objXCenter : objXCenter; graphics.lineStyle(2, hitboxColor, 0.7); diff --git a/assets/scripts/game/allObjects.js b/assets/scripts/game/allObjects.js index 2c038f5f..ce2e24f2 100644 --- a/assets/scripts/game/allObjects.js +++ b/assets/scripts/game/allObjects.js @@ -30691,7 +30691,7 @@ window.allobjects = function() { "gridH": 1, "gridW": 1, "spritesheet": "GJ_GameSheet-uhd", - "type": "deco", + "type": "dblock", "z": 2, "default_detail_color_channel": -1, "default_z_layer": 5, From e26a544c17747103051ba703975d825bdf24e89f Mon Sep 17 00:00:00 2001 From: thisisuhhplanetring Date: Thu, 17 Sep 2026 11:11:32 +0300 Subject: [PATCH 5/6] wave doesnt die on block corners / after slopes in d blocks --- assets/scripts/core/player.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/assets/scripts/core/player.js b/assets/scripts/core/player.js index 67579f6f..cc0b4a3b 100644 --- a/assets/scripts/core/player.js +++ b/assets/scripts/core/player.js @@ -4878,7 +4878,20 @@ if (this.p.isFlying || this.p.isUfo) { const _slopeLeadIn = this._slopeRiding && (this.p.gravityFlipped ? _0x3e7199 <= top + _solidSize : _0x146a97 >= bottom - _solidSize); - if (iscolliding && !isstandingOnAPlatform && !_slopeLeadIn) { + const _sDist = Math.abs(playersY - playersLastY); + const _waveTol = this.p.isWave ? _solidSize - _0x55559d : 0; + const _normalLandBot = _0x146a97 >= bottom && _0x146a97 - bottom <= _sDist + gamemodeAddition; + const _coyoteBot = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x146a97 >= bottom && _0x146a97 - bottom < _solidSize + gamemodeAddition; + const _skipBot = _0x146a97 < bottom && _0x869e42 >= bottom; + const _waveLandBot = this.p.isWave && (this.p.yVelocity <= 0 || this.p.onGround) && _0x146a97 < bottom && bottom - _0x146a97 <= _waveTol; + const _landBot = _normalLandBot || _coyoteBot || _skipBot || _waveLandBot; + const _normalLandTop = _0x3e7199 <= top && top - _0x3e7199 <= _sDist + gamemodeAddition; + const _coyoteTop = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x3e7199 <= top && top - _0x3e7199 < _solidSize + gamemodeAddition; + const _skipTop = _0x3e7199 > top && _0x135a9d <= top; + const _waveLandTop = this.p.isWave && (this.p.yVelocity >= 0 || this.p.onGround) && _0x3e7199 > top && _0x3e7199 - top <= _waveTol; + const _landTop = _normalLandTop || _coyoteTop || _skipTop || _waveLandTop; + const _safeOnSurface = this.p.isWave ? (_landBot || _landTop) : isstandingOnAPlatform; + if (iscolliding && !_safeOnSurface && !_slopeLeadIn) { if (window.noClip) { continue; @@ -4889,15 +4902,6 @@ if (this.p.isFlying || this.p.isUfo) { this.killPlayer(); return; } - const _sDist = Math.abs(playersY - playersLastY); - const _normalLandBot = _0x146a97 >= bottom && _0x146a97 - bottom <= _sDist + gamemodeAddition; - const _coyoteBot = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x146a97 >= bottom && _0x146a97 - bottom < _solidSize + gamemodeAddition; - const _skipBot = _0x146a97 < bottom && _0x869e42 >= bottom; - const _landBot = _normalLandBot || _coyoteBot || _skipBot; - const _normalLandTop = _0x3e7199 <= top && top - _0x3e7199 <= _sDist + gamemodeAddition; - const _coyoteTop = this.p.onGround && !this._slopeRiding && this._slopeExitGrace === 0 && _0x3e7199 <= top && top - _0x3e7199 < _solidSize + gamemodeAddition; - const _skipTop = _0x3e7199 > top && _0x135a9d <= top; - const _landTop = _normalLandTop || _coyoteTop || _skipTop; if (pieceWidth + _solidSize - 5 > left && pieceWidth - _solidSize + 5 < right) { if (!this.p.gravityFlipped && _landBot && (this.p.yVelocity <= 0 || this.p.onGround)) { this.p.y = bottom + _solidSize; From b4f70db96fd3897123049e4ab31047dbc35be14d Mon Sep 17 00:00:00 2001 From: thisisuhhplanetring Date: Thu, 17 Sep 2026 11:34:26 +0300 Subject: [PATCH 6/6] wave doesnt get pulled up onto ceiling slopes early --- assets/scripts/core/player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/scripts/core/player.js b/assets/scripts/core/player.js index cc0b4a3b..86ee1e9c 100644 --- a/assets/scripts/core/player.js +++ b/assets/scripts/core/player.js @@ -3191,7 +3191,7 @@ if (this.p.isFlying || this.p.isUfo) { } const crossedUp = pLastHigh <= surfaceY + gamemodeAddition && pHigh > surfaceY; if ((this.p.yVelocity >= 0 || (gFlip ? stickRest : stickPush) || crossedUp) && - pHigh >= surfaceY - slopeTol * 1.5 && pHigh <= surfaceY + slopeTol) { + pHigh >= surfaceY - (this.p.isWave ? 2 : slopeTol * 1.5) && pHigh <= surfaceY + slopeTol) { if (this._slopeRiding && this._slopeExitVel < 0 && tangent > 0) { return { landed: false, died: false }; }